Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/WriteTextDocument_release.yml
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check if these workflow files exist and examine their content
git ls-files '.github/workflows/' | head -20

Repository: FrendsPlatform/Frends.Odf

Length of output: 522


🏁 Script executed:

# Get the specific lines mentioned in the review
echo "=== WriteTextDocument_release.yml (Line 10) ==="
sed -n '8,12p' .github/workflows/WriteTextDocument_release.yml 2>/dev/null || echo "File not found"

echo "=== WriteTextDocument_test_on_main.yml (Line 15) ==="
sed -n '13,17p' .github/workflows/WriteTextDocument_test_on_main.yml 2>/dev/null || echo "File not found"

echo "=== WriteTextDocument_test_on_push.yml (Line 15) ==="
sed -n '13,17p' .github/workflows/WriteTextDocument_test_on_push.yml 2>/dev/null || echo "File not found"

Repository: FrendsPlatform/Frends.Odf

Length of output: 664


🏁 Script executed:

# Search for all 'uses' directives in workflow files to see if there's a pattern
rg 'uses:' .github/workflows/ -A 1 -B 1

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 @main refs, which is a supply-chain risk and violates the static policy.

  • .github/workflows/WriteTextDocument_release.yml#L10: replace @main with a pinned commit SHA for FrendsPlatform/FrendsTasks/.github/workflows/release.yml.
  • .github/workflows/WriteTextDocument_test_on_main.yml#L15: replace @main with a pinned commit SHA for FrendsPlatform/FrendsTasks/.github/workflows/linux_build_main.yml.
  • .github/workflows/WriteTextDocument_test_on_push.yml#L15: replace @main with a pinned commit SHA for FrendsPlatform/FrendsTasks/.github/workflows/linux_build_test.yml.

Note: The same pattern exists in .github/workflows/ReadTextDocument_*.yml and .github/workflows/ReadSpreadsheet_*.yml workflows 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/WriteTextDocument_release.yml at line 10, Replace all
mutable `@main` workflow references with pinned commit SHAs to address
supply-chain security risks. In
`.github/workflows/WriteTextDocument_release.yml` at line 10, replace the
`@main` reference in the
FrendsPlatform/FrendsTasks/.github/workflows/release.yml uses statement with a
specific commit SHA. In `.github/workflows/WriteTextDocument_test_on_main.yml`
at line 15, replace the `@main` reference in the
FrendsPlatform/FrendsTasks/.github/workflows/linux_build_main.yml uses statement
with a specific commit SHA. In
`.github/workflows/WriteTextDocument_test_on_push.yml` at line 15, replace the
`@main` reference in the
FrendsPlatform/FrendsTasks/.github/workflows/linux_build_test.yml uses statement
with a specific commit SHA. Apply the same pattern consistently across all
similar workflow files in the ReadTextDocument_*.yml and ReadSpreadsheet_*.yml
workflows.

Source: Linters/SAST tools

with:
workdir: Frends.Odf.WriteTextDocument
dotnet_version: 8.0.x
strict_analyzers: true
secrets:
feed_api_key: ${{ secrets.TASKS_FEED_API_KEY }}
21 changes: 21 additions & 0 deletions .github/workflows/WriteTextDocument_test_on_main.yml
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 }}
22 changes: 22 additions & 0 deletions .github/workflows/WriteTextDocument_test_on_push.yml
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 }}
7 changes: 7 additions & 0 deletions Frends.Odf.WriteTextDocument/CHANGELOG.md
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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, "Frends.*/CHANGELOG.md: Validate format against Keep a Changelog ... Include all functional changes and indicate breaking changes with upgrade notes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Frends.Odf.WriteTextDocument/CHANGELOG.md` around lines 3 - 7, Replace the
generic "Initial implementation" entry in the Added section of version 1.0.0 in
CHANGELOG.md with concrete functional descriptions of what the task actually
does. Include specific details about the task's functional capabilities such as:
the ability to generate ODT (OpenDocument Text) documents, what inputs/outputs
the task accepts and produces, file overwrite behavior, and validation or error
handling behavior. If there are any breaking changes in this initial release,
add an additional "Breaking Changes" section with upgrade notes to guide
consumers. Ensure the changelog follows the Keep a Changelog format with clear,
consumer-facing language that helps users understand exactly what functionality
is available.

Source: 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();
}
}
Loading
Loading