-
Notifications
You must be signed in to change notification settings - Fork 1
Frends.odf.write text document #7
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
dc35957
46cfcd5
66f71b7
7ed3820
326ecbe
aa90fdf
fbef274
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: WriteTextDocument_release | ||
| permissions: | ||
| contents: write | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| build: | ||
| uses: FrendsPlatform/FrendsTasks/.github/workflows/release.yml@main | ||
| with: | ||
| workdir: Frends.Odf.WriteTextDocument | ||
| 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: WriteTextDocument_test_on_main | ||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| paths: | ||
| - 'Frends.Odf.WriteTextDocument/**' | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| build: | ||
| uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_main.yml@main | ||
| with: | ||
| workdir: Frends.Odf.WriteTextDocument | ||
| 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: WriteTextDocument_test_on_push | ||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| push: | ||
| branches-ignore: | ||
| - main | ||
| paths: | ||
| - 'Frends.Odf.WriteTextDocument/**' | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| build: | ||
| uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_test.yml@main | ||
| with: | ||
| workdir: Frends.Odf.WriteTextDocument | ||
| 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-05-31 | ||
|
|
||
| ### 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 release notes with concrete functional changes. Line 7 is too generic for the stated changelog policy; consumers can’t tell what was added in 1.0.0. List the actual functional capabilities (e.g., task inputs/outputs, ODT generation behavior, overwrite handling, validation/error behavior), and include upgrade notes if any breaking changes exist. As per coding guidelines, " 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| using System; | ||
| using System.IO; | ||
| using System.Threading; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace Frends.Odf.WriteTextDocument.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.odt"); | ||
|
|
||
| [Test] | ||
| public void Should_Throw_Error_When_ThrowErrorOnFailure_Is_True() | ||
| { | ||
| // Inject the fake path into the default input. | ||
| var input = DefaultInput(); | ||
| input.FilePath = fakePath; | ||
|
|
||
| var ex = Assert.Throws<Exception>(() => | ||
| Odf.WriteTextDocument(input, DefaultOptions(), CancellationToken.None)); | ||
|
|
||
| Assert.That(ex, Is.Not.Null); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Return_Failed_Result_When_ThrowErrorOnFailure_Is_False() | ||
| { | ||
| // Inject the fake path into the default input. | ||
| var input = DefaultInput(); | ||
| input.FilePath = fakePath; | ||
|
|
||
| var options = DefaultOptions(); | ||
| options.ThrowErrorOnFailure = false; | ||
|
|
||
| var result = Odf.WriteTextDocument(input, options, CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.False); | ||
| Assert.That(result.Error, Is.Not.Null); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Use_Custom_ErrorMessageOnFailure() | ||
| { | ||
| // Inject the fake path into the default input. | ||
| var input = DefaultInput(); | ||
| input.FilePath = fakePath; | ||
|
|
||
| var options = DefaultOptions(); | ||
| options.ErrorMessageOnFailure = CustomErrorMessage; | ||
|
|
||
| var ex = Assert.Throws<Exception>(() => | ||
| Odf.WriteTextDocument(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.WriteTextDocument\Frends.Odf.WriteTextDocument.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,177 @@ | ||
| using System; | ||
| using System.IO; | ||
| using System.Threading; | ||
| using Frends.Odf.WriteTextDocument.Definitions; | ||
| using Frends.Odf.WriteTextDocument.Tests.Helpers; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace Frends.Odf.WriteTextDocument.Tests; | ||
|
|
||
| [TestFixture] | ||
| internal class FunctionalTests : TestBase | ||
| { | ||
| [Test] | ||
| public void Should_Write_Input_Content() | ||
| { | ||
| var input = DefaultInput(); | ||
| var options = DefaultOptions(); | ||
|
|
||
| var result = Odf.WriteTextDocument(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 .odt file was not created."); | ||
|
|
||
| var xmlString = TestHelper.ReadOdtContent(result.FilePath); | ||
|
|
||
| Assert.That(xmlString, Contains.Substring("Name: John")); | ||
| Assert.That(xmlString, Contains.Substring("Test: Test 1")); | ||
| Assert.That(xmlString, Contains.Substring("Name: Doe")); | ||
| Assert.That(xmlString, Contains.Substring("Test: Test 2")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Throw_When_Input_FilePath_Is_Incorrect() | ||
| { | ||
| var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "fake_path.odt"); | ||
|
|
||
| var input = DefaultInput(); | ||
| input.FilePath = path; | ||
|
|
||
| var exception = Assert.Throws<Exception>(() => Odf.WriteTextDocument(input, DefaultOptions(), CancellationToken.None)); | ||
|
|
||
| Assert.That(exception.Message, Contains.Substring("Destination directory not found")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Throw_When_Input_Content_Is_Incorrect() | ||
| { | ||
| var invalidPayload = @"{ ""Name"": ""John"" }"; | ||
|
|
||
| var input = DefaultInput(); | ||
| input.Payload = invalidPayload; | ||
|
|
||
| var exception = Assert.Throws<Exception>(() => Odf.WriteTextDocument(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.WriteTextDocument(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.WriteTextDocument(input, DefaultOptions(), CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.True); | ||
| Assert.That(File.Exists(result.FilePath), Is.True); | ||
|
|
||
| var xmlString = TestHelper.ReadOdtContent(result.FilePath); | ||
|
|
||
| Assert.That(xmlString, Contains.Substring("Name: John")); | ||
| Assert.That(xmlString, Does.Not.Contain("This is an existing file.")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Write_Empty_Document_With_Empty_Payload() | ||
| { | ||
| var emptyPayload = "[]"; | ||
|
|
||
| var input = DefaultInput(); | ||
| input.Payload = emptyPayload; | ||
|
|
||
| var result = Odf.WriteTextDocument(input, DefaultOptions(), CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.True); | ||
| Assert.That(File.Exists(result.FilePath), Is.True); | ||
|
|
||
| var xmlString = TestHelper.ReadOdtContent(result.FilePath); | ||
|
|
||
| Assert.That(xmlString, Does.Not.Contain("Name: John")); | ||
| Assert.That(xmlString, Does.Not.Contain("text:p>")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Handle_Unicode_Content() | ||
| { | ||
| var unicodePayload = @"[ | ||
| { ""Text1"": ""AäÄaOöÖo."" }, | ||
| { ""Text2"": ""ÖöÄä."" } | ||
| ]"; | ||
|
|
||
| var input = DefaultInput(); | ||
| input.Payload = unicodePayload; | ||
|
|
||
| var result = Odf.WriteTextDocument(input, DefaultOptions(), CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.True); | ||
| Assert.That(File.Exists(result.FilePath), Is.True); | ||
|
|
||
| var xmlString = TestHelper.ReadOdtContent(result.FilePath); | ||
|
|
||
| Assert.That(xmlString, Contains.Substring("Text1: AäÄaOöÖo.")); | ||
| Assert.That(xmlString, Contains.Substring("Text2: ÖöÄä.")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Throw_When_File_Extension_Is_Not_Odt() | ||
| { | ||
| var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".txt"); | ||
|
|
||
| var input = DefaultInput(); | ||
| input.FilePath = path; | ||
|
|
||
| var exception = Assert.Throws<Exception>(() => Odf.WriteTextDocument(input, DefaultOptions(), CancellationToken.None)); | ||
|
|
||
| Assert.That(exception.Message, Contains.Substring("The destination file must have a .odt extension.")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Throw_When_Array_Contains_Non_Objects() | ||
| { | ||
| var invalidPayload = @"[ ""This is a string."" ]"; | ||
|
|
||
| var input = DefaultInput(); | ||
| input.Payload = invalidPayload; | ||
|
|
||
| var exception = Assert.Throws<Exception>(() => Odf.WriteTextDocument(input, DefaultOptions(), CancellationToken.None)); | ||
|
|
||
| Assert.That(exception.Message, Contains.Substring("The JSON payload must contain valid JSON objects.")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Should_Handle_Null_JSON_Values() | ||
| { | ||
| var nullPayload = @"[ | ||
| { ""Name"": ""John"", ""Role"": null } | ||
| ]"; | ||
|
|
||
| var input = DefaultInput(); | ||
| input.Payload = nullPayload; | ||
|
|
||
| var result = Odf.WriteTextDocument(input, DefaultOptions(), CancellationToken.None); | ||
|
|
||
| Assert.That(result.Success, Is.True); | ||
| Assert.That(File.Exists(result.FilePath), Is.True); | ||
|
|
||
| var xmlString = TestHelper.ReadOdtContent(result.FilePath); | ||
|
|
||
| Assert.That(xmlString, Contains.Substring("Name: John")); | ||
| Assert.That(xmlString, Contains.Substring("Role: ")); | ||
| } | ||
| } |
| 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")] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| using System; | ||
| using System.IO.Compression; | ||
| using System.Xml.Linq; | ||
|
|
||
| namespace Frends.Odf.WriteTextDocument.Tests.Helpers; | ||
|
|
||
| internal class TestHelper | ||
| { | ||
| /// <summary> | ||
| /// Reads and returns content.xml from a generated .odt file as a string for testing. | ||
| /// </summary> | ||
| /// <param name="filePath">The path to the generated .odt file.</param> | ||
| /// <returns>content.xml content as a string.</returns> | ||
| internal static string ReadOdtContent(string filePath) | ||
| { | ||
| using var archive = ZipFile.OpenRead(filePath); | ||
| var contentXmlEntry = archive.GetEntry("content.xml") ?? throw new Exception("content.xml is missing from the generated file."); | ||
|
|
||
| using var stream = contentXmlEntry.Open(); | ||
| var xDocument = XDocument.Load(stream); | ||
|
|
||
| return xDocument.ToString(); | ||
| } | ||
| } |
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: 522
🏁 Script executed:
Repository: FrendsPlatform/Frends.Odf
Length of output: 664
🏁 Script executed:
Repository: FrendsPlatform/Frends.Odf
Length of output: 2325
Pin reusable workflow references to immutable commit SHAs.
All three workflows call external reusable workflows using mutable
@mainrefs, which is a supply-chain risk and violates the static policy..github/workflows/WriteTextDocument_release.yml#L10: replace@mainwith a pinned commit SHA forFrendsPlatform/FrendsTasks/.github/workflows/release.yml..github/workflows/WriteTextDocument_test_on_main.yml#L15: replace@mainwith a pinned commit SHA forFrendsPlatform/FrendsTasks/.github/workflows/linux_build_main.yml..github/workflows/WriteTextDocument_test_on_push.yml#L15: replace@mainwith a pinned commit SHA forFrendsPlatform/FrendsTasks/.github/workflows/linux_build_test.yml.Note: The same pattern exists in
.github/workflows/ReadTextDocument_*.ymland.github/workflows/ReadSpreadsheet_*.ymlworkflows and should be addressed systematically across all similar workflow files.🧰 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/WriteTextDocument_release.yml#L10-L10(this comment).github/workflows/WriteTextDocument_test_on_main.yml#L15-L15.github/workflows/WriteTextDocument_test_on_push.yml#L15-L15🤖 Prompt for AI Agents
Source: Linters/SAST tools