up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,26 +1,26 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Concelier.Bson;
using StellaOps.Concelier.Documents;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common.Http;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Connector.Vndr.Adobe;
using StellaOps.Concelier.Connector.Vndr.Adobe.Configuration;
using StellaOps.Concelier.Connector.Vndr.Adobe;
using StellaOps.Concelier.Connector.Vndr.Adobe.Configuration;
using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage;
@@ -28,432 +28,432 @@ using StellaOps.Concelier.Storage;
using StellaOps.Concelier.Storage.PsirtFlags;
using StellaOps.Concelier.Storage.Postgres;
using StellaOps.Concelier.Testing;
namespace StellaOps.Concelier.Connector.Vndr.Adobe.Tests;
namespace StellaOps.Concelier.Connector.Vndr.Adobe.Tests;
[Collection(ConcelierFixtureCollection.Name)]
public sealed class AdobeConnectorFetchTests : IAsyncLifetime
{
private readonly ConcelierPostgresFixture _fixture;
private readonly FakeTimeProvider _timeProvider;
public AdobeConnectorFetchTests(ConcelierPostgresFixture fixture)
{
_fixture = fixture;
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 9, 10, 0, 0, 0, TimeSpan.Zero));
}
[Fact]
public async Task Fetch_WindowsIndexAndPersistsCursor()
{
var handler = new CannedHttpMessageHandler();
await using var provider = await BuildServiceProviderAsync(handler);
SeedIndex(handler);
SeedDetail(handler);
var connector = provider.GetRequiredService<AdobeConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var cursor = state!.Cursor;
var pendingDocuments = ExtractGuidList(cursor, "pendingDocuments");
Assert.Equal(2, pendingDocuments.Count);
// Re-seed responses to simulate unchanged fetch
SeedIndex(handler);
SeedDetail(handler);
await connector.FetchAsync(provider, CancellationToken.None);
state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
cursor = state!.Cursor;
var afterPending = ExtractGuidList(cursor, "pendingDocuments");
Assert.Equal(pendingDocuments.OrderBy(static id => id), afterPending.OrderBy(static id => id));
var fetchCache = cursor.TryGetValue("fetchCache", out var fetchCacheValue) && fetchCacheValue is BsonDocument cacheDoc
? cacheDoc.Elements.Select(static e => e.Name).ToArray()
: Array.Empty<string>();
Assert.Contains("https://helpx.adobe.com/security/products/acrobat/apsb25-85.html", fetchCache);
Assert.Contains("https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html", fetchCache);
}
[Fact]
public async Task Parse_ProducesDtoAndClearsPendingDocuments()
{
var handler = new CannedHttpMessageHandler();
await using var provider = await BuildServiceProviderAsync(handler);
SeedIndex(handler);
SeedDetail(handler);
var connector = provider.GetRequiredService<AdobeConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
var documentStore = provider.GetRequiredService<IDocumentStore>();
var dtoStore = provider.GetRequiredService<IDtoStore>();
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var psirtStore = provider.GetRequiredService<IPsirtFlagStore>();
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var document = await documentStore.FindBySourceAndUriAsync(
VndrAdobeConnectorPlugin.SourceName,
"https://helpx.adobe.com/security/products/acrobat/apsb25-85.html",
CancellationToken.None);
Assert.NotNull(document);
var dtoRecord = await dtoStore.FindByDocumentIdAsync(document!.Id, CancellationToken.None);
Assert.NotNull(dtoRecord);
Assert.Equal("adobe.bulletin.v1", dtoRecord!.SchemaVersion);
var payload = dtoRecord.Payload;
Assert.Equal("APSB25-85", payload.GetValue("advisoryId").AsString);
Assert.Equal("https://helpx.adobe.com/security/products/acrobat/apsb25-85.html", payload.GetValue("detailUrl").AsString);
var products = payload.GetValue("products").AsBsonArray
.Select(static value => value.AsBsonDocument)
.ToArray();
Assert.NotEmpty(products);
var acrobatWindowsProduct = Assert.Single(
products,
static doc => string.Equals(doc.GetValue("product").AsString, "Acrobat DC", StringComparison.Ordinal)
&& string.Equals(doc.GetValue("platform").AsString, "Windows", StringComparison.Ordinal));
Assert.Equal("25.001.20672 and earlier", acrobatWindowsProduct.GetValue("affectedVersion").AsString);
Assert.Equal("25.001.20680", acrobatWindowsProduct.GetValue("updatedVersion").AsString);
var acrobatMacProduct = Assert.Single(
products,
static doc => string.Equals(doc.GetValue("product").AsString, "Acrobat DC", StringComparison.Ordinal)
&& string.Equals(doc.GetValue("platform").AsString, "macOS", StringComparison.Ordinal));
Assert.Equal("25.001.20668 and earlier", acrobatMacProduct.GetValue("affectedVersion").AsString);
Assert.Equal("25.001.20678", acrobatMacProduct.GetValue("updatedVersion").AsString);
var state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var cursor = state!.Cursor;
Assert.True(!cursor.TryGetValue("pendingDocuments", out _)
|| cursor.GetValue("pendingDocuments").AsBsonArray.Count == 0);
Assert.True(!cursor.TryGetValue("pendingMappings", out _)
|| cursor.GetValue("pendingMappings").AsBsonArray.Count == 0);
var advisories = await advisoryStore.GetRecentAsync(5, CancellationToken.None);
Assert.Equal(2, advisories.Count);
var acrobatAdvisory = advisories.Single(a => a.AdvisoryKey == "APSB25-85");
Assert.Contains("APSB25-85", acrobatAdvisory.Aliases);
Assert.Equal(
acrobatAdvisory.References.Select(static r => r.Url).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
acrobatAdvisory.References.Length);
var acrobatWindowsPackage = Assert.Single(
acrobatAdvisory.AffectedPackages,
pkg => string.Equals(pkg.Identifier, "Acrobat DC", StringComparison.Ordinal)
&& string.Equals(pkg.Platform, "Windows", StringComparison.Ordinal));
var acrobatWindowsRange = Assert.Single(acrobatWindowsPackage.VersionRanges);
Assert.Equal("vendor", acrobatWindowsRange.RangeKind);
Assert.Equal("25.001.20680", acrobatWindowsRange.FixedVersion);
Assert.Equal("25.001.20672", acrobatWindowsRange.LastAffectedVersion);
Assert.NotNull(acrobatWindowsRange.Primitives);
var windowsExtensions = acrobatWindowsRange.Primitives!.VendorExtensions;
Assert.NotNull(windowsExtensions);
Assert.True(windowsExtensions!.TryGetValue("adobe.affected.raw", out var rawAffectedWin));
Assert.Equal("25.001.20672 and earlier", rawAffectedWin);
Assert.True(windowsExtensions.TryGetValue("adobe.updated.raw", out var rawUpdatedWin));
Assert.Equal("25.001.20680", rawUpdatedWin);
Assert.Contains(
AffectedPackageStatusCatalog.Fixed,
acrobatWindowsPackage.Statuses.Select(static status => status.Status));
var windowsNormalized = Assert.Single(acrobatWindowsPackage.NormalizedVersions.ToArray());
Assert.Equal(NormalizedVersionSchemes.SemVer, windowsNormalized.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.LessThan, windowsNormalized.Type);
Assert.Equal("25.1.20680", windowsNormalized.Max);
Assert.False(windowsNormalized.MaxInclusive);
Assert.Equal("adobe:Acrobat DC:Windows", windowsNormalized.Notes);
var acrobatMacPackage = Assert.Single(
acrobatAdvisory.AffectedPackages,
pkg => string.Equals(pkg.Identifier, "Acrobat DC", StringComparison.Ordinal)
&& string.Equals(pkg.Platform, "macOS", StringComparison.Ordinal));
var acrobatMacRange = Assert.Single(acrobatMacPackage.VersionRanges);
Assert.Equal("vendor", acrobatMacRange.RangeKind);
Assert.Equal("25.001.20678", acrobatMacRange.FixedVersion);
Assert.Equal("25.001.20668", acrobatMacRange.LastAffectedVersion);
Assert.NotNull(acrobatMacRange.Primitives);
var macExtensions = acrobatMacRange.Primitives!.VendorExtensions;
Assert.NotNull(macExtensions);
Assert.True(macExtensions!.TryGetValue("adobe.affected.raw", out var rawAffectedMac));
Assert.Equal("25.001.20668 and earlier", rawAffectedMac);
Assert.True(macExtensions.TryGetValue("adobe.updated.raw", out var rawUpdatedMac));
Assert.Equal("25.001.20678", rawUpdatedMac);
Assert.Contains(
AffectedPackageStatusCatalog.Fixed,
acrobatMacPackage.Statuses.Select(static status => status.Status));
var macNormalized = Assert.Single(acrobatMacPackage.NormalizedVersions.ToArray());
Assert.Equal(NormalizedVersionSchemes.SemVer, macNormalized.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.LessThan, macNormalized.Type);
Assert.Equal("25.1.20678", macNormalized.Max);
Assert.False(macNormalized.MaxInclusive);
Assert.Equal("adobe:Acrobat DC:macOS", macNormalized.Notes);
var premiereAdvisory = advisories.Single(a => a.AdvisoryKey == "APSB25-87");
Assert.Contains("APSB25-87", premiereAdvisory.Aliases);
Assert.Equal(
premiereAdvisory.References.Select(static r => r.Url).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
premiereAdvisory.References.Length);
var premiereWindowsPackage = Assert.Single(
premiereAdvisory.AffectedPackages,
pkg => string.Equals(pkg.Identifier, "Premiere Pro", StringComparison.Ordinal)
&& string.Equals(pkg.Platform, "Windows", StringComparison.Ordinal));
var premiereWindowsRange = Assert.Single(premiereWindowsPackage.VersionRanges);
Assert.Equal("24.6", premiereWindowsRange.FixedVersion);
Assert.Equal("24.5", premiereWindowsRange.LastAffectedVersion);
Assert.NotNull(premiereWindowsRange.Primitives);
var premiereWindowsExtensions = premiereWindowsRange.Primitives!.VendorExtensions;
Assert.NotNull(premiereWindowsExtensions);
Assert.True(premiereWindowsExtensions!.TryGetValue("adobe.priority", out var premierePriorityWin));
Assert.Equal("Priority 3", premierePriorityWin);
Assert.Contains(
AffectedPackageStatusCatalog.Fixed,
premiereWindowsPackage.Statuses.Select(static status => status.Status));
var premiereWinNormalized = Assert.Single(premiereWindowsPackage.NormalizedVersions.ToArray());
Assert.Equal(NormalizedVersionSchemes.SemVer, premiereWinNormalized.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.LessThan, premiereWinNormalized.Type);
Assert.Equal("24.6", premiereWinNormalized.Max);
Assert.False(premiereWinNormalized.MaxInclusive);
Assert.Equal("adobe:Premiere Pro:Windows", premiereWinNormalized.Notes);
var premiereMacPackage = Assert.Single(
premiereAdvisory.AffectedPackages,
pkg => string.Equals(pkg.Identifier, "Premiere Pro", StringComparison.Ordinal)
&& string.Equals(pkg.Platform, "macOS", StringComparison.Ordinal));
var premiereMacRange = Assert.Single(premiereMacPackage.VersionRanges);
Assert.Equal("24.6", premiereMacRange.FixedVersion);
Assert.Equal("24.5", premiereMacRange.LastAffectedVersion);
Assert.NotNull(premiereMacRange.Primitives);
var premiereMacExtensions = premiereMacRange.Primitives!.VendorExtensions;
Assert.NotNull(premiereMacExtensions);
Assert.True(premiereMacExtensions!.TryGetValue("adobe.priority", out var premierePriorityMac));
Assert.Equal("Priority 3", premierePriorityMac);
Assert.Contains(
AffectedPackageStatusCatalog.Fixed,
premiereMacPackage.Statuses.Select(static status => status.Status));
var premiereMacNormalized = Assert.Single(premiereMacPackage.NormalizedVersions.ToArray());
Assert.Equal(NormalizedVersionSchemes.SemVer, premiereMacNormalized.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.LessThan, premiereMacNormalized.Type);
Assert.Equal("24.6", premiereMacNormalized.Max);
Assert.False(premiereMacNormalized.MaxInclusive);
Assert.Equal("adobe:Premiere Pro:macOS", premiereMacNormalized.Notes);
var ordered = advisories.OrderBy(static a => a.AdvisoryKey, StringComparer.Ordinal).ToArray();
var snapshot = SnapshotSerializer.ToSnapshot(ordered);
var expected = ReadFixture("adobe-advisories.snapshot.json");
var normalizedSnapshot = NormalizeLineEndings(snapshot);
var normalizedExpected = NormalizeLineEndings(expected);
if (!string.Equals(normalizedExpected, normalizedSnapshot, StringComparison.Ordinal))
{
var actualPath = Path.Combine(AppContext.BaseDirectory, "Source", "Vndr", "Adobe", "Fixtures", "adobe-advisories.actual.json");
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
File.WriteAllText(actualPath, snapshot);
}
Assert.Equal(normalizedExpected, normalizedSnapshot);
public sealed class AdobeConnectorFetchTests : IAsyncLifetime
{
private readonly ConcelierPostgresFixture _fixture;
private readonly FakeTimeProvider _timeProvider;
public AdobeConnectorFetchTests(ConcelierPostgresFixture fixture)
{
_fixture = fixture;
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 9, 10, 0, 0, 0, TimeSpan.Zero));
}
[Fact]
public async Task Fetch_WindowsIndexAndPersistsCursor()
{
var handler = new CannedHttpMessageHandler();
await using var provider = await BuildServiceProviderAsync(handler);
SeedIndex(handler);
SeedDetail(handler);
var connector = provider.GetRequiredService<AdobeConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var cursor = state!.Cursor;
var pendingDocuments = ExtractGuidList(cursor, "pendingDocuments");
Assert.Equal(2, pendingDocuments.Count);
// Re-seed responses to simulate unchanged fetch
SeedIndex(handler);
SeedDetail(handler);
await connector.FetchAsync(provider, CancellationToken.None);
state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
cursor = state!.Cursor;
var afterPending = ExtractGuidList(cursor, "pendingDocuments");
Assert.Equal(pendingDocuments.OrderBy(static id => id), afterPending.OrderBy(static id => id));
var fetchCache = cursor.TryGetValue("fetchCache", out var fetchCacheValue) && fetchCacheValue is DocumentObject cacheDoc
? cacheDoc.Elements.Select(static e => e.Name).ToArray()
: Array.Empty<string>();
Assert.Contains("https://helpx.adobe.com/security/products/acrobat/apsb25-85.html", fetchCache);
Assert.Contains("https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html", fetchCache);
}
[Fact]
public async Task Parse_ProducesDtoAndClearsPendingDocuments()
{
var handler = new CannedHttpMessageHandler();
await using var provider = await BuildServiceProviderAsync(handler);
SeedIndex(handler);
SeedDetail(handler);
var connector = provider.GetRequiredService<AdobeConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
var documentStore = provider.GetRequiredService<IDocumentStore>();
var dtoStore = provider.GetRequiredService<IDtoStore>();
var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
var psirtStore = provider.GetRequiredService<IPsirtFlagStore>();
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var document = await documentStore.FindBySourceAndUriAsync(
VndrAdobeConnectorPlugin.SourceName,
"https://helpx.adobe.com/security/products/acrobat/apsb25-85.html",
CancellationToken.None);
Assert.NotNull(document);
var dtoRecord = await dtoStore.FindByDocumentIdAsync(document!.Id, CancellationToken.None);
Assert.NotNull(dtoRecord);
Assert.Equal("adobe.bulletin.v1", dtoRecord!.SchemaVersion);
var payload = dtoRecord.Payload;
Assert.Equal("APSB25-85", payload.GetValue("advisoryId").AsString);
Assert.Equal("https://helpx.adobe.com/security/products/acrobat/apsb25-85.html", payload.GetValue("detailUrl").AsString);
var products = payload.GetValue("products").AsDocumentArray
.Select(static value => value.AsDocumentObject)
.ToArray();
Assert.NotEmpty(products);
var acrobatWindowsProduct = Assert.Single(
products,
static doc => string.Equals(doc.GetValue("product").AsString, "Acrobat DC", StringComparison.Ordinal)
&& string.Equals(doc.GetValue("platform").AsString, "Windows", StringComparison.Ordinal));
Assert.Equal("25.001.20672 and earlier", acrobatWindowsProduct.GetValue("affectedVersion").AsString);
Assert.Equal("25.001.20680", acrobatWindowsProduct.GetValue("updatedVersion").AsString);
var acrobatMacProduct = Assert.Single(
products,
static doc => string.Equals(doc.GetValue("product").AsString, "Acrobat DC", StringComparison.Ordinal)
&& string.Equals(doc.GetValue("platform").AsString, "macOS", StringComparison.Ordinal));
Assert.Equal("25.001.20668 and earlier", acrobatMacProduct.GetValue("affectedVersion").AsString);
Assert.Equal("25.001.20678", acrobatMacProduct.GetValue("updatedVersion").AsString);
var state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
var cursor = state!.Cursor;
Assert.True(!cursor.TryGetValue("pendingDocuments", out _)
|| cursor.GetValue("pendingDocuments").AsDocumentArray.Count == 0);
Assert.True(!cursor.TryGetValue("pendingMappings", out _)
|| cursor.GetValue("pendingMappings").AsDocumentArray.Count == 0);
var advisories = await advisoryStore.GetRecentAsync(5, CancellationToken.None);
Assert.Equal(2, advisories.Count);
var acrobatAdvisory = advisories.Single(a => a.AdvisoryKey == "APSB25-85");
Assert.Contains("APSB25-85", acrobatAdvisory.Aliases);
Assert.Equal(
acrobatAdvisory.References.Select(static r => r.Url).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
acrobatAdvisory.References.Length);
var acrobatWindowsPackage = Assert.Single(
acrobatAdvisory.AffectedPackages,
pkg => string.Equals(pkg.Identifier, "Acrobat DC", StringComparison.Ordinal)
&& string.Equals(pkg.Platform, "Windows", StringComparison.Ordinal));
var acrobatWindowsRange = Assert.Single(acrobatWindowsPackage.VersionRanges);
Assert.Equal("vendor", acrobatWindowsRange.RangeKind);
Assert.Equal("25.001.20680", acrobatWindowsRange.FixedVersion);
Assert.Equal("25.001.20672", acrobatWindowsRange.LastAffectedVersion);
Assert.NotNull(acrobatWindowsRange.Primitives);
var windowsExtensions = acrobatWindowsRange.Primitives!.VendorExtensions;
Assert.NotNull(windowsExtensions);
Assert.True(windowsExtensions!.TryGetValue("adobe.affected.raw", out var rawAffectedWin));
Assert.Equal("25.001.20672 and earlier", rawAffectedWin);
Assert.True(windowsExtensions.TryGetValue("adobe.updated.raw", out var rawUpdatedWin));
Assert.Equal("25.001.20680", rawUpdatedWin);
Assert.Contains(
AffectedPackageStatusCatalog.Fixed,
acrobatWindowsPackage.Statuses.Select(static status => status.Status));
var windowsNormalized = Assert.Single(acrobatWindowsPackage.NormalizedVersions.ToArray());
Assert.Equal(NormalizedVersionSchemes.SemVer, windowsNormalized.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.LessThan, windowsNormalized.Type);
Assert.Equal("25.1.20680", windowsNormalized.Max);
Assert.False(windowsNormalized.MaxInclusive);
Assert.Equal("adobe:Acrobat DC:Windows", windowsNormalized.Notes);
var acrobatMacPackage = Assert.Single(
acrobatAdvisory.AffectedPackages,
pkg => string.Equals(pkg.Identifier, "Acrobat DC", StringComparison.Ordinal)
&& string.Equals(pkg.Platform, "macOS", StringComparison.Ordinal));
var acrobatMacRange = Assert.Single(acrobatMacPackage.VersionRanges);
Assert.Equal("vendor", acrobatMacRange.RangeKind);
Assert.Equal("25.001.20678", acrobatMacRange.FixedVersion);
Assert.Equal("25.001.20668", acrobatMacRange.LastAffectedVersion);
Assert.NotNull(acrobatMacRange.Primitives);
var macExtensions = acrobatMacRange.Primitives!.VendorExtensions;
Assert.NotNull(macExtensions);
Assert.True(macExtensions!.TryGetValue("adobe.affected.raw", out var rawAffectedMac));
Assert.Equal("25.001.20668 and earlier", rawAffectedMac);
Assert.True(macExtensions.TryGetValue("adobe.updated.raw", out var rawUpdatedMac));
Assert.Equal("25.001.20678", rawUpdatedMac);
Assert.Contains(
AffectedPackageStatusCatalog.Fixed,
acrobatMacPackage.Statuses.Select(static status => status.Status));
var macNormalized = Assert.Single(acrobatMacPackage.NormalizedVersions.ToArray());
Assert.Equal(NormalizedVersionSchemes.SemVer, macNormalized.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.LessThan, macNormalized.Type);
Assert.Equal("25.1.20678", macNormalized.Max);
Assert.False(macNormalized.MaxInclusive);
Assert.Equal("adobe:Acrobat DC:macOS", macNormalized.Notes);
var premiereAdvisory = advisories.Single(a => a.AdvisoryKey == "APSB25-87");
Assert.Contains("APSB25-87", premiereAdvisory.Aliases);
Assert.Equal(
premiereAdvisory.References.Select(static r => r.Url).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
premiereAdvisory.References.Length);
var premiereWindowsPackage = Assert.Single(
premiereAdvisory.AffectedPackages,
pkg => string.Equals(pkg.Identifier, "Premiere Pro", StringComparison.Ordinal)
&& string.Equals(pkg.Platform, "Windows", StringComparison.Ordinal));
var premiereWindowsRange = Assert.Single(premiereWindowsPackage.VersionRanges);
Assert.Equal("24.6", premiereWindowsRange.FixedVersion);
Assert.Equal("24.5", premiereWindowsRange.LastAffectedVersion);
Assert.NotNull(premiereWindowsRange.Primitives);
var premiereWindowsExtensions = premiereWindowsRange.Primitives!.VendorExtensions;
Assert.NotNull(premiereWindowsExtensions);
Assert.True(premiereWindowsExtensions!.TryGetValue("adobe.priority", out var premierePriorityWin));
Assert.Equal("Priority 3", premierePriorityWin);
Assert.Contains(
AffectedPackageStatusCatalog.Fixed,
premiereWindowsPackage.Statuses.Select(static status => status.Status));
var premiereWinNormalized = Assert.Single(premiereWindowsPackage.NormalizedVersions.ToArray());
Assert.Equal(NormalizedVersionSchemes.SemVer, premiereWinNormalized.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.LessThan, premiereWinNormalized.Type);
Assert.Equal("24.6", premiereWinNormalized.Max);
Assert.False(premiereWinNormalized.MaxInclusive);
Assert.Equal("adobe:Premiere Pro:Windows", premiereWinNormalized.Notes);
var premiereMacPackage = Assert.Single(
premiereAdvisory.AffectedPackages,
pkg => string.Equals(pkg.Identifier, "Premiere Pro", StringComparison.Ordinal)
&& string.Equals(pkg.Platform, "macOS", StringComparison.Ordinal));
var premiereMacRange = Assert.Single(premiereMacPackage.VersionRanges);
Assert.Equal("24.6", premiereMacRange.FixedVersion);
Assert.Equal("24.5", premiereMacRange.LastAffectedVersion);
Assert.NotNull(premiereMacRange.Primitives);
var premiereMacExtensions = premiereMacRange.Primitives!.VendorExtensions;
Assert.NotNull(premiereMacExtensions);
Assert.True(premiereMacExtensions!.TryGetValue("adobe.priority", out var premierePriorityMac));
Assert.Equal("Priority 3", premierePriorityMac);
Assert.Contains(
AffectedPackageStatusCatalog.Fixed,
premiereMacPackage.Statuses.Select(static status => status.Status));
var premiereMacNormalized = Assert.Single(premiereMacPackage.NormalizedVersions.ToArray());
Assert.Equal(NormalizedVersionSchemes.SemVer, premiereMacNormalized.Scheme);
Assert.Equal(NormalizedVersionRuleTypes.LessThan, premiereMacNormalized.Type);
Assert.Equal("24.6", premiereMacNormalized.Max);
Assert.False(premiereMacNormalized.MaxInclusive);
Assert.Equal("adobe:Premiere Pro:macOS", premiereMacNormalized.Notes);
var ordered = advisories.OrderBy(static a => a.AdvisoryKey, StringComparer.Ordinal).ToArray();
var snapshot = SnapshotSerializer.ToSnapshot(ordered);
var expected = ReadFixture("adobe-advisories.snapshot.json");
var normalizedSnapshot = NormalizeLineEndings(snapshot);
var normalizedExpected = NormalizeLineEndings(expected);
if (!string.Equals(normalizedExpected, normalizedSnapshot, StringComparison.Ordinal))
{
var actualPath = Path.Combine(AppContext.BaseDirectory, "Source", "Vndr", "Adobe", "Fixtures", "adobe-advisories.actual.json");
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
File.WriteAllText(actualPath, snapshot);
}
Assert.Equal(normalizedExpected, normalizedSnapshot);
var psirtStore = provider.GetRequiredService<IPsirtFlagStore>();
var flagRecord = await psirtStore.FindAsync("APSB25-87", CancellationToken.None);
Assert.NotNull(flagRecord);
Assert.Equal("Adobe", flagRecord!.Vendor);
}
[Fact]
public async Task Fetch_WithNotModifiedResponses_KeepsDocumentsMapped()
{
var handler = new CannedHttpMessageHandler();
await using var provider = await BuildServiceProviderAsync(handler);
SeedIndex(handler);
SeedDetail(handler);
var connector = provider.GetRequiredService<AdobeConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
var documentStore = provider.GetRequiredService<IDocumentStore>();
var acrobatDoc = await documentStore.FindBySourceAndUriAsync(
VndrAdobeConnectorPlugin.SourceName,
"https://helpx.adobe.com/security/products/acrobat/apsb25-85.html",
CancellationToken.None);
Assert.NotNull(acrobatDoc);
Assert.Equal(DocumentStatuses.Mapped, acrobatDoc!.Status);
var premiereDoc = await documentStore.FindBySourceAndUriAsync(
VndrAdobeConnectorPlugin.SourceName,
"https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html",
CancellationToken.None);
Assert.NotNull(premiereDoc);
Assert.Equal(DocumentStatuses.Mapped, premiereDoc!.Status);
SeedIndex(handler);
SeedDetailNotModified(handler);
await connector.FetchAsync(provider, CancellationToken.None);
acrobatDoc = await documentStore.FindBySourceAndUriAsync(
VndrAdobeConnectorPlugin.SourceName,
"https://helpx.adobe.com/security/products/acrobat/apsb25-85.html",
CancellationToken.None);
Assert.NotNull(acrobatDoc);
Assert.Equal(DocumentStatuses.Mapped, acrobatDoc!.Status);
premiereDoc = await documentStore.FindBySourceAndUriAsync(
VndrAdobeConnectorPlugin.SourceName,
"https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html",
CancellationToken.None);
Assert.NotNull(premiereDoc);
Assert.Equal(DocumentStatuses.Mapped, premiereDoc!.Status);
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0);
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMap) && pendingMap.AsBsonArray.Count == 0);
}
}
[Fact]
public async Task Fetch_WithNotModifiedResponses_KeepsDocumentsMapped()
{
var handler = new CannedHttpMessageHandler();
await using var provider = await BuildServiceProviderAsync(handler);
SeedIndex(handler);
SeedDetail(handler);
var connector = provider.GetRequiredService<AdobeConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
await connector.ParseAsync(provider, CancellationToken.None);
await connector.MapAsync(provider, CancellationToken.None);
var documentStore = provider.GetRequiredService<IDocumentStore>();
var acrobatDoc = await documentStore.FindBySourceAndUriAsync(
VndrAdobeConnectorPlugin.SourceName,
"https://helpx.adobe.com/security/products/acrobat/apsb25-85.html",
CancellationToken.None);
Assert.NotNull(acrobatDoc);
Assert.Equal(DocumentStatuses.Mapped, acrobatDoc!.Status);
var premiereDoc = await documentStore.FindBySourceAndUriAsync(
VndrAdobeConnectorPlugin.SourceName,
"https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html",
CancellationToken.None);
Assert.NotNull(premiereDoc);
Assert.Equal(DocumentStatuses.Mapped, premiereDoc!.Status);
SeedIndex(handler);
SeedDetailNotModified(handler);
await connector.FetchAsync(provider, CancellationToken.None);
acrobatDoc = await documentStore.FindBySourceAndUriAsync(
VndrAdobeConnectorPlugin.SourceName,
"https://helpx.adobe.com/security/products/acrobat/apsb25-85.html",
CancellationToken.None);
Assert.NotNull(acrobatDoc);
Assert.Equal(DocumentStatuses.Mapped, acrobatDoc!.Status);
premiereDoc = await documentStore.FindBySourceAndUriAsync(
VndrAdobeConnectorPlugin.SourceName,
"https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html",
CancellationToken.None);
Assert.NotNull(premiereDoc);
Assert.Equal(DocumentStatuses.Mapped, premiereDoc!.Status);
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(VndrAdobeConnectorPlugin.SourceName, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsDocumentArray.Count == 0);
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMap) && pendingMap.AsDocumentArray.Count == 0);
}
private async Task<ServiceProvider> BuildServiceProviderAsync(CannedHttpMessageHandler handler)
{
await _fixture.TruncateAllTablesAsync();
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
services.AddSingleton<TimeProvider>(_timeProvider);
services.AddSingleton(handler);
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
services.AddSingleton<TimeProvider>(_timeProvider);
services.AddSingleton(handler);
services.AddConcelierPostgresStorage(options =>
{
options.ConnectionString = _fixture.ConnectionString;
options.SchemaName = _fixture.SchemaName;
options.CommandTimeoutSeconds = 5;
});
services.AddSourceCommon();
services.AddAdobeConnector(opts =>
{
opts.IndexUri = new Uri("https://helpx.adobe.com/security/security-bulletin.html");
opts.InitialBackfill = TimeSpan.FromDays(30);
opts.WindowOverlap = TimeSpan.FromDays(2);
});
services.Configure<HttpClientFactoryOptions>(AdobeOptions.HttpClientName, builderOptions =>
{
builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
{
builder.PrimaryHandler = handler;
});
});
services.AddSourceCommon();
services.AddAdobeConnector(opts =>
{
opts.IndexUri = new Uri("https://helpx.adobe.com/security/security-bulletin.html");
opts.InitialBackfill = TimeSpan.FromDays(30);
opts.WindowOverlap = TimeSpan.FromDays(2);
});
services.Configure<HttpClientFactoryOptions>(AdobeOptions.HttpClientName, builderOptions =>
{
builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
{
builder.PrimaryHandler = handler;
});
});
return services.BuildServiceProvider();
}
private static void SeedIndex(CannedHttpMessageHandler handler)
{
var indexUri = new Uri("https://helpx.adobe.com/security/security-bulletin.html");
var indexHtml = ReadFixture("adobe-index.html");
handler.AddTextResponse(indexUri, indexHtml, "text/html");
}
private static void SeedDetail(CannedHttpMessageHandler handler)
{
AddDetailResponse(
handler,
new Uri("https://helpx.adobe.com/security/products/acrobat/apsb25-85.html"),
"adobe-detail-apsb25-85.html",
"\"apsb25-85\"");
AddDetailResponse(
handler,
new Uri("https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html"),
"adobe-detail-apsb25-87.html",
"\"apsb25-87\"");
}
private static void SeedDetailNotModified(CannedHttpMessageHandler handler)
{
AddNotModifiedResponse(
handler,
new Uri("https://helpx.adobe.com/security/products/acrobat/apsb25-85.html"),
"\"apsb25-85\"");
AddNotModifiedResponse(
handler,
new Uri("https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html"),
"\"apsb25-87\"");
}
private static void AddDetailResponse(CannedHttpMessageHandler handler, Uri uri, string fixture, string? etag)
{
handler.AddResponse(uri, () =>
{
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(ReadFixture(fixture), Encoding.UTF8, "text/html"),
};
if (!string.IsNullOrEmpty(etag))
{
response.Headers.ETag = new EntityTagHeaderValue(etag);
}
return response;
});
}
private static void AddNotModifiedResponse(CannedHttpMessageHandler handler, Uri uri, string? etag)
{
handler.AddResponse(uri, () =>
{
var response = new HttpResponseMessage(HttpStatusCode.NotModified);
if (!string.IsNullOrEmpty(etag))
{
response.Headers.ETag = new EntityTagHeaderValue(etag);
}
return response;
});
}
private static List<Guid> ExtractGuidList(BsonDocument cursor, string field)
{
if (!cursor.TryGetValue(field, out var value) || value is not BsonArray array)
{
return new List<Guid>();
}
var list = new List<Guid>(array.Count);
foreach (var element in array)
{
if (Guid.TryParse(element.AsString, out var guid))
{
list.Add(guid);
}
}
return list;
}
private static string ReadFixture(string name)
{
var candidate = Path.Combine(AppContext.BaseDirectory, "Adobe", "Fixtures", name);
if (!File.Exists(candidate))
{
candidate = Path.Combine(AppContext.BaseDirectory, "Source", "Vndr", "Adobe", "Fixtures", name);
}
return File.ReadAllText(candidate);
}
private static string NormalizeLineEndings(string value)
=> value.Replace("\r\n", "\n", StringComparison.Ordinal);
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => Task.CompletedTask;
}
private static void SeedIndex(CannedHttpMessageHandler handler)
{
var indexUri = new Uri("https://helpx.adobe.com/security/security-bulletin.html");
var indexHtml = ReadFixture("adobe-index.html");
handler.AddTextResponse(indexUri, indexHtml, "text/html");
}
private static void SeedDetail(CannedHttpMessageHandler handler)
{
AddDetailResponse(
handler,
new Uri("https://helpx.adobe.com/security/products/acrobat/apsb25-85.html"),
"adobe-detail-apsb25-85.html",
"\"apsb25-85\"");
AddDetailResponse(
handler,
new Uri("https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html"),
"adobe-detail-apsb25-87.html",
"\"apsb25-87\"");
}
private static void SeedDetailNotModified(CannedHttpMessageHandler handler)
{
AddNotModifiedResponse(
handler,
new Uri("https://helpx.adobe.com/security/products/acrobat/apsb25-85.html"),
"\"apsb25-85\"");
AddNotModifiedResponse(
handler,
new Uri("https://helpx.adobe.com/security/products/premiere_pro/apsb25-87.html"),
"\"apsb25-87\"");
}
private static void AddDetailResponse(CannedHttpMessageHandler handler, Uri uri, string fixture, string? etag)
{
handler.AddResponse(uri, () =>
{
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(ReadFixture(fixture), Encoding.UTF8, "text/html"),
};
if (!string.IsNullOrEmpty(etag))
{
response.Headers.ETag = new EntityTagHeaderValue(etag);
}
return response;
});
}
private static void AddNotModifiedResponse(CannedHttpMessageHandler handler, Uri uri, string? etag)
{
handler.AddResponse(uri, () =>
{
var response = new HttpResponseMessage(HttpStatusCode.NotModified);
if (!string.IsNullOrEmpty(etag))
{
response.Headers.ETag = new EntityTagHeaderValue(etag);
}
return response;
});
}
private static List<Guid> ExtractGuidList(DocumentObject cursor, string field)
{
if (!cursor.TryGetValue(field, out var value) || value is not DocumentArray array)
{
return new List<Guid>();
}
var list = new List<Guid>(array.Count);
foreach (var element in array)
{
if (Guid.TryParse(element.AsString, out var guid))
{
list.Add(guid);
}
}
return list;
}
private static string ReadFixture(string name)
{
var candidate = Path.Combine(AppContext.BaseDirectory, "Adobe", "Fixtures", name);
if (!File.Exists(candidate))
{
candidate = Path.Combine(AppContext.BaseDirectory, "Source", "Vndr", "Adobe", "Fixtures", name);
}
return File.ReadAllText(candidate);
}
private static string NormalizeLineEndings(string value)
=> value.Replace("\r\n", "\n", StringComparison.Ordinal);
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => Task.CompletedTask;
}