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>
172 lines
5.1 KiB
Go
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))
|
|
}
|
|
}
|