199 lines
5.6 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|