116 lines
3.3 KiB
Go
116 lines
3.3 KiB
Go
package api_test
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
"sync"
|
||
|
|
"sync/atomic"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestRouteLabelRedactsExportAndInviteTokens(t *testing.T) {
|
||
|
|
// Exercised via access path: forged large token must not break handlers,
|
||
|
|
// and single-use / captcha flows remain healthy. Direct label coverage lives
|
||
|
|
// in package api (route_label_test.go).
|
||
|
|
e := setup(t)
|
||
|
|
code, _, _ := e.doRaw(t, "GET", "/api/exports/chats/super-secret-export-token-value", "", nil)
|
||
|
|
if code != http.StatusUnauthorized {
|
||
|
|
t.Fatalf("bogus export token: want 401 got %d", code)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMetricsRequiresTokenOutsideDevOpen(t *testing.T) {
|
||
|
|
e := setup(t)
|
||
|
|
// DevMode=true in tests → metrics open without token.
|
||
|
|
code, _, _ := e.doRaw(t, "GET", "/metrics", "", nil)
|
||
|
|
if code != http.StatusOK {
|
||
|
|
t.Fatalf("dev metrics: want 200 got %d", code)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestWatchAntiCheatCapsForgedDelta(t *testing.T) {
|
||
|
|
e := setup(t)
|
||
|
|
token, _ := register(t, e, "Cheat Watch", "cheat-watch@test.dev")
|
||
|
|
|
||
|
|
// Wait so wall-clock cap is tight, then claim a huge delta.
|
||
|
|
time.Sleep(200 * time.Millisecond)
|
||
|
|
code, body := e.do(t, "POST", "/api/lessons/1/heartbeat", token, map[string]any{
|
||
|
|
"position": 90, "delta": 999, "seq": 1,
|
||
|
|
})
|
||
|
|
if code != http.StatusOK {
|
||
|
|
t.Fatalf("heartbeat: %d %v", code, body)
|
||
|
|
}
|
||
|
|
watched, _ := body["watchedSeconds"].(float64)
|
||
|
|
// maxHeartbeatDelta is 15s; with ~0.2s wall clock + slack, credit must stay well under 20s.
|
||
|
|
if watched > 20 {
|
||
|
|
t.Fatalf("forged delta credited too much watch time: %v", watched)
|
||
|
|
}
|
||
|
|
if watched <= 0 {
|
||
|
|
t.Fatalf("expected some credit, got %v", watched)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Replay same seq — must not double-credit.
|
||
|
|
prev := watched
|
||
|
|
code, body = e.do(t, "POST", "/api/lessons/1/heartbeat", token, map[string]any{
|
||
|
|
"position": 91, "delta": 9, "seq": 1,
|
||
|
|
})
|
||
|
|
if code != http.StatusOK {
|
||
|
|
t.Fatalf("replay heartbeat: %d %v", code, body)
|
||
|
|
}
|
||
|
|
watched, _ = body["watchedSeconds"].(float64)
|
||
|
|
if watched != prev {
|
||
|
|
t.Fatalf("seq replay credited again: before=%v after=%v", prev, watched)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRegisterRaceSamePhone(t *testing.T) {
|
||
|
|
e := setup(t)
|
||
|
|
enableRegistration(t, e)
|
||
|
|
whitelistPhones(t, e, "09127770001")
|
||
|
|
|
||
|
|
const n = 8
|
||
|
|
bodies := make([]map[string]any, n)
|
||
|
|
for i := 0; i < n; i++ {
|
||
|
|
bodies[i] = registerBody(t, e, "09127770001",
|
||
|
|
"race-phone-"+itoa(int64(i+1))+"@test.dev",
|
||
|
|
"raceph"+itoa(int64(i+1)))
|
||
|
|
}
|
||
|
|
|
||
|
|
var okCount atomic.Int32
|
||
|
|
var wg sync.WaitGroup
|
||
|
|
wg.Add(n)
|
||
|
|
for i := 0; i < n; i++ {
|
||
|
|
i := i
|
||
|
|
go func() {
|
||
|
|
defer wg.Done()
|
||
|
|
code, _ := e.doHeaders(t, "POST", "/api/auth/register", "", map[string]string{
|
||
|
|
"X-Real-IP": "203.0.113." + itoa(int64(40+i)),
|
||
|
|
}, bodies[i])
|
||
|
|
if code == http.StatusCreated {
|
||
|
|
okCount.Add(1)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
wg.Wait()
|
||
|
|
if okCount.Load() != 1 {
|
||
|
|
t.Fatalf("expected exactly one successful register for same phone, got %d", okCount.Load())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRegisterRequiresCaptcha(t *testing.T) {
|
||
|
|
e := setup(t)
|
||
|
|
enableRegistration(t, e)
|
||
|
|
whitelistPhones(t, e, "09127770099")
|
||
|
|
body := map[string]any{
|
||
|
|
"name": "بدون کپچا", "email": "nocap@test.dev", "username": "nocapuser",
|
||
|
|
"password": "password123", "passwordConfirm": "password123", "phone": "09127770099",
|
||
|
|
"securityQuestion": "رنگ؟", "securityAnswer": "آبی",
|
||
|
|
}
|
||
|
|
code, res := e.doHeaders(t, "POST", "/api/auth/register", "", map[string]string{
|
||
|
|
"X-Real-IP": "203.0.113.199",
|
||
|
|
}, body)
|
||
|
|
if code != http.StatusBadRequest {
|
||
|
|
t.Fatalf("register without captcha: want 400 got %d %v", code, res)
|
||
|
|
}
|
||
|
|
}
|