Files
crm-calls/calls/backend/Calls.Api.Tests/StoreTests.cs

175 lines
7.1 KiB
C#

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Time.Testing;
namespace Calls.Api.Tests;
/// <summary>Комнаты, гостевые ссылки, одноразовые коды и пропуски Jitsi — на временной базе.</summary>
public sealed class StoreTests : IDisposable
{
public const string Secret = "test-jwt-secret-0123456789abcdef0123456789";
private readonly string _dir = Path.Combine(Path.GetTempPath(), "calls-test-" + Guid.NewGuid().ToString("N"));
private readonly FakeTimeProvider _time = new(DateTimeOffset.Parse("2026-10-14T12:00:00Z"));
private readonly Store _store;
private static readonly CrmUser Anna = new("uid-anna", "anna@test", "Анна", 0);
private static readonly CrmUser Bob = new("uid-bob", "bob@test", null, 0);
public StoreTests()
{
var cfg = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["Storage:DataDir"] = _dir,
["Jitsi:AppSecret"] = Secret,
["Jitsi:Domain"] = "meet.test",
["Calls:LinkSecret"] = "test-link-secret-0123456789abcdef0123456789",
}).Build();
var db = new Db(cfg);
db.Init();
_store = new Store(db, new JitsiToken(cfg), cfg, _time);
}
public void Dispose()
{
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
try { Directory.Delete(_dir, true); } catch { /* временная папка */ }
}
[Fact]
public void Room_IsStablePerWorkspace_RandomAndWithoutWorkspaceId()
{
var a1 = _store.Room("wsmv5l7d425re160", Anna.Id);
var a2 = _store.Room("wsmv5l7d425re160", Bob.Id);
var b = _store.Room("shared", Anna.Id);
Assert.Equal(a1, a2);
Assert.NotEqual(a1, b);
Assert.Matches("^ws-[a-z0-9]{20}$", a1);
Assert.DoesNotContain("mv5l7d425re160", a1);
}
[Fact]
public void JoinCode_IsOneTime()
{
var (code, ttl) = _store.CreateJoinCode("shared", Anna);
Assert.Equal(60, ttl);
var s = _store.ExchangeCode(code);
Assert.NotNull(s);
Assert.Equal("member", s.Kind);
Assert.Equal("Анна", s.Name);
Assert.Null(_store.ExchangeCode(code));
}
[Fact]
public void JoinCode_ExpiresAfter60Seconds()
{
var (code, _) = _store.CreateJoinCode("shared", Anna);
_time.Advance(TimeSpan.FromSeconds(61));
Assert.Null(_store.ExchangeCode(code));
}
[Fact]
public void Pass_ForMember_IsModerator_ForOneRoom_15Minutes_SignedHs256()
{
var (code, _) = _store.CreateJoinCode("shared", Anna);
var s = _store.ExchangeCode(code)!;
var p = _store.IssuePass(s.Token)!;
Assert.Equal(_store.Room("shared", Anna.Id), p.Room);
Assert.Equal("meet.test", p.Domain);
var claims = VerifyHs256(p.Jwt);
Assert.Equal("tvorymo-crm", claims.GetProperty("iss").GetString());
Assert.Equal("jitsi", claims.GetProperty("aud").GetString());
Assert.Equal(p.Room, claims.GetProperty("room").GetString());
Assert.Equal(_time.GetUtcNow().AddMinutes(15).ToUnixTimeSeconds(), claims.GetProperty("exp").GetInt64());
var user = claims.GetProperty("context").GetProperty("user");
Assert.True(user.GetProperty("moderator").GetBoolean());
Assert.Equal("Анна", user.GetProperty("name").GetString());
Assert.Equal("anna@test", user.GetProperty("email").GetString());
Assert.Equal(Anna.Id, user.GetProperty("id").GetString());
}
[Fact]
public void Session_GivesFreshPassLater_UntilSessionExpires()
{
var (code, _) = _store.CreateJoinCode("shared", Anna);
var s = _store.ExchangeCode(code)!;
_time.Advance(TimeSpan.FromMinutes(40)); // первый пропуск давно истёк — окно берёт новый
var p = _store.IssuePass(s.Token)!;
Assert.Equal(_time.GetUtcNow().AddMinutes(15).ToUnixTimeSeconds(), p.Exp);
_time.Advance(TimeSpan.FromHours(12));
Assert.Null(_store.IssuePass(s.Token));
}
[Fact]
public void GuestLink_IsPersistent_GuestIsNotModerator()
{
var l1 = _store.GetGuestLink("shared", Anna);
var l2 = _store.GetGuestLink("shared", Bob);
Assert.Equal(l1.Token, l2.Token);
var g = _store.JoinAsGuest(l1.Token, "Гість Іван")!;
Assert.Equal("guest", g.Kind);
var p = _store.IssuePass(g.Token)!;
Assert.Equal(_store.Room("shared", Anna.Id), p.Room);
var user = VerifyHs256(p.Jwt).GetProperty("context").GetProperty("user");
Assert.False(user.GetProperty("moderator").GetBoolean());
Assert.Equal("Гість Іван", user.GetProperty("name").GetString());
}
[Fact]
public void GuestLink_Regenerate_OldStopsWorking_NewWorks_RecordsWho()
{
var old = _store.GetGuestLink("shared", Anna);
var fresh = _store.RegenerateGuestLink("shared", Bob);
Assert.NotEqual(old.Token, fresh.Token);
Assert.Null(_store.JoinAsGuest(old.Token, "Гість"));
Assert.NotNull(_store.JoinAsGuest(fresh.Token, "Гість"));
Assert.Equal("uid-bob", fresh.UpdatedBy);
Assert.Equal("bob@test", fresh.UpdatedByName); // имени нет — email
}
[Fact]
public void GuestLink_OfOtherWorkspace_LeadsToItsOwnRoom()
{
var a = _store.GetGuestLink("shared", Anna);
var b = _store.GetGuestLink("wsother000000", Anna);
var pa = _store.IssuePass(_store.JoinAsGuest(a.Token, "x")!.Token)!;
var pb = _store.IssuePass(_store.JoinAsGuest(b.Token, "x")!.Token)!;
Assert.NotEqual(pa.Room, pb.Room);
}
[Theory]
[InlineData("", null)]
[InlineData(" ", null)]
[InlineData(" Іван Петренко ", "Іван Петренко")]
[InlineData("a\u0000b", "ab")]
public void GuestName_IsCleaned(string input, string? expected) => Assert.Equal(expected, Store.CleanGuestName(input));
[Fact]
public void GuestName_IsCutTo60() => Assert.Equal(60, Store.CleanGuestName(new string('я', 100))!.Length);
[Fact]
public void Garbage_TokensAreRejected()
{
Assert.Null(_store.ExchangeCode("x"));
Assert.Null(_store.IssuePass("'; DROP TABLE call_sessions; --"));
Assert.Null(_store.FindGuestLink(new string('a', 43)));
}
public static JsonElement VerifyHs256(string jwt)
{
var parts = jwt.Split('.');
Assert.Equal(3, parts.Length);
var header = JsonDocument.Parse(B64(parts[0])).RootElement;
Assert.Equal("HS256", header.GetProperty("alg").GetString());
var sig = HMACSHA256.HashData(Encoding.UTF8.GetBytes(Secret), Encoding.ASCII.GetBytes(parts[0] + "." + parts[1]));
Assert.Equal(JitsiToken.B64(sig), parts[2]);
return JsonDocument.Parse(B64(parts[1])).RootElement;
}
private static byte[] B64(string s)
{
s = s.Replace('-', '+').Replace('_', '/');
return Convert.FromBase64String(s.PadRight(s.Length + (4 - s.Length % 4) % 4, '='));
}
}