package httpapi import ( "errors" "net/http" "net/http/httptest" "strings" "testing" "data-terminal-cloud/internal/alerts" "data-terminal-cloud/internal/billing" "data-terminal-cloud/internal/device" "data-terminal-cloud/internal/idempotency" "data-terminal-cloud/internal/opsconfig" ) func TestAlertBillingAndOpsConfigErrorsHaveStableMappings(t *testing.T) { type errorWriter func(http.ResponseWriter, *http.Request, error) tests := []struct { name string writer errorWriter err error status int code string }{ {"alert invalid", writeAlertError, alerts.ErrInvalid, http.StatusBadRequest, "VALIDATION_FAILED"}, {"alert denied", writeAlertError, alerts.ErrAccessDenied, http.StatusForbidden, "OPS_ACCESS_DENIED"}, {"alert missing", writeAlertError, alerts.ErrNotFound, http.StatusNotFound, "ALERT_NOT_FOUND"}, {"alert version", writeAlertError, alerts.ErrVersionConflict, http.StatusPreconditionFailed, "VERSION_CONFLICT"}, {"alert state", writeAlertError, alerts.ErrStateConflict, http.StatusConflict, "ALERT_STATE_CONFLICT"}, {"alert key", writeAlertError, idempotency.ErrKeyConflict, http.StatusConflict, "IDEMPOTENCY_KEY_CONFLICT"}, {"alert dependency", writeAlertError, errors.New("database unavailable"), http.StatusInternalServerError, "INTERNAL_ERROR"}, {"billing invalid", writeBillingError, billing.ErrInvalid, http.StatusBadRequest, "VALIDATION_FAILED"}, {"billing denied", writeBillingError, billing.ErrAccessDenied, http.StatusForbidden, "OPS_ACCESS_DENIED"}, {"billing missing", writeBillingError, billing.ErrNotFound, http.StatusNotFound, "RESOURCE_NOT_FOUND"}, {"billing version", writeBillingError, billing.ErrVersionConflict, http.StatusPreconditionFailed, "VERSION_CONFLICT"}, {"billing self approval", writeBillingError, billing.ErrSelfApproval, http.StatusConflict, "SELF_APPROVAL_FORBIDDEN"}, {"billing state", writeBillingError, billing.ErrStateConflict, http.StatusConflict, "BILLING_STATE_CONFLICT"}, {"billing key", writeBillingError, idempotency.ErrKeyConflict, http.StatusConflict, "IDEMPOTENCY_KEY_CONFLICT"}, {"billing in progress", writeBillingError, idempotency.ErrInProgress, http.StatusConflict, "IDEMPOTENCY_IN_PROGRESS"}, {"billing dependency", writeBillingError, errors.New("database unavailable"), http.StatusInternalServerError, "INTERNAL_ERROR"}, {"config invalid", writeOpsConfigError, opsconfig.ErrInvalid, http.StatusBadRequest, "VALIDATION_FAILED"}, {"config denied", writeOpsConfigError, opsconfig.ErrAccessDenied, http.StatusForbidden, "OPS_ACCESS_DENIED"}, {"config missing", writeOpsConfigError, opsconfig.ErrNotFound, http.StatusNotFound, "RESOURCE_NOT_FOUND"}, {"config version", writeOpsConfigError, opsconfig.ErrVersionConflict, http.StatusPreconditionFailed, "VERSION_CONFLICT"}, {"config self approval", writeOpsConfigError, opsconfig.ErrSelfApproval, http.StatusConflict, "SELF_APPROVAL_FORBIDDEN"}, {"config state", writeOpsConfigError, opsconfig.ErrStateConflict, http.StatusConflict, "CONFIG_STATE_CONFLICT"}, {"config gate", writeOpsConfigError, opsconfig.ErrPublishGate, http.StatusConflict, "CONFIG_PUBLISH_GATE_FAILED"}, {"config key", writeOpsConfigError, idempotency.ErrKeyConflict, http.StatusConflict, "IDEMPOTENCY_KEY_CONFLICT"}, {"config in progress", writeOpsConfigError, idempotency.ErrInProgress, http.StatusConflict, "IDEMPOTENCY_IN_PROGRESS"}, {"config dependency", writeOpsConfigError, errors.New("database unavailable"), http.StatusInternalServerError, "INTERNAL_ERROR"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodPost, "/api/v1/ops/test", nil) test.writer(recorder, request, test.err) if recorder.Code != test.status || !strings.Contains(recorder.Body.String(), test.code) { t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } }) } } func TestTelemetryErrorsHaveStableMappings(t *testing.T) { tests := []struct { err error status int code string }{ {&device.TelemetryConflictError{}, http.StatusConflict, "TELEMETRY_EVENT_CONFLICT"}, {device.ErrTelemetryConfigAhead, http.StatusConflict, "CONFIG_VERSION_CONFLICT"}, {device.ErrTelemetryConversationMissing, http.StatusNotFound, "TELEMETRY_CONVERSATION_NOT_FOUND"}, {device.ErrTelemetryCommandConflict, http.StatusConflict, "DEVICE_COMMAND_STATE_CONFLICT"}, {idempotency.ErrKeyConflict, http.StatusConflict, "IDEMPOTENCY_KEY_REUSED"}, {idempotency.ErrInProgress, http.StatusConflict, "REQUEST_IN_PROGRESS"}, {errors.New("database unavailable"), http.StatusServiceUnavailable, "DEVICE_TELEMETRY_UNAVAILABLE"}, } for _, test := range tests { recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodPost, "/device/v2/telemetry:batch", nil) writeTelemetryError(recorder, request, test.err) if recorder.Code != test.status || !strings.Contains(recorder.Body.String(), test.code) { t.Fatalf("error=%v status=%d body=%s", test.err, recorder.Code, recorder.Body.String()) } } }