package api import ( "context" "errors" "fmt" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5/pgconn" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestClassifyDBError 驗證 pgx error → (status, code) 映射(塊 5.4)。 func TestClassifyDBError(t *testing.T) { cases := []struct { name string err error wantStatus int wantCode string }{ { name: "context deadline → 503", err: context.DeadlineExceeded, wantStatus: http.StatusServiceUnavailable, wantCode: ErrCodeServiceUnavailable, }, { name: "context canceled → 503", err: context.Canceled, wantStatus: http.StatusServiceUnavailable, wantCode: ErrCodeServiceUnavailable, }, { name: "wrapped deadline → 503", err: fmt.Errorf("device: pg List: %w", context.DeadlineExceeded), wantStatus: http.StatusServiceUnavailable, wantCode: ErrCodeServiceUnavailable, }, { name: "unique violation 23505 → 409", err: &pgconn.PgError{Code: "23505", Message: "duplicate key value"}, wantStatus: http.StatusConflict, wantCode: ErrCodeConflict, }, { name: "wrapped unique violation → 409", err: fmt.Errorf("device: pg Save: %w", &pgconn.PgError{Code: "23505"}), wantStatus: http.StatusConflict, wantCode: ErrCodeConflict, }, { name: "other PG error (syntax) → 500", err: &pgconn.PgError{Code: "42601", Message: "syntax error"}, wantStatus: http.StatusInternalServerError, wantCode: ErrCodeInternalError, }, { name: "connect error → 503", err: &pgconn.ConnectError{Config: &pgconn.Config{}}, wantStatus: http.StatusServiceUnavailable, wantCode: ErrCodeServiceUnavailable, }, { name: "unknown error → 500", err: errors.New("something unexpected"), wantStatus: http.StatusInternalServerError, wantCode: ErrCodeInternalError, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { cls := classifyDBError(tc.err) assert.Equal(t, tc.wantStatus, cls.status) assert.Equal(t, tc.wantCode, cls.code) // message 一律是通用字串,不含 raw error 內容 assert.NotEmpty(t, cls.message) }) } } // TestWriteDBError_NoRawLeak 驗證 WriteDBError 不把 raw DB error 寫進 response body(收塊 3 M1)。 func TestWriteDBError_NoRawLeak(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodGet, "/", nil) rawErr := &pgconn.PgError{Code: "42601", Message: "syntax error at or near SELECT", Detail: "secret-schema-detail", Hint: "internal hint that should not leak", } WriteDBError(c, nil, "list devices", rawErr) require.Equal(t, http.StatusInternalServerError, w.Code) body := w.Body.String() // 對外只有通用 code/message,raw error 的細節不得出現 assert.Contains(t, body, ErrCodeInternalError) assert.NotContains(t, body, "syntax error") assert.NotContains(t, body, "secret-schema-detail") assert.NotContains(t, body, "internal hint") } // TestWriteDBError_PGDown503 驗證 PG down(connect error)→ 503 + SERVICE_UNAVAILABLE。 func TestWriteDBError_PGDown503(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodGet, "/", nil) WriteDBError(c, nil, "get model", &pgconn.ConnectError{Config: &pgconn.Config{}}) require.Equal(t, http.StatusServiceUnavailable, w.Code) assert.Contains(t, w.Body.String(), ErrCodeServiceUnavailable) }