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

This commit is contained in:
StellaOps Bot
2025-11-29 11:08:08 +02:00
parent 7e7be4d2fd
commit 3488b22c0c
102 changed files with 18487 additions and 969 deletions

View File

@@ -0,0 +1,167 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Authority.Storage.Postgres.Models;
using StellaOps.Authority.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Authority.Storage.Postgres.Tests;
[Collection(AuthorityPostgresCollection.Name)]
public sealed class ApiKeyRepositoryTests : IAsyncLifetime
{
private readonly AuthorityPostgresFixture _fixture;
private readonly ApiKeyRepository _repository;
private readonly string _tenantId = Guid.NewGuid().ToString();
public ApiKeyRepositoryTests(AuthorityPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
var dataSource = new AuthorityDataSource(Options.Create(options), NullLogger<AuthorityDataSource>.Instance);
_repository = new ApiKeyRepository(dataSource, NullLogger<ApiKeyRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task CreateAndGetByPrefix_RoundTripsApiKey()
{
// Arrange
var keyPrefix = "sk_live_" + Guid.NewGuid().ToString("N")[..8];
var apiKey = new ApiKeyEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
UserId = Guid.NewGuid(),
Name = "CI/CD Key",
KeyHash = "sha256_key_" + Guid.NewGuid().ToString("N"),
KeyPrefix = keyPrefix,
Scopes = ["scan:read", "scan:write"],
Status = ApiKeyStatus.Active,
ExpiresAt = DateTimeOffset.UtcNow.AddYears(1)
};
// Act
await _repository.CreateAsync(_tenantId, apiKey);
var fetched = await _repository.GetByPrefixAsync(keyPrefix);
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(apiKey.Id);
fetched.Name.Should().Be("CI/CD Key");
fetched.Scopes.Should().BeEquivalentTo(["scan:read", "scan:write"]);
}
[Fact]
public async Task GetById_ReturnsApiKey()
{
// Arrange
var apiKey = CreateApiKey(Guid.NewGuid(), "Test Key");
await _repository.CreateAsync(_tenantId, apiKey);
// Act
var fetched = await _repository.GetByIdAsync(_tenantId, apiKey.Id);
// Assert
fetched.Should().NotBeNull();
fetched!.Name.Should().Be("Test Key");
}
[Fact]
public async Task GetByUserId_ReturnsUserApiKeys()
{
// Arrange
var userId = Guid.NewGuid();
var key1 = CreateApiKey(userId, "Key 1");
var key2 = CreateApiKey(userId, "Key 2");
await _repository.CreateAsync(_tenantId, key1);
await _repository.CreateAsync(_tenantId, key2);
// Act
var keys = await _repository.GetByUserIdAsync(_tenantId, userId);
// Assert
keys.Should().HaveCount(2);
}
[Fact]
public async Task List_ReturnsAllKeysForTenant()
{
// Arrange
var key1 = CreateApiKey(Guid.NewGuid(), "Key A");
var key2 = CreateApiKey(Guid.NewGuid(), "Key B");
await _repository.CreateAsync(_tenantId, key1);
await _repository.CreateAsync(_tenantId, key2);
// Act
var keys = await _repository.ListAsync(_tenantId);
// Assert
keys.Should().HaveCount(2);
}
[Fact]
public async Task Revoke_UpdatesStatusAndRevokedFields()
{
// Arrange
var apiKey = CreateApiKey(Guid.NewGuid(), "ToRevoke");
await _repository.CreateAsync(_tenantId, apiKey);
// Act
await _repository.RevokeAsync(_tenantId, apiKey.Id, "security@test.com");
var fetched = await _repository.GetByIdAsync(_tenantId, apiKey.Id);
// Assert
fetched!.Status.Should().Be(ApiKeyStatus.Revoked);
fetched.RevokedAt.Should().NotBeNull();
fetched.RevokedBy.Should().Be("security@test.com");
}
[Fact]
public async Task UpdateLastUsed_SetsLastUsedAt()
{
// Arrange
var apiKey = CreateApiKey(Guid.NewGuid(), "Usage Test");
await _repository.CreateAsync(_tenantId, apiKey);
// Act
await _repository.UpdateLastUsedAsync(_tenantId, apiKey.Id);
var fetched = await _repository.GetByIdAsync(_tenantId, apiKey.Id);
// Assert
fetched!.LastUsedAt.Should().NotBeNull();
fetched.LastUsedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
}
[Fact]
public async Task Delete_RemovesApiKey()
{
// Arrange
var apiKey = CreateApiKey(Guid.NewGuid(), "ToDelete");
await _repository.CreateAsync(_tenantId, apiKey);
// Act
await _repository.DeleteAsync(_tenantId, apiKey.Id);
var fetched = await _repository.GetByIdAsync(_tenantId, apiKey.Id);
// Assert
fetched.Should().BeNull();
}
private ApiKeyEntity CreateApiKey(Guid userId, string name) => new()
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
UserId = userId,
Name = name,
KeyHash = $"sha256_{Guid.NewGuid():N}",
KeyPrefix = $"sk_test_{Guid.NewGuid():N}"[..16],
Scopes = ["read"],
Status = ApiKeyStatus.Active,
ExpiresAt = DateTimeOffset.UtcNow.AddYears(1)
};
}

View File

@@ -0,0 +1,192 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Authority.Storage.Postgres.Models;
using StellaOps.Authority.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Authority.Storage.Postgres.Tests;
[Collection(AuthorityPostgresCollection.Name)]
public sealed class AuditRepositoryTests : IAsyncLifetime
{
private readonly AuthorityPostgresFixture _fixture;
private readonly AuditRepository _repository;
private readonly string _tenantId = Guid.NewGuid().ToString();
public AuditRepositoryTests(AuthorityPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
var dataSource = new AuthorityDataSource(Options.Create(options), NullLogger<AuthorityDataSource>.Instance);
_repository = new AuditRepository(dataSource, NullLogger<AuditRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task Create_ReturnsGeneratedId()
{
// Arrange
var audit = new AuditEntity
{
TenantId = _tenantId,
UserId = Guid.NewGuid(),
Action = "user.login",
ResourceType = "user",
ResourceId = Guid.NewGuid().ToString(),
IpAddress = "192.168.1.1",
UserAgent = "Mozilla/5.0",
CorrelationId = Guid.NewGuid().ToString()
};
// Act
var id = await _repository.CreateAsync(_tenantId, audit);
// Assert
id.Should().BeGreaterThan(0);
}
[Fact]
public async Task List_ReturnsAuditEntriesOrderedByCreatedAtDesc()
{
// Arrange
var audit1 = CreateAudit("action1");
var audit2 = CreateAudit("action2");
await _repository.CreateAsync(_tenantId, audit1);
await Task.Delay(10); // Ensure different timestamps
await _repository.CreateAsync(_tenantId, audit2);
// Act
var audits = await _repository.ListAsync(_tenantId, limit: 10);
// Assert
audits.Should().HaveCount(2);
audits[0].Action.Should().Be("action2"); // Most recent first
}
[Fact]
public async Task GetByUserId_ReturnsUserAudits()
{
// Arrange
var userId = Guid.NewGuid();
var audit = new AuditEntity
{
TenantId = _tenantId,
UserId = userId,
Action = "user.action",
ResourceType = "test"
};
await _repository.CreateAsync(_tenantId, audit);
// Act
var audits = await _repository.GetByUserIdAsync(_tenantId, userId);
// Assert
audits.Should().HaveCount(1);
audits[0].UserId.Should().Be(userId);
}
[Fact]
public async Task GetByResource_ReturnsResourceAudits()
{
// Arrange
var resourceId = Guid.NewGuid().ToString();
var audit = new AuditEntity
{
TenantId = _tenantId,
Action = "resource.update",
ResourceType = "role",
ResourceId = resourceId
};
await _repository.CreateAsync(_tenantId, audit);
// Act
var audits = await _repository.GetByResourceAsync(_tenantId, "role", resourceId);
// Assert
audits.Should().HaveCount(1);
audits[0].ResourceId.Should().Be(resourceId);
}
[Fact]
public async Task GetByCorrelationId_ReturnsCorrelatedAudits()
{
// Arrange
var correlationId = Guid.NewGuid().ToString();
var audit1 = new AuditEntity
{
TenantId = _tenantId,
Action = "step1",
ResourceType = "test",
CorrelationId = correlationId
};
var audit2 = new AuditEntity
{
TenantId = _tenantId,
Action = "step2",
ResourceType = "test",
CorrelationId = correlationId
};
await _repository.CreateAsync(_tenantId, audit1);
await _repository.CreateAsync(_tenantId, audit2);
// Act
var audits = await _repository.GetByCorrelationIdAsync(_tenantId, correlationId);
// Assert
audits.Should().HaveCount(2);
audits.Should().AllSatisfy(a => a.CorrelationId.Should().Be(correlationId));
}
[Fact]
public async Task GetByAction_ReturnsMatchingAudits()
{
// Arrange
await _repository.CreateAsync(_tenantId, CreateAudit("user.login"));
await _repository.CreateAsync(_tenantId, CreateAudit("user.logout"));
await _repository.CreateAsync(_tenantId, CreateAudit("user.login"));
// Act
var audits = await _repository.GetByActionAsync(_tenantId, "user.login");
// Assert
audits.Should().HaveCount(2);
audits.Should().AllSatisfy(a => a.Action.Should().Be("user.login"));
}
[Fact]
public async Task Create_StoresJsonbValues()
{
// Arrange
var audit = new AuditEntity
{
TenantId = _tenantId,
Action = "config.update",
ResourceType = "config",
OldValue = "{\"setting\": \"old\"}",
NewValue = "{\"setting\": \"new\"}"
};
// Act
await _repository.CreateAsync(_tenantId, audit);
var audits = await _repository.GetByActionAsync(_tenantId, "config.update");
// Assert
audits.Should().HaveCount(1);
audits[0].OldValue.Should().Contain("old");
audits[0].NewValue.Should().Contain("new");
}
private AuditEntity CreateAudit(string action) => new()
{
TenantId = _tenantId,
UserId = Guid.NewGuid(),
Action = action,
ResourceType = "test",
ResourceId = Guid.NewGuid().ToString()
};
}

View File

@@ -0,0 +1,133 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Authority.Storage.Postgres.Models;
using StellaOps.Authority.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Authority.Storage.Postgres.Tests;
[Collection(AuthorityPostgresCollection.Name)]
public sealed class PermissionRepositoryTests : IAsyncLifetime
{
private readonly AuthorityPostgresFixture _fixture;
private readonly PermissionRepository _repository;
private readonly string _tenantId = Guid.NewGuid().ToString();
public PermissionRepositoryTests(AuthorityPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
var dataSource = new AuthorityDataSource(Options.Create(options), NullLogger<AuthorityDataSource>.Instance);
_repository = new PermissionRepository(dataSource, NullLogger<PermissionRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task CreateAndGet_RoundTripsPermission()
{
// Arrange
var permission = new PermissionEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "users:read",
Resource = "users",
Action = "read",
Description = "Read user data"
};
// Act
await _repository.CreateAsync(_tenantId, permission);
var fetched = await _repository.GetByIdAsync(_tenantId, permission.Id);
// Assert
fetched.Should().NotBeNull();
fetched!.Name.Should().Be("users:read");
fetched.Resource.Should().Be("users");
fetched.Action.Should().Be("read");
}
[Fact]
public async Task GetByName_ReturnsCorrectPermission()
{
// Arrange
var permission = new PermissionEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "roles:write",
Resource = "roles",
Action = "write"
};
await _repository.CreateAsync(_tenantId, permission);
// Act
var fetched = await _repository.GetByNameAsync(_tenantId, "roles:write");
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(permission.Id);
}
[Fact]
public async Task List_ReturnsAllPermissionsForTenant()
{
// Arrange
var perm1 = new PermissionEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "p1", Resource = "r1", Action = "a1" };
var perm2 = new PermissionEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "p2", Resource = "r2", Action = "a2" };
await _repository.CreateAsync(_tenantId, perm1);
await _repository.CreateAsync(_tenantId, perm2);
// Act
var permissions = await _repository.ListAsync(_tenantId);
// Assert
permissions.Should().HaveCount(2);
}
[Fact]
public async Task GetByResource_ReturnsResourcePermissions()
{
// Arrange
var perm1 = new PermissionEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "scans:read", Resource = "scans", Action = "read" };
var perm2 = new PermissionEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "scans:write", Resource = "scans", Action = "write" };
var perm3 = new PermissionEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "users:read", Resource = "users", Action = "read" };
await _repository.CreateAsync(_tenantId, perm1);
await _repository.CreateAsync(_tenantId, perm2);
await _repository.CreateAsync(_tenantId, perm3);
// Act
var permissions = await _repository.GetByResourceAsync(_tenantId, "scans");
// Assert
permissions.Should().HaveCount(2);
permissions.Should().AllSatisfy(p => p.Resource.Should().Be("scans"));
}
[Fact]
public async Task Delete_RemovesPermission()
{
// Arrange
var permission = new PermissionEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "temp:delete",
Resource = "temp",
Action = "delete"
};
await _repository.CreateAsync(_tenantId, permission);
// Act
await _repository.DeleteAsync(_tenantId, permission.Id);
var fetched = await _repository.GetByIdAsync(_tenantId, permission.Id);
// Assert
fetched.Should().BeNull();
}
}

View File

@@ -0,0 +1,148 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Authority.Storage.Postgres.Models;
using StellaOps.Authority.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Authority.Storage.Postgres.Tests;
[Collection(AuthorityPostgresCollection.Name)]
public sealed class RefreshTokenRepositoryTests : IAsyncLifetime
{
private readonly AuthorityPostgresFixture _fixture;
private readonly RefreshTokenRepository _repository;
private readonly string _tenantId = Guid.NewGuid().ToString();
public RefreshTokenRepositoryTests(AuthorityPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
var dataSource = new AuthorityDataSource(Options.Create(options), NullLogger<AuthorityDataSource>.Instance);
_repository = new RefreshTokenRepository(dataSource, NullLogger<RefreshTokenRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task CreateAndGetByHash_RoundTripsRefreshToken()
{
// Arrange
var token = new RefreshTokenEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
UserId = Guid.NewGuid(),
TokenHash = "refresh_hash_" + Guid.NewGuid().ToString("N"),
AccessTokenId = Guid.NewGuid(),
ClientId = "web-app",
IssuedAt = DateTimeOffset.UtcNow,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30)
};
// Act
await _repository.CreateAsync(_tenantId, token);
var fetched = await _repository.GetByHashAsync(token.TokenHash);
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(token.Id);
fetched.ClientId.Should().Be("web-app");
}
[Fact]
public async Task GetById_ReturnsToken()
{
// Arrange
var token = CreateRefreshToken(Guid.NewGuid());
await _repository.CreateAsync(_tenantId, token);
// Act
var fetched = await _repository.GetByIdAsync(_tenantId, token.Id);
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(token.Id);
}
[Fact]
public async Task GetByUserId_ReturnsUserTokens()
{
// Arrange
var userId = Guid.NewGuid();
var token1 = CreateRefreshToken(userId);
var token2 = CreateRefreshToken(userId);
await _repository.CreateAsync(_tenantId, token1);
await _repository.CreateAsync(_tenantId, token2);
// Act
var tokens = await _repository.GetByUserIdAsync(_tenantId, userId);
// Assert
tokens.Should().HaveCount(2);
}
[Fact]
public async Task Revoke_SetsRevokedFields()
{
// Arrange
var token = CreateRefreshToken(Guid.NewGuid());
await _repository.CreateAsync(_tenantId, token);
// Act
await _repository.RevokeAsync(_tenantId, token.Id, "admin@test.com", null);
var fetched = await _repository.GetByHashAsync(token.TokenHash);
// Assert
fetched!.RevokedAt.Should().NotBeNull();
fetched.RevokedBy.Should().Be("admin@test.com");
}
[Fact]
public async Task Revoke_WithReplacedBy_SetsReplacedByField()
{
// Arrange
var token = CreateRefreshToken(Guid.NewGuid());
await _repository.CreateAsync(_tenantId, token);
var newTokenId = Guid.NewGuid();
// Act
await _repository.RevokeAsync(_tenantId, token.Id, "rotation", newTokenId);
var fetched = await _repository.GetByHashAsync(token.TokenHash);
// Assert
fetched!.RevokedAt.Should().NotBeNull();
fetched.ReplacedBy.Should().Be(newTokenId);
}
[Fact]
public async Task RevokeByUserId_RevokesAllUserTokens()
{
// Arrange
var userId = Guid.NewGuid();
var token1 = CreateRefreshToken(userId);
var token2 = CreateRefreshToken(userId);
await _repository.CreateAsync(_tenantId, token1);
await _repository.CreateAsync(_tenantId, token2);
// Act
await _repository.RevokeByUserIdAsync(_tenantId, userId, "security_action");
var tokens = await _repository.GetByUserIdAsync(_tenantId, userId);
// Assert
tokens.Should().AllSatisfy(t => t.RevokedAt.Should().NotBeNull());
}
private RefreshTokenEntity CreateRefreshToken(Guid userId) => new()
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
UserId = userId,
TokenHash = $"refresh_{Guid.NewGuid():N}",
IssuedAt = DateTimeOffset.UtcNow,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30)
};
}

View File

@@ -0,0 +1,140 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Authority.Storage.Postgres.Models;
using StellaOps.Authority.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Authority.Storage.Postgres.Tests;
[Collection(AuthorityPostgresCollection.Name)]
public sealed class RoleRepositoryTests : IAsyncLifetime
{
private readonly AuthorityPostgresFixture _fixture;
private readonly RoleRepository _repository;
private readonly string _tenantId = Guid.NewGuid().ToString();
public RoleRepositoryTests(AuthorityPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
var dataSource = new AuthorityDataSource(Options.Create(options), NullLogger<AuthorityDataSource>.Instance);
_repository = new RoleRepository(dataSource, NullLogger<RoleRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task CreateAndGet_RoundTripsRole()
{
// Arrange
var role = new RoleEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "admin",
DisplayName = "Administrator",
Description = "Full system access",
IsSystem = true,
Metadata = "{\"level\": 1}"
};
// Act
await _repository.CreateAsync(_tenantId, role);
var fetched = await _repository.GetByIdAsync(_tenantId, role.Id);
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(role.Id);
fetched.Name.Should().Be("admin");
fetched.DisplayName.Should().Be("Administrator");
fetched.IsSystem.Should().BeTrue();
}
[Fact]
public async Task GetByName_ReturnsCorrectRole()
{
// Arrange
var role = new RoleEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "viewer",
DisplayName = "Viewer",
Description = "Read-only access"
};
await _repository.CreateAsync(_tenantId, role);
// Act
var fetched = await _repository.GetByNameAsync(_tenantId, "viewer");
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(role.Id);
}
[Fact]
public async Task List_ReturnsAllRolesForTenant()
{
// Arrange
var role1 = new RoleEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "role1" };
var role2 = new RoleEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "role2" };
await _repository.CreateAsync(_tenantId, role1);
await _repository.CreateAsync(_tenantId, role2);
// Act
var roles = await _repository.ListAsync(_tenantId);
// Assert
roles.Should().HaveCount(2);
roles.Select(r => r.Name).Should().Contain(["role1", "role2"]);
}
[Fact]
public async Task Update_ModifiesRole()
{
// Arrange
var role = new RoleEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
Name = "editor",
DisplayName = "Editor"
};
await _repository.CreateAsync(_tenantId, role);
// Act
var updated = new RoleEntity
{
Id = role.Id,
TenantId = _tenantId,
Name = "editor",
DisplayName = "Content Editor",
Description = "Updated description"
};
await _repository.UpdateAsync(_tenantId, updated);
var fetched = await _repository.GetByIdAsync(_tenantId, role.Id);
// Assert
fetched!.DisplayName.Should().Be("Content Editor");
fetched.Description.Should().Be("Updated description");
}
[Fact]
public async Task Delete_RemovesRole()
{
// Arrange
var role = new RoleEntity { Id = Guid.NewGuid(), TenantId = _tenantId, Name = "temp" };
await _repository.CreateAsync(_tenantId, role);
// Act
await _repository.DeleteAsync(_tenantId, role.Id);
var fetched = await _repository.GetByIdAsync(_tenantId, role.Id);
// Assert
fetched.Should().BeNull();
}
}

View File

@@ -0,0 +1,179 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Authority.Storage.Postgres.Models;
using StellaOps.Authority.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Authority.Storage.Postgres.Tests;
[Collection(AuthorityPostgresCollection.Name)]
public sealed class SessionRepositoryTests : IAsyncLifetime
{
private readonly AuthorityPostgresFixture _fixture;
private readonly SessionRepository _repository;
private readonly string _tenantId = Guid.NewGuid().ToString();
public SessionRepositoryTests(AuthorityPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
var dataSource = new AuthorityDataSource(Options.Create(options), NullLogger<AuthorityDataSource>.Instance);
_repository = new SessionRepository(dataSource, NullLogger<SessionRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task CreateAndGet_RoundTripsSession()
{
// Arrange
var session = new SessionEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
UserId = Guid.NewGuid(),
SessionTokenHash = "session_hash_" + Guid.NewGuid().ToString("N"),
IpAddress = "192.168.1.1",
UserAgent = "Mozilla/5.0",
StartedAt = DateTimeOffset.UtcNow,
LastActivityAt = DateTimeOffset.UtcNow,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(7)
};
// Act
await _repository.CreateAsync(_tenantId, session);
var fetched = await _repository.GetByIdAsync(_tenantId, session.Id);
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(session.Id);
fetched.IpAddress.Should().Be("192.168.1.1");
fetched.UserAgent.Should().Be("Mozilla/5.0");
}
[Fact]
public async Task GetByTokenHash_ReturnsSession()
{
// Arrange
var tokenHash = "lookup_hash_" + Guid.NewGuid().ToString("N");
var session = new SessionEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
UserId = Guid.NewGuid(),
SessionTokenHash = tokenHash,
StartedAt = DateTimeOffset.UtcNow,
LastActivityAt = DateTimeOffset.UtcNow,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(7)
};
await _repository.CreateAsync(_tenantId, session);
// Act
var fetched = await _repository.GetByTokenHashAsync(tokenHash);
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(session.Id);
}
[Fact]
public async Task GetByUserId_WithActiveOnly_ReturnsOnlyActiveSessions()
{
// Arrange
var userId = Guid.NewGuid();
var activeSession = CreateSession(userId);
var endedSession = new SessionEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
UserId = userId,
SessionTokenHash = "ended_" + Guid.NewGuid().ToString("N"),
StartedAt = DateTimeOffset.UtcNow.AddHours(-2),
LastActivityAt = DateTimeOffset.UtcNow.AddHours(-1),
ExpiresAt = DateTimeOffset.UtcNow.AddDays(7),
EndedAt = DateTimeOffset.UtcNow,
EndReason = "logout"
};
await _repository.CreateAsync(_tenantId, activeSession);
await _repository.CreateAsync(_tenantId, endedSession);
// Act
var activeSessions = await _repository.GetByUserIdAsync(_tenantId, userId, activeOnly: true);
var allSessions = await _repository.GetByUserIdAsync(_tenantId, userId, activeOnly: false);
// Assert
activeSessions.Should().HaveCount(1);
allSessions.Should().HaveCount(2);
}
[Fact]
public async Task UpdateLastActivity_UpdatesTimestamp()
{
// Arrange
var session = CreateSession(Guid.NewGuid());
await _repository.CreateAsync(_tenantId, session);
// Act
await Task.Delay(100); // Ensure time difference
await _repository.UpdateLastActivityAsync(_tenantId, session.Id);
var fetched = await _repository.GetByIdAsync(_tenantId, session.Id);
// Assert
fetched!.LastActivityAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
}
[Fact]
public async Task End_SetsEndFieldsCorrectly()
{
// Arrange
var session = CreateSession(Guid.NewGuid());
await _repository.CreateAsync(_tenantId, session);
// Act
await _repository.EndAsync(_tenantId, session.Id, "session_timeout");
var fetched = await _repository.GetByIdAsync(_tenantId, session.Id);
// Assert
fetched!.EndedAt.Should().NotBeNull();
fetched.EndReason.Should().Be("session_timeout");
}
[Fact]
public async Task EndByUserId_EndsAllUserSessions()
{
// Arrange
var userId = Guid.NewGuid();
var session1 = CreateSession(userId);
var session2 = CreateSession(userId);
await _repository.CreateAsync(_tenantId, session1);
await _repository.CreateAsync(_tenantId, session2);
// Act
await _repository.EndByUserIdAsync(_tenantId, userId, "forced_logout");
var sessions = await _repository.GetByUserIdAsync(_tenantId, userId, activeOnly: false);
// Assert
sessions.Should().HaveCount(2);
sessions.Should().AllSatisfy(s =>
{
s.EndedAt.Should().NotBeNull();
s.EndReason.Should().Be("forced_logout");
});
}
private SessionEntity CreateSession(Guid userId) => new()
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
UserId = userId,
SessionTokenHash = $"session_{Guid.NewGuid():N}",
StartedAt = DateTimeOffset.UtcNow,
LastActivityAt = DateTimeOffset.UtcNow,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(7)
};
}

View File

@@ -0,0 +1,135 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Authority.Storage.Postgres.Models;
using StellaOps.Authority.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Authority.Storage.Postgres.Tests;
[Collection(AuthorityPostgresCollection.Name)]
public sealed class TokenRepositoryTests : IAsyncLifetime
{
private readonly AuthorityPostgresFixture _fixture;
private readonly TokenRepository _repository;
private readonly string _tenantId = Guid.NewGuid().ToString();
public TokenRepositoryTests(AuthorityPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
var dataSource = new AuthorityDataSource(Options.Create(options), NullLogger<AuthorityDataSource>.Instance);
_repository = new TokenRepository(dataSource, NullLogger<TokenRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task CreateAndGetByHash_RoundTripsToken()
{
// Arrange
var token = new TokenEntity
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
UserId = Guid.NewGuid(),
TokenHash = "sha256_hash_" + Guid.NewGuid().ToString("N"),
TokenType = TokenType.Access,
Scopes = ["read", "write"],
ClientId = "web-app",
IssuedAt = DateTimeOffset.UtcNow,
ExpiresAt = DateTimeOffset.UtcNow.AddHours(1)
};
// Act
await _repository.CreateAsync(_tenantId, token);
var fetched = await _repository.GetByHashAsync(token.TokenHash);
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(token.Id);
fetched.TokenType.Should().Be(TokenType.Access);
fetched.Scopes.Should().BeEquivalentTo(["read", "write"]);
}
[Fact]
public async Task GetById_ReturnsToken()
{
// Arrange
var token = CreateToken(Guid.NewGuid());
await _repository.CreateAsync(_tenantId, token);
// Act
var fetched = await _repository.GetByIdAsync(_tenantId, token.Id);
// Assert
fetched.Should().NotBeNull();
fetched!.Id.Should().Be(token.Id);
}
[Fact]
public async Task GetByUserId_ReturnsUserTokens()
{
// Arrange
var userId = Guid.NewGuid();
var token1 = CreateToken(userId);
var token2 = CreateToken(userId);
await _repository.CreateAsync(_tenantId, token1);
await _repository.CreateAsync(_tenantId, token2);
// Act
var tokens = await _repository.GetByUserIdAsync(_tenantId, userId);
// Assert
tokens.Should().HaveCount(2);
}
[Fact]
public async Task Revoke_SetsRevokedFields()
{
// Arrange
var token = CreateToken(Guid.NewGuid());
await _repository.CreateAsync(_tenantId, token);
// Act
await _repository.RevokeAsync(_tenantId, token.Id, "admin@test.com");
var fetched = await _repository.GetByHashAsync(token.TokenHash);
// Assert
fetched!.RevokedAt.Should().NotBeNull();
fetched.RevokedBy.Should().Be("admin@test.com");
}
[Fact]
public async Task RevokeByUserId_RevokesAllUserTokens()
{
// Arrange
var userId = Guid.NewGuid();
var token1 = CreateToken(userId);
var token2 = CreateToken(userId);
await _repository.CreateAsync(_tenantId, token1);
await _repository.CreateAsync(_tenantId, token2);
// Act
await _repository.RevokeByUserIdAsync(_tenantId, userId, "security_action");
var tokens = await _repository.GetByUserIdAsync(_tenantId, userId);
// Assert
tokens.Should().AllSatisfy(t => t.RevokedAt.Should().NotBeNull());
}
private TokenEntity CreateToken(Guid userId) => new()
{
Id = Guid.NewGuid(),
TenantId = _tenantId,
UserId = userId,
TokenHash = $"sha256_{Guid.NewGuid():N}",
TokenType = TokenType.Access,
Scopes = ["read"],
IssuedAt = DateTimeOffset.UtcNow,
ExpiresAt = DateTimeOffset.UtcNow.AddHours(1)
};
}