-
Notifications
You must be signed in to change notification settings - Fork 1
Frends.odf.write spreadsheet #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
16231ad
3c57f06
e280f00
c9af902
ef9bc09
4527ee5
0c4cffd
9da7926
941308c
ddcd588
031ff3c
c867f09
93de945
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| name: WriteSpreadsheet_release | ||
| permissions: | ||
| contents: write | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| build: | ||
| uses: FrendsPlatform/FrendsTasks/.github/workflows/release.yml@main | ||
| with: | ||
| workdir: Frends.Odf.WriteSpreadsheet | ||
| dotnet_version: 8.0.x | ||
| strict_analyzers: true | ||
| secrets: | ||
| feed_api_key: ${{ secrets.TASKS_FEED_API_KEY }} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| name: WriteSpreadsheet_test_on_main | ||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| paths: | ||
| - 'Frends.Odf.WriteSpreadsheet/**' | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| build: | ||
| uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_main.yml@main | ||
| with: | ||
| workdir: Frends.Odf.WriteSpreadsheet | ||
| dotnet_version: 8.0.x | ||
| strict_analyzers: true | ||
| secrets: | ||
| badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| name: WriteSpreadsheet_test_on_push | ||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| push: | ||
| branches-ignore: | ||
| - main | ||
| paths: | ||
| - 'Frends.Odf.WriteSpreadsheet/**' | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| build: | ||
| uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_test.yml@main | ||
| with: | ||
| workdir: Frends.Odf.WriteSpreadsheet | ||
| dotnet_version: 8.0.x | ||
| strict_analyzers: true | ||
| secrets: | ||
| badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }} | ||
| test_feed_api_key: ${{ secrets.TASKS_TEST_FEED_API_KEY }} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Changelog | ||
|
|
||
| ## [1.0.0] - 2026-06-08 | ||
|
|
||
| ### Added | ||
|
|
||
| - Initial implementation | ||
|
Comment on lines
+3
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Expand the 1.0.0 entry with concrete functional changes. The current note (“Initial implementation”) is too generic. Please list the shipped behavior (e.g., creating As per coding guidelines, “Validate format against Keep a Changelog … Include all functional changes and indicate breaking changes with upgrade notes.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| using System; | ||
| using System.IO; | ||
| using System.Threading; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace Frends.Odf.WriteSpreadsheet.Tests; | ||
|
|
||
| [TestFixture] | ||
| internal class ErrorHandlerTest : TestBase | ||
| { | ||
| private const string CustomErrorMessage = "CustomErrorMessage"; | ||
|
|
||
| // Fake path to trigger DirectoryNotFoundException. | ||
| private readonly string fakePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "fake_path.ods"); | ||
|
|
||
| [Test] | ||
| public void Should_Throw_Error_When_ThrowErrorOnFailure_Is_True() | ||
| { | ||
| var input = DefaultInput(); | ||
| input.FilePath = fakePath; | ||
|
|
||
| var ex = Assert.Throws<Exception>(() => | ||
| Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None)); | ||
|
|
||
| Assert.That(ex, Is.Not.Null); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Return_Failed_Result_When_ThrowErrorOnFailure_Is_False() | ||
| { | ||
| var input = DefaultInput(); | ||
| input.FilePath = fakePath; | ||
|
|
||
| var options = DefaultOptions(); | ||
| options.ThrowErrorOnFailure = false; | ||
|
|
||
| var result = Odf.WriteSpreadsheet(input, options, CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.False); | ||
| Assert.That(result.Error, Is.Not.Null); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Use_Custom_ErrorMessageOnFailure() | ||
| { | ||
| var input = DefaultInput(); | ||
| input.FilePath = fakePath; | ||
|
|
||
| var options = DefaultOptions(); | ||
| options.ErrorMessageOnFailure = CustomErrorMessage; | ||
|
|
||
| var ex = Assert.Throws<Exception>(() => | ||
| Odf.WriteSpreadsheet(input, options, CancellationToken.None)); | ||
|
|
||
| Assert.That(ex, Is.Not.Null); | ||
| Assert.That(ex.Message, Contains.Substring(CustomErrorMessage)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <TargetFramework>net8.0</TargetFramework> | ||
| <IsPackable>false</IsPackable> | ||
| <Nullable>disable</Nullable> | ||
| <LangVersion>latest</LangVersion> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\Frends.Odf.WriteSpreadsheet\Frends.Odf.WriteSpreadsheet.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <None Include=".env" CopyToOutputDirectory="PreserveNewest" Condition="Exists('.env')" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> | ||
| <PackageReference Include="coverlet.collector" Version="10.0.1"> | ||
| <PrivateAssets>all</PrivateAssets> | ||
| <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
| </PackageReference> | ||
| <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.*" /> | ||
| <PackageReference Include="NUnit" Version="4.*" /> | ||
| <PackageReference Include="NUnit3TestAdapter" Version="6.*" /> | ||
| <PackageReference Include="dotenv.net" Version="4.*" /> | ||
| </ItemGroup> | ||
| </Project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| using System; | ||
| using System.IO; | ||
| using System.Threading; | ||
| using Frends.Odf.WriteSpreadsheet.Definitions; | ||
| using Frends.Odf.WriteSpreadsheet.Tests.Helpers; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace Frends.Odf.WriteSpreadsheet.Tests; | ||
|
|
||
| [TestFixture] | ||
| internal class FunctionalTests : TestBase | ||
| { | ||
| [Test] | ||
| public void Should_Write_Input_Data() | ||
| { | ||
| var input = DefaultInput(); | ||
| var options = DefaultOptions(); | ||
|
|
||
| var result = Odf.WriteSpreadsheet(input, options, CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.True, "Task failed to execute successfully."); | ||
| Assert.That(File.Exists(result.FilePath), Is.True, "The output .ods file was not created."); | ||
|
|
||
| var xmlString = TestHelper.ReadOdsContent(result.FilePath); | ||
|
|
||
| Assert.That(xmlString, Contains.Substring("John")); | ||
| Assert.That(xmlString, Contains.Substring("Test 1")); | ||
| Assert.That(xmlString, Contains.Substring("Doe")); | ||
| Assert.That(xmlString, Contains.Substring("Test 2")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Generate_Headers_If_IncludeHeaderRow_Is_True() | ||
| { | ||
| var input = DefaultInput(); | ||
| var options = DefaultOptions(); | ||
| options.IncludeHeaderRow = true; | ||
|
|
||
| var result = Odf.WriteSpreadsheet(input, options, CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.True); | ||
| var xmlString = TestHelper.ReadOdsContent(result.FilePath); | ||
|
|
||
| Assert.That(xmlString, Contains.Substring("Name")); | ||
| Assert.That(xmlString, Contains.Substring("Test")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Throw_When_Input_FilePath_Is_Incorrect() | ||
| { | ||
| var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "fake_path.ods"); | ||
|
|
||
| var input = DefaultInput(); | ||
| input.FilePath = path; | ||
|
|
||
| var exception = Assert.Throws<Exception>(() => Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None)); | ||
|
|
||
| Assert.That(exception.Message, Contains.Substring("Destination directory not found")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Throw_When_Input_Data_Is_Incorrect() | ||
| { | ||
| var invalidPayload = @"{ ""Name"": ""John"" }"; | ||
|
|
||
| var input = DefaultInput(); | ||
| input.Payload = invalidPayload; | ||
|
|
||
| var exception = Assert.Throws<Exception>(() => Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None)); | ||
|
|
||
| Assert.That(exception.Message, Contains.Substring("must be a valid array of objects")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Throw_When_ActionOnExistingFile_Is_Throw() | ||
| { | ||
| File.WriteAllText(ValidTestFilePath, "This is an existing file."); | ||
|
|
||
| var input = DefaultInput(); | ||
| input.ActionOnExistingFile = ActionOnExistingFile.Throw; | ||
|
|
||
| var exception = Assert.Throws<Exception>(() => Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None)); | ||
|
|
||
| Assert.That(exception.Message, Contains.Substring("File already exists")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Overwrite_When_ActionOnExistingFile_Is_Overwrite() | ||
| { | ||
| File.WriteAllText(ValidTestFilePath, "This is an existing file."); | ||
|
|
||
| var input = DefaultInput(); | ||
| input.ActionOnExistingFile = ActionOnExistingFile.Overwrite; | ||
|
|
||
| var result = Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.True); | ||
| Assert.That(File.Exists(result.FilePath), Is.True); | ||
|
|
||
| var xmlString = TestHelper.ReadOdsContent(result.FilePath); | ||
|
|
||
| Assert.That(xmlString, Contains.Substring("John")); | ||
| Assert.That(xmlString, Does.Not.Contain("This is an existing file.")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Write_Empty_Spreadsheet_With_Empty_Payload() | ||
| { | ||
| var emptyPayload = "[]"; | ||
|
|
||
| var input = DefaultInput(); | ||
| input.Payload = emptyPayload; | ||
|
|
||
| var options = DefaultOptions(); | ||
| options.IncludeHeaderRow = false; | ||
|
|
||
| var result = Odf.WriteSpreadsheet(input, options, CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.True); | ||
| Assert.That(File.Exists(result.FilePath), Is.True); | ||
|
|
||
| var xmlString = TestHelper.ReadOdsContent(result.FilePath); | ||
|
|
||
| Assert.That(xmlString, Does.Not.Contain("John")); | ||
| Assert.That(xmlString, Does.Not.Contain("<table:table-cell")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Handle_Unicode_Content() | ||
| { | ||
| var unicodePayload = @"[ | ||
| { ""Text1"": ""AäÄaOöÖo."" }, | ||
| { ""Text2"": ""ÖöÄä."" } | ||
| ]"; | ||
|
|
||
| var input = DefaultInput(); | ||
| input.Payload = unicodePayload; | ||
|
|
||
| var result = Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.True); | ||
| var xmlString = TestHelper.ReadOdsContent(result.FilePath); | ||
|
|
||
| Assert.That(xmlString, Contains.Substring("AäÄaOöÖo.")); | ||
| Assert.That(xmlString, Contains.Substring("ÖöÄä.")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Store_Formula_Like_Values_As_Strings() | ||
| { | ||
| var formulaPayload = @"[ | ||
| { ""Equals"": ""=SUM(A1:A2)"", ""Plus"": ""+100"", ""Minus"": ""-50"", ""At"": ""@Test"" } | ||
| ]"; | ||
|
|
||
| var input = DefaultInput(); | ||
| input.Payload = formulaPayload; | ||
|
|
||
| var result = Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.True); | ||
| var xmlString = TestHelper.ReadOdsContent(result.FilePath); | ||
|
|
||
| Assert.That(xmlString, Contains.Substring("=SUM(A1:A2)")); | ||
| Assert.That(xmlString, Contains.Substring("+100")); | ||
| Assert.That(xmlString, Contains.Substring("-50")); | ||
| Assert.That(xmlString, Contains.Substring("@Test")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Handle_Partially_Empty_Json() | ||
| { | ||
| var partialPayload = @"[ | ||
| { ""Col1"": ""Row1"" }, | ||
| { ""Col2"": ""Row2"" } | ||
| ]"; | ||
|
|
||
| var input = DefaultInput(); | ||
| input.Payload = partialPayload; | ||
|
|
||
| var result = Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.True); | ||
| var xmlString = TestHelper.ReadOdsContent(result.FilePath); | ||
|
|
||
| Assert.That(xmlString, Contains.Substring("Col1")); | ||
| Assert.That(xmlString, Contains.Substring("Col2")); | ||
| Assert.That(xmlString, Contains.Substring("Row1")); | ||
| Assert.That(xmlString, Contains.Substring("Row2")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Write_Correct_Value_Types() | ||
| { | ||
| var typedPayload = @"[ | ||
| { ""StringCol"": ""Text"", ""NumCol"": 42.5, ""BoolCol"": true, ""DateCol"": ""2026-06-12T12:00:00"" } | ||
| ]"; | ||
|
|
||
| var input = DefaultInput(); | ||
| input.Payload = typedPayload; | ||
|
|
||
| var result = Odf.WriteSpreadsheet(input, DefaultOptions(), CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.True); | ||
| var xmlString = TestHelper.ReadOdsContent(result.FilePath); | ||
|
|
||
| Assert.That(xmlString, Contains.Substring("office:value-type=\"string\"")); | ||
| Assert.That(xmlString, Contains.Substring("office:value-type=\"float\"")); | ||
| Assert.That(xmlString, Contains.Substring("office:value=\"42.5\"")); | ||
| Assert.That(xmlString, Contains.Substring("office:value-type=\"boolean\"")); | ||
| Assert.That(xmlString, Contains.Substring("office:boolean-value=\"true\"")); | ||
| Assert.That(xmlString, Contains.Substring("office:value-type=\"date\"")); | ||
| Assert.That(xmlString, Contains.Substring("office:date-value=\"2026-06-12T12:00:00\"")); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| using System.Diagnostics.CodeAnalysis; | ||
|
|
||
| [assembly: SuppressMessage("StyleCop.CSharp.SpecialRules", "SA0001::XmlCommentAnalysisDisabled", Justification = "Following Frends documentation guidelines")] | ||
| [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:PrefixLocalCallsWithThis", Justification = "Following Frends documentation guidelines")] | ||
| [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:UsingDirectivesMustBePlacedWithinNamespace", Justification = "Following Frends documentation guidelines")] | ||
| [assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:BracesMustNotBeOmitted", Justification = "Following Frends documentation guidelines")] | ||
| [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Documentation checked by custom analyzers")] | ||
| [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:FileMustHaveHeader", Justification = "Following Frends documentation guidelines")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: FrendsPlatform/Frends.Odf
Length of output: 630
Pin all three reusable-workflow refs to immutable commit SHAs instead of mutable
@mainbranches. Mutable refs allow upstream changes to affect your workflows without awareness, creating a supply-chain risk..github/workflows/WriteSpreadsheet_release.yml#L10: replacerelease.yml@mainwithrelease.yml@<commit_sha>.github/workflows/WriteSpreadsheet_test_on_main.yml#L15: replacelinux_build_main.yml@mainwithlinux_build_main.yml@<commit_sha>.github/workflows/WriteSpreadsheet_test_on_push.yml#L15: replacelinux_build_test.yml@mainwithlinux_build_test.yml@<commit_sha>🧰 Tools
🪛 zizmor (1.25.2)
[error] 10-10: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
📍 Affects 3 files
.github/workflows/WriteSpreadsheet_release.yml#L10-L10(this comment).github/workflows/WriteSpreadsheet_test_on_main.yml#L15-L15.github/workflows/WriteSpreadsheet_test_on_push.yml#L15-L15🤖 Prompt for AI Agents
Source: Linters/SAST tools