jim800121chen cabbdde495 feat(visionA-backend): DB 接入後續 — OIDC/pairing FK 收尾 + B4 metadata + nginx healthz + 補測試
DB 接入塊 0-5 上主幹後的收尾工作,讓 DB-on 模式可真人使用 + 補齊功能與測試。

OIDC / pairing FK 修復(接 DB 上線必要):
- 新建 internal/user package(User + Store + InMemory + Postgres);OIDC callback
  驗證 id_token 成功後 fail-closed upsert users(sub 直接當 users.id,MC sub 為 UUID)
- pairing exchange 雲端自建 device(不動 local-tool)+ 同 tx 綁 session token;
  自建 device 空 serial 寫 NULL(避免撞 partial unique)
- device.SaveTx / session.CreateTx 新增 tx-aware 版本

B4 model metadata:
- 轉檔 result 的 analysis_info(input_shape/classes/framework)串進 model:
  converter_client → flow → adapter → model.Model → PG → ModelResponse DTO
- input_shape 優先用陣列、後備四維組 NCHW、缺一不亂組;全 optional 防禦性
- 前端詳細頁顯示(另 repo);轉檔端串接交接檔 b4-converter-handoff.md

nginx healthz(部署層):
- 新增 /healthz/deep 轉發 backend(ping PG+Redis、down 回 503)給 LB
- 修掉 default_server return 444 短路 bug(docker healthcheck 長期 unhealthy 真因)

storage error 統一映射(不洩漏 storage 後端細節)。

測試:補 internal/api(storage/errors handler)、cmd/api-server(seed/adapter)、
internal/db(redis)、relay/session 弱處,含 testcontainers integration。
DB 接入相關 package 真環境覆蓋達 88-94%。全程 Reviewer 審查 + 130 真 PG/Redis dbtest 綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:36:35 +08:00

173 lines
6.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// errors_test.go — errors.go 3 個純 helperWriteError / WriteSuccess / WriteNotImplemented的 unit test。
//
// Owner: testing agent補測任務 — internal/api 弱處純函式)
//
// 這 3 個函式直接寫 gin.Context 的 JSON responseenvelope 形狀對齊 api-spec.md §11
// 用 httptest.NewRecorder() + gin.CreateTestContext 驅動,驗 status code + envelope 結構 +
// request_id 帶入 + details / nil 邊界。
//
// 注意errors_db.go 的 DB 錯誤映射另由 errors_db_test.go 覆蓋,本檔不重複。
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newTestCtx 建立一個帶 ResponseRecorder 的 gin.ContextreqID 非空時塞入 request_id。
func newTestCtx(reqID string) (*gin.Context, *httptest.ResponseRecorder) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
if reqID != "" {
c.Set(ctxKeyRequestID, reqID)
}
return c, w
}
// decodeErrorBody 把 recorder body 解成 ErrorBody。
func decodeErrorBody(t *testing.T, w *httptest.ResponseRecorder) ErrorBody {
t.Helper()
var body ErrorBody
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
return body
}
// ───────────────────────── WriteError ─────────────────────────
// happy寫一個帶 request_id 的 404 錯誤envelope 形狀正確。
func TestWriteError_Happy_WithRequestID(t *testing.T) {
c, w := newTestCtx("req-123")
WriteError(c, http.StatusNotFound, ErrCodeNotFound, "device not found", nil)
assert.Equal(t, http.StatusNotFound, w.Code)
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
body := decodeErrorBody(t, w)
assert.False(t, body.Success, "錯誤 envelope success 必為 false")
require.NotNil(t, body.Error)
assert.Equal(t, ErrCodeNotFound, body.Error.Code)
assert.Equal(t, "device not found", body.Error.Message)
assert.Equal(t, "req-123", body.Error.RequestID, "WriteError 應自動帶上 request_id")
assert.Nil(t, body.Error.Details, "未傳 details 時應為 nilomitempty")
}
// boundary帶 detailsvalidation 細節details 應出現在 envelope。
func TestWriteError_WithDetails(t *testing.T) {
c, w := newTestCtx("req-val")
details := []FieldError{
{Field: "name", Message: "required"},
{Field: "target_chip", Message: "unsupported"},
}
WriteError(c, http.StatusBadRequest, ErrCodeValidationFailed, "validation failed", details)
assert.Equal(t, http.StatusBadRequest, w.Code)
body := decodeErrorBody(t, w)
require.NotNil(t, body.Error)
require.Len(t, body.Error.Details, 2)
assert.Equal(t, "name", body.Error.Details[0].Field)
assert.Equal(t, "required", body.Error.Details[0].Message)
assert.Equal(t, "target_chip", body.Error.Details[1].Field)
}
// empty / edge沒有 request_id 時request_id 欄位被 omitempty 省略(不應出現空字串 key 影響 client
func TestWriteError_NoRequestID_OmitsField(t *testing.T) {
c, w := newTestCtx("") // 不塞 request_id
WriteError(c, http.StatusInternalServerError, ErrCodeInternalError, "boom", nil)
assert.Equal(t, http.StatusInternalServerError, w.Code)
// request_id 為 omitempty空值時不應出現在 JSON
raw := w.Body.String()
assert.NotContains(t, raw, "request_id", "request_id 為空時應被 omitempty 省略")
body := decodeErrorBody(t, w)
require.NotNil(t, body.Error)
assert.Equal(t, ErrCodeInternalError, body.Error.Code)
assert.Empty(t, body.Error.RequestID)
}
// edge空 details slicelen 0→ omitempty 省略(不應出現 "details": [])。
func TestWriteError_EmptyDetailsSlice_Omitted(t *testing.T) {
c, w := newTestCtx("req-x")
WriteError(c, http.StatusBadRequest, ErrCodeValidationFailed, "msg", []FieldError{})
raw := w.Body.String()
assert.NotContains(t, raw, "details", "空 details slice 應被 omitempty 省略")
}
// ───────────────────────── WriteSuccess ─────────────────────────
// happy寫一個 200 成功回應data 帶入。
func TestWriteSuccess_Happy(t *testing.T) {
c, w := newTestCtx("")
payload := map[string]any{"id": "m-1", "name": "yolo"}
WriteSuccess(c, http.StatusOK, payload)
assert.Equal(t, http.StatusOK, w.Code)
var body SuccessBody
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.True(t, body.Success, "成功 envelope success 必為 true")
data, ok := body.Data.(map[string]any)
require.True(t, ok)
assert.Equal(t, "m-1", data["id"])
assert.Equal(t, "yolo", data["name"])
}
// edgedata 為 nil如 201 Created 無 body→ data 被 omitempty 省略,但 success 仍在。
func TestWriteSuccess_NilData_OmitsData(t *testing.T) {
c, w := newTestCtx("")
WriteSuccess(c, http.StatusCreated, nil)
assert.Equal(t, http.StatusCreated, w.Code)
raw := w.Body.String()
assert.Contains(t, raw, `"success":true`)
assert.NotContains(t, raw, "data", "nil data 應被 omitempty 省略")
}
// boundarynon-201 status 也能用WriteSuccess 不限定 status
func TestWriteSuccess_CustomStatus(t *testing.T) {
c, w := newTestCtx("")
WriteSuccess(c, http.StatusAccepted, map[string]string{"state": "queued"})
assert.Equal(t, http.StatusAccepted, w.Code)
var body SuccessBody
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.True(t, body.Success)
}
// ───────────────────────── WriteNotImplemented ─────────────────────────
// happy回 501 NOT_IMPLEMENTEDhint 帶進 message。
func TestWriteNotImplemented_Happy(t *testing.T) {
c, w := newTestCtx("req-ni")
WriteNotImplemented(c, "clusters endpoint pending B5")
assert.Equal(t, http.StatusNotImplemented, w.Code)
body := decodeErrorBody(t, w)
require.NotNil(t, body.Error)
assert.False(t, body.Success)
assert.Equal(t, ErrCodeNotImplemented, body.Error.Code)
assert.Equal(t, "clusters endpoint pending B5", body.Error.Message)
assert.Equal(t, "req-ni", body.Error.RequestID)
}
// edge空 hint 也成立message 為空字串、code 仍為 NOT_IMPLEMENTED
func TestWriteNotImplemented_EmptyHint(t *testing.T) {
c, w := newTestCtx("")
WriteNotImplemented(c, "")
assert.Equal(t, http.StatusNotImplemented, w.Code)
body := decodeErrorBody(t, w)
require.NotNil(t, body.Error)
assert.Equal(t, ErrCodeNotImplemented, body.Error.Code)
assert.Empty(t, body.Error.Message)
}