package responsex_test import ( "context" "encoding/json" "errors" "net/http/httptest" "testing" "code.yun.ink/pkg/responsex" ) // decodeBody 解码响应体为 map,失败则 Fatal func decodeBody(t *testing.T, w *httptest.ResponseRecorder) map[string]any { t.Helper() var body map[string]any if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { t.Fatalf("decode body failed: %v, raw: %s", err, w.Body.String()) } return body } // ---------- Success ---------- func TestSuccess(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.Success(ctx, w, "hello") body := decodeBody(t, w) if body["code"] != float64(200) { t.Errorf("code = %v, want 200", body["code"]) } if body["message"] != "请求成功" { t.Errorf("message = %v, want 请求成功", body["message"]) } if body["data"] != "hello" { t.Errorf("data = %v, want hello", body["data"]) } if w.Code != 200 { t.Errorf("http status = %d, want 200", w.Code) } } func TestSuccessNilData(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.Success(ctx, w, nil) body := decodeBody(t, w) if _, ok := body["data"]; ok { t.Errorf("data should be omitted for nil, got %v", body["data"]) } } func TestSuccessWithHttpStatusCode(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.Success(ctx, w, "created", responsex.SetHttpStatusCode(201), ) body := decodeBody(t, w) if body["code"] != float64(200) { t.Errorf("code = %v, want 200", body["code"]) } if w.Code != 201 { t.Errorf("http status = %d, want 201", w.Code) } } func TestSuccessWithHeader(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.Success(ctx, w, "ok", responsex.SetHeader("X-Request-Id", "req-123"), responsex.SetHeader("X-Custom", "custom-val"), ) if v := w.Header().Get("X-Request-Id"); v != "req-123" { t.Errorf("X-Request-Id = %q, want req-123", v) } if v := w.Header().Get("X-Custom"); v != "custom-val" { t.Errorf("X-Custom = %q, want custom-val", v) } } func TestSuccessWithHeaders(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.Success(ctx, w, "ok", responsex.SetHeaders(map[string]string{ "X-One": "1", "X-Two": "2", }), ) if v := w.Header().Get("X-One"); v != "1" { t.Errorf("X-One = %q, want 1", v) } if v := w.Header().Get("X-Two"); v != "2" { t.Errorf("X-Two = %q, want 2", v) } } func TestSuccessMultipleSetHeader(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.Success(ctx, w, "ok", responsex.SetHeader("X-A", "a1"), responsex.SetHeader("X-A", "a2"), // 同 key 多次 SetHeader 覆盖 ) if v := w.Header().Get("X-A"); v != "a2" { t.Errorf("X-A = %q, want a2", v) } } // ---------- Error ---------- func TestError(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.Error(ctx, w, errors.New("something went wrong")) body := decodeBody(t, w) if body["code"] != float64(400) { t.Errorf("code = %v, want 400", body["code"]) } if body["message"] != "something went wrong" { t.Errorf("message = %v, want 'something went wrong'", body["message"]) } if w.Code != 200 { t.Errorf("http status = %d, want 200 (default)", w.Code) } } func TestErrorWithHttpStatusCode(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.Error(ctx, w, errors.New("internal error"), responsex.SetHttpStatusCode(500), ) if w.Code != 500 { t.Errorf("http status = %d, want 500", w.Code) } } func TestErrorWithHeader(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.Error(ctx, w, errors.New("bad request"), responsex.SetHttpStatusCode(400), responsex.SetHeader("X-Error-Code", "VALIDATION_001"), ) if w.Code != 400 { t.Errorf("http status = %d, want 400", w.Code) } if v := w.Header().Get("X-Error-Code"); v != "VALIDATION_001" { t.Errorf("X-Error-Code = %q, want VALIDATION_001", v) } } func TestErrorWithData(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.ErrorWithData(ctx, w, errors.New("validation failed"), map[string]any{ "field": "email", "msg": "invalid format", }) body := decodeBody(t, w) if body["code"] != float64(400) { t.Errorf("code = %v, want 400", body["code"]) } data, ok := body["data"].(map[string]any) if !ok { t.Fatalf("data should be a map, got %T", body["data"]) } if data["field"] != "email" { t.Errorf("data.field = %v, want email", data["field"]) } } func TestErrorWithDataAndOptions(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.ErrorWithData(ctx, w, errors.New("gone"), "resource deleted", responsex.SetHttpStatusCode(410), responsex.SetHeader("X-Deleted-At", "2024-01-01"), ) if w.Code != 410 { t.Errorf("http status = %d, want 410", w.Code) } body := decodeBody(t, w) if body["data"] != "resource deleted" { t.Errorf("data = %v, want 'resource deleted'", body["data"]) } } func TestErrorNilError(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.Error(ctx, w, nil) body := decodeBody(t, w) if body["code"] != float64(400) { t.Errorf("code = %v, want 400", body["code"]) } if body["message"] != "请求失败" { t.Errorf("message = %v, want 请求失败", body["message"]) } } // ---------- ErrorStr ---------- func TestErrorStr(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.ErrorStr(ctx, w, "unauthorized") body := decodeBody(t, w) if body["message"] == "" { t.Error("message should not be empty") } } func TestErrorStrWithOptions(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.ErrorStr(ctx, w, "forbidden", responsex.SetHttpStatusCode(403), responsex.SetHeader("X-Reason", "no permission"), ) if w.Code != 403 { t.Errorf("http status = %d, want 403", w.Code) } } // ---------- SuccessWithPage ---------- func TestSuccessWithPage(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() page := &responsex.Pagination{ Page: 2, Size: 10, TotalPage: 5, TotalCount: 50, } responsex.SuccessWithPage(ctx, w, []string{"a", "b"}, page) body := decodeBody(t, w) pagination, ok := body["pagination"].(map[string]any) if !ok { t.Fatalf("pagination should be a map, got %T", body["pagination"]) } if pagination["page"] != float64(2) { t.Errorf("page = %v, want 2", pagination["page"]) } if pagination["total_count"] != float64(50) { t.Errorf("total_count = %v, want 50", pagination["total_count"]) } } func TestSuccessWithPageAndOptions(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() page := &responsex.Pagination{Page: 1, Size: 20, TotalPage: 3, TotalCount: 50} responsex.SuccessWithPage(ctx, w, []int{1, 2}, page, responsex.SetHeader("X-Total-Count", "50"), responsex.SetHeader("X-Page", "1"), ) if v := w.Header().Get("X-Total-Count"); v != "50" { t.Errorf("X-Total-Count = %q, want 50", v) } } // ---------- ResponseCtx ---------- func TestResponseCtx_Custom(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.ResponseCtx(ctx, w, responsex.SetCode(0), responsex.SetMessage("custom msg"), responsex.SetHttpStatusCode(418), responsex.SetHeaders(map[string]string{"X-Teapot": "true"}), ) if w.Code != 418 { t.Errorf("http status = %d, want 418", w.Code) } if v := w.Header().Get("X-Teapot"); v != "true" { t.Errorf("X-Teapot = %q, want true", v) } body := decodeBody(t, w) if body["code"] != float64(0) { t.Errorf("code = %v, want 0", body["code"]) } } func TestResponseCtx_HttpStatusDefault(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() // 不设置 SetHttpStatusCode,默认应为 200 responsex.ResponseCtx(ctx, w, responsex.SetCode(500), responsex.SetMessage("error"), ) if w.Code != 200 { t.Errorf("http status = %d, want 200 (default)", w.Code) } } // ---------- FormatMessage ---------- func TestFormatMessage(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.FormatMessage(ctx, w, "test_key", map[string]string{ "name": "world", }, "some_data") body := decodeBody(t, w) // FormatMessage 内部调用 langx,此处仅验证结构完整性和 HTTP 200 if w.Code != 200 { t.Errorf("http status = %d, want 200", w.Code) } _ = body } func TestFormatMessageWithOptions(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.FormatMessage(ctx, w, "key", map[string]string{"k": "v"}, nil, responsex.SetHeader("X-Lang", "zh-CN"), ) if v := w.Header().Get("X-Lang"); v != "zh-CN" { t.Errorf("X-Lang = %q, want zh-CN", v) } } // ---------- GetOffset ---------- func TestGetOffset(t *testing.T) { tests := []struct { name string page, size int wantOffset int wantLimit int }{ {"page 1 size 10", 1, 10, 0, 10}, {"page 2 size 10", 2, 10, 10, 10}, {"page 3 size 20", 3, 20, 40, 20}, {"page 0 defaults to 1", 0, 10, 0, 10}, {"size 0 defaults to 10", 1, 0, 0, 10}, {"negative page defaults to 1", -1, 10, 0, 10}, {"both zero", 0, 0, 0, 10}, {"page 5 size 100", 5, 100, 400, 100}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { offset, limit := responsex.GetOffset(tt.page, tt.size) if offset != tt.wantOffset || limit != tt.wantLimit { t.Errorf("GetOffset(%d,%d) = (%d,%d), want (%d,%d)", tt.page, tt.size, offset, limit, tt.wantOffset, tt.wantLimit) } }) } } // ---------- GetPage ---------- func TestGetPage(t *testing.T) { tests := []struct { name string page, size, total int64 wantPage, wantSize int64 wantTotalPage int64 wantTotalCount int64 }{ {"normal", 1, 10, 100, 1, 10, 10, 100}, {"partial last page", 3, 10, 25, 3, 10, 3, 25}, {"size zero", 1, 0, 100, 1, 0, 0, 100}, {"total zero", 1, 10, 0, 1, 10, 0, 0}, {"exact division", 1, 25, 100, 1, 25, 4, 100}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := responsex.GetPage(tt.page, tt.size, tt.total) if p.Page != tt.wantPage { t.Errorf("Page = %d, want %d", p.Page, tt.wantPage) } if p.Size != tt.wantSize { t.Errorf("Size = %d, want %d", p.Size, tt.wantSize) } if p.TotalPage != tt.wantTotalPage { t.Errorf("TotalPage = %d, want %d", p.TotalPage, tt.wantTotalPage) } if p.TotalCount != tt.wantTotalCount { t.Errorf("TotalCount = %d, want %d", p.TotalCount, tt.wantTotalCount) } }) } } // ---------- SetCode ---------- func TestSetCodeOption(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.ResponseCtx(ctx, w, responsex.SetCode(1001), responsex.SetMessage("custom code"), ) body := decodeBody(t, w) if body["code"] != float64(1001) { t.Errorf("code = %v, want 1001", body["code"]) } } // ---------- Content-Type ---------- func TestContentTypeHeader(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.Success(ctx, w, "ok") ct := w.Header().Get("Content-Type") if ct != "application/json; charset=utf-8" { t.Errorf("Content-Type = %q, want application/json; charset=utf-8", ct) } } // ---------- JSON Structure ---------- func TestErrorResponseJsonStructure(t *testing.T) { w := httptest.NewRecorder() ctx := context.Background() responsex.Error(ctx, w, errors.New("not found")) var resp struct { Code int `json:"code"` Message string `json:"message"` } if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("json unmarshal failed: %v", err) } if resp.Code != 400 { t.Errorf("Code = %d, want 400", resp.Code) } if resp.Message != "not found" { t.Errorf("Message = %q, want 'not found'", resp.Message) } }