Add POST /v1/admin/users/{pubkey}/reset-username and
POST /v1/admin/users/reset-usernames to clear manual_username
and last_synced_at so nostr profile sync re-evaluates users.
Includes OpenAPI docs, audit actions, and tests.
579 lines
16 KiB
Go
579 lines
16 KiB
Go
package http_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/noderunners/nip05api/internal/audit"
|
|
"github.com/noderunners/nip05api/internal/config"
|
|
"github.com/noderunners/nip05api/internal/db"
|
|
"github.com/noderunners/nip05api/internal/dm"
|
|
httpapi "github.com/noderunners/nip05api/internal/http"
|
|
"github.com/noderunners/nip05api/internal/messages"
|
|
"github.com/noderunners/nip05api/internal/user"
|
|
"github.com/noderunners/nip05api/internal/webhook"
|
|
)
|
|
|
|
const testHex = "0e8c41ebcd55a8d8db2e0a8c3a4b9c5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c"
|
|
const testKey = "test-admin-key-twenty-five-chars"
|
|
|
|
type fixture struct {
|
|
srv *httptest.Server
|
|
db *db.DB
|
|
}
|
|
|
|
func newFixture(t *testing.T) *fixture {
|
|
t.Helper()
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
d, err := db.Open(dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := d.Migrate(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cfg := &config.Config{
|
|
Domain: "test.local",
|
|
Port: 0,
|
|
AdminAPIKey: testKey,
|
|
FrontendURL: "https://test.local/nip05",
|
|
Lightning: config.LightningConfig{Enabled: false},
|
|
Expiry: config.ExpiryConfig{GraceDays: 30},
|
|
ReservedUsernames: []string{"admin", "root"},
|
|
RateLimitPerMin: 0, // disabled in tests
|
|
}
|
|
tmpls, _ := messages.Load("/nonexistent.yaml")
|
|
users := user.NewService(user.NewRepo(d), cfg.ReservedUsernames)
|
|
dms := dm.NewService(dm.NewRepo(d), tmpls, false)
|
|
hooks := webhook.NewService(webhook.NewRepo(d), cfg.Domain, false)
|
|
|
|
srv := httptest.NewServer(httpapi.NewServer(httpapi.Deps{
|
|
Cfg: cfg, DB: d, Users: users, DMs: dms, Hooks: hooks,
|
|
Audit: audit.New(d), Version: "test",
|
|
}).Handler)
|
|
t.Cleanup(func() {
|
|
srv.Close()
|
|
_ = d.Close()
|
|
})
|
|
return &fixture{srv: srv, db: d}
|
|
}
|
|
|
|
func (f *fixture) get(t *testing.T, path string) (*http.Response, []byte) {
|
|
t.Helper()
|
|
resp, err := http.Get(f.srv.URL + path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body := readAll(t, resp)
|
|
return resp, body
|
|
}
|
|
|
|
func (f *fixture) admin(t *testing.T, method, path string, payload any) (*http.Response, []byte) {
|
|
t.Helper()
|
|
var body []byte
|
|
if payload != nil {
|
|
var err error
|
|
body, err = json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
req, _ := http.NewRequest(method, f.srv.URL+path, bytes.NewReader(body))
|
|
req.Header.Set("X-API-Key", testKey)
|
|
if payload != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
return resp, readAll(t, resp)
|
|
}
|
|
|
|
func readAll(t *testing.T, resp *http.Response) []byte {
|
|
t.Helper()
|
|
buf := new(bytes.Buffer)
|
|
if _, err := buf.ReadFrom(resp.Body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func TestHealthz(t *testing.T) {
|
|
f := newFixture(t)
|
|
resp, body := f.get(t, "/healthz")
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status %d: %s", resp.StatusCode, body)
|
|
}
|
|
var got map[string]string
|
|
_ = json.Unmarshal(body, &got)
|
|
if got["status"] != "ok" || got["version"] != "test" {
|
|
t.Errorf("body: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestPricing(t *testing.T) {
|
|
f := newFixture(t)
|
|
resp, body := f.get(t, "/v1/pricing")
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status %d: %s", resp.StatusCode, body)
|
|
}
|
|
var got map[string]any
|
|
_ = json.Unmarshal(body, &got)
|
|
if _, ok := got["yearly_sats"]; !ok {
|
|
t.Errorf("missing yearly_sats: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestNostrJSON_EmptyAndPopulated(t *testing.T) {
|
|
f := newFixture(t)
|
|
resp, body := f.get(t, "/.well-known/nostr.json")
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status %d", resp.StatusCode)
|
|
}
|
|
var got map[string]any
|
|
_ = json.Unmarshal(body, &got)
|
|
if got["names"] == nil {
|
|
t.Errorf("missing names key: %s", body)
|
|
}
|
|
|
|
f.admin(t, "POST", "/v1/admin/users", map[string]any{
|
|
"pubkey": testHex, "username": "alice",
|
|
"subscription_type": "yearly", "years": 1,
|
|
})
|
|
|
|
_, body = f.get(t, "/.well-known/nostr.json")
|
|
var populated struct {
|
|
Names map[string]string `json:"names"`
|
|
}
|
|
if err := json.Unmarshal(body, &populated); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if populated.Names["alice"] != testHex {
|
|
t.Errorf("alice not in nostr.json: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestWhitelistPubkeys(t *testing.T) {
|
|
f := newFixture(t)
|
|
|
|
resp, body := f.get(t, "/v1/whitelist/pubkeys")
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("empty list status %d: %s", resp.StatusCode, body)
|
|
}
|
|
var empty []string
|
|
if err := json.Unmarshal(body, &empty); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(empty) != 0 {
|
|
t.Errorf("expected empty array: %s", body)
|
|
}
|
|
|
|
f.admin(t, "POST", "/v1/admin/users", map[string]any{
|
|
"pubkey": testHex, "username": "alice",
|
|
"subscription_type": "yearly", "years": 1,
|
|
})
|
|
|
|
inactiveHex := "1f8c41ebcd55a8d8db2e0a8c3a4b9c5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1d"
|
|
_, err := f.db.ExecContext(context.Background(),
|
|
`INSERT INTO users (pubkey, username, subscription_type, expires_at, is_active, manual_username, created_at)
|
|
VALUES (?, 'bob', 'yearly', NULL, 0, 1, datetime('now'))`,
|
|
inactiveHex)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, body = f.get(t, "/v1/whitelist/pubkeys")
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status %d: %s", resp.StatusCode, body)
|
|
}
|
|
var pubkeys []string
|
|
if err := json.Unmarshal(body, &pubkeys); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(pubkeys) != 1 || pubkeys[0] != testHex {
|
|
t.Errorf("want [%s], got %v (body %s)", testHex, pubkeys, body)
|
|
}
|
|
for _, pk := range pubkeys {
|
|
if pk == inactiveHex {
|
|
t.Errorf("inactive pubkey should not appear: %v", pubkeys)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUsernameAvailability(t *testing.T) {
|
|
f := newFixture(t)
|
|
resp, body := f.get(t, "/v1/usernames/alice/available")
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status %d", resp.StatusCode)
|
|
}
|
|
var got map[string]any
|
|
_ = json.Unmarshal(body, &got)
|
|
if got["available"] != true {
|
|
t.Errorf("expected available=true: %s", body)
|
|
}
|
|
|
|
// Reserved name.
|
|
_, body = f.get(t, "/v1/usernames/admin/available")
|
|
_ = json.Unmarshal(body, &got)
|
|
if got["available"] != false {
|
|
t.Errorf("admin should be reserved: %s", body)
|
|
}
|
|
|
|
// Invalid format.
|
|
_, body = f.get(t, "/v1/usernames/-bad/available")
|
|
_ = json.Unmarshal(body, &got)
|
|
if got["available"] != false {
|
|
t.Errorf("-bad should be invalid: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestAdminAuthGate(t *testing.T) {
|
|
f := newFixture(t)
|
|
resp, _ := f.get(t, "/v1/admin/users")
|
|
if resp.StatusCode != 401 {
|
|
t.Fatalf("expected 401, got %d", resp.StatusCode)
|
|
}
|
|
resp, _ = f.admin(t, "GET", "/v1/admin/users", nil)
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("expected 200 with key, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestAdminLifecycle(t *testing.T) {
|
|
f := newFixture(t)
|
|
|
|
// Add.
|
|
resp, body := f.admin(t, "POST", "/v1/admin/users", map[string]any{
|
|
"pubkey": testHex, "username": "alice",
|
|
"subscription_type": "yearly", "years": 1,
|
|
})
|
|
if resp.StatusCode != 201 {
|
|
t.Fatalf("add status %d: %s", resp.StatusCode, body)
|
|
}
|
|
|
|
// Lookup hex.
|
|
resp, body = f.get(t, "/v1/users/"+testHex)
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("lookup status %d", resp.StatusCode)
|
|
}
|
|
var got map[string]any
|
|
_ = json.Unmarshal(body, &got)
|
|
if got["is_whitelisted"] != true || got["username"] != "alice" {
|
|
t.Errorf("unexpected lookup body: %s", body)
|
|
}
|
|
|
|
// Lookup npub form.
|
|
npub := "npub1p6xyr67d2k5d3kewp2xr5juutehh4zuup50z7wjtt3kharu6pvwqjh7065"
|
|
resp, _ = f.get(t, "/v1/users/"+npub)
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("npub lookup status %d", resp.StatusCode)
|
|
}
|
|
|
|
// Username unavailable now.
|
|
_, body = f.get(t, "/v1/usernames/alice/available")
|
|
_ = json.Unmarshal(body, &got)
|
|
if got["available"] != false {
|
|
t.Errorf("expected unavailable: %s", body)
|
|
}
|
|
|
|
// Extend.
|
|
resp, body = f.admin(t, "POST", "/v1/admin/users/"+testHex+"/extend",
|
|
map[string]any{"years": 2})
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("extend status %d: %s", resp.StatusCode, body)
|
|
}
|
|
|
|
// Update username.
|
|
resp, body = f.admin(t, "PUT", "/v1/admin/users/"+testHex,
|
|
map[string]any{"username": "alice2"})
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("update status %d: %s", resp.StatusCode, body)
|
|
}
|
|
|
|
// Delete.
|
|
resp, _ = f.admin(t, "DELETE", "/v1/admin/users/"+testHex, nil)
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("delete status %d", resp.StatusCode)
|
|
}
|
|
|
|
// Gone.
|
|
resp, _ = f.get(t, "/v1/users/"+testHex)
|
|
if resp.StatusCode != 404 {
|
|
t.Fatalf("expected 404 after delete, got %d", resp.StatusCode)
|
|
}
|
|
|
|
// Audit log should reflect every admin action.
|
|
wantActions := []string{"user.added", "user.extended", "user.username_changed", "user.deleted"}
|
|
rows, qerr := f.db.Query(`SELECT action FROM audit_log`)
|
|
if qerr != nil {
|
|
t.Fatal(qerr)
|
|
}
|
|
defer rows.Close()
|
|
seen := map[string]bool{}
|
|
for rows.Next() {
|
|
var action string
|
|
if err := rows.Scan(&action); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seen[action] = true
|
|
}
|
|
for _, action := range wantActions {
|
|
if !seen[action] {
|
|
t.Errorf("missing audit action %q (got %v)", action, seen)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAdminAdd_BadInputs(t *testing.T) {
|
|
f := newFixture(t)
|
|
|
|
resp, _ := f.admin(t, "POST", "/v1/admin/users", map[string]any{
|
|
"pubkey": "notapubkey", "username": "alice",
|
|
"subscription_type": "yearly",
|
|
})
|
|
if resp.StatusCode != 400 {
|
|
t.Errorf("bad pubkey: expected 400, got %d", resp.StatusCode)
|
|
}
|
|
|
|
resp, _ = f.admin(t, "POST", "/v1/admin/users", map[string]any{
|
|
"pubkey": testHex, "username": "admin",
|
|
"subscription_type": "yearly",
|
|
})
|
|
if resp.StatusCode == 200 || resp.StatusCode == 201 {
|
|
t.Errorf("reserved username accepted: %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestAdminResetUsername(t *testing.T) {
|
|
f := newFixture(t)
|
|
|
|
resp, body := f.admin(t, "POST", "/v1/admin/users", map[string]any{
|
|
"pubkey": testHex, "username": "alice",
|
|
"subscription_type": "yearly", "years": 1,
|
|
})
|
|
if resp.StatusCode != 201 {
|
|
t.Fatalf("add status %d: %s", resp.StatusCode, body)
|
|
}
|
|
|
|
resp, body = f.admin(t, "PUT", "/v1/admin/users/"+testHex,
|
|
map[string]any{"username": "alice2"})
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("update status %d: %s", resp.StatusCode, body)
|
|
}
|
|
var updated map[string]any
|
|
_ = json.Unmarshal(body, &updated)
|
|
if updated["manual_username"] != true {
|
|
t.Fatalf("expected manual_username=true after update, got %v", updated["manual_username"])
|
|
}
|
|
|
|
if _, err := f.db.ExecContext(context.Background(),
|
|
`UPDATE users SET last_synced_at = ? WHERE pubkey = ?`,
|
|
"2099-01-01T00:00:00Z", testHex); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, body = f.admin(t, "POST", "/v1/admin/users/"+testHex+"/reset-username", nil)
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("reset status %d: %s", resp.StatusCode, body)
|
|
}
|
|
var got map[string]any
|
|
if err := json.Unmarshal(body, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got["manual_username"] != false {
|
|
t.Errorf("expected manual_username=false after reset, got %v (body %s)", got["manual_username"], body)
|
|
}
|
|
if got["username"] != "alice2" {
|
|
t.Errorf("expected username unchanged after reset, got %v", got["username"])
|
|
}
|
|
|
|
var lastSynced sql.NullString
|
|
if err := f.db.QueryRow(`SELECT last_synced_at FROM users WHERE pubkey = ?`, testHex).
|
|
Scan(&lastSynced); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if lastSynced.Valid && lastSynced.String != "" {
|
|
t.Errorf("expected last_synced_at to be NULL after reset, got %q", lastSynced.String)
|
|
}
|
|
|
|
resp, body = f.admin(t, "POST", "/v1/admin/users/"+testHex+"/reset-username", nil)
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("idempotent reset status %d: %s", resp.StatusCode, body)
|
|
}
|
|
|
|
if resp, _ := f.admin(t, "DELETE", "/v1/admin/users/"+testHex, nil); resp.StatusCode != 200 {
|
|
t.Fatalf("delete status %d", resp.StatusCode)
|
|
}
|
|
resp, _ = f.admin(t, "POST", "/v1/admin/users/"+testHex+"/reset-username", nil)
|
|
if resp.StatusCode != 404 {
|
|
t.Errorf("expected 404 for unknown pubkey, got %d", resp.StatusCode)
|
|
}
|
|
|
|
resp, _ = f.admin(t, "POST", "/v1/admin/users/notapubkey/reset-username", nil)
|
|
if resp.StatusCode != 400 {
|
|
t.Errorf("expected 400 for invalid pubkey, got %d", resp.StatusCode)
|
|
}
|
|
|
|
row := f.db.QueryRow(`SELECT COUNT(*) FROM audit_log WHERE action = ?`,
|
|
"user.username_reset")
|
|
var n int
|
|
if err := row.Scan(&n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n < 1 {
|
|
t.Errorf("expected user.username_reset audit entry, got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestAdminResetAllUsernames(t *testing.T) {
|
|
f := newFixture(t)
|
|
|
|
// Two more 64-char hex strings — not necessarily valid Schnorr keys, so we
|
|
// insert them via raw SQL to bypass the admin endpoint validation.
|
|
hexB := "1f8c41ebcd55a8d8db2e0a8c3a4b9c5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1d"
|
|
hexInactive := "2a8c41ebcd55a8d8db2e0a8c3a4b9c5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1e"
|
|
|
|
if resp, body := f.admin(t, "POST", "/v1/admin/users", map[string]any{
|
|
"pubkey": testHex, "username": "alice",
|
|
"subscription_type": "yearly", "years": 1,
|
|
}); resp.StatusCode != 201 {
|
|
t.Fatalf("add A status %d: %s", resp.StatusCode, body)
|
|
}
|
|
if resp, body := f.admin(t, "PUT", "/v1/admin/users/"+testHex,
|
|
map[string]any{"username": "alice2"}); resp.StatusCode != 200 {
|
|
t.Fatalf("pin A status %d: %s", resp.StatusCode, body)
|
|
}
|
|
|
|
if _, err := f.db.ExecContext(context.Background(),
|
|
`INSERT INTO users (pubkey, username, subscription_type, expires_at, is_active, manual_username, last_synced_at, created_at)
|
|
VALUES (?, 'bob', 'yearly', NULL, 1, 1, ?, datetime('now'))`,
|
|
hexB, "2099-01-01T00:00:00Z"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := f.db.ExecContext(context.Background(),
|
|
`INSERT INTO users (pubkey, username, subscription_type, expires_at, is_active, manual_username, last_synced_at, created_at)
|
|
VALUES (?, 'carol', 'yearly', NULL, 0, 1, ?, datetime('now'))`,
|
|
hexInactive, "2099-01-01T00:00:00Z"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := f.db.ExecContext(context.Background(),
|
|
`UPDATE users SET last_synced_at = ? WHERE pubkey = ?`,
|
|
"2099-01-01T00:00:00Z", testHex); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resp, body := f.admin(t, "POST", "/v1/admin/users/reset-usernames", nil)
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("bulk reset status %d: %s", resp.StatusCode, body)
|
|
}
|
|
var got map[string]any
|
|
if err := json.Unmarshal(body, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cnt, _ := got["reset"].(float64); cnt != 2 {
|
|
t.Errorf("expected reset=2 (active users only), got %v (body %s)", got["reset"], body)
|
|
}
|
|
|
|
rows, err := f.db.Query(`SELECT pubkey, manual_username, last_synced_at FROM users`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var pk string
|
|
var manual int
|
|
var lastSynced sql.NullString
|
|
if err := rows.Scan(&pk, &manual, &lastSynced); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
switch pk {
|
|
case testHex, hexB:
|
|
if manual != 0 {
|
|
t.Errorf("%s: expected manual_username=0 after reset, got %d", pk, manual)
|
|
}
|
|
if lastSynced.Valid && lastSynced.String != "" {
|
|
t.Errorf("%s: expected last_synced_at NULL after reset, got %q", pk, lastSynced.String)
|
|
}
|
|
case hexInactive:
|
|
if manual != 1 {
|
|
t.Errorf("inactive user manual_username should be untouched, got %d", manual)
|
|
}
|
|
if !lastSynced.Valid || lastSynced.String == "" {
|
|
t.Errorf("inactive user last_synced_at should be untouched, got %v", lastSynced)
|
|
}
|
|
}
|
|
}
|
|
|
|
row := f.db.QueryRow(`SELECT COUNT(*) FROM audit_log WHERE action = ?`,
|
|
"users.usernames_reset")
|
|
var n int
|
|
if err := row.Scan(&n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Errorf("expected 1 users.usernames_reset audit entry, got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestOpenAPI(t *testing.T) {
|
|
f := newFixture(t)
|
|
resp, body := f.get(t, "/openapi.json")
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status %d", resp.StatusCode)
|
|
}
|
|
var spec map[string]any
|
|
if err := json.Unmarshal(body, &spec); err != nil {
|
|
t.Fatalf("openapi not valid json: %v", err)
|
|
}
|
|
if spec["openapi"] == nil {
|
|
t.Errorf("missing openapi field: %s", body[:min(200, len(body))])
|
|
}
|
|
}
|
|
|
|
func TestDocsPage(t *testing.T) {
|
|
f := newFixture(t)
|
|
resp, body := f.get(t, "/docs")
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status %d", resp.StatusCode)
|
|
}
|
|
if !bytes.Contains(body, []byte("swagger-ui")) {
|
|
t.Errorf("expected swagger UI markup")
|
|
}
|
|
}
|
|
|
|
func TestBodyLimit(t *testing.T) {
|
|
f := newFixture(t)
|
|
huge := bytes.Repeat([]byte("a"), 2<<20) // 2 MiB
|
|
body := []byte(`{"pubkey":"` + testHex + `","username":"alice","subscription_type":"yearly","data":"` + string(huge) + `"}`)
|
|
req, _ := http.NewRequest("POST", f.srv.URL+"/v1/admin/users", bytes.NewReader(body))
|
|
req.Header.Set("X-API-Key", testKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 413 {
|
|
t.Errorf("expected 413, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|