ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
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")
|
|
}
|
|
}
|