Files
past-go/past-go/internal/handler/permissions_test.go
T
2026-10-26 17:32:47 +08:00

129 lines
5.9 KiB
Go

package handler
import (
"database/sql/driver"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"past/internal/config"
"past/internal/httpapi"
"past/internal/store"
"past/internal/testdb"
)
func permissionCtx(t *testing.T, method, uri, body string, steps []testdb.Step) (*httpapi.Ctx, *httptest.ResponseRecorder, *testdb.Script) {
t.Helper()
db, s := testdb.Open(t, steps)
out := httptest.NewRecorder()
g, _ := gin.CreateTestContext(out)
g.Request = httptest.NewRequest(method, uri, strings.NewReader(body))
g.Request.Header.Set("Authorization", "Bearer test")
g.Request.Header.Set("Content-Type", "application/json")
app := &httpapi.App{DB: &store.DB{DB: db}, Cfg: &config.Config{}}
return &httpapi.Ctx{G: g, R: g.Request, W: out, DB: app.DB, App: app, Method: method}, out, s
}
func uidStep(uid int64) testdb.Step {
return testdb.Step{Match: "FROM user_tokens", Columns: []string{"user_id"}, Rows: [][]driver.Value{{uid}}}
}
func cidStep(cid int64) testdb.Step {
return testdb.Step{Match: "SELECT couple_id FROM users WHERE id = ?", Columns: []string{"couple_id"}, Rows: [][]driver.Value{{cid}}}
}
func TestAgreeStartsCoolingWithoutUnbinding(t *testing.T) {
c, out, s := permissionCtx(t, "POST", "/api/unbindRespond", `{"agree":true}`, []testdb.Step{
uidStep(2), cidStep(10),
{Match: "FROM couples WHERE id=? FOR UPDATE", Columns: []string{"id", "status"}, Rows: [][]driver.Value{{int64(10), "active"}}},
{Match: "FROM users WHERE id=? AND couple_id=?", Columns: []string{"id"}, Rows: [][]driver.Value{{int64(2)}}},
{Match: "status='pending' AND requester_id!=?", Columns: []string{"id"}, Rows: [][]driver.Value{{int64(8)}}},
{Match: "cooling_until=DATE_ADD(NOW(),INTERVAL 72 HOUR)", Args: []any{int64(8)}},
})
unbindRespond(c)
if out.Code != 200 || s.Commits != 1 {
t.Fatalf("code=%d commits=%d body=%s", out.Code, s.Commits, out.Body.String())
}
}
func TestEitherPartnerCanCancelCoolingButNotAfterDeadline(t *testing.T) {
for _, expired := range []bool{false, true} {
t.Run(map[bool]string{false: "partner cancels", true: "deadline reached"}[expired], func(t *testing.T) {
rows := [][]driver.Value{{int64(8)}}
if expired {
rows = nil
}
steps := []testdb.Step{uidStep(2), cidStep(10), {Match: "FOR UPDATE", Columns: []string{"id", "status"}, Rows: [][]driver.Value{{int64(10), "active"}}}, {Match: "FROM users WHERE id=?", Columns: []string{"id"}, Rows: [][]driver.Value{{int64(2)}}}, {Match: "status='cooling' AND cooling_until>NOW()", Columns: []string{"id"}, Rows: rows}}
if !expired {
steps = append(steps, testdb.Step{Match: "status='cancelled'"})
}
c, out, s := permissionCtx(t, "POST", "/api/unbindCancel", `{}`, steps)
unbindCancel(c)
want := 200
if expired {
want = 409
if s.Commits != 0 {
t.Fatal("expired cancellation committed")
}
}
if out.Code != want {
t.Fatalf("code=%d body=%s", out.Code, out.Body.String())
}
})
}
}
func TestLegacyDetailsStillRequireCurrentMemberAuthors(t *testing.T) {
for _, tt := range []struct {
name, predicate string
handler httpapi.HandlerFunc
}{
{"diary", "d.created_by IN", diaryDetail},
{"timeline", "WHERE id = ? AND created_by IN", timelineDetail},
{"checkin", "c.created_by IN", checkinDetail},
} {
t.Run(tt.name, func(t *testing.T) {
c, out, _ := permissionCtx(t, "GET", "/api/detail?id=9", "", []testdb.Step{uidStep(1), cidStep(22), {Match: "SELECT user1_id, user2_id FROM couples WHERE id = ?", Columns: []string{"user1_id", "user2_id"}, Rows: [][]driver.Value{{int64(1), int64(3)}}}, {Match: tt.predicate, Columns: []string{"id"}}})
tt.handler(c)
if out.Code != 404 {
t.Fatalf("missing or unrelated detail returned %d: %s", out.Code, out.Body.String())
}
})
}
}
func TestLegacyMemberRecordsRemainReadableAcrossHistoricalSpaceIDs(t *testing.T) {
for _, tt := range []struct {
name, predicate string
handler httpapi.HandlerFunc
}{
{"timeline", "FROM timeline WHERE id = ? AND created_by IN", timelineDetail},
{"photos", "WHERE a.created_by IN (?, ?) AND p.album_id = ?", photos},
} {
t.Run(tt.name, func(t *testing.T) {
c, out, _ := permissionCtx(t, "GET", "/api/detail?id=9&album_id=9", "", []testdb.Step{
uidStep(1), cidStep(22),
{Match: "SELECT user1_id, user2_id FROM couples WHERE id = ?", Columns: []string{"user1_id", "user2_id"}, Rows: [][]driver.Value{{int64(1), int64(3)}}},
{Match: tt.predicate, Columns: []string{"id", "couple_id", "created_by", "path"}, Rows: [][]driver.Value{{int64(9), int64(10), int64(1), "uploads/users/1/photos/a.jpg"}}},
})
tt.handler(c)
if out.Code != 200 || !strings.Contains(out.Body.String(), "uploads/users/1/photos/a.jpg") {
t.Fatalf("legacy record disappeared: code=%d body=%s", out.Code, out.Body.String())
}
})
}
}
func TestLegacyPeriodReadsSharedTablesInsteadOfPersonalTables(t *testing.T) {
c, out, _ := permissionCtx(t, "GET", "/api/periodList", "", []testdb.Step{
uidStep(1), cidStep(10),
{Match: "SELECT period_visible FROM users", Columns: []string{"period_visible"}, Rows: [][]driver.Value{{int64(1)}}},
{Match: "SELECT id, period_visible FROM users WHERE couple_id", Columns: []string{"id", "period_visible"}, Rows: [][]driver.Value{{int64(2), int64(1)}}},
{Match: "SELECT user1_id, user2_id, period_source_user_id", Columns: []string{"user1_id", "user2_id", "period_source_user_id"}, Rows: [][]driver.Value{{int64(1), int64(2), int64(2)}}},
{Match: "FROM period_records WHERE couple_id = ?", Args: []any{int64(10)}, Columns: []string{"id", "user_id"}, Rows: [][]driver.Value{{int64(7), int64(2)}}},
{Match: "FROM period_daily_records WHERE couple_id = ?", Args: []any{int64(10)}, Columns: []string{"id"}},
{Match: "FROM period_sex_records WHERE couple_id = ?", Args: []any{int64(10)}, Columns: []string{"id"}},
})
periodList(c)
if out.Code != 200 || !strings.Contains(out.Body.String(), `"user_id":"2"`) {
t.Fatalf("shared period missing: %d %s", out.Code, out.Body.String())
}
}