audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
using StellaOps.Notify.Connectors.Shared;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Notify.Connectors.Shared.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ConnectorValueRedactor class.
|
||||
/// </summary>
|
||||
public sealed class ConnectorValueRedactorTests
|
||||
{
|
||||
[Fact]
|
||||
public void RedactSecret_ReturnsConstantMask()
|
||||
{
|
||||
var result = ConnectorValueRedactor.RedactSecret("my-super-secret-value");
|
||||
Assert.Equal("***", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RedactToken_ShortToken_ReturnsMask()
|
||||
{
|
||||
var result = ConnectorValueRedactor.RedactToken("short");
|
||||
Assert.Equal("***", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RedactToken_LongToken_PreservePrefixAndSuffix()
|
||||
{
|
||||
var token = "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxx1234";
|
||||
var result = ConnectorValueRedactor.RedactToken(token);
|
||||
|
||||
Assert.StartsWith("ghp_xx", result);
|
||||
Assert.EndsWith("1234", result);
|
||||
Assert.Contains("***", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RedactToken_CustomLengths_Applied()
|
||||
{
|
||||
var token = "my-very-long-token-value-here";
|
||||
var result = ConnectorValueRedactor.RedactToken(token, prefixLength: 3, suffixLength: 5);
|
||||
|
||||
Assert.StartsWith("my-", result);
|
||||
Assert.EndsWith("-here", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RedactToken_NullValue_ReturnsMask()
|
||||
{
|
||||
var result = ConnectorValueRedactor.RedactToken(null!);
|
||||
Assert.Equal("***", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RedactToken_EmptyValue_ReturnsMask()
|
||||
{
|
||||
var result = ConnectorValueRedactor.RedactToken("");
|
||||
Assert.Equal("***", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RedactToken_WhitespaceValue_ReturnsMask()
|
||||
{
|
||||
var result = ConnectorValueRedactor.RedactToken(" ");
|
||||
Assert.Equal("***", result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("token")]
|
||||
[InlineData("auth_token")]
|
||||
[InlineData("api_secret")]
|
||||
[InlineData("Authorization")]
|
||||
[InlineData("session_cookie")]
|
||||
[InlineData("password")]
|
||||
[InlineData("api_key")]
|
||||
[InlineData("user_credential")]
|
||||
public void IsSensitiveKey_SensitiveKeys_ReturnsTrue(string key)
|
||||
{
|
||||
var result = ConnectorValueRedactor.IsSensitiveKey(key);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("username")]
|
||||
[InlineData("host")]
|
||||
[InlineData("port")]
|
||||
[InlineData("channel")]
|
||||
[InlineData("recipient")]
|
||||
public void IsSensitiveKey_NonSensitiveKeys_ReturnsFalse(string key)
|
||||
{
|
||||
var result = ConnectorValueRedactor.IsSensitiveKey(key);
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsSensitiveKey_NullKey_ReturnsFalse()
|
||||
{
|
||||
var result = ConnectorValueRedactor.IsSensitiveKey(null!);
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsSensitiveKey_EmptyKey_ReturnsFalse()
|
||||
{
|
||||
var result = ConnectorValueRedactor.IsSensitiveKey("");
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsSensitiveKey_WhitespaceKey_ReturnsFalse()
|
||||
{
|
||||
var result = ConnectorValueRedactor.IsSensitiveKey(" ");
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsSensitiveKey_CustomFragments_Used()
|
||||
{
|
||||
var customFragments = new[] { "custom", "special" };
|
||||
|
||||
Assert.True(ConnectorValueRedactor.IsSensitiveKey("my_custom_key", customFragments));
|
||||
Assert.True(ConnectorValueRedactor.IsSensitiveKey("special_value", customFragments));
|
||||
Assert.False(ConnectorValueRedactor.IsSensitiveKey("token", customFragments));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultSensitiveKeyFragments_ContainsExpectedFragments()
|
||||
{
|
||||
var fragments = ConnectorValueRedactor.DefaultSensitiveKeyFragments;
|
||||
|
||||
Assert.Contains("token", fragments);
|
||||
Assert.Contains("secret", fragments);
|
||||
Assert.Contains("authorization", fragments);
|
||||
Assert.Contains("cookie", fragments);
|
||||
Assert.Contains("password", fragments);
|
||||
Assert.Contains("key", fragments);
|
||||
Assert.Contains("credential", fragments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseXunitV3>true</UseXunitV3>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Notify.Connectors.Shared\StellaOps.Notify.Connectors.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"diagnosticMessages": true,
|
||||
"parallelizeAssembly": true,
|
||||
"parallelizeTestCollections": true,
|
||||
"maxParallelThreads": -1
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
using StellaOps.Notify.Storage.InMemory.Documents;
|
||||
using StellaOps.Notify.Storage.InMemory.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Notify.Storage.InMemory.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Fake TimeProvider for deterministic testing.
|
||||
/// </summary>
|
||||
public sealed class FakeTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _utcNow;
|
||||
|
||||
public FakeTimeProvider(DateTimeOffset? initialTime = null)
|
||||
{
|
||||
_utcNow = initialTime ?? new DateTimeOffset(2026, 1, 14, 12, 0, 0, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _utcNow;
|
||||
|
||||
public void Advance(TimeSpan duration) => _utcNow = _utcNow.Add(duration);
|
||||
|
||||
public void SetUtcNow(DateTimeOffset time) => _utcNow = time;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for NotifyChannelRepositoryAdapter.
|
||||
/// </summary>
|
||||
public sealed class NotifyChannelRepositoryAdapterTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider = new();
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertAsync_NewChannel_SetsUpdatedAt()
|
||||
{
|
||||
var repo = new NotifyChannelRepositoryAdapter(_timeProvider);
|
||||
var channel = new NotifyChannelDocument
|
||||
{
|
||||
Id = "ch-001",
|
||||
TenantId = "tenant-001",
|
||||
Name = "Email Channel",
|
||||
ChannelType = "email",
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
var result = await repo.UpsertAsync(channel);
|
||||
|
||||
Assert.Equal(_timeProvider.GetUtcNow(), result.UpdatedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ExistingChannel_ReturnsChannel()
|
||||
{
|
||||
var repo = new NotifyChannelRepositoryAdapter(_timeProvider);
|
||||
var channel = new NotifyChannelDocument
|
||||
{
|
||||
Id = "ch-002",
|
||||
TenantId = "tenant-001",
|
||||
Name = "Slack Channel",
|
||||
ChannelType = "slack"
|
||||
};
|
||||
await repo.UpsertAsync(channel);
|
||||
|
||||
var result = await repo.GetByIdAsync("tenant-001", "ch-002");
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("ch-002", result.Id);
|
||||
Assert.Equal("Slack Channel", result.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_NonExistent_ReturnsNull()
|
||||
{
|
||||
var repo = new NotifyChannelRepositoryAdapter(_timeProvider);
|
||||
|
||||
var result = await repo.GetByIdAsync("tenant-001", "non-existent");
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByNameAsync_ExistingChannel_ReturnsChannel()
|
||||
{
|
||||
var repo = new NotifyChannelRepositoryAdapter(_timeProvider);
|
||||
var channel = new NotifyChannelDocument
|
||||
{
|
||||
Id = "ch-003",
|
||||
TenantId = "tenant-001",
|
||||
Name = "Teams Notifications",
|
||||
ChannelType = "teams"
|
||||
};
|
||||
await repo.UpsertAsync(channel);
|
||||
|
||||
var result = await repo.GetByNameAsync("tenant-001", "Teams Notifications");
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("ch-003", result.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAllAsync_FilteredByEnabled_ReturnsOnlyEnabled()
|
||||
{
|
||||
var repo = new NotifyChannelRepositoryAdapter(_timeProvider);
|
||||
await repo.UpsertAsync(new NotifyChannelDocument { Id = "ch-e1", TenantId = "t1", Name = "E1", ChannelType = "email", Enabled = true });
|
||||
await repo.UpsertAsync(new NotifyChannelDocument { Id = "ch-e2", TenantId = "t1", Name = "E2", ChannelType = "email", Enabled = false });
|
||||
await repo.UpsertAsync(new NotifyChannelDocument { Id = "ch-e3", TenantId = "t1", Name = "E3", ChannelType = "slack", Enabled = true });
|
||||
|
||||
var enabled = await repo.GetAllAsync("t1", enabled: true);
|
||||
var disabled = await repo.GetAllAsync("t1", enabled: false);
|
||||
|
||||
Assert.Equal(2, enabled.Count);
|
||||
Assert.Single(disabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAllAsync_FilteredByChannelType_ReturnsMatchingType()
|
||||
{
|
||||
var repo = new NotifyChannelRepositoryAdapter(_timeProvider);
|
||||
await repo.UpsertAsync(new NotifyChannelDocument { Id = "ch-t1", TenantId = "t1", Name = "T1", ChannelType = "email" });
|
||||
await repo.UpsertAsync(new NotifyChannelDocument { Id = "ch-t2", TenantId = "t1", Name = "T2", ChannelType = "slack" });
|
||||
await repo.UpsertAsync(new NotifyChannelDocument { Id = "ch-t3", TenantId = "t1", Name = "T3", ChannelType = "email" });
|
||||
|
||||
var result = await repo.GetAllAsync("t1", channelType: "email");
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.All(result, c => Assert.Equal("email", c.ChannelType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAsync_ExistingChannel_ReturnsTrue()
|
||||
{
|
||||
var repo = new NotifyChannelRepositoryAdapter(_timeProvider);
|
||||
await repo.UpsertAsync(new NotifyChannelDocument { Id = "ch-del", TenantId = "t1", Name = "Delete Me", ChannelType = "webhook" });
|
||||
|
||||
var deleted = await repo.DeleteAsync("t1", "ch-del");
|
||||
var afterDelete = await repo.GetByIdAsync("t1", "ch-del");
|
||||
|
||||
Assert.True(deleted);
|
||||
Assert.Null(afterDelete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAsync_NonExistent_ReturnsFalse()
|
||||
{
|
||||
var repo = new NotifyChannelRepositoryAdapter(_timeProvider);
|
||||
|
||||
var deleted = await repo.DeleteAsync("t1", "non-existent");
|
||||
|
||||
Assert.False(deleted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetEnabledByTypeAsync_ReturnsOnlyEnabledOfType()
|
||||
{
|
||||
var repo = new NotifyChannelRepositoryAdapter(_timeProvider);
|
||||
await repo.UpsertAsync(new NotifyChannelDocument { Id = "ch-ebt1", TenantId = "t1", Name = "EBT1", ChannelType = "slack", Enabled = true });
|
||||
await repo.UpsertAsync(new NotifyChannelDocument { Id = "ch-ebt2", TenantId = "t1", Name = "EBT2", ChannelType = "slack", Enabled = false });
|
||||
await repo.UpsertAsync(new NotifyChannelDocument { Id = "ch-ebt3", TenantId = "t1", Name = "EBT3", ChannelType = "email", Enabled = true });
|
||||
|
||||
var result = await repo.GetEnabledByTypeAsync("t1", "slack");
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal("ch-ebt1", result[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertAsync_UpdateExisting_UpdatesTimestamp()
|
||||
{
|
||||
var repo = new NotifyChannelRepositoryAdapter(_timeProvider);
|
||||
var channel = new NotifyChannelDocument { Id = "ch-upd", TenantId = "t1", Name = "Original", ChannelType = "email" };
|
||||
await repo.UpsertAsync(channel);
|
||||
var firstUpdate = _timeProvider.GetUtcNow();
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(5));
|
||||
channel.Name = "Updated";
|
||||
await repo.UpsertAsync(channel);
|
||||
|
||||
var result = await repo.GetByIdAsync("t1", "ch-upd");
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("Updated", result.Name);
|
||||
Assert.True(result.UpdatedAt > firstUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for NotifyChannelDocument.
|
||||
/// </summary>
|
||||
public sealed class NotifyChannelDocumentTests
|
||||
{
|
||||
[Fact]
|
||||
public void NotifyChannelDocument_DefaultValues_AreSet()
|
||||
{
|
||||
var doc = new NotifyChannelDocument();
|
||||
|
||||
Assert.NotEmpty(doc.Id);
|
||||
Assert.Equal(string.Empty, doc.TenantId);
|
||||
Assert.Equal(string.Empty, doc.Name);
|
||||
Assert.Equal(string.Empty, doc.ChannelType);
|
||||
Assert.True(doc.Enabled);
|
||||
Assert.Equal("{}", doc.Config);
|
||||
Assert.Null(doc.Credentials);
|
||||
Assert.Equal("{}", doc.Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for NotifyRuleDocument.
|
||||
/// </summary>
|
||||
public sealed class NotifyRuleDocumentTests
|
||||
{
|
||||
[Fact]
|
||||
public void NotifyRuleDocument_DefaultValues_AreSet()
|
||||
{
|
||||
var doc = new NotifyRuleDocument();
|
||||
|
||||
Assert.NotEmpty(doc.Id);
|
||||
Assert.Equal(string.Empty, doc.TenantId);
|
||||
Assert.Equal(string.Empty, doc.Name);
|
||||
Assert.True(doc.Enabled);
|
||||
Assert.Equal(0, doc.Priority);
|
||||
Assert.Equal("{}", doc.EventFilter);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for NotifyTemplateDocument.
|
||||
/// </summary>
|
||||
public sealed class NotifyTemplateDocumentTests
|
||||
{
|
||||
[Fact]
|
||||
public void NotifyTemplateDocument_DefaultValues_AreSet()
|
||||
{
|
||||
var doc = new NotifyTemplateDocument();
|
||||
|
||||
Assert.NotEmpty(doc.Id);
|
||||
Assert.Equal(string.Empty, doc.TenantId);
|
||||
Assert.Equal(string.Empty, doc.Name);
|
||||
Assert.Equal(string.Empty, doc.Subject);
|
||||
Assert.Equal(string.Empty, doc.Body);
|
||||
Assert.Equal("text", doc.Format);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for NotifyDeliveryDocument.
|
||||
/// </summary>
|
||||
public sealed class NotifyDeliveryDocumentTests
|
||||
{
|
||||
[Fact]
|
||||
public void NotifyDeliveryDocument_DefaultValues_AreSet()
|
||||
{
|
||||
var doc = new NotifyDeliveryDocument();
|
||||
|
||||
Assert.NotEmpty(doc.Id);
|
||||
Assert.Equal(string.Empty, doc.TenantId);
|
||||
Assert.Equal("pending", doc.Status);
|
||||
Assert.Equal(0, doc.RetryCount);
|
||||
Assert.Equal("{}", doc.Payload);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("pending")]
|
||||
[InlineData("sending")]
|
||||
[InlineData("sent")]
|
||||
[InlineData("failed")]
|
||||
[InlineData("retrying")]
|
||||
public void NotifyDeliveryDocument_Status_SupportedValues(string status)
|
||||
{
|
||||
var doc = new NotifyDeliveryDocument { Status = status };
|
||||
|
||||
Assert.Equal(status, doc.Status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseXunitV3>true</UseXunitV3>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Notify.Storage.InMemory\StellaOps.Notify.Storage.InMemory.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"diagnosticMessages": true,
|
||||
"parallelizeAssembly": true,
|
||||
"parallelizeTestCollections": true,
|
||||
"maxParallelThreads": -1
|
||||
}
|
||||
Reference in New Issue
Block a user