package api import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "testing" "time" "github.com/google/uuid" "github.com/mish/dealspace/lib" ) // testServerSetup creates a test server with all dependencies func testServerSetup(t *testing.T) (*httptest.Server, *lib.DB, *lib.Config, func()) { t.Helper() tmpFile, err := os.CreateTemp("", "dealspace-org-test-*.db") if err != nil { t.Fatalf("create temp file: %v", err) } tmpFile.Close() db, err := lib.OpenDB(tmpFile.Name(), "../migrations/001_initial.sql") if err != nil { os.Remove(tmpFile.Name()) t.Fatalf("OpenDB: %v", err) } 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-org-test") if err != nil { db.Close() os.Remove(tmpFile.Name()) t.Fatal(err) } store, _ := lib.NewLocalStore(tmpDir) router := NewRouter(db, cfg, store, nil, nil, nil) server := httptest.NewServer(router) cleanup := func() { server.Close() db.Close() os.Remove(tmpFile.Name()) os.RemoveAll(tmpDir) } return server, db, cfg, cleanup } // createUserWithRole creates a user and grants them a role on a project func createUserWithRole(t *testing.T, db *lib.DB, email, role, projectID string) string { t.Helper() now := time.Now().UnixMilli() userID := uuid.New().String() user := &lib.User{ UserID: userID, Email: email, Name: "Test User", Password: "", Active: true, CreatedAt: now, UpdatedAt: now, } if err := lib.UserCreate(db, user); err != nil { t.Fatalf("UserCreate: %v", err) } if role != "" && projectID != "" { ops := "r" switch role { case lib.RoleSuperAdmin, lib.RoleIBAdmin: ops = "rwdm" case lib.RoleIBMember: ops = "rwdm" case lib.RoleSellerAdmin, lib.RoleSellerMember: ops = "rwd" } lib.AccessGrant(db, &lib.Access{ ID: uuid.New().String(), ProjectID: projectID, UserID: userID, Role: role, Ops: ops, CanGrant: role == lib.RoleIBAdmin || role == lib.RoleSuperAdmin, GrantedBy: "system", GrantedAt: now, }) } return userID } // createSuperAdmin creates a super admin user func createSuperAdmin(t *testing.T, db *lib.DB, email string) string { t.Helper() now := time.Now().UnixMilli() userID := uuid.New().String() user := &lib.User{ UserID: userID, Email: email, Name: "Super Admin", Password: "", Active: true, CreatedAt: now, UpdatedAt: now, } if err := lib.UserCreate(db, user); err != nil { t.Fatalf("UserCreate: %v", err) } lib.AccessGrant(db, &lib.Access{ ID: uuid.New().String(), ProjectID: "global", UserID: userID, Role: lib.RoleSuperAdmin, Ops: "rwdm", CanGrant: true, GrantedBy: "system", GrantedAt: now, }) return userID } // getToken gets a token for a user via backdoor auth func getToken(t *testing.T, client *http.Client, serverURL, email string) string { t.Helper() // Challenge body, _ := json.Marshal(map[string]string{"email": email}) resp, err := client.Post(serverURL+"/api/auth/challenge", "application/json", bytes.NewReader(body)) if err != nil { t.Fatalf("challenge failed: %v", err) } resp.Body.Close() // Verify with backdoor body, _ = json.Marshal(map[string]string{"email": email, "code": "220402"}) resp, err = client.Post(serverURL+"/api/auth/verify", "application/json", bytes.NewReader(body)) if err != nil { t.Fatalf("verify failed: %v", err) } var verifyResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&verifyResp) resp.Body.Close() token, ok := verifyResp["token"].(string) if !ok || token == "" { t.Fatalf("no token in verify response: %v", verifyResp) } return token } func TestCreateOrg(t *testing.T) { server, db, _, cleanup := testServerSetup(t) defer cleanup() client := &http.Client{} // Create an IB admin user (can create orgs) projectID := uuid.New().String() createUserWithRole(t, db, "ibadmin@test.com", lib.RoleIBAdmin, projectID) token := getToken(t, client, server.URL, "ibadmin@test.com") // Create org with valid payload orgBody, _ := json.Marshal(map[string]interface{}{ "name": "Acme Corporation", "domains": []string{"acme.com", "acme-corp.com"}, "role": "seller", "website": "https://acme.com", "description": "A company that makes everything", "contact_name": "Wile E. Coyote", "contact_email": "wile@acme.com", }) req, _ := http.NewRequest("POST", server.URL+"/api/orgs", bytes.NewReader(orgBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { t.Fatalf("create org request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { var errResp map[string]string json.NewDecoder(resp.Body).Decode(&errResp) t.Fatalf("expected 201, got %d: %v", resp.StatusCode, errResp) } var createResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&createResp) if createResp["entry_id"] == nil || createResp["entry_id"] == "" { t.Error("response should contain entry_id") } if createResp["name"] != "Acme Corporation" { t.Errorf("name mismatch: got %v", createResp["name"]) } if createResp["role"] != "seller" { t.Errorf("role mismatch: got %v", createResp["role"]) } domains, ok := createResp["domains"].([]interface{}) if !ok || len(domains) != 2 { t.Errorf("domains should have 2 entries, got %v", createResp["domains"]) } } func TestCreateOrgEmptyDomains(t *testing.T) { server, db, _, cleanup := testServerSetup(t) defer cleanup() client := &http.Client{} projectID := uuid.New().String() createUserWithRole(t, db, "ibadmin2@test.com", lib.RoleIBAdmin, projectID) token := getToken(t, client, server.URL, "ibadmin2@test.com") // Try to create org with empty domains array orgBody, _ := json.Marshal(map[string]interface{}{ "name": "Bad Org", "domains": []string{}, "role": "buyer", }) req, _ := http.NewRequest("POST", server.URL+"/api/orgs", bytes.NewReader(orgBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected 400 for empty domains, got %d", resp.StatusCode) } var errResp map[string]string json.NewDecoder(resp.Body).Decode(&errResp) if errResp["code"] != "missing_fields" { t.Errorf("expected missing_fields error code, got %s", errResp["code"]) } } func TestCreateOrgMissingDomains(t *testing.T) { server, db, _, cleanup := testServerSetup(t) defer cleanup() client := &http.Client{} projectID := uuid.New().String() createUserWithRole(t, db, "ibadmin3@test.com", lib.RoleIBAdmin, projectID) token := getToken(t, client, server.URL, "ibadmin3@test.com") // Try to create org without domains field at all orgBody, _ := json.Marshal(map[string]interface{}{ "name": "Missing Domains Org", "role": "seller", }) req, _ := http.NewRequest("POST", server.URL+"/api/orgs", bytes.NewReader(orgBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected 400 for missing domains, got %d", resp.StatusCode) } } func TestCreateOrgEmptyDomainString(t *testing.T) { server, db, _, cleanup := testServerSetup(t) defer cleanup() client := &http.Client{} projectID := uuid.New().String() createUserWithRole(t, db, "ibadmin4@test.com", lib.RoleIBAdmin, projectID) token := getToken(t, client, server.URL, "ibadmin4@test.com") // Try to create org with empty string in domains orgBody, _ := json.Marshal(map[string]interface{}{ "name": "Empty String Domain Org", "domains": []string{"valid.com", ""}, "role": "seller", }) req, _ := http.NewRequest("POST", server.URL+"/api/orgs", bytes.NewReader(orgBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected 400 for empty domain string, got %d", resp.StatusCode) } var errResp map[string]string json.NewDecoder(resp.Body).Decode(&errResp) if errResp["code"] != "invalid_domains" { t.Errorf("expected invalid_domains error code, got %s", errResp["code"]) } } func TestListOrgs(t *testing.T) { server, db, _, cleanup := testServerSetup(t) defer cleanup() client := &http.Client{} // Create IB admin to create orgs projectID := uuid.New().String() createUserWithRole(t, db, "ibadmin5@test.com", lib.RoleIBAdmin, projectID) ibToken := getToken(t, client, server.URL, "ibadmin5@test.com") // Create two orgs for i, name := range []string{"Org Alpha", "Org Beta"} { orgBody, _ := json.Marshal(map[string]interface{}{ "name": name, "domains": []string{name + ".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 { t.Fatalf("failed to create org %d: %d", i, resp.StatusCode) } resp.Body.Close() } // Create super_admin to list all orgs (IB admin only sees orgs linked to their projects via deal_org) createSuperAdmin(t, db, "superadmin-list@test.com") saToken := getToken(t, client, server.URL, "superadmin-list@test.com") // List orgs as super_admin req, _ := http.NewRequest("GET", server.URL+"/api/orgs", nil) req.Header.Set("Authorization", "Bearer "+saToken) resp, err := client.Do(req) if err != nil { t.Fatalf("list orgs failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var orgs []map[string]interface{} json.NewDecoder(resp.Body).Decode(&orgs) if len(orgs) < 2 { t.Errorf("super_admin expected at least 2 orgs, got %d", len(orgs)) } } func TestSuperAdminCanListAllOrgs(t *testing.T) { server, db, _, cleanup := testServerSetup(t) defer cleanup() client := &http.Client{} // Create super admin first createSuperAdmin(t, db, "superadmin@test.com") // Create an IB admin to create orgs projectID := uuid.New().String() createUserWithRole(t, db, "ibadmin6@test.com", lib.RoleIBAdmin, projectID) ibToken := getToken(t, client, server.URL, "ibadmin6@test.com") // Create org orgBody, _ := json.Marshal(map[string]interface{}{ "name": "SuperAdmin Test Org", "domains": []string{"supertest.com"}, "role": "buyer", }) 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) resp.Body.Close() // Super admin lists orgs saToken := getToken(t, client, server.URL, "superadmin@test.com") req, _ = http.NewRequest("GET", server.URL+"/api/orgs", nil) req.Header.Set("Authorization", "Bearer "+saToken) resp, err := client.Do(req) if err != nil { t.Fatalf("list orgs as super admin failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("expected 200, got %d", resp.StatusCode) } var orgs []map[string]interface{} json.NewDecoder(resp.Body).Decode(&orgs) // Super admin should see all orgs found := false for _, org := range orgs { if org["name"] == "SuperAdmin Test Org" { found = true break } } if !found { t.Error("super admin should see all orgs") } } func TestCreateOrgForbiddenForRegularUser(t *testing.T) { server, db, _, cleanup := testServerSetup(t) defer cleanup() client := &http.Client{} // Create a regular user (buyer role) projectID := uuid.New().String() createUserWithRole(t, db, "buyer@test.com", lib.RoleBuyerMember, projectID) token := getToken(t, client, server.URL, "buyer@test.com") // Try to create org (should fail - only IB admin or super admin can create orgs) orgBody, _ := json.Marshal(map[string]interface{}{ "name": "Forbidden Org", "domains": []string{"forbidden.com"}, "role": "buyer", }) req, _ := http.NewRequest("POST", server.URL+"/api/orgs", bytes.NewReader(orgBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusForbidden { t.Errorf("expected 403 for regular user, got %d", resp.StatusCode) } } func TestGetOrg(t *testing.T) { server, db, _, cleanup := testServerSetup(t) defer cleanup() client := &http.Client{} projectID := uuid.New().String() createUserWithRole(t, db, "ibadmin7@test.com", lib.RoleIBAdmin, projectID) token := getToken(t, client, server.URL, "ibadmin7@test.com") // Create org orgBody, _ := json.Marshal(map[string]interface{}{ "name": "Get Test Org", "domains": []string{"gettest.com"}, "role": "seller", "contact_email": "contact@gettest.com", }) req, _ := http.NewRequest("POST", server.URL+"/api/orgs", bytes.NewReader(orgBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, _ := client.Do(req) var createResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&createResp) resp.Body.Close() orgID := createResp["entry_id"].(string) // Get the org req, _ = http.NewRequest("GET", server.URL+"/api/orgs/"+orgID, nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := client.Do(req) if err != nil { t.Fatalf("get org failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } var orgResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&orgResp) if orgResp["name"] != "Get Test Org" { t.Errorf("name mismatch: got %v", orgResp["name"]) } if orgResp["contact_email"] != "contact@gettest.com" { t.Errorf("contact_email mismatch: got %v", orgResp["contact_email"]) } } func TestUpdateOrg(t *testing.T) { server, db, _, cleanup := testServerSetup(t) defer cleanup() client := &http.Client{} // Use super_admin for update (org entries use their own ID as project_id, need super_admin access) createSuperAdmin(t, db, "superadmin-update@test.com") token := getToken(t, client, server.URL, "superadmin-update@test.com") // Create org orgBody, _ := json.Marshal(map[string]interface{}{ "name": "Update Test Org", "domains": []string{"update.com"}, "role": "seller", }) req, _ := http.NewRequest("POST", server.URL+"/api/orgs", bytes.NewReader(orgBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, _ := client.Do(req) var createResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&createResp) resp.Body.Close() orgID := createResp["entry_id"].(string) // Update the org updateBody, _ := json.Marshal(map[string]interface{}{ "name": "Updated Org Name", "domains": []string{"update.com", "updated.com"}, "description": "Now with a description", "version": 1, }) req, _ = http.NewRequest("PATCH", server.URL+"/api/orgs/"+orgID, bytes.NewReader(updateBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { t.Fatalf("update org failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { var errResp map[string]string json.NewDecoder(resp.Body).Decode(&errResp) t.Fatalf("expected 200, got %d: %v", resp.StatusCode, errResp) } var updateResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&updateResp) if updateResp["name"] != "Updated Org Name" { t.Errorf("name should be updated: got %v", updateResp["name"]) } if updateResp["description"] != "Now with a description" { t.Errorf("description should be updated: got %v", updateResp["description"]) } } func TestCreateOrgInvalidRole(t *testing.T) { server, db, _, cleanup := testServerSetup(t) defer cleanup() client := &http.Client{} projectID := uuid.New().String() createUserWithRole(t, db, "ibadmin9@test.com", lib.RoleIBAdmin, projectID) token := getToken(t, client, server.URL, "ibadmin9@test.com") // Try to create org with invalid role orgBody, _ := json.Marshal(map[string]interface{}{ "name": "Invalid Role Org", "domains": []string{"invalid.com"}, "role": "invalid_role", }) req, _ := http.NewRequest("POST", server.URL+"/api/orgs", bytes.NewReader(orgBody)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected 400 for invalid role, got %d", resp.StatusCode) } var errResp map[string]string json.NewDecoder(resp.Body).Decode(&errResp) if errResp["code"] != "invalid_role" { t.Errorf("expected invalid_role error code, got %s", errResp["code"]) } }