package api import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "testing" "github.com/mish/dealspace/lib" ) func TestFullFlow(t *testing.T) { // Setup test database 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, } // Create test store tmpDir, err := os.MkdirTemp("", "dealspace-store-test") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir) store, _ := lib.NewLocalStore(tmpDir) // Create router router := NewRouter(db, cfg, store, nil, nil) server := httptest.NewServer(router) defer server.Close() client := &http.Client{} // Step 1: POST /api/setup → create admin t.Log("Step 1: Setup admin user") setupBody := map[string]string{ "email": "admin@test.com", "name": "Admin User", "password": "SecurePassword123!", } setupJSON, _ := json.Marshal(setupBody) resp, err := client.Post(server.URL+"/api/setup", "application/json", bytes.NewReader(setupJSON)) if err != nil { t.Fatalf("setup request failed: %v", err) } if resp.StatusCode != http.StatusCreated { var errResp map[string]string json.NewDecoder(resp.Body).Decode(&errResp) t.Fatalf("setup expected 201, got %d: %v", resp.StatusCode, errResp) } resp.Body.Close() // Verify setup cannot be called again resp, _ = client.Post(server.URL+"/api/setup", "application/json", bytes.NewReader(setupJSON)) if resp.StatusCode != http.StatusForbidden { t.Errorf("second setup should return 403 Forbidden, got %d", resp.StatusCode) } resp.Body.Close() // Step 2: POST /api/auth/login → get token t.Log("Step 2: Login") loginBody := map[string]string{ "email": "admin@test.com", "password": "SecurePassword123!", } loginJSON, _ := json.Marshal(loginBody) resp, err = client.Post(server.URL+"/api/auth/login", "application/json", bytes.NewReader(loginJSON)) if err != nil { t.Fatalf("login request failed: %v", err) } if resp.StatusCode != http.StatusOK { var errResp map[string]string json.NewDecoder(resp.Body).Decode(&errResp) t.Fatalf("login expected 200, got %d: %v", resp.StatusCode, errResp) } var loginResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&loginResp) resp.Body.Close() token, ok := loginResp["token"].(string) if !ok || token == "" { t.Fatal("login response should contain token") } t.Logf("Got token: %s...", token[:20]) // Wrong password should fail wrongLogin := map[string]string{ "email": "admin@test.com", "password": "WrongPassword", } wrongJSON, _ := json.Marshal(wrongLogin) resp, _ = client.Post(server.URL+"/api/auth/login", "application/json", bytes.NewReader(wrongJSON)) if resp.StatusCode != http.StatusUnauthorized { t.Errorf("wrong password should return 401, got %d", resp.StatusCode) } resp.Body.Close() // Step 3: GET /api/auth/me → verify user returned t.Log("Step 3: 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]string 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"]) } t.Logf("Current user: %s (%s)", meResp["name"], meResp["email"]) // Step 4: POST /api/projects → create project t.Log("Step 4: Create project") projectBody := map[string]string{ "name": "Test Deal Project", "deal_type": "M&A", } projectJSON, _ := json.Marshal(projectBody) req, _ = http.NewRequest("POST", server.URL+"/api/projects", bytes.NewReader(projectJSON)) 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 5: GET /api/projects → verify project listed t.Log("Step 5: 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)) } t.Logf("Found %d projects", len(listResp)) // Step 6: POST /api/auth/logout → token invalidated t.Log("Step 6: 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 7: GET /api/auth/me with old token → 401 t.Log("Step 7: 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) 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) server := httptest.NewServer(router) defer server.Close() // These endpoints require auth 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, } tmpDir, _ := os.MkdirTemp("", "dealspace-store-entry-test") defer os.RemoveAll(tmpDir) store, _ := lib.NewLocalStore(tmpDir) router := NewRouter(db, cfg, store, nil, nil) server := httptest.NewServer(router) defer server.Close() client := &http.Client{} // Setup and login setupBody, _ := json.Marshal(map[string]string{ "email": "entry@test.com", "name": "Entry Test", "password": "pass12345678", }) client.Post(server.URL+"/api/setup", "application/json", bytes.NewReader(setupBody)) loginBody, _ := json.Marshal(map[string]string{ "email": "entry@test.com", "password": "pass12345678", }) resp, _ := client.Post(server.URL+"/api/auth/login", "application/json", bytes.NewReader(loginBody)) var loginResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&loginResp) resp.Body.Close() token := loginResp["token"].(string) // 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 (should not appear in list) 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)) } }