package voice_test import ( "encoding/base64" "encoding/json" "strings" "testing" "time" "glchat/internal/voice" ) func TestIssueTokenHasLiveKitClaims(t *testing.T) { issuer := voice.NewIssuer("key123", "secret456", time.Hour) issuer.SetClock(func() time.Time { return time.Unix(1_700_000_000, 0) }) token, err := issuer.Issue(voice.RoomName(10, 20), "42", "Алиса", voice.Grants{ RoomJoin: true, CanPublish: true, CanSubscribe: true, CanPublishData: true, }) if err != nil { t.Fatalf("Issue: %v", err) } parts := strings.Split(token, ".") if len(parts) != 3 { t.Fatalf("JWT состоит из %d частей, ожидалось 3", len(parts)) } header, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { t.Fatalf("decode header: %v", err) } if !strings.Contains(string(header), "HS256") { t.Fatalf("header = %s", header) } payload, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { t.Fatalf("decode payload: %v", err) } var claims struct { Issuer string `json:"iss"` Subject string `json:"sub"` Name string `json:"name"` Video struct { Room string `json:"room"` RoomJoin bool `json:"roomJoin"` CanPublish bool `json:"canPublish"` } `json:"video"` ExpiresAt int64 `json:"exp"` } if err := json.Unmarshal(payload, &claims); err != nil { t.Fatalf("decode claims: %v", err) } if claims.Issuer != "key123" || claims.Subject != "42" || claims.Name != "Алиса" { t.Fatalf("claims = %+v", claims) } if claims.Video.Room != "guild_10_channel_20" || !claims.Video.RoomJoin || !claims.Video.CanPublish { t.Fatalf("video grants = %+v", claims.Video) } if claims.ExpiresAt != 1_700_000_000+3600 { t.Fatalf("exp = %d", claims.ExpiresAt) } } func TestIssuerDisabledWithoutKeys(t *testing.T) { issuer := voice.NewIssuer("", "", time.Hour) if issuer.Enabled() { t.Fatal("пустые ключи должны означать выключенный голос") } if _, err := issuer.Issue("room", "1", "user", voice.Grants{}); err == nil { t.Fatal("Issue без ключей должен возвращать ошибку") } } func TestParseRoomName(t *testing.T) { guildID, channelID, ok := voice.ParseRoomName("guild_123_channel_456") if !ok || guildID != 123 || channelID != 456 { t.Fatalf("ParseRoomName = %d/%d/%t", guildID, channelID, ok) } if _, _, ok := voice.ParseRoomName("что-то другое"); ok { t.Fatal("посторонняя строка не должна разбираться как имя комнаты") } } func TestResolveLiveKitURL(t *testing.T) { url, err := voice.ResolveLiveKitURL("wss://gl.example/rtc/") if err != nil || url != "wss://gl.example/rtc" { t.Fatalf("ResolveLiveKitURL = %q, %v", url, err) } if _, err := voice.ResolveLiveKitURL("http://gl.example"); err == nil { t.Fatal("http-адрес должен отклоняться") } } func TestVerifyWebhookSignature(t *testing.T) { issuer := voice.NewIssuer("key123", "secret456", time.Hour) body := []byte(`{"event":"participant_joined"}`) // Без заголовка — отказ. if err := issuer.VerifyWebhook("", body); err == nil { t.Fatal("подпись обязательна") } // Чужой подписью — отказ. other := voice.NewIssuer("key123", "another-secret", time.Hour) token, err := other.IssueWebhookForTest(body) if err != nil { t.Fatalf("issue: %v", err) } if err := issuer.VerifyWebhook(token, body); err == nil { t.Fatal("подпись чужим секретом должна отклоняться") } // Своей подписью, но другим телом — отказ. valid, err := issuer.IssueWebhookForTest(body) if err != nil { t.Fatalf("issue: %v", err) } if err := issuer.VerifyWebhook(valid, []byte(`{"event":"room_finished"}`)); err == nil { t.Fatal("несовпадение тела должно отклоняться") } // Корректная подпись — успех. if err := issuer.VerifyWebhook(valid, body); err != nil { t.Fatalf("VerifyWebhook: %v", err) } }