160 lines
6.0 KiB
Go
160 lines
6.0 KiB
Go
package callmemory
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"data-terminal-cloud/internal/volcsubtitle"
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"net/url"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestRecordingIdentityAndSignedURL(t *testing.T) {
|
||
|
|
prefixes, err := recordingPrefixes("KP01-020026091601", "bot_20260927080903000")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
key := prefixes[0] + "1790467744348_000058_c014e733.mp4"
|
||
|
|
record, ok := parseRecording(key, prefixes[0], "USER", 1045859)
|
||
|
|
if !ok || record.Duration != 58 || record.ID == "" {
|
||
|
|
t.Fatal(record, ok)
|
||
|
|
}
|
||
|
|
for _, bad := range []string{strings.Replace(key, "KP01-020026091601", "other", 1), key + "/extra", strings.Replace(key, "000058", "invalid", 1)} {
|
||
|
|
if _, ok := parseRecording(bad, prefixes[0], "USER", 1); ok {
|
||
|
|
t.Fatal("accepted foreign/malformed object")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if _, err = recordingPrefixes("../device", "bot_20260927080903000"); err == nil {
|
||
|
|
t.Fatal("unsafe device")
|
||
|
|
}
|
||
|
|
if _, err = recordingPrefixes("device", "persistent-bot"); err == nil {
|
||
|
|
t.Fatal("unverified room")
|
||
|
|
}
|
||
|
|
store, err := NewTOSRecordings("memory927", "cn-beijing", "test-ak", "test-sk", "")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
for _, action := range []string{"PLAY", "DOWNLOAD"} {
|
||
|
|
if action == "PLAY" {
|
||
|
|
if _, e := store.Sign(context.Background(), key, action, 60); e == nil {
|
||
|
|
t.Fatal("default domain playback allowed")
|
||
|
|
}
|
||
|
|
if e := store.ConfigurePlayback("https://recordings.example.com"); e != nil {
|
||
|
|
t.Fatal(e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
signed, e := store.Sign(context.Background(), key, action, 60)
|
||
|
|
if e != nil {
|
||
|
|
t.Fatal(e)
|
||
|
|
}
|
||
|
|
u, e := url.Parse(signed)
|
||
|
|
if e != nil {
|
||
|
|
t.Fatal(e)
|
||
|
|
}
|
||
|
|
host := "memory927.tos-cn-beijing.volces.com"
|
||
|
|
if action == "PLAY" {
|
||
|
|
host = "recordings.example.com"
|
||
|
|
}
|
||
|
|
if u.Host != host || u.Scheme != "https" || u.Query().Get("response-content-type") != "audio/mp4" || u.Query().Get("X-Tos-Expires") != "60" {
|
||
|
|
t.Fatal("incorrect direct URL options")
|
||
|
|
}
|
||
|
|
disposition := "inline"
|
||
|
|
if action == "DOWNLOAD" {
|
||
|
|
disposition = "attachment"
|
||
|
|
}
|
||
|
|
if !strings.HasPrefix(u.Query().Get("response-content-disposition"), disposition) {
|
||
|
|
t.Fatal("wrong disposition")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRecordingIdentityFromAuthenticatedSubtitle(t *testing.T) {
|
||
|
|
f, d := directFixture(t)
|
||
|
|
ctx := context.Background()
|
||
|
|
sid := uuid.NewString()
|
||
|
|
if _, err := f.s.DirectRequest(ctx, d, "start", DirectInput{SessionID: sid, BootID: f.boot}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
n := int64(1)
|
||
|
|
packet := volcsubtitle.Packet{Type: "subtitle", Data: []volcsubtitle.Segment{{UserID: "bot_20260927080903000", RoundID: &n, Sequence: &n, Definite: true, Text: "你好"}}}
|
||
|
|
if err := f.s.DirectSubtitle(ctx, sid, packet); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := f.s.DirectSubtitle(ctx, sid, packet); err != nil {
|
||
|
|
t.Fatal("duplicate", err)
|
||
|
|
}
|
||
|
|
var stored string
|
||
|
|
if err := f.p.QueryRow(ctx, `SELECT budget_snapshot->>'rtc_agent_user_id' FROM call_session WHERE id=$1::uuid`, sid).Scan(&stored); err != nil || stored != packet.Data[0].UserID {
|
||
|
|
t.Fatal("identity not retained", err)
|
||
|
|
}
|
||
|
|
packet.Data[0].UserID = "bot_20260927090903000"
|
||
|
|
if err := f.s.DirectSubtitle(ctx, sid, packet); err == nil {
|
||
|
|
t.Fatal("room reassignment accepted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type fakeRecordings struct {
|
||
|
|
items []CloudRecording
|
||
|
|
listed, signed int
|
||
|
|
seconds int64
|
||
|
|
}
|
||
|
|
|
||
|
|
func (f *fakeRecordings) PlaybackReady() bool { return true }
|
||
|
|
|
||
|
|
func (f *fakeRecordings) List(context.Context, string, string) ([]CloudRecording, error) {
|
||
|
|
f.listed++
|
||
|
|
return f.items, nil
|
||
|
|
}
|
||
|
|
func (f *fakeRecordings) Sign(_ context.Context, key, action string, seconds int64) (string, error) {
|
||
|
|
f.signed++
|
||
|
|
f.seconds = seconds
|
||
|
|
return "https://memory927.tos-cn-beijing.volces.com/fixture", nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCloudRecordingAuthorization(t *testing.T) {
|
||
|
|
f := setup(t)
|
||
|
|
m, _, _ := f.call(t)
|
||
|
|
f.exec(t, `UPDATE call_session SET budget_snapshot='{"device_name":"device","rtc_agent_user_id":"bot_20260927080903000"}' WHERE id=$1::uuid`, m.SessionID)
|
||
|
|
// The fixture copies active consent types; explicitly add raw audio if absent.
|
||
|
|
f.exec(t, `INSERT INTO consent_record(tenant_id,subject_id,consent_type,scope,policy_version,notice_digest,actor_user_id,evidence_ref) SELECT tenant_id,subject_id,'raw_audio_storage',scope,policy_version,notice_digest,actor_user_id,evidence_ref FROM consent_record c WHERE subject_id=$1::uuid AND NOT EXISTS(SELECT 1 FROM consent_record r WHERE r.subject_id=c.subject_id AND r.consent_type='raw_audio_storage') LIMIT 1`, f.subject)
|
||
|
|
store := &fakeRecordings{items: []CloudRecording{{ID: hash([]byte("fixture")), Key: "fixture", Speaker: "USER", Duration: 58, Bytes: 100}}}
|
||
|
|
f.s.ConfigureRecordings(store)
|
||
|
|
in := recordingInput{ElderID: f.elder, SessionID: m.SessionID, RecordingID: store.items[0].ID, Action: "PLAY"}
|
||
|
|
deny := func(code string) {
|
||
|
|
t.Helper()
|
||
|
|
_, e := f.s.MemoryRequest(context.Background(), f.principal, "calls/recording-access", jsonBytes(in))
|
||
|
|
if e == nil || !strings.Contains(e.Error(), code) {
|
||
|
|
t.Fatalf("wanted %s got %v", code, e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
deny("RECORDING_ACCESS_DENIED")
|
||
|
|
if store.signed != 0 || store.listed != 0 {
|
||
|
|
t.Fatal("storage accessed before authorization")
|
||
|
|
}
|
||
|
|
f.exec(t, `INSERT INTO recording_access_grant(tenant_id,subject_id,grantee_user_id,permission,purpose,granted_by,expires_at) VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000101','PLAY','test','10000000-0000-0000-0000-000000000101',now()+interval '90 seconds')`, f.tenant, f.subject)
|
||
|
|
f.request(t, "calls/recording-access", in)
|
||
|
|
if store.signed != 1 || store.seconds > 90 {
|
||
|
|
t.Fatal("grant TTL not enforced")
|
||
|
|
}
|
||
|
|
in.Action = "DOWNLOAD"
|
||
|
|
deny("RECORDING_ACCESS_DENIED")
|
||
|
|
in.Action = "PLAY"
|
||
|
|
in.RecordingID = hash([]byte("another-file"))
|
||
|
|
deny("RECORDING_NOT_FOUND")
|
||
|
|
in.RecordingID = store.items[0].ID
|
||
|
|
in.SessionID = uuid.NewString()
|
||
|
|
deny("CALL_NOT_FOUND")
|
||
|
|
in.SessionID = m.SessionID
|
||
|
|
other := setup(t)
|
||
|
|
otherCall, _, _ := other.call(t)
|
||
|
|
in.SessionID = otherCall.SessionID
|
||
|
|
deny("CALL_NOT_FOUND")
|
||
|
|
in.SessionID = m.SessionID
|
||
|
|
before := store.signed
|
||
|
|
f.exec(t, `UPDATE consent_record SET withdrawn_at=now() WHERE subject_id=$1::uuid AND consent_type='raw_audio_storage'`, f.subject)
|
||
|
|
deny("RECORDING_ACCESS_DENIED")
|
||
|
|
if store.signed != before {
|
||
|
|
t.Fatal("revoked consent exposed URL")
|
||
|
|
}
|
||
|
|
}
|