feat: Add tests for RichGraphPublisher and RichGraphWriter
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
- Implement unit tests for RichGraphPublisher to verify graph publishing to CAS. - Implement unit tests for RichGraphWriter to ensure correct writing of canonical graphs and metadata. feat: Implement AOC Guard validation logic - Add AOC Guard validation logic to enforce document structure and field constraints. - Introduce violation codes for various validation errors. - Implement tests for AOC Guard to validate expected behavior. feat: Create Console Status API client and service - Implement ConsoleStatusClient for fetching console status and streaming run events. - Create ConsoleStatusService to manage console status polling and event subscriptions. - Add tests for ConsoleStatusClient to verify API interactions. feat: Develop Console Status component - Create ConsoleStatusComponent for displaying console status and run events. - Implement UI for showing status metrics and handling user interactions. - Add styles for console status display. test: Add tests for Console Status store - Implement tests for ConsoleStatusStore to verify event handling and state management.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# StellaOps.Authority.Storage.Postgres — Agent Charter
|
||||
|
||||
## Mission
|
||||
Deliver PostgreSQL-backed persistence for Authority (tenants, users, roles, permissions, tokens, refresh tokens, API keys, sessions, audit) per `docs/db/SPECIFICATION.md` §5.1 and enable the Mongo → Postgres dual-write/backfill cutover with deterministic behaviour.
|
||||
|
||||
## Working Directory
|
||||
- `src/Authority/__Libraries/StellaOps.Authority.Storage.Postgres`
|
||||
|
||||
## Required Reading
|
||||
- docs/modules/authority/architecture.md
|
||||
- docs/db/README.md
|
||||
- docs/db/SPECIFICATION.md (Authority schema §5.1; shared rules)
|
||||
- docs/db/RULES.md
|
||||
- docs/db/VERIFICATION.md
|
||||
- docs/db/tasks/PHASE_1_AUTHORITY.md
|
||||
- src/Authority/StellaOps.Authority/AGENTS.md (host integration expectations)
|
||||
|
||||
## Working Agreement
|
||||
- Update related sprint rows in `docs/implplan/SPRINT_*.md` when work starts/finishes; keep statuses `TODO → DOING → DONE/BLOCKED`.
|
||||
- Keep migrations idempotent and deterministic (stable ordering, UTC timestamps). Use curated NuGet cache at `local-nugets/`; no external feeds.
|
||||
- Align schema and repository contracts to `docs/db/SPECIFICATION.md`; mirror any contract/schema change into that spec and the sprint’s Decisions & Risks.
|
||||
- Tests live in `src/Authority/__Tests/StellaOps.Authority.Storage.Postgres.Tests`; maintain deterministic Testcontainers config (fixed image tag, seeded data) and cover all repositories plus determinism of token/refresh generation.
|
||||
- Use `StellaOps.Cryptography` abstractions for password/hash handling; never log secrets or hashes. Ensure transaction boundaries and retries follow `docs/db/RULES.md`.
|
||||
- Coordinate with Authority host service (`StellaOps.Authority`) before altering DI registrations or shared models; avoid cross-module edits unless the sprint explicitly allows and logs them.
|
||||
@@ -45,7 +45,7 @@ public sealed class TokenRepository : RepositoryBase<AuthorityDataSource>, IToke
|
||||
SELECT id, tenant_id, user_id, token_hash, token_type, scopes, client_id, issued_at, expires_at, revoked_at, revoked_by, metadata
|
||||
FROM authority.tokens
|
||||
WHERE tenant_id = @tenant_id AND user_id = @user_id AND revoked_at IS NULL
|
||||
ORDER BY issued_at DESC
|
||||
ORDER BY issued_at DESC, id ASC
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "user_id", userId); },
|
||||
@@ -168,7 +168,7 @@ public sealed class RefreshTokenRepository : RepositoryBase<AuthorityDataSource>
|
||||
SELECT id, tenant_id, user_id, token_hash, access_token_id, client_id, issued_at, expires_at, revoked_at, revoked_by, replaced_by, metadata
|
||||
FROM authority.refresh_tokens
|
||||
WHERE tenant_id = @tenant_id AND user_id = @user_id AND revoked_at IS NULL
|
||||
ORDER BY issued_at DESC
|
||||
ORDER BY issued_at DESC, id ASC
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "user_id", userId); },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -136,6 +137,70 @@ public sealed class RefreshTokenRepositoryTests : IAsyncLifetime
|
||||
tokens.Should().AllSatisfy(t => t.RevokedAt.Should().NotBeNull());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByUserId_IsDeterministic_WhenIssuedAtTies()
|
||||
{
|
||||
// Arrange: fixed IDs with same IssuedAt to assert stable ordering
|
||||
var userId = Guid.NewGuid();
|
||||
var issuedAt = new DateTimeOffset(2025, 11, 30, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
var tokens = new[]
|
||||
{
|
||||
new RefreshTokenEntity
|
||||
{
|
||||
Id = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
||||
TenantId = _tenantId,
|
||||
UserId = userId,
|
||||
TokenHash = "rhash1-" + Guid.NewGuid().ToString("N"),
|
||||
AccessTokenId = Guid.Parse("10000000-0000-0000-0000-000000000000"),
|
||||
ClientId = "web-app",
|
||||
IssuedAt = issuedAt,
|
||||
ExpiresAt = issuedAt.AddDays(30)
|
||||
},
|
||||
new RefreshTokenEntity
|
||||
{
|
||||
Id = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
||||
TenantId = _tenantId,
|
||||
UserId = userId,
|
||||
TokenHash = "rhash2-" + Guid.NewGuid().ToString("N"),
|
||||
AccessTokenId = Guid.Parse("20000000-0000-0000-0000-000000000000"),
|
||||
ClientId = "web-app",
|
||||
IssuedAt = issuedAt,
|
||||
ExpiresAt = issuedAt.AddDays(30)
|
||||
},
|
||||
new RefreshTokenEntity
|
||||
{
|
||||
Id = Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"),
|
||||
TenantId = _tenantId,
|
||||
UserId = userId,
|
||||
TokenHash = "rhash3-" + Guid.NewGuid().ToString("N"),
|
||||
AccessTokenId = Guid.Parse("30000000-0000-0000-0000-000000000000"),
|
||||
ClientId = "web-app",
|
||||
IssuedAt = issuedAt,
|
||||
ExpiresAt = issuedAt.AddDays(30)
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var token in tokens.Reverse())
|
||||
{
|
||||
await _repository.CreateAsync(_tenantId, token);
|
||||
}
|
||||
|
||||
// Act
|
||||
var first = await _repository.GetByUserIdAsync(_tenantId, userId);
|
||||
var second = await _repository.GetByUserIdAsync(_tenantId, userId);
|
||||
|
||||
var expectedOrder = tokens
|
||||
.OrderByDescending(t => t.IssuedAt)
|
||||
.ThenBy(t => t.Id)
|
||||
.Select(t => t.Id)
|
||||
.ToArray();
|
||||
|
||||
// Assert
|
||||
first.Select(t => t.Id).Should().ContainInOrder(expectedOrder);
|
||||
second.Should().BeEquivalentTo(first, o => o.WithStrictOrdering());
|
||||
}
|
||||
|
||||
private RefreshTokenEntity CreateRefreshToken(Guid userId) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -121,6 +122,71 @@ public sealed class TokenRepositoryTests : IAsyncLifetime
|
||||
tokens.Should().AllSatisfy(t => t.RevokedAt.Should().NotBeNull());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByUserId_IsDeterministic_WhenIssuedAtTies()
|
||||
{
|
||||
// Arrange: same IssuedAt, fixed IDs to validate ordering
|
||||
var userId = Guid.NewGuid();
|
||||
var issuedAt = new DateTimeOffset(2025, 11, 30, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
var tokens = new[]
|
||||
{
|
||||
new TokenEntity
|
||||
{
|
||||
Id = Guid.Parse("11111111-1111-1111-1111-111111111111"),
|
||||
TenantId = _tenantId,
|
||||
UserId = userId,
|
||||
TokenHash = "hash1-" + Guid.NewGuid().ToString("N"),
|
||||
TokenType = TokenType.Access,
|
||||
Scopes = ["a"],
|
||||
IssuedAt = issuedAt,
|
||||
ExpiresAt = issuedAt.AddHours(1)
|
||||
},
|
||||
new TokenEntity
|
||||
{
|
||||
Id = Guid.Parse("22222222-2222-2222-2222-222222222222"),
|
||||
TenantId = _tenantId,
|
||||
UserId = userId,
|
||||
TokenHash = "hash2-" + Guid.NewGuid().ToString("N"),
|
||||
TokenType = TokenType.Access,
|
||||
Scopes = ["a"],
|
||||
IssuedAt = issuedAt,
|
||||
ExpiresAt = issuedAt.AddHours(1)
|
||||
},
|
||||
new TokenEntity
|
||||
{
|
||||
Id = Guid.Parse("33333333-3333-3333-3333-333333333333"),
|
||||
TenantId = _tenantId,
|
||||
UserId = userId,
|
||||
TokenHash = "hash3-" + Guid.NewGuid().ToString("N"),
|
||||
TokenType = TokenType.Access,
|
||||
Scopes = ["a"],
|
||||
IssuedAt = issuedAt,
|
||||
ExpiresAt = issuedAt.AddHours(1)
|
||||
}
|
||||
};
|
||||
|
||||
// Insert out of order to ensure repository enforces deterministic ordering
|
||||
foreach (var token in tokens.Reverse())
|
||||
{
|
||||
await _repository.CreateAsync(_tenantId, token);
|
||||
}
|
||||
|
||||
// Act
|
||||
var first = await _repository.GetByUserIdAsync(_tenantId, userId);
|
||||
var second = await _repository.GetByUserIdAsync(_tenantId, userId);
|
||||
|
||||
var expectedOrder = tokens
|
||||
.OrderByDescending(t => t.IssuedAt)
|
||||
.ThenBy(t => t.Id)
|
||||
.Select(t => t.Id)
|
||||
.ToArray();
|
||||
|
||||
// Assert
|
||||
first.Select(t => t.Id).Should().ContainInOrder(expectedOrder);
|
||||
second.Should().BeEquivalentTo(first, o => o.WithStrictOrdering());
|
||||
}
|
||||
|
||||
private TokenEntity CreateToken(Guid userId) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
|
||||
Reference in New Issue
Block a user