package apierror_test import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/veylant/ia-gateway/internal/apierror" ) func TestNewAuthError(t *testing.T) { e := apierror.NewAuthError("bad token") assert.Equal(t, http.StatusUnauthorized, e.HTTPStatus) assert.Equal(t, "authentication_error", e.Type) assert.Equal(t, "bad token", e.Message) assert.NotEmpty(t, e.Code) } func TestNewForbiddenError(t *testing.T) { e := apierror.NewForbiddenError("no access") assert.Equal(t, http.StatusForbidden, e.HTTPStatus) assert.Equal(t, "permission_error", e.Type) } func TestNewBadRequestError(t *testing.T) { e := apierror.NewBadRequestError("missing model") assert.Equal(t, http.StatusBadRequest, e.HTTPStatus) assert.Equal(t, "invalid_request_error", e.Type) } func TestNewUpstreamError(t *testing.T) { e := apierror.NewUpstreamError("OpenAI down") assert.Equal(t, http.StatusBadGateway, e.HTTPStatus) assert.Equal(t, "api_error", e.Type) } func TestNewRateLimitError(t *testing.T) { e := apierror.NewRateLimitError("too many requests") assert.Equal(t, http.StatusTooManyRequests, e.HTTPStatus) assert.Equal(t, "rate_limit_error", e.Type) assert.Equal(t, 1, e.RetryAfterSec, "NewRateLimitError must set RetryAfterSec=1 (RFC 6585)") } func TestWriteError_RetryAfter_SetWhenPresent(t *testing.T) { rec := httptest.NewRecorder() apierror.WriteError(rec, apierror.NewRateLimitError("slow down")) assert.Equal(t, "1", rec.Header().Get("Retry-After")) } func TestWriteError_NoRetryAfter_WhenZero(t *testing.T) { rec := httptest.NewRecorder() apierror.WriteError(rec, apierror.NewAuthError("denied")) assert.Empty(t, rec.Header().Get("Retry-After")) } func TestWriteErrorWithRequestID_SetsHeader(t *testing.T) { rec := httptest.NewRecorder() apierror.WriteErrorWithRequestID(rec, apierror.NewAuthError("denied"), "req-abc-123") assert.Equal(t, "req-abc-123", rec.Header().Get("X-Request-Id")) assert.Equal(t, http.StatusUnauthorized, rec.Code) } func TestWriteErrorWithRequestID_EmptyID_NoHeader(t *testing.T) { rec := httptest.NewRecorder() apierror.WriteErrorWithRequestID(rec, apierror.NewAuthError("denied"), "") assert.Empty(t, rec.Header().Get("X-Request-Id")) } func TestNewTimeoutError(t *testing.T) { e := apierror.NewTimeoutError("upstream timed out") assert.Equal(t, http.StatusGatewayTimeout, e.HTTPStatus) } func TestNewInternalError(t *testing.T) { e := apierror.NewInternalError("unexpected panic") assert.Equal(t, http.StatusInternalServerError, e.HTTPStatus) } func TestAPIError_Error(t *testing.T) { e := apierror.NewAuthError("some message") assert.Equal(t, "some message", e.Error()) } func TestWriteError_SetsStatusAndContentType(t *testing.T) { rec := httptest.NewRecorder() apierror.WriteError(rec, apierror.NewAuthError("denied")) assert.Equal(t, http.StatusUnauthorized, rec.Code) assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) } func TestWriteError_BodyIsOpenAIEnvelope(t *testing.T) { rec := httptest.NewRecorder() apierror.WriteError(rec, apierror.NewRateLimitError("slow down")) var body struct { Error struct { Type string `json:"type"` Message string `json:"message"` Code string `json:"code"` } `json:"error"` } require.NoError(t, json.NewDecoder(rec.Body).Decode(&body)) assert.Equal(t, "rate_limit_error", body.Error.Type) assert.Equal(t, "slow down", body.Error.Message) assert.NotEmpty(t, body.Error.Code) }