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
This commit is contained in:
parent
e0d72747e0
commit
3327a4286d
3 changed files with 162 additions and 0 deletions
|
|
@ -17,5 +17,38 @@ services:
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
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:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
minio_data:
|
||||||
|
|
|
||||||
51
backend/internal/files/store_test.go
Normal file
51
backend/internal/files/store_test.go
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
78
backend/internal/web/handlers_files_test.go
Normal file
78
backend/internal/web/handlers_files_test.go
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue