using Microsoft.Extensions.DependencyInjection; using StackExchange.Redis; using System.Security.Cryptography; using System.Text; using Tiku.Application.Security; using Tiku.Infrastructure; using Tiku.Infrastructure.Security; namespace Tiku.IntegrationTests; public sealed class RedisSecurityStoreTests { [Fact] public async Task Unavailable_redis_fails_closed_for_security_operations() { await using var provider = BuildProvider( "localhost:6399,connectTimeout=200,syncTimeout=200,asyncTimeout=200,abortConnect=false", $"integration-unavailable-{Guid.NewGuid():N}"); var store = provider.GetRequiredService(); await Assert.ThrowsAsync(() => store.ConsumeAsync( [ new DistributedRateLimitBucket("password:ip:test", 1, TimeSpan.FromSeconds(1)) ])); Assert.False(await store.PingAsync()); } [Fact] public async Task Two_instances_share_atomic_limit_and_keys_contain_no_plaintext_identifier() { var connectionString = Environment.GetEnvironmentVariable("TIKU_TEST_REDIS"); if (string.IsNullOrWhiteSpace(connectionString)) { return; } var environment = $"integration-{Guid.NewGuid():N}"; await using var first = BuildProvider(connectionString, environment); await using var second = BuildProvider(connectionString, environment); var store1 = first.GetRequiredService(); var store2 = second.GetRequiredService(); const string phone = "13812345678"; var phoneHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(phone))).ToLowerInvariant(); var bucket = new DistributedRateLimitBucket( $"sms-verify:tenant-id:login:{phoneHash}", 1, TimeSpan.FromMinutes(1)); try { var attempts = await Task.WhenAll(Enumerable.Range(0, 12).Select(index => (index & 1) == 0 ? store1.ConsumeAsync([bucket]) : store2.ConsumeAsync([bucket]))); Assert.Single(attempts, result => result.Allowed); Assert.All(attempts.Where(result => !result.Allowed), result => Assert.NotNull(result.RetryAfter)); var multiplexer = first.GetRequiredService(); var server = multiplexer.GetServer(multiplexer.GetEndPoints().Single()); var keys = server.Keys(pattern: $"tiku:{environment}:*").Select(key => key.ToString()).ToArray(); Assert.NotEmpty(keys); Assert.DoesNotContain(keys, key => key.Contains(phone, StringComparison.Ordinal)); } finally { var multiplexer = first.GetRequiredService(); var server = multiplexer.GetServer(multiplexer.GetEndPoints().Single()); var keys = server.Keys(pattern: $"tiku:{environment}:*").ToArray(); if (keys.Length > 0) { await multiplexer.GetDatabase().KeyDeleteAsync(keys); } } } private static ServiceProvider BuildProvider(string connectionString, string environment) { var services = new ServiceCollection(); services.AddLogging(); services.AddRedisSecurity(connectionString, environment); return services.BuildServiceProvider(); } }