dealspace/api/orgs_test.go

618 lines
17 KiB
Go

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"])
}
}