package rpc import ( "context" "crypto/tls" "crypto/x509" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "time" agentpb "git.ipao.vip/rogee/go-sip/gen/agent" "git.ipao.vip/rogee/go-sip/internal/configread" "git.ipao.vip/rogee/go-sip/internal/oss" "git.ipao.vip/rogee/go-sip/internal/store" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" ) const recordingDispatcherID = "c046b893-8628-4589-ae50-619d049248a6" func recordingRPCFixture(t *testing.T) (*RecordingServer, *store.CurrentStore, context.Context, *agentpb.RequestRecordingUploadRequest, configread.CurrentSnapshot) { t.Helper() database, err := store.OpenCurrent(filepath.Join(t.TempDir(), "recordings.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = database.Close() }) readExample := func(name string, dst any) { t.Helper() raw, err := os.ReadFile(filepath.Join("..", "..", "contracts", "local", "examples", name+".json")) if err != nil { t.Fatal(err) } if err := json.Unmarshal(raw, dst); err != nil { t.Fatal(err) } } var snapshot configread.CurrentSnapshot readExample("config-read-task-asr", &snapshot.Task) readExample("config-read-sip", &snapshot.SIP) readExample("config-read-quota", &snapshot.Quota) var providers struct { Providers []configread.CurrentProvider `json:"providers"` } readExample("config-read-providers", &providers) snapshot.Providers = make(map[string]configread.CurrentProvider) for _, provider := range providers.Providers { snapshot.Providers[provider.ProviderRef] = provider } snapshot.SIP.Trunks = []byte(strings.Replace(string(snapshot.SIP.Trunks), `"max_concurrent_calls":null`, `"max_concurrent_calls":2`, 1)) if err := database.ApplyDiscoverySnapshot(recordingDispatcherID, []configread.CurrentDiscoveredTask{{TaskID: snapshot.Task.TaskID, TenantID: snapshot.Task.TenantID, TaskRevision: snapshot.Task.TaskRevision, Status: "running"}}); err != nil { t.Fatal(err) } if err := database.SaveSnapshot(snapshot); err != nil { t.Fatal(err) } if err := database.MarkReadyForSIP(recordingDispatcherID, 8); err != nil { t.Fatal(err) } command := store.CurrentExecuteCommand{DispatcherID: recordingDispatcherID, EventID: "call-recording-1", TenantID: 1001, TaskID: "task-asr", Callee: "15003164745", IssuedAt: "2026-09-21T01:30:00Z"} if _, _, err := database.RecordExecute(command); err != nil { t.Fatal(err) } now := time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC) if err := database.ReserveExecute(recordingDispatcherID, command.EventID, store.CurrentCallReservation{ TrunkID: "trunk-mock", SIPRevision: 8, CallerID: "BD00000000", DialedCallee: command.Callee, Deadline: now.Add(2 * time.Minute), }, now); err != nil { t.Fatal(err) } client, err := oss.NewClient(oss.Config{ Endpoint: "https://oss.example.invalid", Region: "cn-test", Bucket: "mock-bucket", KeyPrefix: "approved", AccessKeyID: "local-test-key", AccessKeySecret: "local-test-secret", GrantTTL: 15 * time.Minute, MaxAssetBytes: 1024, }) if err != nil { t.Fatal(err) } leaf := &x509.Certificate{Raw: []byte("isolated-test-agent-peer")} fingerprint := CertificateFingerprint(leaf) ctx := peer.NewContext(context.Background(), &peer.Peer{AuthInfo: credentials.TLSInfo{State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{leaf}}}}}) server := &RecordingServer{ Store: database, OSS: client, DispatcherID: recordingDispatcherID, TrustedFingerprints: map[string]struct{}{fingerprint: {}}, Now: func() time.Time { return now }, AuthorizeSession: func(meta *agentpb.RequestMeta) error { if meta.AgentId != "agent-mock" || meta.CellId != "cell-mock" || meta.BootId != "boot-mock" || meta.DispatcherEpoch != "epoch-7" || meta.SessionGeneration != 1 { return errors.New("Agent boot/session is not activated") } return nil }, } request := &agentpb.RequestRecordingUploadRequest{ Meta: &agentpb.RequestMeta{AgentId: "agent-mock", CellId: "cell-mock", BootId: "boot-mock", DispatcherEpoch: "epoch-7", SessionGeneration: 1, OperationId: "request-1", IdempotencyKey: "request-1"}, DispatcherId: recordingDispatcherID, TenantId: 1001, SourceEventId: command.EventID, UploadId: "upload-recording-1", Asset: &agentpb.AssetDescriptor{Kind: agentpb.AssetKind_ASSET_KIND_RECORDING, ExecutionId: command.EventID, CallId: command.EventID, AssetId: "recording-1", Format: "wav", Channels: 1, SampleRateHz: 16000, DurationMs: 3000, SizeBytes: 128, ChecksumSha256: strings.Repeat("a", 64)}, } return server, database, ctx, request, snapshot } func recordingResultPayload(t *testing.T, task configread.CurrentSnapshot, grant *agentpb.UploadGrant) []byte { t.Helper() result := map[string]any{ "task_id": "task-asr", "caller_profile_id": task.Task.CallerProfileID, "callee": "15003164745", "trunk_id": "trunk-mock", "started_at": "2026-09-21T01:30:01Z", "ended_at": "2026-09-21T01:30:06Z", "duration_ms": 5000, "outcome": "no_answer", "reason_code": 486, "reason_message": "busy", "transcript": []any{}, "opt_out": false, "recording": map[string]any{}, } if grant != nil { result["outcome"] = "answered" result["reason_code"] = 200 result["reason_message"] = "completed" result["recording"] = map[string]any{ "status": "uploaded", "bucket": grant.GetBucket(), "object_key": grant.GetObjectKey(), "format": "wav", "channels": 1, "sample_rate_hz": 16000, "duration_ms": 3000, "size_bytes": 128, "checksum_sha256": strings.Repeat("a", 64), } } raw, err := json.Marshal(result) if err != nil { t.Fatal(err) } return raw } func TestRecordingServerAuthenticatesOriginalGrantAndExplicitReissue(t *testing.T) { server, database, ctx, request, _ := recordingRPCFixture(t) if _, err := server.RequestRecordingUpload(context.Background(), request); status.Code(err) != codes.Unauthenticated { t.Fatalf("upload token issued without verified mTLS peer: %v", err) } if _, err := database.LoadRecordingUpload(request.DispatcherId, request.SourceEventId); !errors.Is(err, store.ErrCurrentUploadNotFound) { t.Fatalf("unauthenticated Agent persisted a grant: %v", err) } wrongDispatcher := proto.Clone(request).(*agentpb.RequestRecordingUploadRequest) wrongDispatcher.DispatcherId = "another-dispatcher" if _, err := server.RequestRecordingUpload(ctx, wrongDispatcher); status.Code(err) != codes.PermissionDenied { t.Fatalf("another Dispatcher obtained this D's token: %v", err) } wrongTenant := proto.Clone(request).(*agentpb.RequestRecordingUploadRequest) wrongTenant.TenantId++ if _, err := server.RequestRecordingUpload(ctx, wrongTenant); status.Code(err) != codes.FailedPrecondition { t.Fatalf("foreign tenant obtained an OSS grant: %v", err) } wrongSession := proto.Clone(request).(*agentpb.RequestRecordingUploadRequest) wrongSession.Meta.BootId = "not-active-boot" if _, err := server.RequestRecordingUpload(ctx, wrongSession); status.Code(err) != codes.PermissionDenied { t.Fatalf("changed Agent boot obtained an OSS token: %v", err) } response, err := server.RequestRecordingUpload(ctx, request) grant := response.GetGrant() if err != nil || grant.GetBucket() != "mock-bucket" || grant.GetObjectKey() == "" || grant.GetUploadId() != request.UploadId || grant.GetExpiresAtUnixMs() != server.Now().Add(15*time.Minute).UnixMilli() { t.Fatalf("approved bound recording grant absent or changed: bucket=%q key=%q err=%v", grant.GetBucket(), grant.GetObjectKey(), err) } stored, err := database.LoadRecordingUpload(request.DispatcherId, request.SourceEventId) if err != nil || stored.Bucket != grant.GetBucket() || stored.ObjectKey != grant.GetObjectKey() || stored.UploadID != request.UploadId || stored.ChecksumSHA256 != request.Asset.ChecksumSha256 { t.Fatalf("Dispatcher did not persist original OSS asset: stored=%+v err=%v", stored, err) } reissuedResponse, err := server.RequestRecordingUpload(ctx, request) reissued := reissuedResponse.GetGrant() if err != nil || reissued.GetBucket() != grant.GetBucket() || reissued.GetObjectKey() != grant.GetObjectKey() || reissued.GetUploadId() != grant.GetUploadId() { t.Fatalf("explicit token request changed original target: err=%v", err) } changedAsset := proto.Clone(request).(*agentpb.RequestRecordingUploadRequest) changedAsset.Asset.AssetId = "another-recording" if _, err := server.RequestRecordingUpload(ctx, changedAsset); status.Code(err) != codes.AlreadyExists { t.Fatalf("new object replaced approved recording: %v", err) } } func TestRecordingServerConfirmedEndAndUploadedResultOutbox(t *testing.T) { server, database, ctx, request, snapshot := recordingRPCFixture(t) response, err := server.RequestRecordingUpload(ctx, request) if err != nil { t.Fatal(err) } grant := response.GetGrant() result := &agentpb.ReportCallResultRequest{Meta: request.Meta, DispatcherId: request.DispatcherId, TenantId: request.TenantId, SourceEventId: request.SourceEventId, ResultPayloadJson: recordingResultPayload(t, snapshot, grant), Upload: &agentpb.UploadObservation{UploadId: request.UploadId, RecordingId: request.Asset.AssetId, PutStatusCode: 200, SizeBytes: 128, ChecksumSha256: request.Asset.ChecksumSha256}} if _, err := server.ReportCallResult(ctx, result); status.Code(err) != codes.FailedPrecondition { t.Fatalf("result escaped before confirmed end: %v", err) } end := &agentpb.ReportCallEndedRequest{Meta: request.Meta, DispatcherId: request.DispatcherId, TenantId: request.TenantId, SourceEventId: request.SourceEventId} if _, err := server.ReportCallEnded(ctx, end); err != nil { t.Fatalf("early Agent end did not persist original acknowledgment: %v", err) } if err := database.MarkExecuteDispatched(request.DispatcherId, request.SourceEventId); err != nil { t.Fatalf("late originate RPC response broke ended call: %v", err) } if _, err := server.ReportCallEnded(ctx, end); err != nil { t.Fatalf("repeated Agent end fact lost idempotency: %v", err) } withoutUpload := proto.Clone(result).(*agentpb.ReportCallResultRequest) withoutUpload.Upload = nil withoutUpload.ResultPayloadJson = recordingResultPayload(t, snapshot, nil) if _, err := server.ReportCallResult(ctx, withoutUpload); status.Code(err) != codes.FailedPrecondition { t.Fatalf("OSS failure was relabeled as no recording: %v", err) } badProof := proto.Clone(result).(*agentpb.ReportCallResultRequest) badProof.Upload.PutStatusCode = 503 if _, err := server.ReportCallResult(ctx, badProof); status.Code(err) != codes.FailedPrecondition { t.Fatalf("failed PUT was reported as uploaded: %v", err) } first, err := server.ReportCallResult(ctx, result) if err != nil || first.GetReceipt().GetFactId() == "" { t.Fatalf("confirmed original upload could not report one result: fact=%q err=%v", first.GetReceipt().GetFactId(), err) } stored, err := database.LoadRecordingUpload(request.DispatcherId, request.SourceEventId) if err != nil || stored.ConfirmedAt == "" { t.Fatalf("uploaded fact was not committed with result outbox: state=%+v err=%v", stored, err) } duplicate, err := server.ReportCallResult(ctx, result) if err != nil || duplicate.GetReceipt().GetFactId() != first.GetReceipt().GetFactId() { t.Fatalf("duplicate result created another event: original=%q duplicate=%q err=%v", first.GetReceipt().GetFactId(), duplicate.GetReceipt().GetFactId(), err) } outbox, err := database.ListPendingOutbox(request.DispatcherId) if err != nil || len(outbox) != 2 || outbox[0].EventType != "call.execute" || outbox[1].EventType != "call.execute.result" { t.Fatalf("Agent end and upload did not produce exactly one ACK+result: events=%+v err=%v", outbox, err) } if _, err := server.RequestRecordingUpload(ctx, request); status.Code(err) != codes.FailedPrecondition { t.Fatalf("confirmed uploaded asset got another PUT token: %v", err) } } func TestRecordingServerRejectsMalformedResultBeforeOutbox(t *testing.T) { server, database, ctx, request, _ := recordingRPCFixture(t) end := &agentpb.ReportCallEndedRequest{Meta: request.Meta, DispatcherId: request.DispatcherId, TenantId: request.TenantId, SourceEventId: request.SourceEventId} if _, err := server.ReportCallEnded(ctx, end); err != nil { t.Fatal(err) } bad := &agentpb.ReportCallResultRequest{Meta: request.Meta, DispatcherId: request.DispatcherId, TenantId: request.TenantId, SourceEventId: request.SourceEventId, ResultPayloadJson: []byte(`{"task_id":`)} if _, err := server.ReportCallResult(ctx, bad); status.Code(err) != codes.InvalidArgument { t.Fatalf("malformed result was not reported as bad Agent input: %v", err) } pending, err := database.ListPendingOutbox(request.DispatcherId) if err != nil || len(pending) != 1 || pending[0].EventType != "call.execute" { t.Fatalf("malformed payload escaped into final result: events=%+v err=%v", pending, err) } } func TestRecordingServerNoRecordingResultCannotGainLaterGrant(t *testing.T) { server, database, ctx, request, snapshot := recordingRPCFixture(t) end := &agentpb.ReportCallEndedRequest{Meta: request.Meta, DispatcherId: request.DispatcherId, TenantId: request.TenantId, SourceEventId: request.SourceEventId} if _, err := server.ReportCallEnded(ctx, end); err != nil { t.Fatal(err) } result := &agentpb.ReportCallResultRequest{Meta: request.Meta, DispatcherId: request.DispatcherId, TenantId: request.TenantId, SourceEventId: request.SourceEventId, ResultPayloadJson: recordingResultPayload(t, snapshot, nil)} first, err := server.ReportCallResult(ctx, result) if err != nil || first.GetReceipt().GetFactId() == "" { t.Fatalf("no-recording confirmed end was not reported: fact=%q err=%v", first.GetReceipt().GetFactId(), err) } if repeated, err := server.ReportCallResult(ctx, result); err != nil || repeated.GetReceipt().GetFactId() != first.GetReceipt().GetFactId() { t.Fatalf("no-recording result replay changed identity: err=%v", err) } if _, err := server.RequestRecordingUpload(ctx, request); status.Code(err) != codes.AlreadyExists { t.Fatalf("final no-recording result gained a later object: %v", err) } outbox, err := database.ListPendingOutbox(request.DispatcherId) if err != nil || len(outbox) != 2 { t.Fatalf("no-recording end has multiple or missing results: events=%+v err=%v", outbox, err) } }