using System.Reflection; using System.Security.Cryptography; using System.Text; using OF_DL.Models; using OF_DL.Models.Config; using OF_DL.Models.OfdlApi; using OF_DL.Services; namespace OF_DL.Tests.Services; public class ApiServiceTests { [Fact] public void GetDynamicHeaders_ReturnsSignedHeaders() { FakeAuthService authService = new() { CurrentAuth = new Auth { UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_cookie=abc;" } }; ApiService service = CreateService(authService); DynamicRules rules = new() { AppToken = "app-token", StaticParam = "static", Prefix = "prefix", Suffix = "suffix", ChecksumConstant = 7, ChecksumIndexes = [0, 5, 10, 15] }; using DynamicRulesCacheScope _ = new(rules); Dictionary headers = service.GetDynamicHeaders("/api2/v2/users", "?limit=1"); Assert.Equal("application/json, text/plain", headers["accept"]); Assert.Equal("app-token", headers["app-token"]); Assert.Equal("auth_cookie=abc;", headers["cookie"]); Assert.Equal("unit-test-agent", headers["user-agent"]); Assert.Equal("xbc-token", headers["x-bc"]); Assert.Equal("123", headers["user-id"]); Assert.True(long.TryParse(headers["time"], out long timestamp)); string expectedSign = BuildSign(rules, timestamp, "/api2/v2/users?limit=1", "123"); Assert.Equal(expectedSign, headers["sign"]); } [Fact] public void GetDynamicHeaders_ThrowsWhenRulesInvalid() { FakeAuthService authService = new() { CurrentAuth = new Auth { UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_cookie=abc;" } }; ApiService service = CreateService(authService); DynamicRules rules = new() { AppToken = null, StaticParam = "static", Prefix = null, Suffix = "suffix", ChecksumConstant = null, ChecksumIndexes = [] }; using DynamicRulesCacheScope _ = new(rules); Exception ex = Assert.Throws(() => service.GetDynamicHeaders("/api2/v2/users", "?limit=1")); Assert.Contains("Invalid dynamic rules", ex.Message); } [Fact] public void GetDynamicHeaders_ThrowsWhenAuthMissingFields() { FakeAuthService authService = new() { CurrentAuth = new Auth { UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = null } }; ApiService service = CreateService(authService); DynamicRules rules = new() { AppToken = "app-token", StaticParam = "static", Prefix = "prefix", Suffix = "suffix", ChecksumConstant = 1, ChecksumIndexes = [0] }; using DynamicRulesCacheScope _ = new(rules); Exception ex = Assert.Throws(() => service.GetDynamicHeaders("/api2/v2/users", "?limit=1")); Assert.Contains("Auth service is missing required fields", ex.Message); } private static ApiService CreateService(FakeAuthService authService) => new(authService, new FakeConfigService(new Config()), new FakeDbService()); private static string BuildSign(DynamicRules rules, long timestamp, string pathWithQuery, string userId) { string input = $"{rules.StaticParam}\n{timestamp}\n{pathWithQuery}\n{userId}"; byte[] hashBytes = SHA1.HashData(Encoding.UTF8.GetBytes(input)); string hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); Assert.NotNull(rules.ChecksumConstant); int checksum = rules.ChecksumIndexes.Aggregate(0, (current, index) => current + hashString[index]) + rules.ChecksumConstant.Value; string checksumHex = checksum.ToString("X").ToLowerInvariant(); return $"{rules.Prefix}:{hashString}:{checksumHex}:{rules.Suffix}"; } private sealed class DynamicRulesCacheScope : IDisposable { private static readonly FieldInfo s_rulesField = typeof(ApiService).GetField("s_cachedDynamicRules", BindingFlags.NonPublic | BindingFlags.Static) ?? throw new InvalidOperationException("Unable to access cached rules field."); private static readonly FieldInfo s_expirationField = typeof(ApiService).GetField("s_cachedDynamicRulesExpiration", BindingFlags.NonPublic | BindingFlags.Static) ?? throw new InvalidOperationException("Unable to access cached rules expiration field."); private readonly object? _priorRules; private readonly DateTime? _priorExpiration; public DynamicRulesCacheScope(DynamicRules rules) { _priorRules = s_rulesField.GetValue(null); _priorExpiration = (DateTime?)s_expirationField.GetValue(null); s_rulesField.SetValue(null, rules); s_expirationField.SetValue(null, DateTime.UtcNow.AddHours(1)); } public void Dispose() { s_rulesField.SetValue(null, _priorRules); s_expirationField.SetValue(null, _priorExpiration); } } }