save progress
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# Concelier Analyzer Tests Charter
|
||||
|
||||
## Mission
|
||||
Own tests for the Concelier analyzer rules.
|
||||
|
||||
## Responsibilities
|
||||
- Keep analyzer diagnostics deterministic and scoped to Concelier connectors.
|
||||
- Maintain test coverage for allowed/blocked HttpClient usage.
|
||||
|
||||
## Key Paths
|
||||
- `ConnectorHttpClientSandboxAnalyzerTests.cs`
|
||||
|
||||
## Required Reading
|
||||
- `docs/modules/concelier/architecture.md`
|
||||
- `docs/modules/platform/architecture-overview.md`
|
||||
|
||||
## Working Agreement
|
||||
- 1. Update task status to `DOING`/`DONE` in both corresponding sprint file `/docs/implplan/SPRINT_*.md` and the local `TASKS.md` when you start or finish work.
|
||||
- 2. Review this charter and the Required Reading documents before coding; confirm prerequisites are met.
|
||||
- 3. Keep changes deterministic (stable ordering, timestamps, hashes) and align with offline/air-gap expectations.
|
||||
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using StellaOps.TestKit;
|
||||
|
||||
namespace StellaOps.Concelier.Analyzers.Tests;
|
||||
|
||||
public sealed class ConnectorHttpClientSandboxAnalyzerTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReportsDiagnostic_ForHttpClientInConnectorNamespace()
|
||||
{
|
||||
const string source = """
|
||||
using System.Net.Http;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Demo;
|
||||
|
||||
public sealed class ClientFactory
|
||||
{
|
||||
public HttpClient Create() => new HttpClient();
|
||||
}
|
||||
""";
|
||||
|
||||
var diagnostics = await AnalyzeAsync(source, "StellaOps.Concelier.Connector.Demo");
|
||||
Assert.Contains(diagnostics, d => d.Id == ConnectorHttpClientSandboxAnalyzer.DiagnosticId);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DoesNotReportDiagnostic_OutsideConnectorNamespace()
|
||||
{
|
||||
const string source = """
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Sample.App;
|
||||
|
||||
public sealed class ClientFactory
|
||||
{
|
||||
public HttpClient Create() => new HttpClient();
|
||||
}
|
||||
""";
|
||||
|
||||
var diagnostics = await AnalyzeAsync(source, "Sample.App");
|
||||
Assert.DoesNotContain(diagnostics, d => d.Id == ConnectorHttpClientSandboxAnalyzer.DiagnosticId);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("StellaOps.Concelier.Connector.Demo.Tests")]
|
||||
[InlineData("StellaOps.Concelier.Connector.Demo.Test")]
|
||||
[InlineData("StellaOps.Concelier.Connector.Demo.Testing")]
|
||||
public async Task DoesNotReportDiagnostic_InTestAssemblies(string assemblyName)
|
||||
{
|
||||
const string source = """
|
||||
using System.Net.Http;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Demo;
|
||||
|
||||
public sealed class ClientFactory
|
||||
{
|
||||
public HttpClient Create() => new HttpClient();
|
||||
}
|
||||
""";
|
||||
|
||||
var diagnostics = await AnalyzeAsync(source, assemblyName);
|
||||
Assert.DoesNotContain(diagnostics, d => d.Id == ConnectorHttpClientSandboxAnalyzer.DiagnosticId);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DoesNotReportDiagnostic_ForOtherTypes()
|
||||
{
|
||||
const string source = """
|
||||
namespace StellaOps.Concelier.Connector.Demo;
|
||||
|
||||
public sealed class ClientFactory
|
||||
{
|
||||
public object Create() => new object();
|
||||
}
|
||||
""";
|
||||
|
||||
var diagnostics = await AnalyzeAsync(source, "StellaOps.Concelier.Connector.Demo");
|
||||
Assert.DoesNotContain(diagnostics, d => d.Id == ConnectorHttpClientSandboxAnalyzer.DiagnosticId);
|
||||
}
|
||||
|
||||
private static async Task<ImmutableArray<Diagnostic>> AnalyzeAsync(string source, string assemblyName)
|
||||
{
|
||||
var compilation = CSharpCompilation.Create(
|
||||
assemblyName,
|
||||
new[] { CSharpSyntaxTree.ParseText(source) },
|
||||
CreateMetadataReferences(),
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
|
||||
var analyzer = new ConnectorHttpClientSandboxAnalyzer();
|
||||
var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));
|
||||
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
|
||||
}
|
||||
|
||||
private static IEnumerable<MetadataReference> CreateMetadataReferences()
|
||||
{
|
||||
yield return MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location);
|
||||
yield return MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location);
|
||||
yield return MetadataReference.CreateFromFile(typeof(HttpClient).GetTypeInfo().Assembly.Location);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Analyzers\StellaOps.Concelier.Analyzers\StellaOps.Concelier.Analyzers.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,8 @@
|
||||
# Concelier Analyzer Tests Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0144-A | DONE | Tests for StellaOps.Concelier.Analyzers. |
|
||||
@@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.Connector.Acsc;
|
||||
using StellaOps.Concelier.Connector.Acsc.Configuration;
|
||||
using StellaOps.Concelier.Connector.Acsc.Internal;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
@@ -120,7 +121,65 @@ public sealed class AcscConnectorFetchTests
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<ConnectorTestHarness> BuildHarnessAsync(bool preferRelay)
|
||||
[Fact]
|
||||
public async Task ProbeAsync_HeadNotAllowedFallsBackToGetAndPrefersDirect()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync(preferRelay: true);
|
||||
|
||||
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
|
||||
var cursor = AcscCursor.Empty.WithPreferredEndpoint(AcscEndpointPreference.Relay);
|
||||
await stateRepository.UpdateCursorAsync(
|
||||
AcscConnectorPlugin.SourceName,
|
||||
cursor.ToDocumentObject(),
|
||||
harness.TimeProvider.GetUtcNow(),
|
||||
CancellationToken.None);
|
||||
|
||||
harness.Handler.AddResponse(HttpMethod.Head, AlertsDirectUri, _ => new HttpResponseMessage(HttpStatusCode.MethodNotAllowed));
|
||||
harness.Handler.AddResponse(HttpMethod.Get, AlertsDirectUri, _ => new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<AcscConnector>();
|
||||
await connector.ProbeAsync(CancellationToken.None);
|
||||
|
||||
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.Equal("Direct", state!.Cursor.GetValue("preferredEndpoint").AsString);
|
||||
|
||||
Assert.Collection(harness.Handler.Requests,
|
||||
request =>
|
||||
{
|
||||
Assert.Equal(HttpMethod.Head, request.Method);
|
||||
Assert.Equal(AlertsDirectUri, request.Uri);
|
||||
},
|
||||
request =>
|
||||
{
|
||||
Assert.Equal(HttpMethod.Get, request.Method);
|
||||
Assert.Equal(AlertsDirectUri, request.Uri);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProbeAsync_RelayNotConfiguredForcesDirectPreference()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync(preferRelay: true, includeRelay: false);
|
||||
|
||||
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
|
||||
var cursor = AcscCursor.Empty.WithPreferredEndpoint(AcscEndpointPreference.Relay);
|
||||
await stateRepository.UpdateCursorAsync(
|
||||
AcscConnectorPlugin.SourceName,
|
||||
cursor.ToDocumentObject(),
|
||||
harness.TimeProvider.GetUtcNow(),
|
||||
CancellationToken.None);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<AcscConnector>();
|
||||
await connector.ProbeAsync(CancellationToken.None);
|
||||
|
||||
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.Equal("Direct", state!.Cursor.GetValue("preferredEndpoint").AsString);
|
||||
Assert.Empty(harness.Handler.Requests);
|
||||
}
|
||||
|
||||
private async Task<ConnectorTestHarness> BuildHarnessAsync(bool preferRelay, bool includeRelay = true)
|
||||
{
|
||||
var initialTime = new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero);
|
||||
var harness = new ConnectorTestHarness(_fixture, initialTime, AcscOptions.HttpClientName);
|
||||
@@ -129,7 +188,7 @@ public sealed class AcscConnectorFetchTests
|
||||
services.AddAcscConnector(options =>
|
||||
{
|
||||
options.BaseEndpoint = BaseEndpoint;
|
||||
options.RelayEndpoint = RelayEndpoint;
|
||||
options.RelayEndpoint = includeRelay ? RelayEndpoint : null;
|
||||
options.EnableRelayFallback = true;
|
||||
options.PreferRelayByDefault = preferRelay;
|
||||
options.ForceRelay = false;
|
||||
|
||||
@@ -8,7 +8,10 @@ using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Acsc;
|
||||
using StellaOps.Concelier.Connector.Acsc.Configuration;
|
||||
using StellaOps.Concelier.Connector.Acsc.Internal;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Html;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Storage;
|
||||
@@ -46,6 +49,14 @@ public sealed class AcscConnectorParseTests
|
||||
var document = await documentStore.FindBySourceAndUriAsync(AcscConnectorPlugin.SourceName, feedUri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(document);
|
||||
|
||||
var rawStorage = harness.ServiceProvider.GetRequiredService<RawDocumentStorage>();
|
||||
var rawBytes = await rawStorage.DownloadAsync(document!.PayloadId!.Value, CancellationToken.None);
|
||||
Assert.NotEmpty(rawBytes);
|
||||
var rawText = Encoding.UTF8.GetString(rawBytes);
|
||||
Assert.Contains("<rss", rawText, StringComparison.OrdinalIgnoreCase);
|
||||
var sanityDto = AcscFeedParser.Parse(rawBytes, "alerts", new DateTimeOffset(2025, 10, 12, 6, 0, 0, TimeSpan.Zero), new HtmlContentSanitizer());
|
||||
Assert.NotEmpty(sanityDto.Entries);
|
||||
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var refreshed = await documentStore.FindAsync(document!.Id, CancellationToken.None);
|
||||
@@ -60,7 +71,9 @@ public sealed class AcscConnectorParseTests
|
||||
var payload = dtoRecord.Payload;
|
||||
Assert.NotNull(payload);
|
||||
Assert.Equal("alerts", payload.GetValue("feedSlug").AsString);
|
||||
Assert.Single(payload.GetValue("entries").AsDocumentArray);
|
||||
var entriesValue = payload.GetValue("entries");
|
||||
Assert.Equal(DocumentType.Array, entriesValue.DocumentType);
|
||||
Assert.Single(entriesValue.AsDocumentArray);
|
||||
|
||||
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(AcscConnectorPlugin.SourceName, CancellationToken.None);
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Concelier.Connector.Acsc.Internal;
|
||||
using StellaOps.Concelier.Connector.Common.Html;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Acsc.Tests.Acsc;
|
||||
|
||||
public sealed class AcscFeedParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_AtomFeed_ExtractsMetadataAndEntries()
|
||||
{
|
||||
const string payload = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>ACSC Atom</title>
|
||||
<updated>2025-10-12T05:00:00Z</updated>
|
||||
<link rel="alternate" href="https://origin.example/atom" />
|
||||
<entry>
|
||||
<id>urn:uuid:9d6c472e-2ad8-4c6f-9ac8-111111111111</id>
|
||||
<title>Atom Advisory</title>
|
||||
<link href="https://origin.example/atom/1" />
|
||||
<published>2025-10-11T01:00:00Z</published>
|
||||
<updated>2025-10-11T02:00:00Z</updated>
|
||||
<summary><![CDATA[
|
||||
<p><strong>Serial number:</strong> ACSC-2025-050</p>
|
||||
<p>Atom content.</p>
|
||||
]]></summary>
|
||||
</entry>
|
||||
</feed>
|
||||
""";
|
||||
|
||||
var dto = AcscFeedParser.Parse(
|
||||
Encoding.UTF8.GetBytes(payload),
|
||||
"atom",
|
||||
new DateTimeOffset(2025, 10, 12, 6, 0, 0, TimeSpan.Zero),
|
||||
new HtmlContentSanitizer());
|
||||
|
||||
Assert.Equal("ACSC Atom", dto.FeedTitle);
|
||||
Assert.Equal("https://origin.example/atom", dto.FeedLink);
|
||||
Assert.Single(dto.Entries);
|
||||
|
||||
var entry = dto.Entries[0];
|
||||
Assert.Equal("urn:uuid:9d6c472e-2ad8-4c6f-9ac8-111111111111", entry.EntryId);
|
||||
Assert.Equal("https://origin.example/atom/1", entry.Link);
|
||||
Assert.Equal(DateTimeOffset.Parse("2025-10-11T01:00:00Z"), entry.Published);
|
||||
Assert.False(string.IsNullOrWhiteSpace(entry.Summary));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_MissingIdentifiers_UsesDeterministicFallbackId()
|
||||
{
|
||||
const string payload = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>ACSC Alerts</title>
|
||||
<item>
|
||||
<description>Advisory text without identifiers.</description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
""";
|
||||
|
||||
var sanitizer = new HtmlContentSanitizer();
|
||||
var first = AcscFeedParser.Parse(
|
||||
Encoding.UTF8.GetBytes(payload),
|
||||
"alerts",
|
||||
new DateTimeOffset(2025, 10, 12, 6, 0, 0, TimeSpan.Zero),
|
||||
sanitizer);
|
||||
var second = AcscFeedParser.Parse(
|
||||
Encoding.UTF8.GetBytes(payload),
|
||||
"alerts",
|
||||
new DateTimeOffset(2025, 10, 12, 7, 0, 0, TimeSpan.Zero),
|
||||
sanitizer);
|
||||
|
||||
Assert.Single(first.Entries);
|
||||
Assert.Single(second.Entries);
|
||||
|
||||
var firstId = first.Entries[0].EntryId;
|
||||
var secondId = second.Entries[0].EntryId;
|
||||
Assert.False(string.IsNullOrWhiteSpace(firstId));
|
||||
Assert.Equal(firstId, secondId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_RssPayloadWithContentEncoded_ReturnsEntries()
|
||||
{
|
||||
const string payload = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
<channel>
|
||||
<title>ACSC Alerts</title>
|
||||
<link>https://origin.example/feeds/alerts</link>
|
||||
<lastBuildDate>Sun, 12 Oct 2025 04:20:00 GMT</lastBuildDate>
|
||||
<item>
|
||||
<title>ACSC-2025-001 Example Advisory</title>
|
||||
<link>https://origin.example/advisories/example</link>
|
||||
<guid>https://origin.example/advisories/example</guid>
|
||||
<pubDate>Sun, 12 Oct 2025 03:00:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[
|
||||
<p><strong>Serial number:</strong> ACSC-2025-001</p>
|
||||
<p><strong>Advisory type:</strong> Alert</p>
|
||||
<p>First paragraph describing issue.</p>
|
||||
]]></content:encoded>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
""";
|
||||
|
||||
var dto = AcscFeedParser.Parse(
|
||||
Encoding.UTF8.GetBytes(payload),
|
||||
"alerts",
|
||||
new DateTimeOffset(2025, 10, 12, 6, 0, 0, TimeSpan.Zero),
|
||||
new HtmlContentSanitizer());
|
||||
|
||||
Assert.Single(dto.Entries);
|
||||
Assert.Equal("ACSC-2025-001 Example Advisory", dto.Entries[0].Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_RssPayload_RoundTripsThroughDocumentObject()
|
||||
{
|
||||
const string payload = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
<channel>
|
||||
<title>ACSC Alerts</title>
|
||||
<link>https://origin.example/feeds/alerts</link>
|
||||
<lastBuildDate>Sun, 12 Oct 2025 04:20:00 GMT</lastBuildDate>
|
||||
<item>
|
||||
<title>ACSC-2025-001 Example Advisory</title>
|
||||
<link>https://origin.example/advisories/example</link>
|
||||
<guid>https://origin.example/advisories/example</guid>
|
||||
<pubDate>Sun, 12 Oct 2025 03:00:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[
|
||||
<p><strong>Serial number:</strong> ACSC-2025-001</p>
|
||||
<p><strong>Advisory type:</strong> Alert</p>
|
||||
<p>First paragraph describing issue.</p>
|
||||
]]></content:encoded>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
""";
|
||||
|
||||
var dto = AcscFeedParser.Parse(
|
||||
Encoding.UTF8.GetBytes(payload),
|
||||
"alerts",
|
||||
new DateTimeOffset(2025, 10, 12, 6, 0, 0, TimeSpan.Zero),
|
||||
new HtmlContentSanitizer());
|
||||
|
||||
var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = false,
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(dto, jsonOptions);
|
||||
var document = DocumentObject.Parse(json);
|
||||
Assert.Single(document.GetValue("entries").AsDocumentArray);
|
||||
|
||||
var roundTrip = DocumentObject.Parse(document.ToJson());
|
||||
Assert.Single(roundTrip.GetValue("entries").AsDocumentArray);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using StellaOps.Concelier.Connector.Acsc.Internal;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Acsc.Tests.Acsc;
|
||||
|
||||
public sealed class AcscMapperTests
|
||||
{
|
||||
[Fact]
|
||||
public void Map_UsesDeterministicFallbackKeyWhenIdentifiersMissing()
|
||||
{
|
||||
var entry = new AcscEntryDto(
|
||||
EntryId: string.Empty,
|
||||
Title: string.Empty,
|
||||
Link: null,
|
||||
FeedSlug: "alerts",
|
||||
Published: null,
|
||||
Updated: null,
|
||||
Summary: "Summary text",
|
||||
ContentHtml: string.Empty,
|
||||
ContentText: string.Empty,
|
||||
References: Array.Empty<AcscReferenceDto>(),
|
||||
Aliases: Array.Empty<string>(),
|
||||
Fields: new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
var feed = new AcscFeedDto(
|
||||
FeedSlug: "alerts",
|
||||
FeedTitle: "ACSC Alerts",
|
||||
FeedLink: "https://origin.example/alerts",
|
||||
FeedUpdated: new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero),
|
||||
ParsedAt: new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero),
|
||||
Entries: new[] { entry });
|
||||
|
||||
var documentId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
||||
var document = new DocumentRecord(
|
||||
documentId,
|
||||
"acsc",
|
||||
"https://origin.example/alerts/rss",
|
||||
new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero),
|
||||
"sha256");
|
||||
|
||||
var dtoRecord = new DtoRecord(
|
||||
Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
||||
documentId,
|
||||
"acsc",
|
||||
"acsc.feed.v1",
|
||||
new DocumentObject(),
|
||||
new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero),
|
||||
SchemaVersion: "acsc.feed.v1");
|
||||
|
||||
var first = AcscMapper.Map(
|
||||
feed,
|
||||
document,
|
||||
dtoRecord,
|
||||
"acsc",
|
||||
new DateTimeOffset(2025, 10, 12, 1, 0, 0, TimeSpan.Zero));
|
||||
|
||||
var second = AcscMapper.Map(
|
||||
feed,
|
||||
document,
|
||||
dtoRecord,
|
||||
"acsc",
|
||||
new DateTimeOffset(2025, 10, 12, 2, 0, 0, TimeSpan.Zero));
|
||||
|
||||
Assert.Single(first);
|
||||
Assert.Single(second);
|
||||
Assert.Equal(first[0].AdvisoryKey, second[0].AdvisoryKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using StellaOps.Concelier.Connector.Acsc.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Acsc.Tests.Acsc;
|
||||
|
||||
public sealed class AcscOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Validate_ForceRelayRequiresRelayEndpoint()
|
||||
{
|
||||
var options = new AcscOptions
|
||||
{
|
||||
RelayEndpoint = null,
|
||||
ForceRelay = true,
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => options.Validate());
|
||||
Assert.Contains("ForceRelay", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages",
|
||||
"affectedPackages",
|
||||
"aliases",
|
||||
"references",
|
||||
"summary"
|
||||
@@ -46,7 +46,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages",
|
||||
"affectedPackages",
|
||||
"aliases",
|
||||
"references",
|
||||
"summary"
|
||||
@@ -92,7 +92,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages"
|
||||
"affectedPackages"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -112,7 +112,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages"
|
||||
"affectedPackages"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -139,7 +139,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages",
|
||||
"affectedPackages",
|
||||
"aliases",
|
||||
"references",
|
||||
"summary"
|
||||
@@ -162,7 +162,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages",
|
||||
"affectedPackages",
|
||||
"aliases",
|
||||
"references",
|
||||
"summary"
|
||||
@@ -204,4 +204,4 @@
|
||||
"summary": "Serial number: ACSC-2025-010\n\nSeverity: Critical\n\nSystems affected: ExampleCo Router X, ExampleCo Router Y\n\nRemote code execution on ExampleCo routers. See vendor patch.\n\nCVE references: CVE-2025-0001",
|
||||
"title": "Critical router vulnerability"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages",
|
||||
"affectedPackages",
|
||||
"aliases",
|
||||
"references",
|
||||
"summary"
|
||||
@@ -46,7 +46,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages",
|
||||
"affectedPackages",
|
||||
"aliases",
|
||||
"references",
|
||||
"summary"
|
||||
@@ -92,7 +92,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages"
|
||||
"affectedPackages"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -112,7 +112,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages"
|
||||
"affectedPackages"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -139,7 +139,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages",
|
||||
"affectedPackages",
|
||||
"aliases",
|
||||
"references",
|
||||
"summary"
|
||||
@@ -162,7 +162,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages",
|
||||
"affectedPackages",
|
||||
"aliases",
|
||||
"references",
|
||||
"summary"
|
||||
@@ -204,4 +204,4 @@
|
||||
"summary": "Serial number: ACSC-2025-010\n\nSeverity: Critical\n\nSystems affected: ExampleCo Router X, ExampleCo Router Y\n\nRemote code execution on ExampleCo routers. See vendor patch.\n\nCVE references: CVE-2025-0001",
|
||||
"title": "Critical router vulnerability"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages",
|
||||
"affectedPackages",
|
||||
"aliases",
|
||||
"references",
|
||||
"summary"
|
||||
@@ -46,7 +46,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages",
|
||||
"affectedPackages",
|
||||
"aliases",
|
||||
"references",
|
||||
"summary"
|
||||
@@ -88,4 +88,4 @@
|
||||
"summary": "Serial number: ACSC-2025-001\n\nAdvisory type: Alert\n\nFirst paragraph describing issue.\n\nSecond paragraph with Vendor patch.",
|
||||
"title": "ACSC-2025-001 Example Advisory"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages",
|
||||
"affectedPackages",
|
||||
"aliases",
|
||||
"references",
|
||||
"summary"
|
||||
@@ -46,7 +46,7 @@
|
||||
"decisionReason": null,
|
||||
"recordedAt": "2025-10-12T00:00:00+00:00",
|
||||
"fieldMask": [
|
||||
"affectedpackages",
|
||||
"affectedPackages",
|
||||
"aliases",
|
||||
"references",
|
||||
"summary"
|
||||
@@ -88,4 +88,4 @@
|
||||
"summary": "Serial number: ACSC-2025-001\n\nAdvisory type: Alert\n\nFirst paragraph describing issue.\n\nSecond paragraph with Vendor patch.",
|
||||
"title": "ACSC-2025-001 Example Advisory"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user