Files
past-go/past-go/internal/httpapi/router_test.go
T
Blizzard 4934a69581 feat: migrate to PostgreSQL GORM and MinIO file storage
Use UUID models and soft deletion, persist file relationships with deduplicated monthly MinIO objects, and add admin file management. Update mini-program media handling and add local dev configuration.
2026-11-01 11:48:45 +08:00

128 lines
5.3 KiB
Go

package httpapi
import (
"bytes"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
"testing/fstest"
"past/internal/config"
)
func TestCleanAPIRoutes(t *testing.T) {
app := &App{Cfg: &config.Config{DataDir: t.TempDir()}, Admin: NewAdminSessions()}
router := NewRouter(app, fstest.MapFS{"index.html": {Data: []byte("admin")}})
router.Handle("site_info", func(c *Ctx) error { return c.JSON(200, M{"action": c.Action, "value": c.Q("value")}) })
router.Handle("admin_login", func(c *Ctx) error {
c.AdminLogin("1", "test")
return c.JSON(200, M{"success": true})
})
router.Handle("admin_stats", func(c *Ctx) error { return c.JSON(200, M{"success": true}) })
router.Handle("site_stats", func(c *Ctx) error {
return c.JSON(200, M{"value": c.Form("value"), "token": c.Token()})
})
engine := router.Engine(false)
for _, uri := range []string{"/api/site_info?value=hello", "/api/site_info?action=wrong&value=hello", "/backend/api/index.php?action=site_info&value=hello"} {
t.Run(uri, func(t *testing.T) {
out := httptest.NewRecorder()
engine.ServeHTTP(out, httptest.NewRequest(http.MethodGet, uri, nil))
var result map[string]any
if err := json.Unmarshal(out.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
if out.Code != 200 || result["action"] != "site_info" || result["value"] != "hello" {
t.Fatalf("%d %s", out.Code, out.Body.String())
}
if out.Header().Get("Cache-Control") != "no-store" {
t.Fatal("API responses must not be cached")
}
})
}
for _, uri := range []string{"/api/couple_info", "/api/period_visible_set", "/api/upload", "/api/admin_stats", "/api/adminSubscribeLogs"} {
out := httptest.NewRecorder()
engine.ServeHTTP(out, httptest.NewRequest(http.MethodPost, uri, strings.NewReader("{}")))
if out.Code != 401 {
t.Fatalf("unauthenticated %s: %d %s", uri, out.Code, out.Body.String())
}
}
out := httptest.NewRecorder()
engine.ServeHTTP(out, httptest.NewRequest(http.MethodOptions, "/api/upload", nil))
if out.Code != 200 || out.Header().Get("Access-Control-Allow-Headers") == "" {
t.Fatal("missing preflight support")
}
// Multipart parameters remain available after routing by URL path.
var body bytes.Buffer
form := multipart.NewWriter(&body)
form.WriteField("value", "upload-value")
form.WriteField("token", "android-form-token")
form.Close()
req := httptest.NewRequest(http.MethodPost, "/api/site_stats", &body)
req.Header.Set("Content-Type", form.FormDataContentType())
out = httptest.NewRecorder()
engine.ServeHTTP(out, req)
if out.Code != 200 || !strings.Contains(out.Body.String(), "android-form-token") || !strings.Contains(out.Body.String(), "upload-value") {
t.Fatalf("multipart response: %d %s", out.Code, out.Body.String())
}
// Admin login and authenticated requests share the existing Cookie session.
out = httptest.NewRecorder()
engine.ServeHTTP(out, httptest.NewRequest(http.MethodPost, "/api/admin_login", nil))
cookies := out.Result().Cookies()
if len(cookies) == 0 || cookies[0].Path != "/" {
t.Fatal("admin Cookie must cover the new API paths")
}
req = httptest.NewRequest(http.MethodGet, "/api/admin_stats", nil)
req.AddCookie(cookies[0])
out = httptest.NewRecorder()
engine.ServeHTTP(out, req)
if out.Code != 200 {
t.Fatalf("authenticated admin: %d %s", out.Code, out.Body.String())
}
}
func TestCamelCaseContract(t *testing.T) {
router := NewRouter(&App{Cfg: &config.Config{DataDir: t.TempDir()}, Admin: NewAdminSessions()}, fstest.MapFS{})
router.Handle("site_info", func(c *Ctx) error {
return c.OK(M{"user_id": int64(9007199254740993), "cycle_length": c.InInt("cycle_length"), "profile_data": c.InMap("profile_data"), "page_size": c.Q("page_size"), "period_visible": c.Form("period_visible"), "avatar_path": "uploads/some_file.jpg"})
})
engine := router.Engine(false)
out := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/siteInfo?pageSize=20", strings.NewReader(`{"cycleLength":25,"profileData":{"startDate":"2026-10-05"}}`))
req.Header.Set("Content-Type", "application/json")
engine.ServeHTTP(out, req)
body := out.Body.String()
for _, expected := range []string{`"userId":9007199254740993`, `"cycleLength":25`, `"profileData":{"startDate":"2026-10-05"}`, `"pageSize":"20"`, `uploads/some_file.jpg`} {
if !strings.Contains(body, expected) {
t.Fatalf("missing %s: %d %s", expected, out.Code, body)
}
}
if strings.Contains(body, `"cycle_length"`) {
t.Fatal("snake field leaked onto camel API")
}
// Legacy routes retain their existing JSON fields.
out = httptest.NewRecorder()
engine.ServeHTTP(out, httptest.NewRequest(http.MethodGet, "/backend/api/index.php?action=site_info", nil))
if !strings.Contains(out.Body.String(), `"user_id"`) {
t.Fatal("legacy contract changed")
}
var upload bytes.Buffer
form := multipart.NewWriter(&upload)
form.WriteField("periodVisible", "0")
form.Close()
req = httptest.NewRequest(http.MethodPost, "/api/siteInfo", &upload)
req.Header.Set("Content-Type", form.FormDataContentType())
out = httptest.NewRecorder()
engine.ServeHTTP(out, req)
if !strings.Contains(out.Body.String(), `"periodVisible":"0"`) {
t.Fatalf("camel multipart: %s", out.Body.String())
}
out = httptest.NewRecorder()
engine.ServeHTTP(out, httptest.NewRequest(http.MethodGet, "/api/adminStats", nil))
if out.Code != 401 {
t.Fatal("camel action bypassed admin authentication")
}
}