Files
2026-08-04 22:11:13 +01:00

199 lines
5.6 KiB
Go

package api
import (
"bytes"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"sync"
"testing"
"github.com/kandev/kandev/internal/agentctl/server/config"
"github.com/kandev/kandev/internal/agentctl/server/process"
)
func TestMaterializeAttachment_StreamsIntoSessionDirectory(t *testing.T) {
workDir := t.TempDir()
log := newTestLogger()
cfg := &config.InstanceConfig{Port: 0, WorkDir: workDir, AuthToken: "test-token"}
server := NewServer(cfg, process.NewManager(cfg, log), nil, nil, log)
content := []byte("diagnostic bytes")
var body bytes.Buffer
writer := multipart.NewWriter(&body)
for key, value := range map[string]string{
"session_id": "acp-session",
"attachment_id": "attachment-1",
"name": "diagnostic.zip",
"mime_type": "application/zip",
"size_bytes": "16",
} {
if err := writer.WriteField(key, value); err != nil {
t.Fatal(err)
}
}
part, err := writer.CreateFormFile("file", "diagnostic.zip")
if err != nil {
t.Fatal(err)
}
if _, err := part.Write(content); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPost, "/api/v1/attachments/materialize", &body)
req.Header.Set("Authorization", "Bearer test-token")
req.Header.Set("Content-Type", writer.FormDataContentType())
response := httptest.NewRecorder()
server.Router().ServeHTTP(response, req)
if response.Code != http.StatusCreated {
t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
}
var result materializedAttachmentResponse
if err := json.Unmarshal(response.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
if result.Name != "diagnostic.zip" || result.SizeBytes != int64(len(content)) {
t.Fatalf("response = %+v", result)
}
path := filepath.Join(workDir, ".kandev", "attachments", "acp-session", result.Name)
got, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, content) {
t.Fatalf("materialized content = %q, want %q", got, content)
}
}
func TestMaterializeAttachment_RejectsDescriptorSizeMismatch(t *testing.T) {
workDir := t.TempDir()
log := newTestLogger()
cfg := &config.InstanceConfig{Port: 0, WorkDir: workDir, AuthToken: "test-token"}
server := NewServer(cfg, process.NewManager(cfg, log), nil, nil, log)
var body bytes.Buffer
writer := multipart.NewWriter(&body)
for key, value := range map[string]string{
"session_id": "acp-session",
"attachment_id": "attachment-1",
"name": "diagnostic.zip",
"mime_type": "application/zip",
"size_bytes": "17",
} {
if err := writer.WriteField(key, value); err != nil {
t.Fatal(err)
}
}
part, err := writer.CreateFormFile("file", "diagnostic.zip")
if err != nil {
t.Fatal(err)
}
_, _ = part.Write([]byte("diagnostic bytes"))
if err := writer.Close(); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPost, "/api/v1/attachments/materialize", &body)
req.Header.Set("Authorization", "Bearer test-token")
req.Header.Set("Content-Type", writer.FormDataContentType())
response := httptest.NewRecorder()
server.Router().ServeHTTP(response, req)
if response.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
}
}
func TestMaterializeAttachment_RejectsPathTraversalIdentity(t *testing.T) {
workDir := t.TempDir()
log := newTestLogger()
cfg := &config.InstanceConfig{Port: 0, WorkDir: workDir, AuthToken: "test-token"}
server := NewServer(cfg, process.NewManager(cfg, log), nil, nil, log)
var body bytes.Buffer
writer := multipart.NewWriter(&body)
for key, value := range map[string]string{
"session_id": "../escape",
"attachment_id": "attachment-1",
"name": "diagnostic.zip",
"mime_type": "application/zip",
"size_bytes": "0",
} {
if err := writer.WriteField(key, value); err != nil {
t.Fatal(err)
}
}
part, err := writer.CreateFormFile("file", "diagnostic.zip")
if err != nil {
t.Fatal(err)
}
if _, err := part.Write(nil); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPost, "/api/v1/attachments/materialize", &body)
req.Header.Set("Authorization", "Bearer test-token")
req.Header.Set("Content-Type", writer.FormDataContentType())
response := httptest.NewRecorder()
server.Router().ServeHTTP(response, req)
if response.Code != http.StatusBadRequest {
t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
}
}
func TestInstallMaterializedAttachment_NeverOverwritesConcurrentNames(t *testing.T) {
dir := t.TempDir()
type result struct {
name string
err error
data []byte
}
results := make(chan result, 8)
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
tmpPath := filepath.Join(dir, "upload-"+strconv.Itoa(i))
content := []byte("content-" + strconv.Itoa(i))
if err := os.WriteFile(tmpPath, content, 0o600); err != nil {
results <- result{err: err}
return
}
name, err := installMaterializedAttachment(dir, tmpPath, "same.txt")
var got []byte
if err == nil {
got, err = os.ReadFile(filepath.Join(dir, name))
}
results <- result{name: name, err: err, data: got}
}(i)
}
wg.Wait()
close(results)
seen := make(map[string]struct{})
for got := range results {
if got.err != nil {
t.Fatal(got.err)
}
if _, exists := seen[got.name]; exists {
t.Fatalf("duplicate destination name %q", got.name)
}
seen[got.name] = struct{}{}
if len(got.data) == 0 {
t.Fatalf("empty installed file for %q", got.name)
}
}
}