package api import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "testing" "time" "github.com/google/uuid" "github.com/mish/dealspace/lib" ) // setupTestUser creates a user directly in the DB and returns a config with backdoor enabled func setupTestUser(t *testing.T, db *lib.DB, cfg *lib.Config, email, name string) { t.Helper() now := time.Now().UnixMilli() user := &lib.User{ UserID: uuid.New().String(), Email: email, Name: name, Password: "", Active: true, CreatedAt: now, UpdatedAt: now, } if err := lib.UserCreate(db, user); err != nil { t.Fatalf("create test user: %v", err) } } // loginWithBackdoor uses the challenge/verify flow with backdoor code func loginWithBackdoor(t *testing.T, client *http.Client, serverURL, email string) string { t.Helper() // Step 1: Send challenge challengeBody, _ := json.Marshal(map[string]string{"email": email}) resp, err := client.Post(serverURL+"/api/auth/challenge", "application/json", bytes.NewReader(challengeBody)) if err != nil { t.Fatalf("challenge request failed: %v", err) } resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("challenge expected 200, got %d", resp.StatusCode) } // Step 2: Verify with backdoor code verifyBody, _ := json.Marshal(map[string]string{"email": email, "code": "220402"}) resp, err = client.Post(serverURL+"/api/auth/verify", "application/json", bytes.NewReader(verifyBody)) if err != nil { t.Fatalf("verify request failed: %v", err) } var verifyResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&verifyResp) resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("verify expected 200, got %d: %v", resp.StatusCode, verifyResp) } token, ok := verifyResp["token"].(string) if !ok || token == "" { t.Fatal("verify response should contain token") } return token } func TestFullFlow(t *testing.T) { tmpFile, err := os.CreateTemp("", "dealspace-integration-test-*.db") if err != nil { t.Fatalf("create temp file: %v", err) } tmpFile.Close() defer os.Remove(tmpFile.Name()) db, err := lib.OpenDB(tmpFile.Name(), "../migrations/001_initial.sql") if err != nil { t.Fatalf("OpenDB: %v", err) } defer db.Close() masterKey := make([]byte, 32) for i := range masterKey { masterKey[i] = byte(i) } jwtSecret := []byte("test-jwt-secret-32-bytes-long!!") cfg := &lib.Config{ MasterKey: masterKey, JWTSecret: jwtSecret, Env: "development", BackdoorCode: "220402", } tmpDir, err := os.MkdirTemp("", "dealspace-store-test") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir) store, _ := lib.NewLocalStore(tmpDir) router := NewRouter(db, cfg, store, nil, nil, nil) server := httptest.NewServer(router) defer server.Close() client := &http.Client{} // Create test user directly in DB setupTestUser(t, db, cfg, "admin@test.com", "Admin User") // Step 1: Login with challenge/verify + backdoor t.Log("Step 1: Login via challenge/verify") token := loginWithBackdoor(t, client, server.URL, "admin@test.com") t.Logf("Got token: %s...", token[:20]) // Wrong code should fail wrongVerify, _ := json.Marshal(map[string]string{"email": "admin@test.com", "code": "000000"}) resp, _ := client.Post(server.URL+"/api/auth/verify", "application/json", bytes.NewReader(wrongVerify)) if resp.StatusCode != http.StatusUnauthorized { t.Errorf("wrong code should return 401, got %d", resp.StatusCode) } resp.Body.Close() // Step 2: GET /api/auth/me t.Log("Step 2: Get current user") req, _ := http.NewRequest("GET", server.URL+"/api/auth/me", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err = client.Do(req) if err != nil { t.Fatalf("me request failed: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("me expected 200, got %d", resp.StatusCode) } var meResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&meResp) resp.Body.Close() if meResp["email"] != "admin@test.com" { t.Errorf("me response email mismatch: got %s", meResp["email"]) } // Step 3: POST /api/projects t.Log("Step 3: Create project") projectBody, _ := json.Marshal(map[string]string{ "name": "Test Deal Project", "deal_type": "M&A", }) req, _ = http.NewRequest("POST", server.URL+"/api/projects", bytes.NewReader(projectBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, err = client.Do(req) if err != nil { t.Fatalf("create project request failed: %v", err) } if resp.StatusCode != http.StatusCreated { var errResp map[string]string json.NewDecoder(resp.Body).Decode(&errResp) t.Fatalf("create project expected 201, got %d: %v", resp.StatusCode, errResp) } var projectResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&projectResp) resp.Body.Close() projectID := projectResp["project_id"].(string) if projectID == "" { t.Fatal("project response should contain project_id") } t.Logf("Created project: %s", projectID) // Step 4: GET /api/projects t.Log("Step 4: List projects") req, _ = http.NewRequest("GET", server.URL+"/api/projects", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err = client.Do(req) if err != nil { t.Fatalf("list projects request failed: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("list projects expected 200, got %d", resp.StatusCode) } var listResp []lib.Entry json.NewDecoder(resp.Body).Decode(&listResp) resp.Body.Close() if len(listResp) < 1 { t.Errorf("expected at least 1 project, got %d", len(listResp)) } // Step 5: Logout t.Log("Step 5: Logout") req, _ = http.NewRequest("POST", server.URL+"/api/auth/logout", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err = client.Do(req) if err != nil { t.Fatalf("logout request failed: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("logout expected 200, got %d", resp.StatusCode) } resp.Body.Close() // Step 6: Token should be invalid after logout t.Log("Step 6: Verify token invalidated") req, _ = http.NewRequest("GET", server.URL+"/api/auth/me", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err = client.Do(req) if err != nil { t.Fatalf("me after logout request failed: %v", err) } if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("me after logout expected 401, got %d", resp.StatusCode) } resp.Body.Close() t.Log("Full flow test passed!") } func TestHealthEndpoint(t *testing.T) { tmpFile, _ := os.CreateTemp("", "dealspace-health-test-*.db") tmpFile.Close() defer os.Remove(tmpFile.Name()) db, _ := lib.OpenDB(tmpFile.Name(), "../migrations/001_initial.sql") defer db.Close() cfg := &lib.Config{ MasterKey: make([]byte, 32), JWTSecret: []byte("test-secret"), } router := NewRouter(db, cfg, nil, nil, nil, nil) server := httptest.NewServer(router) defer server.Close() resp, err := http.Get(server.URL + "/health") if err != nil { t.Fatalf("health request failed: %v", err) } if resp.StatusCode != http.StatusOK { t.Errorf("health expected 200, got %d", resp.StatusCode) } var healthResp map[string]string json.NewDecoder(resp.Body).Decode(&healthResp) resp.Body.Close() if healthResp["status"] != "ok" { t.Errorf("health status should be 'ok', got %s", healthResp["status"]) } } func TestUnauthenticatedAccess(t *testing.T) { tmpFile, _ := os.CreateTemp("", "dealspace-unauth-test-*.db") tmpFile.Close() defer os.Remove(tmpFile.Name()) db, _ := lib.OpenDB(tmpFile.Name(), "../migrations/001_initial.sql") defer db.Close() cfg := &lib.Config{ MasterKey: make([]byte, 32), JWTSecret: []byte("test-secret"), } router := NewRouter(db, cfg, nil, nil, nil, nil) server := httptest.NewServer(router) defer server.Close() endpoints := []struct { method string path string }{ {"GET", "/api/auth/me"}, {"POST", "/api/auth/logout"}, {"GET", "/api/projects"}, {"POST", "/api/projects"}, {"GET", "/api/projects/test/entries"}, } for _, ep := range endpoints { req, _ := http.NewRequest(ep.method, server.URL+ep.path, nil) resp, err := http.DefaultClient.Do(req) if err != nil { t.Errorf("%s %s: request failed: %v", ep.method, ep.path, err) continue } if resp.StatusCode != http.StatusUnauthorized { t.Errorf("%s %s: expected 401, got %d", ep.method, ep.path, resp.StatusCode) } resp.Body.Close() } } func TestEntryOperations(t *testing.T) { tmpFile, _ := os.CreateTemp("", "dealspace-entry-test-*.db") tmpFile.Close() defer os.Remove(tmpFile.Name()) db, _ := lib.OpenDB(tmpFile.Name(), "../migrations/001_initial.sql") defer db.Close() masterKey := make([]byte, 32) jwtSecret := []byte("test-secret-32-bytes!!") cfg := &lib.Config{ MasterKey: masterKey, JWTSecret: jwtSecret, Env: "development", BackdoorCode: "220402", } tmpDir, _ := os.MkdirTemp("", "dealspace-store-entry-test") defer os.RemoveAll(tmpDir) store, _ := lib.NewLocalStore(tmpDir) router := NewRouter(db, cfg, store, nil, nil, nil) server := httptest.NewServer(router) defer server.Close() client := &http.Client{} // Create user and login via challenge/verify with backdoor setupTestUser(t, db, cfg, "entry@test.com", "Entry Test") token := loginWithBackdoor(t, client, server.URL, "entry@test.com") // Create project projectBody, _ := json.Marshal(map[string]string{"name": "Entry Test Project"}) req, _ := http.NewRequest("POST", server.URL+"/api/projects", bytes.NewReader(projectBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, _ := client.Do(req) var projectResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&projectResp) resp.Body.Close() projectID := projectResp["project_id"].(string) // Create entry entryBody, _ := json.Marshal(map[string]interface{}{ "project_id": projectID, "type": "request", "depth": 1, "summary": "Test Request", "data": `{"question": "What is the revenue?"}`, "stage": "pre_dataroom", }) req, _ = http.NewRequest("POST", server.URL+"/api/projects/"+projectID+"/entries", bytes.NewReader(entryBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, _ = client.Do(req) if resp.StatusCode != http.StatusCreated { var errResp map[string]string json.NewDecoder(resp.Body).Decode(&errResp) t.Fatalf("create entry expected 201, got %d: %v", resp.StatusCode, errResp) } var entryResp lib.Entry json.NewDecoder(resp.Body).Decode(&entryResp) resp.Body.Close() entryID := entryResp.EntryID if entryID == "" { t.Fatal("entry should have ID") } // List entries req, _ = http.NewRequest("GET", server.URL+"/api/projects/"+projectID+"/entries?type=request", nil) req.Header.Set("Authorization", "Bearer "+token) resp, _ = client.Do(req) if resp.StatusCode != http.StatusOK { t.Fatalf("list entries expected 200, got %d", resp.StatusCode) } var entries []lib.Entry json.NewDecoder(resp.Body).Decode(&entries) resp.Body.Close() if len(entries) != 1 { t.Errorf("expected 1 entry, got %d", len(entries)) } // Update entry updateBody, _ := json.Marshal(map[string]interface{}{ "project_id": projectID, "type": "request", "depth": 1, "summary": "Updated Request", "stage": "dataroom", "version": 1, }) req, _ = http.NewRequest("PUT", server.URL+"/api/projects/"+projectID+"/entries/"+entryID, bytes.NewReader(updateBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, _ = client.Do(req) if resp.StatusCode != http.StatusOK { var errResp map[string]string json.NewDecoder(resp.Body).Decode(&errResp) t.Fatalf("update entry expected 200, got %d: %v", resp.StatusCode, errResp) } resp.Body.Close() // Delete entry req, _ = http.NewRequest("DELETE", server.URL+"/api/projects/"+projectID+"/entries/"+entryID, nil) req.Header.Set("Authorization", "Bearer "+token) resp, _ = client.Do(req) if resp.StatusCode != http.StatusOK { t.Fatalf("delete entry expected 200, got %d", resp.StatusCode) } resp.Body.Close() // Verify deleted req, _ = http.NewRequest("GET", server.URL+"/api/projects/"+projectID+"/entries?type=request", nil) req.Header.Set("Authorization", "Bearer "+token) resp, _ = client.Do(req) json.NewDecoder(resp.Body).Decode(&entries) resp.Body.Close() if len(entries) != 0 { t.Errorf("expected 0 entries after delete, got %d", len(entries)) } } func TestChallengeVerifyFlow(t *testing.T) { tmpFile, _ := os.CreateTemp("", "dealspace-challenge-test-*.db") tmpFile.Close() defer os.Remove(tmpFile.Name()) db, _ := lib.OpenDB(tmpFile.Name(), "../migrations/001_initial.sql") defer db.Close() cfg := &lib.Config{ MasterKey: make([]byte, 32), JWTSecret: []byte("test-jwt-secret-32-bytes-long!!"), Env: "development", BackdoorCode: "220402", } router := NewRouter(db, cfg, nil, nil, nil, nil) server := httptest.NewServer(router) defer server.Close() client := &http.Client{} // Challenge for non-existent user should still return 200 (no info leak) body, _ := json.Marshal(map[string]string{"email": "nobody@test.com"}) resp, _ := client.Post(server.URL+"/api/auth/challenge", "application/json", bytes.NewReader(body)) if resp.StatusCode != http.StatusOK { t.Errorf("challenge for unknown email should return 200, got %d", resp.StatusCode) } resp.Body.Close() // Verify for non-existent user should fail body, _ = json.Marshal(map[string]string{"email": "nobody@test.com", "code": "220402"}) resp, _ = client.Post(server.URL+"/api/auth/verify", "application/json", bytes.NewReader(body)) if resp.StatusCode != http.StatusUnauthorized { t.Errorf("verify for unknown user should return 401, got %d", resp.StatusCode) } resp.Body.Close() // Create user and test backdoor login setupTestUser(t, db, cfg, "test@test.com", "Test User") token := loginWithBackdoor(t, client, server.URL, "test@test.com") if token == "" { t.Fatal("backdoor login should return token") } // Verify the token works req, _ := http.NewRequest("GET", server.URL+"/api/auth/me", nil) req.Header.Set("Authorization", "Bearer "+token) resp, _ = client.Do(req) if resp.StatusCode != http.StatusOK { t.Errorf("me with valid token should return 200, got %d", resp.StatusCode) } resp.Body.Close() } // ---- Additional integration tests ---- func TestFullDealWorkflow(t *testing.T) { tmpFile, err := os.CreateTemp("", "dealspace-deal-workflow-*.db") if err != nil { t.Fatalf("create temp file: %v", err) } tmpFile.Close() defer os.Remove(tmpFile.Name()) db, err := lib.OpenDB(tmpFile.Name(), "../migrations/001_initial.sql") if err != nil { t.Fatalf("OpenDB: %v", err) } defer db.Close() masterKey := make([]byte, 32) for i := range masterKey { masterKey[i] = byte(i) } jwtSecret := []byte("test-jwt-secret-32-bytes-long!!") cfg := &lib.Config{ MasterKey: masterKey, JWTSecret: jwtSecret, Env: "development", BackdoorCode: "220402", } tmpDir, _ := os.MkdirTemp("", "dealspace-deal-workflow-store") defer os.RemoveAll(tmpDir) store, _ := lib.NewLocalStore(tmpDir) router := NewRouter(db, cfg, store, nil, nil, nil) server := httptest.NewServer(router) defer server.Close() client := &http.Client{} // Step 1: Create IB admin user t.Log("Step 1: Create IB admin user") setupTestUser(t, db, cfg, "ibadmin@workflow.com", "IB Admin") ibToken := loginWithBackdoor(t, client, server.URL, "ibadmin@workflow.com") // Step 2: Create a project t.Log("Step 2: Create project") projectBody, _ := json.Marshal(map[string]string{ "name": "Acme Acquisition", "deal_type": "M&A", }) req, _ := http.NewRequest("POST", server.URL+"/api/projects", bytes.NewReader(projectBody)) req.Header.Set("Authorization", "Bearer "+ibToken) req.Header.Set("Content-Type", "application/json") resp, _ := client.Do(req) if resp.StatusCode != http.StatusCreated { t.Fatalf("create project expected 201, got %d", resp.StatusCode) } var projectResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&projectResp) resp.Body.Close() projectID := projectResp["project_id"].(string) t.Logf("Created project: %s", projectID) // Step 3: Create an organization t.Log("Step 3: Create organization") orgBody, _ := json.Marshal(map[string]interface{}{ "name": "Acme Corp", "domains": []string{"acme.com", "acmecorp.com"}, "role": "seller", }) req, _ = http.NewRequest("POST", server.URL+"/api/orgs", bytes.NewReader(orgBody)) req.Header.Set("Authorization", "Bearer "+ibToken) req.Header.Set("Content-Type", "application/json") resp, _ = client.Do(req) if resp.StatusCode != http.StatusCreated { var errResp map[string]string json.NewDecoder(resp.Body).Decode(&errResp) t.Fatalf("create org expected 201, got %d: %v", resp.StatusCode, errResp) } var orgResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&orgResp) resp.Body.Close() orgID := orgResp["entry_id"].(string) t.Logf("Created org: %s", orgID) // Step 4: Link org to project (deal_org) t.Log("Step 4: Link org to project") dealOrgBody, _ := json.Marshal(map[string]interface{}{ "org_id": orgID, "role": "seller", "domain_lock": true, }) req, _ = http.NewRequest("POST", server.URL+"/api/projects/"+projectID+"/orgs", bytes.NewReader(dealOrgBody)) req.Header.Set("Authorization", "Bearer "+ibToken) req.Header.Set("Content-Type", "application/json") resp, _ = client.Do(req) if resp.StatusCode != http.StatusCreated { var errResp map[string]string json.NewDecoder(resp.Body).Decode(&errResp) t.Fatalf("create deal_org expected 201, got %d: %v", resp.StatusCode, errResp) } var dealOrgResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&dealOrgResp) resp.Body.Close() t.Logf("Linked org to project: %v", dealOrgResp["deal_org_id"]) // Step 5: List deal orgs and verify domain is shown t.Log("Step 5: List deal orgs") req, _ = http.NewRequest("GET", server.URL+"/api/projects/"+projectID+"/orgs", nil) req.Header.Set("Authorization", "Bearer "+ibToken) resp, _ = client.Do(req) if resp.StatusCode != http.StatusOK { t.Fatalf("list deal orgs expected 200, got %d", resp.StatusCode) } var dealOrgs []map[string]interface{} json.NewDecoder(resp.Body).Decode(&dealOrgs) resp.Body.Close() if len(dealOrgs) != 1 { t.Fatalf("expected 1 deal org, got %d", len(dealOrgs)) } dealOrg := dealOrgs[0] if dealOrg["org_name"] != "Acme Corp" { t.Errorf("org_name mismatch: got %v", dealOrg["org_name"]) } orgDomains, ok := dealOrg["org_domains"].([]interface{}) if !ok || len(orgDomains) != 2 { t.Errorf("org_domains should have 2 entries: got %v", dealOrg["org_domains"]) } if dealOrg["role"] != "seller" { t.Errorf("role mismatch: got %v", dealOrg["role"]) } if dealOrg["domain_lock"] != true { t.Errorf("domain_lock should be true: got %v", dealOrg["domain_lock"]) } t.Log("Full deal workflow test passed!") } func TestSuperAdminSeeAllProjects(t *testing.T) { tmpFile, err := os.CreateTemp("", "dealspace-superadmin-projects-*.db") if err != nil { t.Fatalf("create temp file: %v", err) } tmpFile.Close() defer os.Remove(tmpFile.Name()) db, err := lib.OpenDB(tmpFile.Name(), "../migrations/001_initial.sql") if err != nil { t.Fatalf("OpenDB: %v", err) } defer db.Close() masterKey := make([]byte, 32) for i := range masterKey { masterKey[i] = byte(i) } jwtSecret := []byte("test-jwt-secret-32-bytes-long!!") cfg := &lib.Config{ MasterKey: masterKey, JWTSecret: jwtSecret, Env: "development", BackdoorCode: "220402", } tmpDir, _ := os.MkdirTemp("", "dealspace-superadmin-store") defer os.RemoveAll(tmpDir) store, _ := lib.NewLocalStore(tmpDir) router := NewRouter(db, cfg, store, nil, nil, nil) server := httptest.NewServer(router) defer server.Close() client := &http.Client{} now := time.Now().UnixMilli() // Create User A and their project t.Log("Create User A and Project A") setupTestUser(t, db, cfg, "usera@test.com", "User A") userAToken := loginWithBackdoor(t, client, server.URL, "usera@test.com") projectABody, _ := json.Marshal(map[string]string{"name": "Project A"}) req, _ := http.NewRequest("POST", server.URL+"/api/projects", bytes.NewReader(projectABody)) req.Header.Set("Authorization", "Bearer "+userAToken) req.Header.Set("Content-Type", "application/json") resp, _ := client.Do(req) resp.Body.Close() // Create User B and their project t.Log("Create User B and Project B") setupTestUser(t, db, cfg, "userb@test.com", "User B") userBToken := loginWithBackdoor(t, client, server.URL, "userb@test.com") projectBBody, _ := json.Marshal(map[string]string{"name": "Project B"}) req, _ = http.NewRequest("POST", server.URL+"/api/projects", bytes.NewReader(projectBBody)) req.Header.Set("Authorization", "Bearer "+userBToken) req.Header.Set("Content-Type", "application/json") resp, _ = client.Do(req) resp.Body.Close() // Create super admin (no explicit project grants except super_admin role) t.Log("Create Super Admin") saID := uuid.New().String() saUser := &lib.User{ UserID: saID, Email: "superadmin@test.com", Name: "Super Admin", Password: "", Active: true, CreatedAt: now, UpdatedAt: now, } lib.UserCreate(db, saUser) lib.AccessGrant(db, &lib.Access{ ID: uuid.New().String(), ProjectID: "global", UserID: saID, Role: lib.RoleSuperAdmin, Ops: "rwdm", CanGrant: true, GrantedBy: "system", GrantedAt: now, }) saToken := loginWithBackdoor(t, client, server.URL, "superadmin@test.com") // User A should only see Project A t.Log("Verify User A sees only Project A") req, _ = http.NewRequest("GET", server.URL+"/api/projects", nil) req.Header.Set("Authorization", "Bearer "+userAToken) resp, _ = client.Do(req) var userAProjects []lib.Entry json.NewDecoder(resp.Body).Decode(&userAProjects) resp.Body.Close() if len(userAProjects) != 1 { t.Errorf("User A should see 1 project, got %d", len(userAProjects)) } // User B should only see Project B t.Log("Verify User B sees only Project B") req, _ = http.NewRequest("GET", server.URL+"/api/projects", nil) req.Header.Set("Authorization", "Bearer "+userBToken) resp, _ = client.Do(req) var userBProjects []lib.Entry json.NewDecoder(resp.Body).Decode(&userBProjects) resp.Body.Close() if len(userBProjects) != 1 { t.Errorf("User B should see 1 project, got %d", len(userBProjects)) } // Super admin should see ALL projects (via admin endpoint) t.Log("Verify Super Admin sees all projects") req, _ = http.NewRequest("GET", server.URL+"/api/admin/projects", nil) req.Header.Set("Authorization", "Bearer "+saToken) resp, _ = client.Do(req) if resp.StatusCode != http.StatusOK { t.Fatalf("admin/projects expected 200, got %d", resp.StatusCode) } var saProjects []lib.Entry json.NewDecoder(resp.Body).Decode(&saProjects) resp.Body.Close() if len(saProjects) < 2 { t.Errorf("Super admin should see at least 2 projects, got %d", len(saProjects)) } // Verify super admin can access Project A directly (even without explicit grant) t.Log("Verify Super Admin can access Project A detail") // Get project A's ID projectAID := "" for _, p := range saProjects { if p.SummaryText == "Project A" { projectAID = p.ProjectID break } } if projectAID == "" { // Fall back to finding via user A's projects projectAID = userAProjects[0].ProjectID } req, _ = http.NewRequest("GET", server.URL+"/api/projects/"+projectAID+"/detail", nil) req.Header.Set("Authorization", "Bearer "+saToken) resp, _ = client.Do(req) if resp.StatusCode != http.StatusOK { t.Errorf("super admin should access Project A detail, got %d", resp.StatusCode) } resp.Body.Close() t.Log("Super admin project visibility test passed!") }