package httpapi import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "data-terminal-cloud/internal/orchestration" "data-terminal-cloud/internal/voiceprovider" ) type fakeProviderCallbackProcessor struct{ calls int } func (p *fakeProviderCallbackProcessor) Process(context.Context, voiceprovider.CallbackEvent, []byte) (orchestration.ProviderEventResult, error) { p.calls++ return orchestration.ProviderEventResult{ConversationID: "conversation-1", Status: "ACTIVE"}, nil } func TestProviderCallbackRequiresSignatureAndProcessesEvent(t *testing.T) { secret := []byte("01234567890123456789012345678901") processor := &fakeProviderCallbackProcessor{} handler := VoiceProviderCallbackHandler(secret, time.Minute, processor) event := voiceprovider.CallbackEvent{ Provider: "mock", EventID: "event-1", TaskRef: "task-1", ConversationID: "conversation-1", EventType: "voice_chat.started", Status: "ACTIVE", OccurredAt: time.Now().UTC(), } body, _ := json.Marshal(event) unsigned := httptest.NewRequest(http.MethodPost, "/callbacks/v1/vertc/voice-chat", strings.NewReader(string(body))) unsignedRecorder := httptest.NewRecorder() handler.ServeHTTP(unsignedRecorder, unsigned) if unsignedRecorder.Code != http.StatusUnauthorized { t.Fatalf("unsigned status=%d", unsignedRecorder.Code) } timestamp := time.Now().UTC().Format(time.RFC3339) signed := httptest.NewRequest(http.MethodPost, "/callbacks/v1/vertc/voice-chat", strings.NewReader(string(body))) signed.Header.Set(voiceprovider.HeaderCallbackTimestamp, timestamp) signed.Header.Set(voiceprovider.HeaderCallbackSignature, voiceprovider.SignCallback(secret, timestamp, body)) recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, signed) if recorder.Code != http.StatusOK || processor.calls != 1 || !containsText(recorder.Body.String(), "ACTIVE") { t.Fatalf("status=%d calls=%d body=%s", recorder.Code, processor.calls, recorder.Body.String()) } } func TestProviderCallbackValidatesUsageMetrics(t *testing.T) { validMetric := voiceprovider.UsageMetric{UsageCategory: "LLM", MetricCode: "input_tokens", Quantity: "123.456", Unit: "token"} tests := []struct { name string event voiceprovider.CallbackEvent valid bool }{ {"non usage without metrics", voiceprovider.CallbackEvent{EventType: "voice_chat.started"}, true}, {"non usage with metrics", voiceprovider.CallbackEvent{EventType: "voice_chat.started", Usage: []voiceprovider.UsageMetric{validMetric}}, false}, {"valid usage", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL", Usage: []voiceprovider.UsageMetric{validMetric}}, true}, {"wrong status", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "ACTIVE", Usage: []voiceprovider.UsageMetric{validMetric}}, false}, {"empty metrics", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL"}, false}, {"unknown category", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL", Usage: []voiceprovider.UsageMetric{{UsageCategory: "UNKNOWN", MetricCode: "input_tokens", Quantity: "1", Unit: "token"}}}, false}, {"invalid code", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL", Usage: []voiceprovider.UsageMetric{{UsageCategory: "LLM", MetricCode: "Input", Quantity: "1", Unit: "token"}}}, false}, {"invalid unit", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL", Usage: []voiceprovider.UsageMetric{{UsageCategory: "LLM", MetricCode: "input_tokens", Quantity: "1", Unit: "Token"}}}, false}, {"invalid quantity", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL", Usage: []voiceprovider.UsageMetric{{UsageCategory: "LLM", MetricCode: "input_tokens", Quantity: "01", Unit: "token"}}}, false}, {"duplicate metric", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL", Usage: []voiceprovider.UsageMetric{validMetric, validMetric}}, false}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if actual := validProviderUsage(test.event); actual != test.valid { t.Fatalf("valid=%v want=%v event=%+v", actual, test.valid, test.event) } }) } } func TestProviderCallbackValidatesFinalTurns(t *testing.T) { now := time.Now().UTC() validTurn := voiceprovider.FinalTurn{TurnKey: "turn-1", SequenceNo: 1, Speaker: "subject", StartAt: now, EndAt: now.Add(time.Second), FinalText: "测试终稿", ProviderRef: "provider-turn-1"} tests := []struct { name string event voiceprovider.CallbackEvent valid bool }{ {"non transcript without turns", voiceprovider.CallbackEvent{EventType: "voice_chat.started"}, true}, {"non transcript with turns", voiceprovider.CallbackEvent{EventType: "voice_chat.started", Turns: []voiceprovider.FinalTurn{validTurn}}, false}, {"valid transcript", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{validTurn}}, true}, {"wrong status", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "ACTIVE", Turns: []voiceprovider.FinalTurn{validTurn}}, false}, {"empty turns", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL"}, false}, {"missing key", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{{SequenceNo: 1, Speaker: "subject", StartAt: now, EndAt: now, FinalText: "text"}}}, false}, {"invalid sequence", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{{TurnKey: "turn", Speaker: "subject", StartAt: now, EndAt: now, FinalText: "text"}}}, false}, {"invalid speaker", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{{TurnKey: "turn", SequenceNo: 1, Speaker: "system", StartAt: now, EndAt: now, FinalText: "text"}}}, false}, {"end before start", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{{TurnKey: "turn", SequenceNo: 1, Speaker: "agent", StartAt: now, EndAt: now.Add(-time.Second), FinalText: "text"}}}, false}, {"duplicate key", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{validTurn, {TurnKey: "turn-1", SequenceNo: 2, Speaker: "agent", StartAt: now, EndAt: now, FinalText: "text"}}}, false}, {"duplicate sequence", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{validTurn, {TurnKey: "turn-2", SequenceNo: 1, Speaker: "agent", StartAt: now, EndAt: now, FinalText: "text"}}}, false}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if actual := validProviderTurns(test.event); actual != test.valid { t.Fatalf("valid=%v want=%v event=%+v", actual, test.valid, test.event) } }) } }