package api import ( "bytes" "encoding/json" "mime/multipart" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) func testServerWithDrops(t *testing.T) (*Server, string) { t.Helper() srv, _ := testServer(t) dropsDir := t.TempDir() srv.SetDropsDir(dropsDir) return srv, dropsDir } func TestHandleListDrops_Empty(t *testing.T) { srv, _ := testServerWithDrops(t) req := httptest.NewRequest("GET", "/api/drops", nil) rec := httptest.NewRecorder() srv.mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("want 200, got %d", rec.Code) } var files []map[string]interface{} if err := json.NewDecoder(rec.Body).Decode(&files); err != nil { t.Fatalf("decode response: %v", err) } if len(files) != 0 { t.Errorf("want empty list, got %d entries", len(files)) } } func TestHandleListDrops_WithFile(t *testing.T) { srv, dropsDir := testServerWithDrops(t) // Create a file in the drops dir. if err := os.WriteFile(filepath.Join(dropsDir, "hello.txt"), []byte("world"), 0600); err != nil { t.Fatal(err) } req := httptest.NewRequest("GET", "/api/drops", nil) rec := httptest.NewRecorder() srv.mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("want 200, got %d: %s", rec.Code, rec.Body.String()) } var files []map[string]interface{} if err := json.NewDecoder(rec.Body).Decode(&files); err != nil { t.Fatalf("decode response: %v", err) } if len(files) != 1 { t.Fatalf("want 1 file, got %d", len(files)) } if files[0]["name"] != "hello.txt" { t.Errorf("name: want %q, got %v", "hello.txt", files[0]["name"]) } } func TestHandlePostDrop_Multipart(t *testing.T) { srv, dropsDir := testServerWithDrops(t) var buf bytes.Buffer w := multipart.NewWriter(&buf) fw, err := w.CreateFormFile("file", "test.txt") if err != nil { t.Fatal(err) } fw.Write([]byte("hello world")) //nolint:errcheck w.Close() req := httptest.NewRequest("POST", "/api/drops", &buf) req.Header.Set("Content-Type", w.FormDataContentType()) rec := httptest.NewRecorder() srv.mux.ServeHTTP(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("want 201, got %d: %s", rec.Code, rec.Body.String()) } var resp map[string]interface{} if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode response: %v", err) } if resp["name"] != "test.txt" { t.Errorf("name: want %q, got %v", "test.txt", resp["name"]) } // Verify file was created on disk. content, err := os.ReadFile(filepath.Join(dropsDir, "test.txt")) if err != nil { t.Fatalf("reading uploaded file: %v", err) } if string(content) != "hello world" { t.Errorf("content: want %q, got %q", "hello world", content) } } func TestHandleGetDrop_Download(t *testing.T) { srv, dropsDir := testServerWithDrops(t) if err := os.WriteFile(filepath.Join(dropsDir, "download.txt"), []byte("download me"), 0600); err != nil { t.Fatal(err) } req := httptest.NewRequest("GET", "/api/drops/download.txt", nil) rec := httptest.NewRecorder() srv.mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("want 200, got %d", rec.Code) } cd := rec.Header().Get("Content-Disposition") if !strings.Contains(cd, "attachment") { t.Errorf("want Content-Disposition: attachment, got %q", cd) } if rec.Body.String() != "download me" { t.Errorf("body: want %q, got %q", "download me", rec.Body.String()) } } func TestHandleGetDrop_PathTraversal(t *testing.T) { srv, _ := testServerWithDrops(t) // Attempt path traversal — should be rejected. req := httptest.NewRequest("GET", "/api/drops/..%2Fetc%2Fpasswd", nil) rec := httptest.NewRecorder() srv.mux.ServeHTTP(rec, req) // The Go net/http router will handle %2F-encoded slashes as literal characters, // so the filename becomes "../etc/passwd". Our handler should reject it. if rec.Code == http.StatusOK { t.Error("expected non-200 for path traversal attempt") } } func TestHandleGetDrop_NotFound(t *testing.T) { srv, _ := testServerWithDrops(t) req := httptest.NewRequest("GET", "/api/drops/notexist.txt", nil) rec := httptest.NewRecorder() srv.mux.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("want 404, got %d", rec.Code) } }