feat(backend): implement HTTP API, auth, chat, and admin surfaces
Expose learning, registration controls, mentor chat, realtime WebSocket, and staff administration.
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWhitelistImportListDelete(t *testing.T) {
|
||||
e := setup(t)
|
||||
admin := login(t, e, bootstrapAdminEmail)
|
||||
|
||||
code, body := e.do(t, "POST", "/api/admin/phone-whitelist/import", admin, map[string]any{
|
||||
"text": "09123330001\n09123330002,09123330001\n12345",
|
||||
})
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("import: %d %v", code, body)
|
||||
}
|
||||
if body["inserted"] != float64(2) {
|
||||
t.Fatalf("inserted: %v", body["inserted"])
|
||||
}
|
||||
if body["invalid"] != float64(1) {
|
||||
t.Fatalf("invalid: %v", body["invalid"])
|
||||
}
|
||||
|
||||
code, body = e.do(t, "POST", "/api/admin/phone-whitelist/import", admin, map[string]any{
|
||||
"phones": []string{"09123330001", "09123330003"},
|
||||
})
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("reimport: %d %v", code, body)
|
||||
}
|
||||
if body["inserted"] != float64(1) || body["skipped"] != float64(1) {
|
||||
t.Fatalf("expected insert1 skip1, got %v", body)
|
||||
}
|
||||
|
||||
code, body = e.do(t, "GET", "/api/admin/phone-whitelist?status=available", admin, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("list: %d %v", code, body)
|
||||
}
|
||||
entries := body["items"].([]any)
|
||||
if len(entries) != 3 {
|
||||
t.Fatalf("expected 3 available, got %d", len(entries))
|
||||
}
|
||||
if body["total"] != float64(3) {
|
||||
t.Fatalf("total: %v", body["total"])
|
||||
}
|
||||
id := int64(entries[0].(map[string]any)["id"].(float64))
|
||||
|
||||
code, body = e.do(t, "DELETE", "/api/admin/phone-whitelist/"+itoa(id), admin, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("delete: %d %v", code, body)
|
||||
}
|
||||
code, body = e.do(t, "GET", "/api/admin/phone-whitelist?status=available", admin, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("list after delete: %d %v", code, body)
|
||||
}
|
||||
if len(body["items"].([]any)) != 2 {
|
||||
t.Fatalf("expected 2 after delete, got %v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInviteRegisterWithoutWhitelistOK(t *testing.T) {
|
||||
e := setup(t)
|
||||
inv := createInvite(t, e, 2, "", nil)
|
||||
token := inv["token"].(string)
|
||||
|
||||
code, body := e.doHeaders(t, "POST", "/api/auth/register-invite/"+token, "", map[string]string{
|
||||
"X-Forwarded-For": "203.0.113.201",
|
||||
}, registerBody(t, e, "09124440001", "inv-nowl@test.dev", "invnowl"))
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("invite without whitelist should work: %d %v", code, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInviteRegisterConsumesWhitelist(t *testing.T) {
|
||||
e := setup(t)
|
||||
whitelistPhones(t, e, "09124440010")
|
||||
inv := createInvite(t, e, 2, "", nil)
|
||||
token := inv["token"].(string)
|
||||
|
||||
code, body := e.doHeaders(t, "POST", "/api/auth/register-invite/"+token, "", map[string]string{
|
||||
"X-Forwarded-For": "203.0.113.202",
|
||||
}, registerBody(t, e, "09124440010", "inv-wl@test.dev", "invwl"))
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("invite register: %d %v", code, body)
|
||||
}
|
||||
|
||||
enableRegistration(t, e)
|
||||
code, body = e.doHeaders(t, "POST", "/api/auth/register", "", map[string]string{
|
||||
"X-Forwarded-For": "203.0.113.203",
|
||||
}, registerBody(t, e, "09124440010", "pub-after-inv@test.dev", "pubafterinv"))
|
||||
if code != http.StatusConflict {
|
||||
t.Fatalf("expected conflict after invite consumed phone, got %d %v", code, body)
|
||||
}
|
||||
|
||||
admin := login(t, e, bootstrapAdminEmail)
|
||||
code, body = e.do(t, "GET", "/api/admin/phone-whitelist?status=consumed", admin, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("list consumed: %d %v", code, body)
|
||||
}
|
||||
entries := body["items"].([]any)
|
||||
if len(entries) != 1 {
|
||||
t.Fatalf("expected consumed entry, got %v", body)
|
||||
}
|
||||
if entries[0].(map[string]any)["phone"] != "09124440010" {
|
||||
t.Fatalf("phone: %v", entries[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicRegisterAfterWhitelistConsumeFails(t *testing.T) {
|
||||
e := setup(t)
|
||||
enableRegistration(t, e)
|
||||
whitelistPhones(t, e, "09124440020")
|
||||
|
||||
code, body := e.doHeaders(t, "POST", "/api/auth/register", "", map[string]string{
|
||||
"X-Forwarded-For": "203.0.113.210",
|
||||
}, registerBody(t, e, "09124440020", "wl-once@test.dev", "wlonce"))
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("first: %d %v", code, body)
|
||||
}
|
||||
|
||||
// Re-import same phone is skipped (already exists as consumed), so second public
|
||||
// attempt hits phone uniqueness — still blocked.
|
||||
code, body = e.doHeaders(t, "POST", "/api/auth/register", "", map[string]string{
|
||||
"X-Forwarded-For": "203.0.113.211",
|
||||
}, registerBody(t, e, "09124440020", "wl-twice@test.dev", "wltwice"))
|
||||
if code != http.StatusConflict {
|
||||
t.Fatalf("expected second use blocked, got %d %v", code, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitelistImportBlockedWhenBlacklistedUntilForce(t *testing.T) {
|
||||
e := setup(t)
|
||||
admin := login(t, e, bootstrapAdminEmail)
|
||||
|
||||
code, body := e.do(t, "POST", "/api/admin/phone-blacklist/import", admin, map[string]any{
|
||||
"phones": []string{"09123330100"},
|
||||
})
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("blacklist seed: %d %v", code, body)
|
||||
}
|
||||
|
||||
code, body = e.do(t, "POST", "/api/admin/phone-whitelist/import", admin, map[string]any{
|
||||
"phones": []string{"09123330100", "09123330101"},
|
||||
})
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("whitelist import: %d %v", code, body)
|
||||
}
|
||||
if body["inserted"] != float64(1) {
|
||||
t.Fatalf("inserted without resolve: %v", body["inserted"])
|
||||
}
|
||||
conflicts, ok := body["blacklistedConflicts"].([]any)
|
||||
if !ok || len(conflicts) != 1 || conflicts[0] != "09123330100" {
|
||||
t.Fatalf("blacklistedConflicts: %v", body["blacklistedConflicts"])
|
||||
}
|
||||
|
||||
code, body = e.do(t, "GET", "/api/admin/phone-blacklist", admin, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("blacklist list: %d %v", code, body)
|
||||
}
|
||||
if body["total"] != float64(1) {
|
||||
t.Fatalf("expected blacklist intact, got %v", body)
|
||||
}
|
||||
|
||||
code, body = e.do(t, "POST", "/api/admin/phone-whitelist/import", admin, map[string]any{
|
||||
"phones": []string{"09123330100"},
|
||||
"resolveConflicts": true,
|
||||
})
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("force whitelist import: %d %v", code, body)
|
||||
}
|
||||
if body["inserted"] != float64(1) {
|
||||
t.Fatalf("inserted: %v", body["inserted"])
|
||||
}
|
||||
if body["removedBlacklist"] != float64(1) {
|
||||
t.Fatalf("removedBlacklist: %v", body["removedBlacklist"])
|
||||
}
|
||||
|
||||
code, body = e.do(t, "GET", "/api/admin/phone-blacklist", admin, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("blacklist after force: %d %v", code, body)
|
||||
}
|
||||
if body["total"] != float64(0) {
|
||||
t.Fatalf("expected blacklist cleared, got %v", body)
|
||||
}
|
||||
|
||||
code, body = e.do(t, "GET", "/api/admin/phone-whitelist?q=09123330100", admin, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("whitelist after force: %d %v", code, body)
|
||||
}
|
||||
if body["total"] != float64(1) {
|
||||
t.Fatalf("expected phone on whitelist, got %v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitelistImportForceAlias(t *testing.T) {
|
||||
e := setup(t)
|
||||
admin := login(t, e, bootstrapAdminEmail)
|
||||
code, body := e.do(t, "POST", "/api/admin/phone-blacklist/import", admin, map[string]any{
|
||||
"phones": []string{"09123330110"},
|
||||
})
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("blacklist seed: %d %v", code, body)
|
||||
}
|
||||
|
||||
code, body = e.do(t, "POST", "/api/admin/phone-whitelist/import", admin, map[string]any{
|
||||
"phones": []string{"09123330110"},
|
||||
"force": true,
|
||||
})
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("force import: %d %v", code, body)
|
||||
}
|
||||
if body["inserted"] != float64(1) || body["removedBlacklist"] != float64(1) {
|
||||
t.Fatalf("force result: %v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitelistListPagination(t *testing.T) {
|
||||
e := setup(t)
|
||||
admin := login(t, e, bootstrapAdminEmail)
|
||||
|
||||
phones := make([]string, 0, 25)
|
||||
for i := 1; i <= 25; i++ {
|
||||
phones = append(phones, fmt.Sprintf("0912334%04d", i))
|
||||
}
|
||||
code, body := e.do(t, "POST", "/api/admin/phone-whitelist/import", admin, map[string]any{
|
||||
"phones": phones,
|
||||
})
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("import: %d %v", code, body)
|
||||
}
|
||||
if body["inserted"] != float64(25) {
|
||||
t.Fatalf("inserted: %v", body["inserted"])
|
||||
}
|
||||
|
||||
code, body = e.do(t, "GET", "/api/admin/phone-whitelist?page=1&pageSize=10&status=available", admin, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("page1: %d %v", code, body)
|
||||
}
|
||||
items := body["items"].([]any)
|
||||
if len(items) != 10 {
|
||||
t.Fatalf("page1 len: %d", len(items))
|
||||
}
|
||||
if body["total"] != float64(25) || body["page"] != float64(1) || body["pageSize"] != float64(10) {
|
||||
t.Fatalf("page1 meta: %v", body)
|
||||
}
|
||||
|
||||
code, body = e.do(t, "GET", "/api/admin/phone-whitelist?page=3&pageSize=10&status=available", admin, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("page3: %d %v", code, body)
|
||||
}
|
||||
items = body["items"].([]any)
|
||||
if len(items) != 5 {
|
||||
t.Fatalf("page3 len: %d", len(items))
|
||||
}
|
||||
if body["total"] != float64(25) || body["page"] != float64(3) {
|
||||
t.Fatalf("page3 meta: %v", body)
|
||||
}
|
||||
|
||||
code, body = e.do(t, "GET", "/api/admin/phone-whitelist?page=1&pageSize=500&status=available", admin, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("capped: %d %v", code, body)
|
||||
}
|
||||
if body["pageSize"] != float64(100) {
|
||||
t.Fatalf("expected pageSize capped to 100, got %v", body["pageSize"])
|
||||
}
|
||||
if len(body["items"].([]any)) != 25 {
|
||||
t.Fatalf("capped list len: %d", len(body["items"].([]any)))
|
||||
}
|
||||
|
||||
code, body = e.do(t, "GET", "/api/admin/phone-whitelist?page=1&pageSize=10&q=09123340001", admin, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("search: %d %v", code, body)
|
||||
}
|
||||
if body["total"] != float64(1) {
|
||||
t.Fatalf("search total: %v", body["total"])
|
||||
}
|
||||
items = body["items"].([]any)
|
||||
if len(items) != 1 || items[0].(map[string]any)["phone"] != "09123340001" {
|
||||
t.Fatalf("search items: %v", items)
|
||||
}
|
||||
|
||||
code, body = e.do(t, "GET", "/api/admin/phone-whitelist?page=1", admin, nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("default pageSize: %d %v", code, body)
|
||||
}
|
||||
if body["pageSize"] != float64(10) {
|
||||
t.Fatalf("expected default pageSize 10, got %v", body["pageSize"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitelistImportRejectsExistingUserPhone(t *testing.T) {
|
||||
e := setup(t)
|
||||
admin := login(t, e, bootstrapAdminEmail)
|
||||
|
||||
code, body := e.do(t, "POST", "/api/admin/users", admin, map[string]any{
|
||||
"name": "کاربر WL", "email": "wl-exist@test.dev", "username": "wlexist",
|
||||
"password": "password123", "role": "student",
|
||||
"phone": "09123330999", "securityQuestion": "رنگ؟", "securityAnswer": "آبی",
|
||||
})
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("create user: %d %v", code, body)
|
||||
}
|
||||
|
||||
code, body = e.do(t, "POST", "/api/admin/phone-whitelist/import", admin, map[string]any{
|
||||
"phones": []string{"09123330999", "09123330998"},
|
||||
})
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("whitelist import: %d %v", code, body)
|
||||
}
|
||||
if body["inserted"] != float64(1) {
|
||||
t.Fatalf("inserted: %v", body["inserted"])
|
||||
}
|
||||
if body["rejected"] != float64(1) {
|
||||
t.Fatalf("rejected: %v", body["rejected"])
|
||||
}
|
||||
rejected, _ := body["rejectedPhones"].([]any)
|
||||
if len(rejected) != 1 || rejected[0] != "09123330999" {
|
||||
t.Fatalf("rejectedPhones: %v", body["rejectedPhones"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user