Files
story/internal/httpapi/business_error_mappings_test.go

88 lines
5.0 KiB
Go

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())
}
}
}