Some checks failed
CI/CD / detect-changes (pull_request) Successful in 3s
CI/CD / test-frontend (pull_request) Successful in 6s
CI/CD / build-and-deploy (pull_request) Has been cancelled
CI/CD / test-go-pg (pull_request) Has been cancelled
CI/CD / test-sqlite (pull_request) Has been cancelled
Add batch user resolve endpoint, sw.users SDK module, and migrate all surfaces to show display_name instead of UUIDs. Canonical fallback chain: display_name → username → "Unknown". - GET /api/v1/users/resolve?ids=... (max 100, map response) - sw.users.resolve(), resolveMany(), displayName() with 60s cache - Chat participants resolved from users table, not snapshot - Admin users/teams/groups/team-admin show display_name - Chat-core participants.display_name column deprecated - 5 new handler tests, OpenAPI spec updated Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
180 lines
4.8 KiB
Go
180 lines
4.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"armature/config"
|
|
"armature/database"
|
|
"armature/middleware"
|
|
"armature/store"
|
|
postgres "armature/store/postgres"
|
|
sqlite "armature/store/sqlite"
|
|
)
|
|
|
|
// ── Presence/Resolve Test Harness ──────────────
|
|
|
|
type presenceHarness struct {
|
|
*testHarness
|
|
stores store.Stores
|
|
userToken string
|
|
userID string
|
|
}
|
|
|
|
func setupPresenceHarness(t *testing.T) *presenceHarness {
|
|
t.Helper()
|
|
database.RequireTestDB(t)
|
|
database.TruncateAll(t)
|
|
|
|
cfg := &config.Config{
|
|
JWTSecret: testJWTSecret,
|
|
BasePath: "",
|
|
}
|
|
|
|
var stores store.Stores
|
|
if database.IsSQLite() {
|
|
stores = sqlite.NewStores(database.TestDB)
|
|
} else {
|
|
stores = postgres.NewStores(database.TestDB)
|
|
}
|
|
userCache := middleware.NewUserStatusCache()
|
|
|
|
r := gin.New()
|
|
api := r.Group("/api/v1")
|
|
protected := api.Group("")
|
|
protected.Use(middleware.Auth(cfg, stores.Users, userCache))
|
|
|
|
presence := NewPresenceHandler(stores)
|
|
protected.GET("/users/resolve", presence.ResolveUsers)
|
|
protected.GET("/users/search", presence.SearchUsers)
|
|
|
|
// Seed a caller user
|
|
userID := database.SeedTestUser(t, "caller", "caller@test.com")
|
|
database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true WHERE id = $1"), userID)
|
|
userToken := makeToken(userID, "caller@test.com", "user")
|
|
|
|
return &presenceHarness{
|
|
testHarness: &testHarness{router: r, t: t},
|
|
stores: stores,
|
|
userToken: userToken,
|
|
userID: userID,
|
|
}
|
|
}
|
|
|
|
// ── Tests ──────────────────────────────────────
|
|
|
|
func TestResolveUsers_SingleID(t *testing.T) {
|
|
h := setupPresenceHarness(t)
|
|
|
|
// Seed a user with display_name
|
|
targetID := database.SeedTestUser(t, "alice", "alice@test.com")
|
|
database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true, display_name = $1 WHERE id = $2"), "Alice Smith", targetID)
|
|
|
|
w := h.request("GET", "/api/v1/users/resolve?ids="+targetID, h.userToken, nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
Data map[string]store.UserSearchResult `json:"data"`
|
|
}
|
|
json.NewDecoder(w.Body).Decode(&resp)
|
|
|
|
u, ok := resp.Data[targetID]
|
|
if !ok {
|
|
t.Fatalf("expected user %s in response, got %v", targetID, resp.Data)
|
|
}
|
|
if u.Username != "alice" {
|
|
t.Errorf("expected username 'alice', got %q", u.Username)
|
|
}
|
|
if u.DisplayName != "Alice Smith" {
|
|
t.Errorf("expected display_name 'Alice Smith', got %q", u.DisplayName)
|
|
}
|
|
}
|
|
|
|
func TestResolveUsers_MultipleIDs(t *testing.T) {
|
|
h := setupPresenceHarness(t)
|
|
|
|
id1 := database.SeedTestUser(t, "bob", "bob@test.com")
|
|
id2 := database.SeedTestUser(t, "carol", "carol@test.com")
|
|
database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true WHERE id = $1"), id1)
|
|
database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true, display_name = $1 WHERE id = $2"), "Carol D", id2)
|
|
|
|
w := h.request("GET", "/api/v1/users/resolve?ids="+id1+","+id2, h.userToken, nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
Data map[string]store.UserSearchResult `json:"data"`
|
|
}
|
|
json.NewDecoder(w.Body).Decode(&resp)
|
|
|
|
if len(resp.Data) != 2 {
|
|
t.Fatalf("expected 2 users, got %d", len(resp.Data))
|
|
}
|
|
if resp.Data[id1].Username != "bob" {
|
|
t.Errorf("expected bob, got %q", resp.Data[id1].Username)
|
|
}
|
|
if resp.Data[id2].DisplayName != "Carol D" {
|
|
t.Errorf("expected 'Carol D', got %q", resp.Data[id2].DisplayName)
|
|
}
|
|
}
|
|
|
|
func TestResolveUsers_MissingID(t *testing.T) {
|
|
h := setupPresenceHarness(t)
|
|
|
|
w := h.request("GET", "/api/v1/users/resolve?ids=00000000-0000-0000-0000-000000000099", h.userToken, nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
Data map[string]store.UserSearchResult `json:"data"`
|
|
}
|
|
json.NewDecoder(w.Body).Decode(&resp)
|
|
|
|
if len(resp.Data) != 0 {
|
|
t.Errorf("expected empty map for missing ID, got %d entries", len(resp.Data))
|
|
}
|
|
}
|
|
|
|
func TestResolveUsers_EmptyParam(t *testing.T) {
|
|
h := setupPresenceHarness(t)
|
|
|
|
w := h.request("GET", "/api/v1/users/resolve?ids=", h.userToken, nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
Data map[string]interface{} `json:"data"`
|
|
}
|
|
json.NewDecoder(w.Body).Decode(&resp)
|
|
|
|
if len(resp.Data) != 0 {
|
|
t.Errorf("expected empty map, got %d entries", len(resp.Data))
|
|
}
|
|
}
|
|
|
|
func TestResolveUsers_NoParam(t *testing.T) {
|
|
h := setupPresenceHarness(t)
|
|
|
|
w := h.request("GET", "/api/v1/users/resolve", h.userToken, nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
Data map[string]interface{} `json:"data"`
|
|
}
|
|
json.NewDecoder(w.Body).Decode(&resp)
|
|
|
|
if len(resp.Data) != 0 {
|
|
t.Errorf("expected empty map, got %d entries", len(resp.Data))
|
|
}
|
|
}
|