From 3327a4286d046b9d27de6a385ed4e8b6c59e8839 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Fri, 15 May 2026 12:19:23 +0200 Subject: [PATCH] test(05-01): add RED test scaffold for FILE-01..06 and MinIO in compose.yaml - Create handlers_files_test.go: six TestFile* stubs (all t.Skip), stubbedFileStorer no-op implementing files.FileStorer - Create store_test.go: compile-time interface assertion, TestNewStore_SkipIfNoEndpoint skips when S3_ENDPOINT unset - Update compose.yaml: add minio (port 9000/9001) and minio-init services; minio-init uses restart: no (Pitfall 7); add minio_data volume --- backend/compose.yaml | 33 +++++++++ backend/internal/files/store_test.go | 51 ++++++++++++++ backend/internal/web/handlers_files_test.go | 78 +++++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 backend/internal/files/store_test.go create mode 100644 backend/internal/web/handlers_files_test.go diff --git a/backend/compose.yaml b/backend/compose.yaml index 1f5777f..2203ba9 100644 --- a/backend/compose.yaml +++ b/backend/compose.yaml @@ -17,5 +17,38 @@ services: timeout: 5s retries: 10 + minio: + image: minio/minio:latest + container_name: xtablo-backend-minio + restart: unless-stopped + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + ports: + - "9000:9000" # S3 API + - "9001:9001" # Console UI + command: server /data --console-address ":9001" + volumes: + - minio_data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 10 + + minio-init: + image: minio/mc:latest + depends_on: + minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + mc alias set local http://minio:9000 minioadmin minioadmin; + mc mb --ignore-existing local/xtablo-dev; + echo 'bucket ready'; + " + restart: "no" + volumes: postgres_data: + minio_data: diff --git a/backend/internal/files/store_test.go b/backend/internal/files/store_test.go new file mode 100644 index 0000000..042dd4d --- /dev/null +++ b/backend/internal/files/store_test.go @@ -0,0 +1,51 @@ +package files + +import ( + "context" + "os" + "testing" +) + +// TestStoreImplementsFileStorer is a compile-time assertion that *Store +// satisfies the FileStorer interface. +func TestStoreImplementsFileStorer(t *testing.T) { + var _ FileStorer = (*Store)(nil) +} + +// TestNewStore_SkipIfNoEndpoint verifies that NewStore returns a non-nil client +// when S3_ENDPOINT is configured. The test is skipped in CI / local dev unless +// a real MinIO instance is running and S3_ENDPOINT is set. +func TestNewStore_SkipIfNoEndpoint(t *testing.T) { + endpoint := os.Getenv("S3_ENDPOINT") + if endpoint == "" { + t.Skip("S3_ENDPOINT not set — skipping live NewStore test") + } + + bucket := os.Getenv("S3_BUCKET") + if bucket == "" { + bucket = "xtablo-dev" + } + region := os.Getenv("S3_REGION") + if region == "" { + region = "us-east-1" + } + accessKey := os.Getenv("S3_ACCESS_KEY") + if accessKey == "" { + accessKey = "minioadmin" + } + secretKey := os.Getenv("S3_SECRET_KEY") + if secretKey == "" { + secretKey = "minioadmin" + } + + store, err := NewStore(context.Background(), endpoint, bucket, region, accessKey, secretKey, true) + if err != nil { + t.Fatalf("NewStore: %v", err) + } + if store == nil { + t.Fatal("NewStore returned nil store without error") + } + if store.client == nil { + t.Fatal("NewStore returned store with nil S3 client") + } +} diff --git a/backend/internal/web/handlers_files_test.go b/backend/internal/web/handlers_files_test.go new file mode 100644 index 0000000..d2a1709 --- /dev/null +++ b/backend/internal/web/handlers_files_test.go @@ -0,0 +1,78 @@ +package web + +// handlers_files_test.go — Wave 0 RED test scaffold for FILE-01..06. +// +// All test functions call t.Skip("FILE handler tests: not yet implemented") so they +// compile but do NOT fail before Plan 02/03 implements the handlers and routes. +// This file is the RED baseline; Plan 02/03 turns it green. +// +// Pattern: mirrors handlers_tasks_test.go exactly — setupTestDB, loginUser, +// preInsertUser, getCSRFToken, and testCSRFKey are reused from the existing +// test files in the same package. + +import ( + "context" + "io" + "testing" + + "backend/internal/files" +) + +// stubbedFileStorer is a no-op FileStorer for test injection. +// Plan 02/03 will replace the Skip calls with real assertions that use this stub. +type stubbedFileStorer struct{} + +func (s *stubbedFileStorer) Upload(_ context.Context, _ string, _ io.Reader) (string, int64, error) { + return "application/octet-stream", 0, nil +} + +func (s *stubbedFileStorer) Delete(_ context.Context, _ string) error { + return nil +} + +func (s *stubbedFileStorer) PresignDownload(_ context.Context, _ string) (string, error) { + return "https://example.com/presigned", nil +} + +// Compile-time assertion: stubbedFileStorer satisfies the files.FileStorer interface. +var _ files.FileStorer = (*stubbedFileStorer)(nil) + +// ---- TestFileUpload (FILE-01, FILE-02) ---- + +// TestFileUpload verifies that POST /tablos/{id}/files uploads a file, +// creates a DB row, and stores bytes in S3 (FILE-01 server-proxied upload, FILE-02). +func TestFileUpload(t *testing.T) { + t.Skip("FILE handler tests: not yet implemented") +} + +// ---- TestFilesList (FILE-03) ---- + +// TestFilesList verifies that GET /tablos/{id}/files lists files with +// original filename and size, newest first (FILE-03). +func TestFilesList(t *testing.T) { + t.Skip("FILE handler tests: not yet implemented") +} + +// ---- TestFileDownload (FILE-04) ---- + +// TestFileDownload verifies that GET /tablos/{id}/files/{file_id}/download +// returns a 302 redirect to a signed time-limited URL (FILE-04). +func TestFileDownload(t *testing.T) { + t.Skip("FILE handler tests: not yet implemented") +} + +// ---- TestFileDelete (FILE-05) ---- + +// TestFileDelete verifies that POST /tablos/{id}/files/{file_id}/delete +// removes the DB row (and invokes S3 delete via the stub) (FILE-05). +func TestFileDelete(t *testing.T) { + t.Skip("FILE handler tests: not yet implemented") +} + +// ---- TestFileOwnership (FILE-06) ---- + +// TestFileOwnership verifies that a non-owner gets 404 on +// GET /tablos/{id}/files, POST /tablos/{id}/files, and all file sub-routes (FILE-06). +func TestFileOwnership(t *testing.T) { + t.Skip("FILE handler tests: not yet implemented") +}