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.
128 lines
5.3 KiB
Go
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")
|
|
}
|
|
}
|