more audit work
This commit is contained in:
@@ -15,8 +15,8 @@ public sealed class CannedHttpMessageHandlerTests
|
||||
handler.SetFallback(_ => new HttpResponseMessage(HttpStatusCode.NotFound));
|
||||
|
||||
using var client = handler.CreateClient();
|
||||
var firstResponse = await client.GetAsync(requestUri);
|
||||
var secondResponse = await client.GetAsync(new Uri("https://example.test/other"));
|
||||
var firstResponse = await client.GetAsync(requestUri, TestContext.Current.CancellationToken);
|
||||
var secondResponse = await client.GetAsync(new Uri("https://example.test/other"), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, secondResponse.StatusCode);
|
||||
@@ -32,6 +32,6 @@ public sealed class CannedHttpMessageHandlerTests
|
||||
handler.AddException(HttpMethod.Get, requestUri, new InvalidOperationException("boom"));
|
||||
|
||||
using var client = handler.CreateClient();
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetAsync(requestUri));
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetAsync(requestUri, TestContext.Current.CancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,29 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.InMemoryRunner;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.InMemoryDriver;
|
||||
using StellaOps.Aoc;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Core.Aoc;
|
||||
using StellaOps.Concelier.Core.Linksets;
|
||||
using StellaOps.Concelier.RawModels;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Cryptography;
|
||||
using LegacyContracts = StellaOps.Concelier.Storage;
|
||||
using StorageContracts = StellaOps.Concelier.Storage.Contracts;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Common.Tests;
|
||||
|
||||
public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
{
|
||||
private readonly InMemoryDbRunner _runner;
|
||||
private readonly IStorageDatabase _database;
|
||||
private readonly RawDocumentStorage _rawStorage;
|
||||
private readonly ICryptoHash _hash;
|
||||
|
||||
public SourceFetchServiceGuardTests()
|
||||
{
|
||||
_runner = InMemoryDbRunner.Start(singleNodeReplSet: true);
|
||||
var client = new InMemoryClient(_runner.ConnectionString);
|
||||
_database = client.GetDatabase($"source-fetch-guard-{Guid.NewGuid():N}");
|
||||
_rawStorage = new RawDocumentStorage();
|
||||
_hash = CryptoHashFactory.CreateDefault();
|
||||
}
|
||||
@@ -41,15 +35,15 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
var handler = new StaticHttpMessageHandler(() => CreateSuccessResponse(responsePayload));
|
||||
var client = new HttpClient(handler, disposeHandler: false);
|
||||
var httpClientFactory = new StaticHttpClientFactory(client);
|
||||
var documentStore = new RecordingDocumentStore();
|
||||
var documentStore = new RecordingStorageDocumentStore();
|
||||
var legacyStore = new NoopDocumentStore();
|
||||
var guard = new RecordingAdvisoryRawWriteGuard();
|
||||
var jitter = new NoJitterSource();
|
||||
|
||||
var httpOptions = new TestOptionsMonitor<StellaOps.Concelier.Connector.Common.Http.SourceHttpClientOptions>(new StellaOps.Concelier.Connector.Common.Http.SourceHttpClientOptions());
|
||||
var storageOptions = Options.Create(new StorageOptions
|
||||
var storageOptions = Options.Create(new LegacyContracts.StorageOptions
|
||||
{
|
||||
ConnectionString = _runner.ConnectionString,
|
||||
DatabaseName = _database.DatabaseNamespace.DatabaseName,
|
||||
DefaultTenant = "tenant-default",
|
||||
});
|
||||
|
||||
var linksetMapper = new NoopAdvisoryLinksetMapper();
|
||||
@@ -57,6 +51,7 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
var service = new SourceFetchService(
|
||||
httpClientFactory,
|
||||
_rawStorage,
|
||||
legacyStore,
|
||||
documentStore,
|
||||
NullLogger<SourceFetchService>.Instance,
|
||||
jitter,
|
||||
@@ -90,11 +85,11 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
Assert.True(documentStore.UpsertCount > 0);
|
||||
Assert.Equal("msrc", documentStore.LastRecord!.Metadata!["source.vendor"]);
|
||||
Assert.Equal("tenant-default", documentStore.LastRecord.Metadata!["tenant"]);
|
||||
Assert.NotNull(documentStore.LastRecord.PayloadId);
|
||||
Assert.NotNull(documentStore.LastRecord.Payload);
|
||||
|
||||
// verify raw payload stored
|
||||
var filesCollection = _database.GetCollection<DocumentObject>("documents.files");
|
||||
var count = await filesCollection.CountDocumentsAsync(FilterDefinition<DocumentObject>.Empty);
|
||||
Assert.Equal(1, count);
|
||||
var rawPayload = await _rawStorage.DownloadAsync(documentStore.LastRecord.PayloadId!.Value, CancellationToken.None);
|
||||
Assert.Equal(responsePayload, Encoding.UTF8.GetString(rawPayload));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -103,15 +98,15 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
var handler = new StaticHttpMessageHandler(() => CreateSuccessResponse("{\"id\":\"CVE-2025-2222\"}"));
|
||||
var client = new HttpClient(handler, disposeHandler: false);
|
||||
var httpClientFactory = new StaticHttpClientFactory(client);
|
||||
var documentStore = new RecordingDocumentStore();
|
||||
var documentStore = new RecordingStorageDocumentStore();
|
||||
var legacyStore = new NoopDocumentStore();
|
||||
var guard = new RecordingAdvisoryRawWriteGuard { ShouldThrow = true };
|
||||
var jitter = new NoJitterSource();
|
||||
|
||||
var httpOptions = new TestOptionsMonitor<StellaOps.Concelier.Connector.Common.Http.SourceHttpClientOptions>(new StellaOps.Concelier.Connector.Common.Http.SourceHttpClientOptions());
|
||||
var storageOptions = Options.Create(new StorageOptions
|
||||
var storageOptions = Options.Create(new LegacyContracts.StorageOptions
|
||||
{
|
||||
ConnectionString = _runner.ConnectionString,
|
||||
DatabaseName = _database.DatabaseNamespace.DatabaseName,
|
||||
DefaultTenant = "tenant-default",
|
||||
});
|
||||
|
||||
var linksetMapper = new NoopAdvisoryLinksetMapper();
|
||||
@@ -119,6 +114,7 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
var service = new SourceFetchService(
|
||||
httpClientFactory,
|
||||
_rawStorage,
|
||||
legacyStore,
|
||||
documentStore,
|
||||
NullLogger<SourceFetchService>.Instance,
|
||||
jitter,
|
||||
@@ -140,16 +136,14 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
await Assert.ThrowsAsync<ConcelierAocGuardException>(() => service.FetchAsync(request, CancellationToken.None));
|
||||
Assert.Equal(0, documentStore.UpsertCount);
|
||||
|
||||
var filesCollection = _database.GetCollection<DocumentObject>("documents.files");
|
||||
var count = await filesCollection.CountDocumentsAsync(FilterDefinition<DocumentObject>.Empty);
|
||||
Assert.Equal(0, count);
|
||||
var recordId = CreateDeterministicGuid($"{request.SourceName}:{request.RequestUri}");
|
||||
await Assert.ThrowsAsync<FileNotFoundException>(() => _rawStorage.DownloadAsync(recordId, CancellationToken.None));
|
||||
}
|
||||
|
||||
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
_runner.Dispose();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -184,24 +178,59 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
=> Task.FromResult(_responseFactory());
|
||||
}
|
||||
|
||||
private sealed class RecordingDocumentStore : IDocumentStore
|
||||
private sealed class RecordingStorageDocumentStore : StorageContracts.IStorageDocumentStore
|
||||
{
|
||||
public DocumentRecord? LastRecord { get; private set; }
|
||||
private readonly Dictionary<Guid, StorageContracts.StorageDocument> _byId = new();
|
||||
private readonly Dictionary<(string Source, string Uri), StorageContracts.StorageDocument> _bySourceUri = new();
|
||||
|
||||
public StorageContracts.StorageDocument? LastRecord { get; private set; }
|
||||
public int UpsertCount { get; private set; }
|
||||
|
||||
public Task<DocumentRecord> UpsertAsync(DocumentRecord record, CancellationToken cancellationToken)
|
||||
public Task<StorageContracts.StorageDocument?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
|
||||
{
|
||||
_bySourceUri.TryGetValue((sourceName, uri), out var record);
|
||||
return Task.FromResult<StorageContracts.StorageDocument?>(record);
|
||||
}
|
||||
|
||||
public Task<StorageContracts.StorageDocument?> FindAsync(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
_byId.TryGetValue(id, out var record);
|
||||
return Task.FromResult<StorageContracts.StorageDocument?>(record);
|
||||
}
|
||||
|
||||
public Task<StorageContracts.StorageDocument> UpsertAsync(StorageContracts.StorageDocument record, CancellationToken cancellationToken)
|
||||
{
|
||||
UpsertCount++;
|
||||
LastRecord = record;
|
||||
_byId[record.Id] = record;
|
||||
_bySourceUri[(record.SourceName, record.Uri)] = record;
|
||||
return Task.FromResult(record);
|
||||
}
|
||||
|
||||
public Task<DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<DocumentRecord?>(null);
|
||||
public Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_byId.TryGetValue(id, out var existing))
|
||||
{
|
||||
var updated = existing with { Status = status };
|
||||
_byId[id] = updated;
|
||||
_bySourceUri[(updated.SourceName, updated.Uri)] = updated;
|
||||
LastRecord = updated;
|
||||
}
|
||||
|
||||
public Task<DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<DocumentRecord?>(null);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NoopDocumentStore : LegacyContracts.IDocumentStore
|
||||
{
|
||||
public Task<LegacyContracts.DocumentRecord?> FindBySourceAndUriAsync(string sourceName, string uri, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<LegacyContracts.DocumentRecord?>(null);
|
||||
|
||||
public Task<LegacyContracts.DocumentRecord?> FindAsync(Guid id, CancellationToken cancellationToken)
|
||||
=> Task.FromResult<LegacyContracts.DocumentRecord?>(null);
|
||||
|
||||
public Task<LegacyContracts.DocumentRecord> UpsertAsync(LegacyContracts.DocumentRecord record, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(record);
|
||||
|
||||
public Task UpdateStatusAsync(Guid id, string status, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
@@ -254,6 +283,14 @@ public sealed class SourceFetchServiceGuardTests : IAsyncLifetime
|
||||
{
|
||||
public RawLinkset Map(AdvisoryRawDocument document) => new();
|
||||
}
|
||||
|
||||
private static Guid CreateDeterministicGuid(string value)
|
||||
{
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(value ?? string.Empty));
|
||||
bytes[6] = (byte)((bytes[6] & 0x0F) | 0x50);
|
||||
bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80);
|
||||
return new Guid(bytes.AsSpan(0, 16));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using StellaOps.Concelier.InMemoryRunner;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.InMemoryDriver;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
@@ -16,9 +13,6 @@ namespace StellaOps.Concelier.Connector.Common.Tests;
|
||||
|
||||
public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly InMemoryDbRunner _runner;
|
||||
private readonly InMemoryClient _client;
|
||||
private readonly IStorageDatabase _database;
|
||||
private readonly DocumentStore _documentStore;
|
||||
private readonly RawDocumentStorage _rawStorage;
|
||||
private readonly InMemorySourceStateRepository _stateRepository;
|
||||
@@ -27,10 +21,7 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
|
||||
|
||||
public SourceStateSeedProcessorTests()
|
||||
{
|
||||
_runner = InMemoryDbRunner.Start(singleNodeReplSet: true);
|
||||
_client = new InMemoryClient(_runner.ConnectionString);
|
||||
_database = _client.GetDatabase($"source-state-seed-{Guid.NewGuid():N}");
|
||||
_documentStore = new DocumentStore(_database, NullLogger<DocumentStore>.Instance);
|
||||
_documentStore = new DocumentStore();
|
||||
_rawStorage = new RawDocumentStorage();
|
||||
_stateRepository = new InMemorySourceStateRepository();
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 28, 12, 0, 0, TimeSpan.Zero));
|
||||
@@ -98,16 +89,16 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
|
||||
Assert.NotNull(storedDocument.Metadata);
|
||||
Assert.Equal("value", storedDocument.Metadata!["test.meta"]);
|
||||
|
||||
var filesCollection = _database.GetCollection<DocumentObject>("documents.files");
|
||||
var fileCount = await filesCollection.CountDocumentsAsync(FilterDefinition<DocumentObject>.Empty);
|
||||
Assert.Equal(1, fileCount);
|
||||
var payload = await _rawStorage.DownloadAsync(storedDocument.PayloadId!.Value, CancellationToken.None);
|
||||
Assert.Equal("{\"id\":\"ADV-1\"}", Encoding.UTF8.GetString(payload));
|
||||
|
||||
var state = await _stateRepository.TryGetAsync("vndr.test", CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
var stateValue = state!;
|
||||
Assert.Equal(_timeProvider.GetUtcNow().UtcDateTime, stateValue.LastSuccess);
|
||||
|
||||
var cursor = stateValue.Cursor;
|
||||
Assert.NotNull(stateValue.Cursor);
|
||||
var cursor = stateValue.Cursor!;
|
||||
var pendingDocs = cursor["pendingDocuments"].AsDocumentArray.Select(v => Guid.Parse(v.AsString)).ToList();
|
||||
Assert.Contains(documentId, pendingDocs);
|
||||
|
||||
@@ -156,9 +147,8 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
|
||||
var previousGridId = existingRecord!.PayloadId;
|
||||
Assert.NotNull(previousGridId);
|
||||
|
||||
var filesCollection = _database.GetCollection<DocumentObject>("documents.files");
|
||||
var initialFiles = await filesCollection.Find(FilterDefinition<DocumentObject>.Empty).ToListAsync();
|
||||
Assert.Single(initialFiles);
|
||||
var initialPayload = await _rawStorage.DownloadAsync(previousGridId!.Value, CancellationToken.None);
|
||||
Assert.Equal("{\"id\":\"ADV-2\",\"rev\":1}", Encoding.UTF8.GetString(initialPayload));
|
||||
|
||||
var updatedSpecification = new SourceStateSeedSpecification
|
||||
{
|
||||
@@ -190,11 +180,9 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
|
||||
Assert.NotNull(refreshedRecord);
|
||||
Assert.Equal(documentId, refreshedRecord!.Id);
|
||||
Assert.NotNull(refreshedRecord.PayloadId);
|
||||
Assert.NotEqual(previousGridId?.ToString(), refreshedRecord.PayloadId?.ToString());
|
||||
|
||||
var files = await filesCollection.Find(FilterDefinition<DocumentObject>.Empty).ToListAsync();
|
||||
Assert.Single(files);
|
||||
Assert.NotEqual(previousGridId?.ToString(), files[0]["_id"].AsObjectId.ToString());
|
||||
var updatedPayload = await _rawStorage.DownloadAsync(refreshedRecord.PayloadId!.Value, CancellationToken.None);
|
||||
Assert.Equal("{\"id\":\"ADV-2\",\"rev\":2}", Encoding.UTF8.GetString(updatedPayload));
|
||||
}
|
||||
|
||||
private SourceStateSeedProcessor CreateProcessor()
|
||||
@@ -210,8 +198,7 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _client.DropDatabaseAsync(_database.DatabaseNamespace.DatabaseName);
|
||||
_runner.Dispose();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,3 +8,5 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| AUDIT-0160-M | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
|
||||
| AUDIT-0160-T | DONE | Revalidated 2026-01-06; findings recorded in audit report. |
|
||||
| AUDIT-0160-A | DONE | Waived (test project; revalidated 2026-01-06). |
|
||||
| AUDIT-0374-T | DONE | Revalidated 2026-01-08 (storage store + raw payload checks). |
|
||||
| AUDIT-0374-A | DONE | Revalidated 2026-01-08 (storage store + raw payload checks). |
|
||||
|
||||
Reference in New Issue
Block a user