Files
NixMsg/internal/admin/a3_test.go
T

288 lines
8.4 KiB
Go

package admin_test
import (
"context"
"database/sql"
"encoding/json"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
"git.asio.asia/nixevol/NixMsg/internal/admin"
"git.asio.asia/nixevol/NixMsg/internal/app/group"
"git.asio.asia/nixevol/NixMsg/internal/auth"
"git.asio.asia/nixevol/NixMsg/internal/config"
"git.asio.asia/nixevol/NixMsg/internal/store"
)
func setupA3(t *testing.T) (*store.DB, *httptest.Server, *http.Client) {
t.Helper()
dir := t.TempDir()
db, err := store.Open(filepath.Join(dir, "data"), "FULL")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = db.Close() })
hash := auth.NewStubHashPool()
if seedErr := admin.SeedAdminPassword(context.Background(), db, hash, testPassword); seedErr != nil {
t.Fatal(seedErr)
}
gApp := group.New(group.Config{DB: db, MaxGroupMembers: 100})
h := admin.New(admin.Deps{
DB: db,
Hash: hash,
Tokens: admin.NewRandomAPITokens(),
Locks: admin.NewMemoryLoginLocks(),
Groups: gApp,
Config: config.Default(),
Version: "0.1.0-test",
})
srv := httptest.NewServer(h)
t.Cleanup(srv.Close)
jar, err := cookiejar.New(nil)
if err != nil {
t.Fatal(err)
}
client := &http.Client{Jar: jar}
login(t, client, srv.URL)
return db, srv, client
}
func csrf() map[string]string {
return map[string]string{"X-Nixmsg-Request": "1"}
}
func insertEndpoint(t *testing.T, db *store.DB, id, source string, enabled bool, online bool) {
t.Helper()
now := time.Now().UnixMilli()
en := 1
if !enabled {
en = 0
}
var onlineSince, offlineSince any
if online {
onlineSince = now
} else {
offlineSince = now
}
err := db.Queue.Do(context.Background(), func(tx *sql.Tx) error {
_, e := tx.Exec(`INSERT INTO endpoints(
id, name, remark, source, login_hash, talk_hash, talk_version,
default_delay_ms, enabled, created_at, online_since, offline_since
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`,
id, id, "", source, "stub$hash", nil, 0, 0, en, now, onlineSince, offlineSince)
return e
})
if err != nil {
t.Fatal(err)
}
}
func TestRegistrationToggleAndGenerate(t *testing.T) {
_, srv, client := setupA3(t)
base := srv.URL
res := doReq(t, client, http.MethodGet, base+"/api/admin/registration", "", nil)
env := decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("get: %d %+v", res.StatusCode, env)
}
var got map[string]any
_ = json.Unmarshal(env.Data, &got)
if got["enabled"] != false {
t.Fatalf("default enabled=%v", got["enabled"])
}
res = doReq(t, client, http.MethodPut, base+"/api/admin/registration",
`{"enabled":true,"code":"abcdefgh"}`, csrf())
env = decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("put enable: %d %+v", res.StatusCode, env)
}
_ = json.Unmarshal(env.Data, &got)
if got["enabled"] != true || got["code"] != "abcdefgh" {
t.Fatalf("after put: %v", got)
}
res = doReq(t, client, http.MethodPut, base+"/api/admin/registration",
`{"generate":true}`, csrf())
env = decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("generate: %d %+v", res.StatusCode, env)
}
_ = json.Unmarshal(env.Data, &got)
code, _ := got["code"].(string)
if len(code) != 16 {
t.Fatalf("generated len=%d code=%q", len(code), code)
}
res = doReq(t, client, http.MethodPut, base+"/api/admin/registration",
`{"enabled":false}`, csrf())
env = decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("disable: %d %+v", res.StatusCode, env)
}
_ = json.Unmarshal(env.Data, &got)
if got["enabled"] != false {
t.Fatalf("disabled=%v", got["enabled"])
}
}
func TestMessageDetailHasNoBody(t *testing.T) {
db, srv, client := setupA3(t)
base := srv.URL
now := time.Now().UnixMilli()
err := db.Queue.Do(context.Background(), func(tx *sql.Tx) error {
if _, e := tx.Exec(`INSERT INTO messages(
seq, id, sender_id, dest_kind, dest_id, meta, content_type, body_enc,
send_at, keep, ttl_seconds, receipt, state, reason, created_at
) VALUES(1,'m1','a','endpoint','b','{}','text/plain','enc',?,?,0,1,'dispatched','',?)`,
now, 1, now); e != nil {
return e
}
if _, e := tx.Exec(`INSERT INTO message_bodies(seq, body) VALUES(1, ?)`,
[]byte("SECRET_BODY_SHOULD_NOT_APPEAR")); e != nil {
return e
}
_, e := tx.Exec(`INSERT INTO deliveries(
seq, endpoint_id, send_at, keep, state, reason, attempts, updated_at, pushed_at
) VALUES(1,'b',?,1,'pending','',2,?,?)`, now, now, now)
return e
})
if err != nil {
t.Fatal(err)
}
res := doReq(t, client, http.MethodGet, base+"/api/admin/messages/1", "", nil)
env := decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("detail: %d %+v", res.StatusCode, env)
}
raw := string(env.Data)
if strings.Contains(raw, "body") || strings.Contains(raw, "SECRET_BODY") {
t.Fatalf("response contains body/secret: %s", raw)
}
var detail map[string]any
_ = json.Unmarshal(env.Data, &detail)
dels, _ := detail["deliveries"].([]any)
if len(dels) != 1 {
t.Fatalf("deliveries=%v", detail["deliveries"])
}
d0 := dels[0].(map[string]any)
if d0["attempts"].(float64) != 2 {
t.Fatalf("attempts=%v", d0["attempts"])
}
res = doReq(t, client, http.MethodGet, base+"/api/admin/messages", "", nil)
env = decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("list: %d %+v", res.StatusCode, env)
}
if strings.Contains(string(env.Data), "SECRET_BODY") || strings.Contains(string(env.Data), `"body"`) {
t.Fatalf("list leaked body: %s", env.Data)
}
}
func TestGroupsCRUD(t *testing.T) {
db, srv, client := setupA3(t)
base := srv.URL
insertEndpoint(t, db, "alice", "admin", true, false)
insertEndpoint(t, db, "bob", "admin", true, true)
insertEndpoint(t, db, "carol", "self", true, false)
res := doReq(t, client, http.MethodPost, base+"/api/admin/groups",
`{"name":"一组","owner_id":"alice","member_ids":["bob","carol"]}`, csrf())
env := decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("create: %d %+v", res.StatusCode, env)
}
var created struct {
ID string `json:"id"`
OwnerID string `json:"owner_id"`
}
_ = json.Unmarshal(env.Data, &created)
if created.ID == "" || created.OwnerID != "alice" {
t.Fatalf("created=%+v", created)
}
res = doReq(t, client, http.MethodPatch, base+"/api/admin/groups/"+created.ID,
`{"name":"新名"}`, csrf())
env = decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("rename: %d %+v", res.StatusCode, env)
}
res = doReq(t, client, http.MethodPost, base+"/api/admin/groups/"+created.ID+"/transfer",
`{"endpoint_id":"bob"}`, csrf())
env = decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("transfer: %d %+v", res.StatusCode, env)
}
res = doReq(t, client, http.MethodDelete,
base+"/api/admin/groups/"+created.ID+"/members/carol", "", csrf())
env = decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("remove: %d %+v", res.StatusCode, env)
}
res = doReq(t, client, http.MethodDelete, base+"/api/admin/groups/"+created.ID, "", csrf())
env = decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("dissolve: %d %+v", res.StatusCode, env)
}
}
func TestOverviewAndSettings(t *testing.T) {
db, srv, client := setupA3(t)
base := srv.URL
insertEndpoint(t, db, "a1", "admin", true, true)
insertEndpoint(t, db, "s1", "self", true, false)
insertEndpoint(t, db, "d1", "admin", false, false)
res := doReq(t, client, http.MethodGet, base+"/api/admin/overview", "", nil)
env := decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("overview: %d %+v", res.StatusCode, env)
}
var ov map[string]any
_ = json.Unmarshal(env.Data, &ov)
if ov["version"] != "0.1.0-test" {
t.Fatalf("version=%v", ov["version"])
}
if ov["endpoints_total"].(float64) != 3 {
t.Fatalf("total=%v", ov["endpoints_total"])
}
if ov["endpoints_self"].(float64) != 1 {
t.Fatalf("self=%v", ov["endpoints_self"])
}
if ov["endpoints_online"].(float64) != 1 {
t.Fatalf("online=%v", ov["endpoints_online"])
}
if ov["endpoints_disabled"].(float64) != 1 {
t.Fatalf("disabled=%v", ov["endpoints_disabled"])
}
res = doReq(t, client, http.MethodGet, base+"/api/admin/settings", "", nil)
env = decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("settings: %d %+v", res.StatusCode, env)
}
raw := string(env.Data)
if strings.Contains(raw, "token") || strings.Contains(raw, "password") {
t.Fatalf("settings leaked secret: %s", raw)
}
var st map[string]any
_ = json.Unmarshal(env.Data, &st)
if st["listen"] == nil || st["limits"] == nil {
t.Fatalf("settings=%v", st)
}
}