755 lines
21 KiB
Go
755 lines
21 KiB
Go
package api
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/mish/dealspace/lib"
|
|
)
|
|
|
|
// createTestProjectWithAccess creates a project and grants the user IB admin access
|
|
func createTestProjectWithAccess(t *testing.T, db *lib.DB, cfg *lib.Config, userID string) string {
|
|
t.Helper()
|
|
|
|
projectID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
|
|
// Create project entry
|
|
key, _ := lib.DeriveProjectKey(cfg.MasterKey, projectID)
|
|
summary, _ := lib.Pack(key, "Test Import Project")
|
|
data, _ := lib.Pack(key, `{"name": "Test Import Project"}`)
|
|
|
|
db.Conn.Exec(
|
|
`INSERT INTO entries (entry_id, project_id, parent_id, type, depth,
|
|
search_key, search_key2, summary, data, stage,
|
|
assignee_id, return_to_id, origin_id,
|
|
version, deleted_at, deleted_by, key_version,
|
|
created_at, updated_at, created_by)
|
|
VALUES (?,?,?,?,?, ?,?,?,?,?, ?,?,?, ?,?,?,?, ?,?,?)`,
|
|
projectID, projectID, "", lib.TypeProject, 0,
|
|
nil, nil, summary, data, lib.StagePreDataroom,
|
|
"", "", "",
|
|
1, nil, nil, 1,
|
|
now, now, userID,
|
|
)
|
|
|
|
// Grant access
|
|
lib.AccessGrant(db, &lib.Access{
|
|
ID: uuid.New().String(),
|
|
ProjectID: projectID,
|
|
UserID: userID,
|
|
Role: lib.RoleIBAdmin,
|
|
Ops: "rwdm",
|
|
CanGrant: true,
|
|
GrantedBy: "system",
|
|
GrantedAt: now,
|
|
})
|
|
|
|
return projectID
|
|
}
|
|
|
|
// uploadFile performs a multipart file upload
|
|
func uploadFile(t *testing.T, client *http.Client, url, token, filename, contentType string, content []byte, formFields map[string]string) *http.Response {
|
|
t.Helper()
|
|
|
|
var buf bytes.Buffer
|
|
writer := multipart.NewWriter(&buf)
|
|
|
|
// Add form fields
|
|
for key, val := range formFields {
|
|
writer.WriteField(key, val)
|
|
}
|
|
|
|
// Add file
|
|
part, err := writer.CreateFormFile("file", filename)
|
|
if err != nil {
|
|
t.Fatalf("create form file: %v", err)
|
|
}
|
|
part.Write(content)
|
|
writer.Close()
|
|
|
|
req, _ := http.NewRequest("POST", url, &buf)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("upload request failed: %v", err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func TestImportRequestsCSV(t *testing.T) {
|
|
server, db, cfg, cleanup := testServerSetup(t)
|
|
defer cleanup()
|
|
|
|
client := &http.Client{}
|
|
|
|
// Create user
|
|
userID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
user := &lib.User{
|
|
UserID: userID,
|
|
Email: "import@test.com",
|
|
Name: "Import Test",
|
|
Password: "",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
lib.UserCreate(db, user)
|
|
|
|
projectID := createTestProjectWithAccess(t, db, cfg, userID)
|
|
token := getToken(t, client, server.URL, "import@test.com")
|
|
|
|
// Create a valid CSV
|
|
csvContent := `Section,Item #,Description,Priority
|
|
Financial,1.1,Provide audited financial statements,High
|
|
Financial,1.2,Revenue breakdown by product,Medium
|
|
Legal,2.1,List of all pending litigation,Critical
|
|
Legal,2.2,Employment contracts for executives,Low
|
|
HR,3.1,Organization chart,Nice to have`
|
|
|
|
resp := uploadFile(t, client, server.URL+"/api/projects/"+projectID+"/requests/import",
|
|
token, "requests.csv", "text/csv", []byte(csvContent), nil)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
t.Fatalf("expected 200, got %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var importResp map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&importResp)
|
|
|
|
imported := int(importResp["imported"].(float64))
|
|
if imported != 5 {
|
|
t.Errorf("expected 5 imported, got %d", imported)
|
|
}
|
|
|
|
skipped := int(importResp["skipped"].(float64))
|
|
if skipped != 0 {
|
|
t.Errorf("expected 0 skipped, got %d", skipped)
|
|
}
|
|
|
|
sections := importResp["sections"].([]interface{})
|
|
if len(sections) != 3 {
|
|
t.Errorf("expected 3 sections, got %d: %v", len(sections), sections)
|
|
}
|
|
}
|
|
|
|
func TestImportRequestsXLSX(t *testing.T) {
|
|
server, db, cfg, cleanup := testServerSetup(t)
|
|
defer cleanup()
|
|
|
|
client := &http.Client{}
|
|
|
|
userID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
user := &lib.User{
|
|
UserID: userID,
|
|
Email: "xlsx@test.com",
|
|
Name: "XLSX Test",
|
|
Password: "",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
lib.UserCreate(db, user)
|
|
|
|
projectID := createTestProjectWithAccess(t, db, cfg, userID)
|
|
token := getToken(t, client, server.URL, "xlsx@test.com")
|
|
|
|
// Create a minimal valid XLSX
|
|
xlsxContent := createMinimalXLSX([][]string{
|
|
{"Section", "Item #", "Description", "Priority"},
|
|
{"Finance", "F-1", "Cash flow statement", "High"},
|
|
{"Finance", "F-2", "Balance sheet", "Medium"},
|
|
})
|
|
|
|
resp := uploadFile(t, client, server.URL+"/api/projects/"+projectID+"/requests/import",
|
|
token, "requests.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
xlsxContent, nil)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
t.Fatalf("expected 200, got %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var importResp map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&importResp)
|
|
|
|
imported := int(importResp["imported"].(float64))
|
|
if imported != 2 {
|
|
t.Errorf("expected 2 imported, got %d", imported)
|
|
}
|
|
}
|
|
|
|
// createMinimalXLSX creates a minimal valid XLSX file from rows
|
|
func createMinimalXLSX(rows [][]string) []byte {
|
|
var buf bytes.Buffer
|
|
zw := zip.NewWriter(&buf)
|
|
|
|
// [Content_Types].xml
|
|
ct := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
<Default Extension="xml" ContentType="application/xml"/>
|
|
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
|
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
|
<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>
|
|
</Types>`
|
|
w, _ := zw.Create("[Content_Types].xml")
|
|
w.Write([]byte(ct))
|
|
|
|
// _rels/.rels
|
|
rels := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
|
</Relationships>`
|
|
w, _ = zw.Create("_rels/.rels")
|
|
w.Write([]byte(rels))
|
|
|
|
// xl/_rels/workbook.xml.rels
|
|
wbrels := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
|
|
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>
|
|
</Relationships>`
|
|
w, _ = zw.Create("xl/_rels/workbook.xml.rels")
|
|
w.Write([]byte(wbrels))
|
|
|
|
// xl/workbook.xml
|
|
wb := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
|
<sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets>
|
|
</workbook>`
|
|
w, _ = zw.Create("xl/workbook.xml")
|
|
w.Write([]byte(wb))
|
|
|
|
// Collect all strings and build shared strings
|
|
var allStrings []string
|
|
stringIndex := make(map[string]int)
|
|
for _, row := range rows {
|
|
for _, cell := range row {
|
|
if _, exists := stringIndex[cell]; !exists {
|
|
stringIndex[cell] = len(allStrings)
|
|
allStrings = append(allStrings, cell)
|
|
}
|
|
}
|
|
}
|
|
|
|
// xl/sharedStrings.xml
|
|
ssb := bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="` + fmt.Sprintf("%d", len(allStrings)) + `" uniqueCount="` + fmt.Sprintf("%d", len(allStrings)) + `">`)
|
|
for _, s := range allStrings {
|
|
ssb.WriteString("<si><t>" + escapeXML(s) + "</t></si>")
|
|
}
|
|
ssb.WriteString("</sst>")
|
|
w, _ = zw.Create("xl/sharedStrings.xml")
|
|
w.Write(ssb.Bytes())
|
|
|
|
// xl/worksheets/sheet1.xml
|
|
sheetb := bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
|
<sheetData>`)
|
|
for ri, row := range rows {
|
|
sheetb.WriteString(fmt.Sprintf(`<row r="%d">`, ri+1))
|
|
for ci, cell := range row {
|
|
colLetter := string(rune('A' + ci))
|
|
cellRef := fmt.Sprintf("%s%d", colLetter, ri+1)
|
|
idx := stringIndex[cell]
|
|
sheetb.WriteString(fmt.Sprintf(`<c r="%s" t="s"><v>%d</v></c>`, cellRef, idx))
|
|
}
|
|
sheetb.WriteString("</row>")
|
|
}
|
|
sheetb.WriteString("</sheetData></worksheet>")
|
|
w, _ = zw.Create("xl/worksheets/sheet1.xml")
|
|
w.Write(sheetb.Bytes())
|
|
|
|
zw.Close()
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func escapeXML(s string) string {
|
|
var buf bytes.Buffer
|
|
for _, r := range s {
|
|
switch r {
|
|
case '<':
|
|
buf.WriteString("<")
|
|
case '>':
|
|
buf.WriteString(">")
|
|
case '&':
|
|
buf.WriteString("&")
|
|
case '"':
|
|
buf.WriteString(""")
|
|
case '\'':
|
|
buf.WriteString("'")
|
|
default:
|
|
buf.WriteRune(r)
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func TestImportSmartHeaderDetection(t *testing.T) {
|
|
server, db, cfg, cleanup := testServerSetup(t)
|
|
defer cleanup()
|
|
|
|
client := &http.Client{}
|
|
|
|
userID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
user := &lib.User{
|
|
UserID: userID,
|
|
Email: "smartheader@test.com",
|
|
Name: "Smart Header",
|
|
Password: "",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
lib.UserCreate(db, user)
|
|
|
|
projectID := createTestProjectWithAccess(t, db, cfg, userID)
|
|
token := getToken(t, client, server.URL, "smartheader@test.com")
|
|
|
|
// CSV with 3 title rows before the actual header
|
|
csvContent := `Project XYZ Due Diligence Request List
|
|
Confidential - Internal Use Only
|
|
Generated: 2026-02-28
|
|
Section,Item #,Information Requested,Priority
|
|
Financial,1.1,Audited financials 3 years,High
|
|
Financial,1.2,Monthly management reports,Medium`
|
|
|
|
resp := uploadFile(t, client, server.URL+"/api/projects/"+projectID+"/requests/import",
|
|
token, "with-title-rows.csv", "text/csv", []byte(csvContent), nil)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
t.Fatalf("expected 200, got %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var importResp map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&importResp)
|
|
|
|
// Should detect header at row 4 (index 3) and import 2 rows
|
|
imported := int(importResp["imported"].(float64))
|
|
if imported != 2 {
|
|
t.Errorf("expected 2 imported (header detection), got %d", imported)
|
|
}
|
|
}
|
|
|
|
func TestImportModeReplace(t *testing.T) {
|
|
server, db, cfg, cleanup := testServerSetup(t)
|
|
defer cleanup()
|
|
|
|
client := &http.Client{}
|
|
|
|
userID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
user := &lib.User{
|
|
UserID: userID,
|
|
Email: "replace@test.com",
|
|
Name: "Replace Test",
|
|
Password: "",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
lib.UserCreate(db, user)
|
|
|
|
projectID := createTestProjectWithAccess(t, db, cfg, userID)
|
|
token := getToken(t, client, server.URL, "replace@test.com")
|
|
|
|
// First import
|
|
csv1 := `Section,Item #,Description,Priority
|
|
Finance,1,First batch item 1,High
|
|
Finance,2,First batch item 2,Medium`
|
|
|
|
resp := uploadFile(t, client, server.URL+"/api/projects/"+projectID+"/requests/import",
|
|
token, "batch1.csv", "text/csv", []byte(csv1), map[string]string{"mode": "add"})
|
|
resp.Body.Close()
|
|
|
|
// Second import with mode=replace
|
|
csv2 := `Section,Item #,Description,Priority
|
|
Legal,A,Second batch only,High`
|
|
|
|
resp = uploadFile(t, client, server.URL+"/api/projects/"+projectID+"/requests/import",
|
|
token, "batch2.csv", "text/csv", []byte(csv2), map[string]string{"mode": "replace"})
|
|
resp.Body.Close()
|
|
|
|
// List requests - should only have 1 (from second batch)
|
|
req, _ := http.NewRequest("GET", server.URL+"/api/projects/"+projectID+"/requests", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("list requests failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var requests []map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&requests)
|
|
|
|
if len(requests) != 1 {
|
|
t.Errorf("after replace, expected 1 request, got %d", len(requests))
|
|
}
|
|
if len(requests) > 0 && requests[0]["section"] != "Legal" {
|
|
t.Errorf("expected Legal section, got %v", requests[0]["section"])
|
|
}
|
|
}
|
|
|
|
func TestImportModeAdd(t *testing.T) {
|
|
server, db, cfg, cleanup := testServerSetup(t)
|
|
defer cleanup()
|
|
|
|
client := &http.Client{}
|
|
|
|
userID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
user := &lib.User{
|
|
UserID: userID,
|
|
Email: "addmode@test.com",
|
|
Name: "Add Mode",
|
|
Password: "",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
lib.UserCreate(db, user)
|
|
|
|
projectID := createTestProjectWithAccess(t, db, cfg, userID)
|
|
token := getToken(t, client, server.URL, "addmode@test.com")
|
|
|
|
// First import
|
|
csv1 := `Section,Item #,Description,Priority
|
|
Finance,1,First item,High`
|
|
|
|
resp := uploadFile(t, client, server.URL+"/api/projects/"+projectID+"/requests/import",
|
|
token, "add1.csv", "text/csv", []byte(csv1), map[string]string{"mode": "add"})
|
|
resp.Body.Close()
|
|
|
|
// Second import with mode=add (accumulate)
|
|
csv2 := `Section,Item #,Description,Priority
|
|
Legal,2,Second item,Medium`
|
|
|
|
resp = uploadFile(t, client, server.URL+"/api/projects/"+projectID+"/requests/import",
|
|
token, "add2.csv", "text/csv", []byte(csv2), map[string]string{"mode": "add"})
|
|
resp.Body.Close()
|
|
|
|
// List requests - should have 2 (accumulated)
|
|
req, _ := http.NewRequest("GET", server.URL+"/api/projects/"+projectID+"/requests", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("list requests failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var requests []map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&requests)
|
|
|
|
if len(requests) != 2 {
|
|
t.Errorf("after add mode, expected 2 requests, got %d", len(requests))
|
|
}
|
|
}
|
|
|
|
func TestListRequests(t *testing.T) {
|
|
server, db, cfg, cleanup := testServerSetup(t)
|
|
defer cleanup()
|
|
|
|
client := &http.Client{}
|
|
|
|
userID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
user := &lib.User{
|
|
UserID: userID,
|
|
Email: "list@test.com",
|
|
Name: "List Test",
|
|
Password: "",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
lib.UserCreate(db, user)
|
|
|
|
projectID := createTestProjectWithAccess(t, db, cfg, userID)
|
|
token := getToken(t, client, server.URL, "list@test.com")
|
|
|
|
// Import some requests
|
|
csv := `Section,Item #,Description,Priority
|
|
Alpha,A-1,First alpha item,High
|
|
Alpha,A-2,Second alpha item,Medium
|
|
Beta,B-1,First beta item,Low`
|
|
|
|
resp := uploadFile(t, client, server.URL+"/api/projects/"+projectID+"/requests/import",
|
|
token, "list.csv", "text/csv", []byte(csv), nil)
|
|
resp.Body.Close()
|
|
|
|
// List requests
|
|
req, _ := http.NewRequest("GET", server.URL+"/api/projects/"+projectID+"/requests", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("list requests failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var requests []map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&requests)
|
|
|
|
if len(requests) != 3 {
|
|
t.Errorf("expected 3 requests, got %d", len(requests))
|
|
}
|
|
|
|
// Verify structure
|
|
for _, r := range requests {
|
|
if r["entry_id"] == nil || r["entry_id"] == "" {
|
|
t.Error("request should have entry_id")
|
|
}
|
|
if r["section"] == nil {
|
|
t.Error("request should have section")
|
|
}
|
|
if r["priority"] == nil {
|
|
t.Error("request should have priority")
|
|
}
|
|
if r["status"] == nil {
|
|
t.Error("request should have status")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPriorityNormalization(t *testing.T) {
|
|
server, db, cfg, cleanup := testServerSetup(t)
|
|
defer cleanup()
|
|
|
|
client := &http.Client{}
|
|
|
|
userID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
user := &lib.User{
|
|
UserID: userID,
|
|
Email: "priority@test.com",
|
|
Name: "Priority Test",
|
|
Password: "",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
lib.UserCreate(db, user)
|
|
|
|
projectID := createTestProjectWithAccess(t, db, cfg, userID)
|
|
token := getToken(t, client, server.URL, "priority@test.com")
|
|
|
|
// CSV with various priority formats
|
|
csv := `Section,Item #,Description,Priority
|
|
Test,1,Critical priority,CRITICAL
|
|
Test,2,Nice to have priority,Nice to have
|
|
Test,3,Normal priority,Normal
|
|
Test,4,Urgent priority,Urgent
|
|
Test,5,High priority,High
|
|
Test,6,Low priority,Low
|
|
Test,7,Optional priority,optional`
|
|
|
|
resp := uploadFile(t, client, server.URL+"/api/projects/"+projectID+"/requests/import",
|
|
token, "priority.csv", "text/csv", []byte(csv), nil)
|
|
resp.Body.Close()
|
|
|
|
// List and verify priority normalization
|
|
req, _ := http.NewRequest("GET", server.URL+"/api/projects/"+projectID+"/requests", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("list requests failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var requests []map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&requests)
|
|
|
|
expected := map[string]string{
|
|
"Critical priority": "high", // CRITICAL → high
|
|
"Nice to have priority": "low", // Nice to have → low
|
|
"Normal priority": "medium",
|
|
"Urgent priority": "high", // Urgent → high
|
|
"High priority": "high",
|
|
"Low priority": "low",
|
|
"Optional priority": "low", // optional → low
|
|
}
|
|
|
|
for _, r := range requests {
|
|
title := r["title"].(string)
|
|
if expectedPri, ok := expected[title]; ok {
|
|
if r["priority"] != expectedPri {
|
|
t.Errorf("%s: expected priority %q, got %q", title, expectedPri, r["priority"])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestImportNoFile(t *testing.T) {
|
|
server, db, cfg, cleanup := testServerSetup(t)
|
|
defer cleanup()
|
|
|
|
client := &http.Client{}
|
|
|
|
userID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
user := &lib.User{
|
|
UserID: userID,
|
|
Email: "nofile@test.com",
|
|
Name: "No File",
|
|
Password: "",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
lib.UserCreate(db, user)
|
|
|
|
projectID := createTestProjectWithAccess(t, db, cfg, userID)
|
|
token := getToken(t, client, server.URL, "nofile@test.com")
|
|
|
|
// Try import without file
|
|
req, _ := http.NewRequest("POST", server.URL+"/api/projects/"+projectID+"/requests/import", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Content-Type", "multipart/form-data")
|
|
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 file, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestImportAccessDenied(t *testing.T) {
|
|
server, db, cfg, cleanup := testServerSetup(t)
|
|
defer cleanup()
|
|
|
|
client := &http.Client{}
|
|
|
|
// Create project owner
|
|
ownerID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
owner := &lib.User{
|
|
UserID: ownerID,
|
|
Email: "owner@test.com",
|
|
Name: "Owner",
|
|
Password: "",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
lib.UserCreate(db, owner)
|
|
projectID := createTestProjectWithAccess(t, db, cfg, ownerID)
|
|
|
|
// Create buyer (read-only, no write access)
|
|
buyerID := uuid.New().String()
|
|
buyer := &lib.User{
|
|
UserID: buyerID,
|
|
Email: "buyer-readonly@test.com",
|
|
Name: "Buyer",
|
|
Password: "",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
lib.UserCreate(db, buyer)
|
|
lib.AccessGrant(db, &lib.Access{
|
|
ID: uuid.New().String(),
|
|
ProjectID: projectID,
|
|
UserID: buyerID,
|
|
Role: lib.RoleBuyerMember,
|
|
Ops: "r", // read only
|
|
CanGrant: false,
|
|
GrantedBy: "system",
|
|
GrantedAt: now,
|
|
})
|
|
|
|
buyerToken := getToken(t, client, server.URL, "buyer-readonly@test.com")
|
|
|
|
csv := `Section,Item #,Description,Priority
|
|
Test,1,Test item,High`
|
|
|
|
resp := uploadFile(t, client, server.URL+"/api/projects/"+projectID+"/requests/import",
|
|
buyerToken, "denied.csv", "text/csv", []byte(csv), nil)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusForbidden {
|
|
t.Errorf("expected 403 for buyer import, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestListRequestsAccessDenied(t *testing.T) {
|
|
server, db, cfg, cleanup := testServerSetup(t)
|
|
defer cleanup()
|
|
|
|
client := &http.Client{}
|
|
|
|
// Create project
|
|
ownerID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
owner := &lib.User{
|
|
UserID: ownerID,
|
|
Email: "listowner@test.com",
|
|
Name: "List Owner",
|
|
Password: "",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
lib.UserCreate(db, owner)
|
|
projectID := createTestProjectWithAccess(t, db, cfg, ownerID)
|
|
|
|
// Create user with no access
|
|
noAccessID := uuid.New().String()
|
|
noAccess := &lib.User{
|
|
UserID: noAccessID,
|
|
Email: "noaccess-list@test.com",
|
|
Name: "No Access",
|
|
Password: "",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
lib.UserCreate(db, noAccess)
|
|
|
|
noAccessToken := getToken(t, client, server.URL, "noaccess-list@test.com")
|
|
|
|
req, _ := http.NewRequest("GET", server.URL+"/api/projects/"+projectID+"/requests", nil)
|
|
req.Header.Set("Authorization", "Bearer "+noAccessToken)
|
|
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 no access list, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// This test file doesn't exist in the original but is mentioned, so skip it
|
|
func init() {
|
|
// Ensure tests don't fail due to missing template directory
|
|
os.MkdirAll("/tmp/dealspace-test-templates", 0755)
|
|
}
|