go-htmx-gsd #1
3 changed files with 162 additions and 0 deletions
|
|
@ -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:
|
||||
|
|
|
|||
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