Files
task-master/internal/server/attachment_handlers_test.go
T
skrueckenandCursor 6e6b13270a Add ticket file attachments with simple in-browser Excel editing.
Store uploads on disk under DataDir and expose them in the web ticket view so .xlsx files can be previewed and saved as cell values without YouTrack or TUI sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 23:53:17 +01:00

172 lines
5.1 KiB
Go

package server
import (
"bytes"
"encoding/json"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"task/internal/sqlite"
)
func TestAttachmentUploadListDownloadPutDelete(t *testing.T) {
tmp := t.TempDir()
db, err := sqlite.Open(filepath.Join(tmp, "t.db"), []byte("secret"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
if _, err := db.CreateUser("admin", "pass", true); err != nil {
t.Fatal(err)
}
dataDir := filepath.Join(tmp, "data")
srv := New(db)
srv.DataDir = dataDir
handler := srv.Handler()
loginBody, _ := json.Marshal(map[string]string{"username": "admin", "password": "pass"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("login %d %s", rec.Code, rec.Body.String())
}
cookie := rec.Result().Cookies()[0]
createBody, _ := json.Marshal(map[string]string{"name": "Demo"})
req = httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(createBody))
req.Header.Set("Content-Type", "application/json")
req.AddCookie(cookie)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("create project %d %s", rec.Code, rec.Body.String())
}
var proj map[string]any
_ = json.Unmarshal(rec.Body.Bytes(), &proj)
pid, _ := proj["id"].(string)
ticketBody, _ := json.Marshal(map[string]string{"title": "Work"})
req = httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+pid+"/tickets", bytes.NewReader(ticketBody))
req.Header.Set("Content-Type", "application/json")
req.AddCookie(cookie)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("create ticket %d %s", rec.Code, rec.Body.String())
}
var ticket map[string]any
_ = json.Unmarshal(rec.Body.Bytes(), &ticket)
tid, _ := ticket["id"].(string)
var body bytes.Buffer
mw := multipart.NewWriter(&body)
fw, err := mw.CreateFormFile("file", "notes.txt")
if err != nil {
t.Fatal(err)
}
if _, err := fw.Write([]byte("hello attach")); err != nil {
t.Fatal(err)
}
_ = mw.Close()
req = httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+pid+"/tickets/"+tid+"/attachments", &body)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.AddCookie(cookie)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("upload %d %s", rec.Code, rec.Body.String())
}
var att map[string]any
_ = json.Unmarshal(rec.Body.Bytes(), &att)
aid, _ := att["id"].(string)
if aid == "" {
t.Fatal("no attachment id")
}
if att["filename"] != "notes.txt" {
t.Fatalf("filename %v", att["filename"])
}
if int64(att["sizeBytes"].(float64)) != 12 {
t.Fatalf("size %v", att["sizeBytes"])
}
diskPath := filepath.Join(dataDir, "attachments", tid, aid)
if _, err := os.Stat(diskPath); err != nil {
t.Fatalf("disk file: %v", err)
}
req = httptest.NewRequest(http.MethodGet, "/api/v1/projects/"+pid+"/tickets/"+tid+"/attachments", nil)
req.AddCookie(cookie)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("list %d %s", rec.Code, rec.Body.String())
}
var list []map[string]any
_ = json.Unmarshal(rec.Body.Bytes(), &list)
if len(list) != 1 {
t.Fatalf("list len %d", len(list))
}
req = httptest.NewRequest(http.MethodGet, "/api/v1/projects/"+pid+"/tickets/"+tid+"/attachments/"+aid, nil)
req.AddCookie(cookie)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("download %d %s", rec.Code, rec.Body.String())
}
got, _ := io.ReadAll(rec.Body)
if string(got) != "hello attach" {
t.Fatalf("download body %q", got)
}
putBody := []byte("updated content!!")
req = httptest.NewRequest(http.MethodPut, "/api/v1/projects/"+pid+"/tickets/"+tid+"/attachments/"+aid, bytes.NewReader(putBody))
req.Header.Set("Content-Type", "text/plain")
req.AddCookie(cookie)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("put %d %s", rec.Code, rec.Body.String())
}
_ = json.Unmarshal(rec.Body.Bytes(), &att)
if int64(att["sizeBytes"].(float64)) != int64(len(putBody)) {
t.Fatalf("put size %v", att["sizeBytes"])
}
disk, err := os.ReadFile(diskPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(disk, putBody) {
t.Fatalf("disk after put %q", disk)
}
req = httptest.NewRequest(http.MethodDelete, "/api/v1/projects/"+pid+"/tickets/"+tid+"/attachments/"+aid, nil)
req.AddCookie(cookie)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("delete %d %s", rec.Code, rec.Body.String())
}
if _, err := os.Stat(diskPath); !os.IsNotExist(err) {
t.Fatalf("file should be gone: %v", err)
}
req = httptest.NewRequest(http.MethodGet, "/api/v1/projects/"+pid+"/tickets/"+tid+"/attachments", nil)
req.AddCookie(cookie)
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
_ = json.Unmarshal(rec.Body.Bytes(), &list)
if len(list) != 0 {
t.Fatalf("list after delete %d", len(list))
}
}