package httpapi import ( "context" "net/http" "net/http/httptest" "strings" "testing" "data-terminal-cloud/internal/device" "data-terminal-cloud/internal/deviceauth" ) type fakeTelemetrySubmitter struct { response device.TelemetryBatchResponse replayed bool err error calls int } func (s *fakeTelemetrySubmitter) Submit(context.Context, deviceauth.Identity, string, []byte, device.TelemetryBatchInput) (device.TelemetryBatchResponse, bool, error) { s.calls++ return s.response, s.replayed, s.err } func TestSubmitDeviceTelemetryAcceptsTypedBatch(t *testing.T) { service := &fakeTelemetrySubmitter{response: device.TelemetryBatchResponse{ AcceptedCount: 2, DesiredConfigRevision: 2, Actions: []device.TelemetryAction{{Type: "refresh_config"}}, }, replayed: true} body := `{"events":[{"event_key":"status-101","sequence_no":101,"occurred_at":"2026-08-23T12:00:00Z","event_type":"status","status":{"firmware_version":"1.0.1","reported_config_revision":1,"network":{"type":"wifi","rssi":-50},"rtc":{"joined":false,"uplink":false,"downlink":false},"recording_queue":{"pending_segments":2,"failed_segments":1,"last_failure_code":"MEDIA_UPLOAD_TIMEOUT","last_failure_at":"2026-08-23T11:59:58Z"}}},{"event_key":"error-102","sequence_no":102,"occurred_at":"2026-08-23T12:00:01Z","event_type":"error","error":{"code":"RTC_RECONNECTING","severity":"warning"}}]}` request := httptest.NewRequest(http.MethodPost, "/device/v2/telemetry:batch", strings.NewReader(body)) request.Header.Set("Idempotency-Key", "telemetry-0123456789abcdef") request = request.WithContext(deviceauth.WithIdentity(request.Context(), deviceauth.Identity{PublicID: "device-1"})) recorder := httptest.NewRecorder() SubmitDeviceTelemetryHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusOK || service.calls != 1 || recorder.Header().Get("Idempotency-Replayed") != "true" || !containsText(recorder.Body.String(), "refresh_config") { t.Fatalf("status=%d calls=%d replay=%q body=%s", recorder.Code, service.calls, recorder.Header().Get("Idempotency-Replayed"), recorder.Body.String()) } } func TestSubmitDeviceTelemetryRejectsMixedPayloadAndSensitiveCommandResult(t *testing.T) { service := &fakeTelemetrySubmitter{} body := `{"events":[{"event_key":"command-1","sequence_no":1,"occurred_at":"2026-08-23T12:00:00Z","event_type":"command_result","error":{"code":"X","severity":"error"},"command_result":{"command_id":"10000000-0000-0000-0000-000000000001","status":"SUCCEEDED","result_summary":{"api_token":"must-not-pass"}}}]}` request := httptest.NewRequest(http.MethodPost, "/device/v2/telemetry:batch", strings.NewReader(body)) request.Header.Set("Idempotency-Key", "telemetry-0123456789abcdef") request = request.WithContext(deviceauth.WithIdentity(request.Context(), deviceauth.Identity{PublicID: "device-1"})) recorder := httptest.NewRecorder() SubmitDeviceTelemetryHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusBadRequest || service.calls != 0 { t.Fatalf("status=%d calls=%d body=%s", recorder.Code, service.calls, recorder.Body.String()) } } func TestSubmitDeviceTelemetryReturnsStructuredEventConflict(t *testing.T) { service := &fakeTelemetrySubmitter{err: &device.TelemetryConflictError{Conflicts: []device.TelemetryConflict{{EventKey: "error-1", SequenceNo: 1, Reason: "EVENT_KEY_OR_SEQUENCE_ALREADY_USED"}}}} body := `{"events":[{"event_key":"error-1","sequence_no":1,"occurred_at":"2026-08-23T12:00:00Z","event_type":"error","error":{"code":"RTC_FAILED","severity":"error"}}]}` request := httptest.NewRequest(http.MethodPost, "/device/v2/telemetry:batch", strings.NewReader(body)) request.Header.Set("Idempotency-Key", "telemetry-0123456789abcdef") request = request.WithContext(deviceauth.WithIdentity(request.Context(), deviceauth.Identity{PublicID: "device-1"})) recorder := httptest.NewRecorder() SubmitDeviceTelemetryHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusConflict || !containsText(recorder.Body.String(), "TELEMETRY_EVENT_CONFLICT") || !containsText(recorder.Body.String(), "EVENT_KEY_OR_SEQUENCE_ALREADY_USED") { t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } }