dealspace/api/integration_test.go

428 lines
13 KiB
Go

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