206 lines
10 KiB
Go
206 lines
10 KiB
Go
package media
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"data-terminal-cloud/internal/audit"
|
|
"data-terminal-cloud/internal/database"
|
|
"data-terminal-cloud/internal/deviceauth"
|
|
identitypkg "data-terminal-cloud/internal/identity"
|
|
"data-terminal-cloud/internal/orchestration"
|
|
"data-terminal-cloud/internal/outbox"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
const (
|
|
mediaTestConsentID = "90000000-0000-0000-0000-000000000801"
|
|
mediaTestDeviceID = "10000000-0000-0000-0000-000000000501"
|
|
mediaTestDevicePub = "10000000-0000-0000-0000-000000000511"
|
|
)
|
|
|
|
type mediaTestStore struct {
|
|
wav []byte
|
|
}
|
|
|
|
func (*mediaTestStore) PresignUpload(_ context.Context, reference string, _ time.Duration) (string, error) {
|
|
return "https://upload.test/" + reference, nil
|
|
}
|
|
|
|
func (*mediaTestStore) PresignMediaDownload(_ context.Context, reference string, _ time.Duration, _, _ string, _ bool) (string, error) {
|
|
return "https://download.test/" + reference, nil
|
|
}
|
|
|
|
func (*mediaTestStore) VerifyDigest(context.Context, string, string, int64) error { return nil }
|
|
|
|
func (s *mediaTestStore) ReadPrefix(context.Context, string, int64) ([]byte, error) {
|
|
return append([]byte(nil), s.wav...), nil
|
|
}
|
|
|
|
func TestUploadVerificationAndLocalDeleteLifecycle(t *testing.T) {
|
|
databaseURL := os.Getenv("TEST_DATABASE_URL")
|
|
if databaseURL == "" {
|
|
t.Skip("TEST_DATABASE_URL is not set")
|
|
}
|
|
ctx := context.Background()
|
|
pool, err := database.Open(ctx, databaseURL, "media-service-test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer pool.Close()
|
|
cleanupMediaFixture(t, ctx, pool)
|
|
defer cleanupMediaFixture(t, context.Background(), pool)
|
|
if _, err := pool.Exec(ctx, `INSERT INTO consent_record (
|
|
id,public_id,tenant_id,subject_id,consent_type,scope,policy_version,notice_digest,actor_user_id
|
|
) VALUES (
|
|
$1::uuid,'90000000-0000-0000-0000-000000000802','10000000-0000-0000-0000-000000000001',
|
|
'10000000-0000-0000-0000-000000000401','raw_audio_storage','{}'::jsonb,'media-test-v1',repeat('d',64),
|
|
'10000000-0000-0000-0000-000000000101'
|
|
)`, mediaTestConsentID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `INSERT INTO consent_snapshot (
|
|
id,public_id,tenant_id,subject_id,snapshot_version,effective_consents,source_consent_ids
|
|
) VALUES (
|
|
'90000000-0000-0000-0000-000000000803','90000000-0000-0000-0000-000000000804',
|
|
'10000000-0000-0000-0000-000000000001','10000000-0000-0000-0000-000000000401',9001,
|
|
'{"voice_chat":true,"transcript_storage":true,"raw_audio_storage":true}'::jsonb,
|
|
'["90000000-0000-0000-0000-000000000802"]'::jsonb
|
|
)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
identity := deviceauth.Identity{ID: mediaTestDeviceID, PublicID: mediaTestDevicePub, TenantID: "10000000-0000-0000-0000-000000000001", Status: "registered"}
|
|
tx, err := pool.BeginTx(ctx, pgx.TxOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resume := true
|
|
created, internalID, err := orchestration.NewRepository().Create(ctx, tx, identity, orchestration.CreateInput{
|
|
DeviceTime: time.Now().UTC(), Capabilities: orchestration.Capabilities{RTC: true, AudioTracks: []string{"near"}, UploadResume: &resume},
|
|
Network: orchestration.Network{Type: "test"}, RequestedGoal: "media_integration",
|
|
}, orchestration.AllocationOptions{Now: time.Now().UTC(), AllocationExpires: time.Now().UTC().Add(time.Minute), TokenExpires: time.Now().UTC().Add(5 * time.Minute), RTCAppID: "test", EngineMode: "s2s_hybrid", HeartbeatInterval: 15})
|
|
if err != nil {
|
|
_ = tx.Rollback(ctx)
|
|
t.Fatal(err)
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `UPDATE conversation SET status='COMPLETED',ended_at=now(),retention_mode='transcript_and_audio' WHERE id=$1::uuid`, internalID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `UPDATE rtc_task SET status='COMPLETED',lease_expires_at=NULL WHERE conversation_id=$1::uuid`, internalID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wav := syntheticWAV(16000, 1, 16000)
|
|
digestValue := sha256.Sum256(wav)
|
|
declaration := SegmentDeclaration{Track: "near", SequenceNo: 1, Codec: "wav_pcm_s16le", ContentType: "audio/wav", DurationMS: 1000, ExpectedSize: int64(len(wav)), ExpectedDigest: hex.EncodeToString(digestValue[:])}
|
|
store := &mediaTestStore{wav: wav}
|
|
service, err := NewService(pool, outbox.NewStore(pool), audit.NewRepository(), store, 15*time.Minute)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
batch, err := service.PrepareUploadBatch(ctx, identity, created.ConversationID, "media-integration-batch", "media-prepare", []SegmentDeclaration{declaration})
|
|
if err != nil || batch.Status != "UPLOADING" || len(batch.Segments) != 1 || batch.Segments[0].UploadURL == "" {
|
|
t.Fatalf("prepared batch=%#v err=%v", batch, err)
|
|
}
|
|
replayed, err := service.PrepareUploadBatch(ctx, identity, created.ConversationID, "media-integration-batch", "media-prepare-replay", []SegmentDeclaration{declaration})
|
|
if err != nil || replayed.ID != batch.ID {
|
|
t.Fatalf("replayed batch=%#v err=%v", replayed, err)
|
|
}
|
|
verifying, err := service.CompleteUploadBatch(ctx, identity, batch.ID, "media-complete")
|
|
if err != nil || verifying.Status != "VERIFYING" {
|
|
t.Fatalf("verifying batch=%#v err=%v", verifying, err)
|
|
}
|
|
processor, err := NewProcessor(pool, store, audit.NewRepository())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
payload, _ := json.Marshal(map[string]string{"batch_id": batch.ID})
|
|
if err := processor.Handle(ctx, outbox.Event{Payload: payload}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
completed, err := service.GetUploadBatch(ctx, identity, batch.ID, false)
|
|
if err != nil || completed.Status != "COMPLETED" || completed.VerifiedCount != 1 || completed.Segments[0].Status != "VERIFIED" {
|
|
t.Fatalf("completed batch=%#v err=%v", completed, err)
|
|
}
|
|
segmentID := completed.Segments[0].ID
|
|
owner := identitypkg.Principal{Issuer: "http://localhost:58180/realms/voice-local", Subject: "00000000-0000-0000-0000-000000000101"}
|
|
editor := identitypkg.Principal{Issuer: "http://localhost:58180/realms/voice-local", Subject: "00000000-0000-0000-0000-000000000102"}
|
|
grant, err := service.GrantRecordingAccess(ctx, owner, "10000000-0000-0000-0000-000000000411", "10000000-0000-0000-0000-000000000112", "PLAY", "媒体集成测试播放", "media-grant", time.Now().UTC().Add(time.Hour))
|
|
if err != nil || grant.Permission != "PLAY" {
|
|
t.Fatalf("recording grant=%#v err=%v", grant, err)
|
|
}
|
|
grants, err := service.ListRecordingAccessGrants(ctx, owner, "10000000-0000-0000-0000-000000000411")
|
|
if err != nil || len(grants) != 1 || grants[0].ID != grant.ID {
|
|
t.Fatalf("recording grants=%#v err=%v", grants, err)
|
|
}
|
|
conversationMedia, err := service.GetConversationMedia(ctx, editor, created.ConversationID)
|
|
if err != nil || !conversationMedia.CanPlay || conversationMedia.CanDownload || len(conversationMedia.Segments) != 1 {
|
|
t.Fatalf("conversation media=%#v err=%v", conversationMedia, err)
|
|
}
|
|
access, err := service.CreateMediaAccess(ctx, editor, segmentID, "PLAY", "媒体集成测试试听", "media-access-play")
|
|
if err != nil || access.Action != "PLAY" || access.AccessURL == "" {
|
|
t.Fatalf("media access=%#v err=%v", access, err)
|
|
}
|
|
if _, err := service.CreateMediaAccess(ctx, editor, segmentID, "DOWNLOAD", "未授权下载验证", "media-access-denied"); !errors.Is(err, ErrRecordingAccessDenied) {
|
|
t.Fatalf("download access error=%v", err)
|
|
}
|
|
revoked, err := service.RevokeRecordingAccess(ctx, owner, grant.ID, "media-revoke")
|
|
if err != nil || revoked.RevokedAt == nil {
|
|
t.Fatalf("revoked grant=%#v err=%v", revoked, err)
|
|
}
|
|
if _, err := service.GetConversationMedia(ctx, editor, created.ConversationID); !errors.Is(err, ErrRecordingAccessDenied) {
|
|
t.Fatalf("revoked conversation media error=%v", err)
|
|
}
|
|
if _, err := service.AcknowledgeLocalDelete(ctx, identity, segmentID, "DELETED", "", "media-delete-early"); !errors.Is(err, ErrLocalDeleteTooEarly) {
|
|
t.Fatalf("early local delete error=%v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `UPDATE media_segment SET local_delete_after=now()-interval '1 second' WHERE public_id=$1::uuid`, segmentID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
deleted, err := service.AcknowledgeLocalDelete(ctx, identity, segmentID, "DELETED", "", "media-delete")
|
|
if err != nil || deleted.LocalDeleteStatus != "DELETED" || deleted.LocalDeleteReportedAt == nil {
|
|
t.Fatalf("deleted segment=%#v err=%v", deleted, err)
|
|
}
|
|
}
|
|
|
|
func cleanupMediaFixture(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
|
|
t.Helper()
|
|
var ids []string
|
|
rows, _ := pool.Query(ctx, `SELECT id::text FROM conversation WHERE device_id=$1::uuid AND EXISTS(SELECT 1 FROM context_snapshot cs WHERE cs.id=conversation.context_snapshot_id AND cs.sanitized_summary LIKE '%media_integration%')`, mediaTestDeviceID)
|
|
if rows != nil {
|
|
for rows.Next() {
|
|
var id string
|
|
_ = rows.Scan(&id)
|
|
ids = append(ids, id)
|
|
}
|
|
rows.Close()
|
|
}
|
|
for _, id := range ids {
|
|
for _, statement := range []string{
|
|
`DELETE FROM outbox_event WHERE aggregate_id IN (SELECT public_id FROM upload_batch WHERE conversation_id=$1::uuid)`,
|
|
`DELETE FROM media_access_log WHERE conversation_id=$1::uuid`,
|
|
`DELETE FROM audit_event WHERE resource_id IN (SELECT public_id FROM upload_batch WHERE conversation_id=$1::uuid UNION SELECT public_id FROM media_segment WHERE conversation_id=$1::uuid)`,
|
|
`DELETE FROM upload_batch_segment WHERE upload_batch_id IN (SELECT id FROM upload_batch WHERE conversation_id=$1::uuid)`,
|
|
`DELETE FROM upload_batch WHERE conversation_id=$1::uuid`, `DELETE FROM media_segment WHERE conversation_id=$1::uuid`,
|
|
`DELETE FROM conversation_state_history WHERE conversation_id=$1::uuid`, `DELETE FROM rtc_task WHERE conversation_id=$1::uuid`, `DELETE FROM conversation WHERE id=$1::uuid`,
|
|
} {
|
|
_, _ = pool.Exec(ctx, statement, id)
|
|
}
|
|
}
|
|
_, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE request_id LIKE 'media-%'`)
|
|
_, _ = pool.Exec(ctx, `DELETE FROM recording_access_grant WHERE purpose='媒体集成测试播放'`)
|
|
_, _ = pool.Exec(ctx, `DELETE FROM context_snapshot WHERE sanitized_summary LIKE '%media_integration%'`)
|
|
_, _ = pool.Exec(ctx, `DELETE FROM consent_snapshot WHERE id IN ('90000000-0000-0000-0000-000000000803'::uuid) OR (subject_id='10000000-0000-0000-0000-000000000401'::uuid AND snapshot_version=9001)`)
|
|
_, _ = pool.Exec(ctx, `DELETE FROM consent_record WHERE id=$1::uuid`, mediaTestConsentID)
|
|
}
|