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

@@ -4,22 +4,22 @@ using System.Globalization;
using Microsoft.Extensions.Configuration;
using StellaOps.Configuration;
using Xunit;
namespace StellaOps.Configuration.Tests;
public class StellaOpsAuthorityOptionsTests
{
[Fact]
public void Validate_Throws_When_IssuerMissing()
{
var options = new StellaOpsAuthorityOptions();
var exception = Assert.Throws<InvalidOperationException>(() => options.Validate());
Assert.Contains("issuer", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
namespace StellaOps.Configuration.Tests;
public class StellaOpsAuthorityOptionsTests
{
[Fact]
public void Validate_Throws_When_IssuerMissing()
{
var options = new StellaOpsAuthorityOptions();
var exception = Assert.Throws<InvalidOperationException>(() => options.Validate());
Assert.Contains("issuer", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Validate_Normalises_Collections()
{
var options = new StellaOpsAuthorityOptions
@@ -67,35 +67,35 @@ public class StellaOpsAuthorityOptionsTests
Assert.Contains("remote inference", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
[Fact]
public void Validate_Normalises_PluginDescriptors()
{
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.stella-ops.test"),
SchemaVersion = 1
};
options.Storage.ConnectionString = "mongodb://localhost:27017/authority";
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
var descriptor = new AuthorityPluginDescriptorOptions
{
AssemblyName = "StellaOps.Authority.Plugin.Standard",
ConfigFile = " standard.yaml ",
Enabled = true
};
descriptor.Capabilities.Add("password");
descriptor.Capabilities.Add("PASSWORD");
options.Plugins.Descriptors["standard"] = descriptor;
options.Validate();
var normalized = options.Plugins.Descriptors["standard"];
Assert.Equal("standard.yaml", normalized.ConfigFile);
Assert.Single(normalized.Capabilities);
SchemaVersion = 1
};
options.Storage.ConnectionString = "mongodb://localhost:27017/authority";
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
var descriptor = new AuthorityPluginDescriptorOptions
{
AssemblyName = "StellaOps.Authority.Plugin.Standard",
ConfigFile = " standard.yaml ",
Enabled = true
};
descriptor.Capabilities.Add("password");
descriptor.Capabilities.Add("PASSWORD");
options.Plugins.Descriptors["standard"] = descriptor;
options.Validate();
var normalized = options.Plugins.Descriptors["standard"];
Assert.Equal("standard.yaml", normalized.ConfigFile);
Assert.Single(normalized.Capabilities);
Assert.Equal("password", normalized.Capabilities[0]);
}
@@ -163,149 +163,149 @@ public class StellaOpsAuthorityOptionsTests
Assert.Contains("consentVersion", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Validate_Throws_When_StorageConnectionStringMissing()
{
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.stella-ops.test"),
SchemaVersion = 1
};
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
var exception = Assert.Throws<InvalidOperationException>(() => options.Validate());
Assert.Contains("Mongo connection string", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Build_Binds_From_Configuration()
{
var context = StellaOpsAuthorityConfiguration.Build(options =>
{
options.ConfigureBuilder = builder =>
{
builder.AddInMemoryCollection(new Dictionary<string, string?>
{
["Authority:SchemaVersion"] = "2",
["Authority:Issuer"] = "https://authority.internal",
["Authority:AccessTokenLifetime"] = "00:30:00",
["Authority:RefreshTokenLifetime"] = "30.00:00:00",
["Authority:Storage:ConnectionString"] = "mongodb://example/stellaops",
["Authority:Storage:DatabaseName"] = "overrideDb",
["Authority:Storage:CommandTimeout"] = "00:01:30",
["Authority:PluginDirectories:0"] = "/var/lib/stellaops/plugins",
["Authority:BypassNetworks:0"] = "127.0.0.1/32",
["Authority:Security:RateLimiting:Token:PermitLimit"] = "25",
["Authority:Security:RateLimiting:Token:Window"] = "00:00:30",
["Authority:Security:RateLimiting:Authorize:Enabled"] = "true",
["Authority:Security:RateLimiting:Internal:Enabled"] = "true",
["Authority:Security:RateLimiting:Internal:PermitLimit"] = "3",
["Authority:Signing:Enabled"] = "true",
["Authority:Signing:ActiveKeyId"] = "authority-signing-dev",
["Authority:Signing:KeyPath"] = "../certificates/authority-signing-dev.pem",
["Authority:Signing:KeySource"] = "file"
});
};
});
var options = context.Options;
Assert.Equal(2, options.SchemaVersion);
Assert.Equal(new Uri("https://authority.internal"), options.Issuer);
Assert.Equal(TimeSpan.FromMinutes(30), options.AccessTokenLifetime);
Assert.Equal(TimeSpan.FromDays(30), options.RefreshTokenLifetime);
Assert.Equal(new[] { "/var/lib/stellaops/plugins" }, options.PluginDirectories);
Assert.Equal(new[] { "127.0.0.1/32" }, options.BypassNetworks);
Assert.Equal("mongodb://example/stellaops", options.Storage.ConnectionString);
Assert.Equal("overrideDb", options.Storage.DatabaseName);
Assert.Equal(TimeSpan.FromMinutes(1.5), options.Storage.CommandTimeout);
Assert.Equal(25, options.Security.RateLimiting.Token.PermitLimit);
Assert.Equal(TimeSpan.FromSeconds(30), options.Security.RateLimiting.Token.Window);
Assert.True(options.Security.RateLimiting.Authorize.Enabled);
Assert.True(options.Security.RateLimiting.Internal.Enabled);
Assert.Equal(3, options.Security.RateLimiting.Internal.PermitLimit);
Assert.True(options.Signing.Enabled);
Assert.Equal("authority-signing-dev", options.Signing.ActiveKeyId);
Assert.Equal("../certificates/authority-signing-dev.pem", options.Signing.KeyPath);
Assert.Equal("file", options.Signing.KeySource);
}
[Fact]
public void Validate_Normalises_ExceptionRoutingTemplates()
{
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.stella-ops.test"),
SchemaVersion = 1
};
options.Storage.ConnectionString = "mongodb://localhost:27017/authority";
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Exceptions.RoutingTemplates.Add(new AuthorityExceptionRoutingTemplateOptions
{
Id = " SecOps ",
AuthorityRouteId = " approvals/secops ",
RequireMfa = true,
Description = " Security approvals "
});
options.Validate();
Assert.True(options.Exceptions.RequiresMfaForApprovals);
var template = Assert.Single(options.Exceptions.NormalizedRoutingTemplates);
Assert.Equal("SecOps", template.Key);
Assert.Equal("SecOps", template.Value.Id);
Assert.Equal("approvals/secops", template.Value.AuthorityRouteId);
Assert.Equal("Security approvals", template.Value.Description);
Assert.True(template.Value.RequireMfa);
}
[Fact]
public void Validate_Throws_When_ExceptionRoutingTemplatesDuplicate()
{
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.stella-ops.test"),
SchemaVersion = 1
};
options.Storage.ConnectionString = "mongodb://localhost:27017/authority";
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Exceptions.RoutingTemplates.Add(new AuthorityExceptionRoutingTemplateOptions
{
Id = "secops",
AuthorityRouteId = "route/a"
});
options.Exceptions.RoutingTemplates.Add(new AuthorityExceptionRoutingTemplateOptions
{
Id = "SecOps",
AuthorityRouteId = "route/b"
});
var exception = Assert.Throws<InvalidOperationException>(() => options.Validate());
Assert.Contains("secops", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Validate_Throws_When_RateLimitingInvalid()
{
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.stella-ops.test"),
SchemaVersion = 1
};
options.Storage.ConnectionString = "mongodb://localhost:27017/authority";
options.Security.RateLimiting.Token.PermitLimit = 0;
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
var exception = Assert.Throws<InvalidOperationException>(() => options.Validate());
Assert.Contains("permitLimit", exception.Message, StringComparison.OrdinalIgnoreCase);
}
}
[Fact]
public void Validate_Throws_When_StorageConnectionStringMissing()
{
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.stella-ops.test"),
SchemaVersion = 1
};
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
var exception = Assert.Throws<InvalidOperationException>(() => options.Validate());
Assert.Contains("Mongo connection string", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Build_Binds_From_Configuration()
{
var context = StellaOpsAuthorityConfiguration.Build(options =>
{
options.ConfigureBuilder = builder =>
{
builder.AddInMemoryCollection(new Dictionary<string, string?>
{
["Authority:SchemaVersion"] = "2",
["Authority:Issuer"] = "https://authority.internal",
["Authority:AccessTokenLifetime"] = "00:30:00",
["Authority:RefreshTokenLifetime"] = "30.00:00:00",
["Authority:Storage:ConnectionString"] = "mongodb://example/stellaops",
["Authority:Storage:DatabaseName"] = "overrideDb",
["Authority:Storage:CommandTimeout"] = "00:01:30",
["Authority:PluginDirectories:0"] = "/var/lib/stellaops/plugins",
["Authority:BypassNetworks:0"] = "127.0.0.1/32",
["Authority:Security:RateLimiting:Token:PermitLimit"] = "25",
["Authority:Security:RateLimiting:Token:Window"] = "00:00:30",
["Authority:Security:RateLimiting:Authorize:Enabled"] = "true",
["Authority:Security:RateLimiting:Internal:Enabled"] = "true",
["Authority:Security:RateLimiting:Internal:PermitLimit"] = "3",
["Authority:Signing:Enabled"] = "true",
["Authority:Signing:ActiveKeyId"] = "authority-signing-dev",
["Authority:Signing:KeyPath"] = "../certificates/authority-signing-dev.pem",
["Authority:Signing:KeySource"] = "file"
});
};
});
var options = context.Options;
Assert.Equal(2, options.SchemaVersion);
Assert.Equal(new Uri("https://authority.internal"), options.Issuer);
Assert.Equal(TimeSpan.FromMinutes(30), options.AccessTokenLifetime);
Assert.Equal(TimeSpan.FromDays(30), options.RefreshTokenLifetime);
Assert.Equal(new[] { "/var/lib/stellaops/plugins" }, options.PluginDirectories);
Assert.Equal(new[] { "127.0.0.1/32" }, options.BypassNetworks);
Assert.Equal("mongodb://example/stellaops", options.Storage.ConnectionString);
Assert.Equal("overrideDb", options.Storage.DatabaseName);
Assert.Equal(TimeSpan.FromMinutes(1.5), options.Storage.CommandTimeout);
Assert.Equal(25, options.Security.RateLimiting.Token.PermitLimit);
Assert.Equal(TimeSpan.FromSeconds(30), options.Security.RateLimiting.Token.Window);
Assert.True(options.Security.RateLimiting.Authorize.Enabled);
Assert.True(options.Security.RateLimiting.Internal.Enabled);
Assert.Equal(3, options.Security.RateLimiting.Internal.PermitLimit);
Assert.True(options.Signing.Enabled);
Assert.Equal("authority-signing-dev", options.Signing.ActiveKeyId);
Assert.Equal("../certificates/authority-signing-dev.pem", options.Signing.KeyPath);
Assert.Equal("file", options.Signing.KeySource);
}
[Fact]
public void Validate_Normalises_ExceptionRoutingTemplates()
{
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.stella-ops.test"),
SchemaVersion = 1
};
options.Storage.ConnectionString = "mongodb://localhost:27017/authority";
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Exceptions.RoutingTemplates.Add(new AuthorityExceptionRoutingTemplateOptions
{
Id = " SecOps ",
AuthorityRouteId = " approvals/secops ",
RequireMfa = true,
Description = " Security approvals "
});
options.Validate();
Assert.True(options.Exceptions.RequiresMfaForApprovals);
var template = Assert.Single(options.Exceptions.NormalizedRoutingTemplates);
Assert.Equal("SecOps", template.Key);
Assert.Equal("SecOps", template.Value.Id);
Assert.Equal("approvals/secops", template.Value.AuthorityRouteId);
Assert.Equal("Security approvals", template.Value.Description);
Assert.True(template.Value.RequireMfa);
}
[Fact]
public void Validate_Throws_When_ExceptionRoutingTemplatesDuplicate()
{
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.stella-ops.test"),
SchemaVersion = 1
};
options.Storage.ConnectionString = "mongodb://localhost:27017/authority";
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Exceptions.RoutingTemplates.Add(new AuthorityExceptionRoutingTemplateOptions
{
Id = "secops",
AuthorityRouteId = "route/a"
});
options.Exceptions.RoutingTemplates.Add(new AuthorityExceptionRoutingTemplateOptions
{
Id = "SecOps",
AuthorityRouteId = "route/b"
});
var exception = Assert.Throws<InvalidOperationException>(() => options.Validate());
Assert.Contains("secops", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Validate_Throws_When_RateLimitingInvalid()
{
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.stella-ops.test"),
SchemaVersion = 1
};
options.Storage.ConnectionString = "mongodb://localhost:27017/authority";
options.Security.RateLimiting.Token.PermitLimit = 0;
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
var exception = Assert.Throws<InvalidOperationException>(() => options.Validate());
Assert.Contains("permitLimit", exception.Message, StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -1,57 +1,57 @@
using System;
using StellaOps.Cryptography.Audit;
namespace StellaOps.Cryptography.Tests.Audit;
public class AuthEventRecordTests
{
[Fact]
public void AuthEventRecord_InitializesCollections()
{
var record = new AuthEventRecord
{
EventType = "authority.test",
Outcome = AuthEventOutcome.Success
};
Assert.NotNull(record.Scopes);
Assert.Empty(record.Scopes);
Assert.NotNull(record.Properties);
Assert.Empty(record.Properties);
Assert.False(record.Tenant.HasValue);
Assert.False(record.Project.HasValue);
}
[Fact]
public void ClassifiedString_NormalizesWhitespace()
{
var value = ClassifiedString.Personal(" ");
Assert.Null(value.Value);
Assert.False(value.HasValue);
Assert.Equal(AuthEventDataClassification.Personal, value.Classification);
}
[Fact]
public void Subject_DefaultsToEmptyCollections()
{
var subject = new AuthEventSubject();
Assert.NotNull(subject.Attributes);
Assert.Empty(subject.Attributes);
}
[Fact]
public void Record_AssignsTimestamp_WhenNotProvided()
{
var record = new AuthEventRecord
{
EventType = "authority.test",
Outcome = AuthEventOutcome.Success
};
Assert.NotEqual(default, record.OccurredAt);
Assert.InRange(
record.OccurredAt,
DateTimeOffset.UtcNow.AddSeconds(-5),
DateTimeOffset.UtcNow.AddSeconds(5));
}
}
using System;
using StellaOps.Cryptography.Audit;
namespace StellaOps.Cryptography.Tests.Audit;
public class AuthEventRecordTests
{
[Fact]
public void AuthEventRecord_InitializesCollections()
{
var record = new AuthEventRecord
{
EventType = "authority.test",
Outcome = AuthEventOutcome.Success
};
Assert.NotNull(record.Scopes);
Assert.Empty(record.Scopes);
Assert.NotNull(record.Properties);
Assert.Empty(record.Properties);
Assert.False(record.Tenant.HasValue);
Assert.False(record.Project.HasValue);
}
[Fact]
public void ClassifiedString_NormalizesWhitespace()
{
var value = ClassifiedString.Personal(" ");
Assert.Null(value.Value);
Assert.False(value.HasValue);
Assert.Equal(AuthEventDataClassification.Personal, value.Classification);
}
[Fact]
public void Subject_DefaultsToEmptyCollections()
{
var subject = new AuthEventSubject();
Assert.NotNull(subject.Attributes);
Assert.Empty(subject.Attributes);
}
[Fact]
public void Record_AssignsTimestamp_WhenNotProvided()
{
var record = new AuthEventRecord
{
EventType = "authority.test",
Outcome = AuthEventOutcome.Success
};
Assert.NotEqual(default, record.OccurredAt);
Assert.InRange(
record.OccurredAt,
DateTimeOffset.UtcNow.AddSeconds(-5),
DateTimeOffset.UtcNow.AddSeconds(5));
}
}

View File

@@ -1,52 +1,52 @@
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed class BouncyCastleEd25519CryptoProviderTests
{
[Fact]
public async Task SignAndVerify_WithBouncyCastleProvider_Succeeds()
{
var services = new ServiceCollection();
services.AddStellaOpsCrypto();
services.AddBouncyCastleEd25519Provider();
using var provider = services.BuildServiceProvider();
var registry = provider.GetRequiredService<ICryptoProviderRegistry>();
var bcProvider = provider.GetServices<ICryptoProvider>()
.OfType<BouncyCastleEd25519CryptoProvider>()
.Single();
var keyId = "ed25519-unit-test";
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)(i + 1)).ToArray();
var keyReference = new CryptoKeyReference(keyId, bcProvider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: DateTimeOffset.UtcNow);
bcProvider.UpsertSigningKey(signingKey);
var resolution = registry.ResolveSigner(
CryptoCapability.Signing,
SignatureAlgorithms.Ed25519,
keyReference,
bcProvider.Name);
var payload = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var signature = await resolution.Signer.SignAsync(payload);
Assert.True(await resolution.Signer.VerifyAsync(payload, signature));
var jwk = resolution.Signer.ExportPublicJsonWebKey();
Assert.Equal("OKP", jwk.Kty);
Assert.Equal("Ed25519", jwk.Crv);
Assert.Equal(SignatureAlgorithms.EdDsa, jwk.Alg);
Assert.Equal(keyId, jwk.Kid);
}
}
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed class BouncyCastleEd25519CryptoProviderTests
{
[Fact]
public async Task SignAndVerify_WithBouncyCastleProvider_Succeeds()
{
var services = new ServiceCollection();
services.AddStellaOpsCrypto();
services.AddBouncyCastleEd25519Provider();
using var provider = services.BuildServiceProvider();
var registry = provider.GetRequiredService<ICryptoProviderRegistry>();
var bcProvider = provider.GetServices<ICryptoProvider>()
.OfType<BouncyCastleEd25519CryptoProvider>()
.Single();
var keyId = "ed25519-unit-test";
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)(i + 1)).ToArray();
var keyReference = new CryptoKeyReference(keyId, bcProvider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: DateTimeOffset.UtcNow);
bcProvider.UpsertSigningKey(signingKey);
var resolution = registry.ResolveSigner(
CryptoCapability.Signing,
SignatureAlgorithms.Ed25519,
keyReference,
bcProvider.Name);
var payload = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var signature = await resolution.Signer.SignAsync(payload);
Assert.True(await resolution.Signer.VerifyAsync(payload, signature));
var jwk = resolution.Signer.ExportPublicJsonWebKey();
Assert.Equal("OKP", jwk.Kty);
Assert.Equal("Ed25519", jwk.Crv);
Assert.Equal(SignatureAlgorithms.EdDsa, jwk.Alg);
Assert.Equal(keyId, jwk.Kid);
}
}

View File

@@ -1,112 +1,112 @@
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.DependencyInjection;
using StellaOps.Plugin.DependencyInjection;
using Xunit;
namespace StellaOps.Plugin.Tests.DependencyInjection;
public sealed class PluginServiceRegistrationTests
{
[Fact]
public void RegisterAssemblyMetadata_RegistersScopedDescriptor()
{
var services = new ServiceCollection();
PluginServiceRegistration.RegisterAssemblyMetadata(
services,
typeof(ScopedTestService).Assembly,
NullLogger.Instance);
var descriptor = Assert.Single(services, static d => d.ServiceType == typeof(IScopedService));
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
Assert.Equal(typeof(ScopedTestService), descriptor.ImplementationType);
}
[Fact]
public void RegisterAssemblyMetadata_HonoursRegisterAsSelf()
{
var services = new ServiceCollection();
PluginServiceRegistration.RegisterAssemblyMetadata(
services,
typeof(SelfRegisteringService).Assembly,
NullLogger.Instance);
Assert.Contains(services, static d =>
d.ServiceType == typeof(SelfRegisteringService) &&
d.ImplementationType == typeof(SelfRegisteringService));
}
[Fact]
public void RegisterAssemblyMetadata_ReplacesExistingDescriptorsWhenRequested()
{
var services = new ServiceCollection();
services.AddSingleton<IReplacementService, ExistingReplacementService>();
PluginServiceRegistration.RegisterAssemblyMetadata(
services,
typeof(ReplacementService).Assembly,
NullLogger.Instance);
var descriptor = Assert.Single(
services,
static d => d.ServiceType == typeof(IReplacementService) &&
d.ImplementationType == typeof(ReplacementService));
Assert.Equal(ServiceLifetime.Transient, descriptor.Lifetime);
}
[Fact]
public void RegisterAssemblyMetadata_SkipsInvalidAssignments()
{
var services = new ServiceCollection();
PluginServiceRegistration.RegisterAssemblyMetadata(
services,
typeof(InvalidServiceBinding).Assembly,
NullLogger.Instance);
Assert.DoesNotContain(services, static d => d.ServiceType == typeof(IAnotherService));
}
private interface IScopedService
{
}
private interface ISelfContract
{
}
private interface IReplacementService
{
}
private interface IAnotherService
{
}
private sealed class ExistingReplacementService : IReplacementService
{
}
[ServiceBinding(typeof(IScopedService), ServiceLifetime.Scoped)]
private sealed class ScopedTestService : IScopedService
{
}
[ServiceBinding(typeof(ISelfContract), ServiceLifetime.Singleton, RegisterAsSelf = true)]
private sealed class SelfRegisteringService : ISelfContract
{
}
[ServiceBinding(typeof(IReplacementService), ServiceLifetime.Transient, ReplaceExisting = true)]
private sealed class ReplacementService : IReplacementService
{
}
[ServiceBinding(typeof(IAnotherService), ServiceLifetime.Singleton)]
private sealed class InvalidServiceBinding
{
}
}
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.DependencyInjection;
using StellaOps.Plugin.DependencyInjection;
using Xunit;
namespace StellaOps.Plugin.Tests.DependencyInjection;
public sealed class PluginServiceRegistrationTests
{
[Fact]
public void RegisterAssemblyMetadata_RegistersScopedDescriptor()
{
var services = new ServiceCollection();
PluginServiceRegistration.RegisterAssemblyMetadata(
services,
typeof(ScopedTestService).Assembly,
NullLogger.Instance);
var descriptor = Assert.Single(services, static d => d.ServiceType == typeof(IScopedService));
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
Assert.Equal(typeof(ScopedTestService), descriptor.ImplementationType);
}
[Fact]
public void RegisterAssemblyMetadata_HonoursRegisterAsSelf()
{
var services = new ServiceCollection();
PluginServiceRegistration.RegisterAssemblyMetadata(
services,
typeof(SelfRegisteringService).Assembly,
NullLogger.Instance);
Assert.Contains(services, static d =>
d.ServiceType == typeof(SelfRegisteringService) &&
d.ImplementationType == typeof(SelfRegisteringService));
}
[Fact]
public void RegisterAssemblyMetadata_ReplacesExistingDescriptorsWhenRequested()
{
var services = new ServiceCollection();
services.AddSingleton<IReplacementService, ExistingReplacementService>();
PluginServiceRegistration.RegisterAssemblyMetadata(
services,
typeof(ReplacementService).Assembly,
NullLogger.Instance);
var descriptor = Assert.Single(
services,
static d => d.ServiceType == typeof(IReplacementService) &&
d.ImplementationType == typeof(ReplacementService));
Assert.Equal(ServiceLifetime.Transient, descriptor.Lifetime);
}
[Fact]
public void RegisterAssemblyMetadata_SkipsInvalidAssignments()
{
var services = new ServiceCollection();
PluginServiceRegistration.RegisterAssemblyMetadata(
services,
typeof(InvalidServiceBinding).Assembly,
NullLogger.Instance);
Assert.DoesNotContain(services, static d => d.ServiceType == typeof(IAnotherService));
}
private interface IScopedService
{
}
private interface ISelfContract
{
}
private interface IReplacementService
{
}
private interface IAnotherService
{
}
private sealed class ExistingReplacementService : IReplacementService
{
}
[ServiceBinding(typeof(IScopedService), ServiceLifetime.Scoped)]
private sealed class ScopedTestService : IScopedService
{
}
[ServiceBinding(typeof(ISelfContract), ServiceLifetime.Singleton, RegisterAsSelf = true)]
private sealed class SelfRegisteringService : ISelfContract
{
}
[ServiceBinding(typeof(IReplacementService), ServiceLifetime.Transient, ReplaceExisting = true)]
private sealed class ReplacementService : IReplacementService
{
}
[ServiceBinding(typeof(IAnotherService), ServiceLifetime.Singleton)]
private sealed class InvalidServiceBinding
{
}
}