2026-01-04 23:54:30 +00:00
|
|
|
//go:build integration
|
|
|
|
|
|
|
|
|
|
package catalog_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"os"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/get-drexa/drexa/internal/account"
|
|
|
|
|
"github.com/get-drexa/drexa/internal/auth"
|
|
|
|
|
"github.com/get-drexa/drexa/internal/blob"
|
|
|
|
|
"github.com/get-drexa/drexa/internal/catalog"
|
|
|
|
|
"github.com/get-drexa/drexa/internal/database"
|
|
|
|
|
"github.com/get-drexa/drexa/internal/drive"
|
|
|
|
|
"github.com/get-drexa/drexa/internal/organization"
|
|
|
|
|
"github.com/get-drexa/drexa/internal/registration"
|
|
|
|
|
"github.com/get-drexa/drexa/internal/sharing"
|
|
|
|
|
"github.com/get-drexa/drexa/internal/user"
|
|
|
|
|
"github.com/get-drexa/drexa/internal/virtualfs"
|
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"github.com/testcontainers/testcontainers-go/modules/postgres"
|
|
|
|
|
"github.com/uptrace/bun"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type testEnv struct {
|
|
|
|
|
db *bun.DB
|
|
|
|
|
vfs *virtualfs.VirtualFS
|
|
|
|
|
app *fiber.App
|
|
|
|
|
accessToken string
|
|
|
|
|
driveID string
|
|
|
|
|
rootDirID uuid.UUID
|
|
|
|
|
scope *virtualfs.Scope
|
|
|
|
|
orgSlug string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setupTestEnv(t *testing.T, ctx context.Context) *testEnv {
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
pg, err := runPostgres(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Skipf("postgres testcontainer unavailable: %v", err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() { _ = pg.Terminate(ctx) })
|
|
|
|
|
|
|
|
|
|
postgresURL, err := pg.ConnectionString(ctx, "sslmode=disable")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("postgres connection string: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db := database.NewFromPostgres(postgresURL)
|
|
|
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
|
|
|
|
|
|
if err := database.RunMigrations(ctx, db); err != nil {
|
|
|
|
|
t.Fatalf("RunMigrations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
blobRoot, err := os.MkdirTemp("", "drexa-blobs-*")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("temp blob dir: %v", err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() { _ = os.RemoveAll(blobRoot) })
|
|
|
|
|
|
|
|
|
|
blobStore := blob.NewFSStore(blob.FSStoreConfig{Root: blobRoot})
|
|
|
|
|
if err := blobStore.Initialize(ctx); err != nil {
|
|
|
|
|
t.Fatalf("blob store init: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vfs, err := virtualfs.New(blobStore, virtualfs.NewFlatKeyResolver())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("virtualfs.New: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userSvc := user.NewService()
|
|
|
|
|
orgSvc := organization.NewService()
|
|
|
|
|
accSvc := account.NewService()
|
|
|
|
|
driveSvc := drive.NewService()
|
|
|
|
|
sharingSvc, err := sharing.NewService(vfs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("sharing.NewService: %v", err)
|
|
|
|
|
}
|
|
|
|
|
regSvc := registration.NewService(userSvc, orgSvc, accSvc, driveSvc, vfs)
|
|
|
|
|
|
|
|
|
|
regResult, err := regSvc.Register(ctx, db, registration.RegisterOptions{
|
|
|
|
|
Email: "test@example.com",
|
|
|
|
|
Password: "password123",
|
|
|
|
|
DisplayName: "Test User",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Register: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
authSvc := auth.NewService(userSvc, auth.TokenConfig{
|
|
|
|
|
Issuer: "drexa-test",
|
|
|
|
|
Audience: "drexa-test",
|
|
|
|
|
SecretKey: []byte("drexa-test-secret"),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
grant, err := authSvc.GrantForUser(ctx, db, regResult.User)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GrantForUser: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
authMiddleware := auth.NewAuthMiddleware(authSvc, db, auth.CookieConfig{})
|
|
|
|
|
|
|
|
|
|
root, err := vfs.FindRootDirectory(ctx, db, regResult.Drive.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("FindRootDirectory: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scope := &virtualfs.Scope{
|
|
|
|
|
DriveID: regResult.Drive.ID,
|
|
|
|
|
RootNodeID: root.ID,
|
|
|
|
|
AllowedOps: virtualfs.AllAllowedOps,
|
|
|
|
|
ActorKind: virtualfs.ScopeActorAccount,
|
|
|
|
|
ActorID: regResult.Account.ID,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app := fiber.New()
|
|
|
|
|
api := app.Group("/api")
|
|
|
|
|
|
|
|
|
|
// Set up org-scoped routes like the real server does
|
|
|
|
|
orgMiddleware := organization.NewMiddleware(orgSvc, accSvc, db)
|
|
|
|
|
orgAPI := api.Group("/:orgSlug", authMiddleware, orgMiddleware)
|
|
|
|
|
|
|
|
|
|
driveHTTP := drive.NewHTTPHandler(driveSvc, accSvc, vfs, db, authMiddleware)
|
|
|
|
|
scopedRouter := driveHTTP.RegisterRoutes(orgAPI)
|
|
|
|
|
|
|
|
|
|
catalogHTTP := catalog.NewHTTPHandler(sharingSvc, vfs, db)
|
|
|
|
|
catalogHTTP.RegisterRoutes(scopedRouter)
|
|
|
|
|
|
|
|
|
|
return &testEnv{
|
|
|
|
|
db: db,
|
|
|
|
|
vfs: vfs,
|
|
|
|
|
app: app,
|
|
|
|
|
accessToken: grant.AccessToken,
|
|
|
|
|
driveID: regResult.Drive.PublicID,
|
|
|
|
|
rootDirID: root.ID,
|
|
|
|
|
scope: scope,
|
|
|
|
|
orgSlug: organization.ReservedSlug,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHTTP_CatalogEndpoints(t *testing.T) {
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
env := setupTestEnv(t, ctx)
|
|
|
|
|
|
|
|
|
|
// Create test data using VFS directly:
|
|
|
|
|
// - 2 files in root: file1.txt, file2.txt
|
|
|
|
|
// - 1 directory in root: subdir
|
|
|
|
|
// - 1 file in subdir: nested.txt
|
|
|
|
|
|
|
|
|
|
file1, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
|
|
|
|
|
ParentID: env.rootDirID,
|
|
|
|
|
Name: "file1.txt",
|
|
|
|
|
}, env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateFile file1: %v", err)
|
|
|
|
|
}
|
|
|
|
|
err = env.vfs.WriteFile(ctx, env.db, file1, virtualfs.FileContentFromReader(bytes.NewReader([]byte("content1"))), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("WriteFile file1: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file2, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
|
|
|
|
|
ParentID: env.rootDirID,
|
|
|
|
|
Name: "file2.txt",
|
|
|
|
|
}, env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateFile file2: %v", err)
|
|
|
|
|
}
|
|
|
|
|
err = env.vfs.WriteFile(ctx, env.db, file2, virtualfs.FileContentFromReader(bytes.NewReader([]byte("content2"))), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("WriteFile file2: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
subdir, err := env.vfs.CreateDirectory(ctx, env.db, env.rootDirID, "subdir", env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateDirectory subdir: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nestedFile, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
|
|
|
|
|
ParentID: subdir.ID,
|
|
|
|
|
Name: "nested.txt",
|
|
|
|
|
}, env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateFile nested: %v", err)
|
|
|
|
|
}
|
|
|
|
|
err = env.vfs.WriteFile(ctx, env.db, nestedFile, virtualfs.FileContentFromReader(bytes.NewReader([]byte("nested content"))), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("WriteFile nested: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper to build URLs with org slug
|
|
|
|
|
driveURL := func(path string) string {
|
|
|
|
|
return fmt.Sprintf("/api/%s/drives/%s%s", env.orgSlug, env.driveID, path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Directory tests
|
|
|
|
|
t.Run("list root directory", func(t *testing.T) {
|
|
|
|
|
var resp struct {
|
|
|
|
|
Items []struct {
|
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
} `json:"items"`
|
|
|
|
|
}
|
|
|
|
|
doJSONAuth(t, env.app, http.MethodGet, driveURL("/directories/root/content"), env.accessToken, nil, http.StatusOK, &resp)
|
|
|
|
|
|
|
|
|
|
if len(resp.Items) != 3 {
|
|
|
|
|
t.Fatalf("expected 3 items in root, got %d", len(resp.Items))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
names := map[string]string{}
|
|
|
|
|
for _, item := range resp.Items {
|
|
|
|
|
names[item.Name] = item.Kind
|
|
|
|
|
}
|
|
|
|
|
if names["file1.txt"] != "file" {
|
|
|
|
|
t.Errorf("expected file1.txt to be a file")
|
|
|
|
|
}
|
|
|
|
|
if names["file2.txt"] != "file" {
|
|
|
|
|
t.Errorf("expected file2.txt to be a file")
|
|
|
|
|
}
|
|
|
|
|
if names["subdir"] != "directory" {
|
|
|
|
|
t.Errorf("expected subdir to be a directory")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("fetch directory by ID", func(t *testing.T) {
|
|
|
|
|
var resp struct {
|
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
doJSONAuth(t, env.app, http.MethodGet, driveURL(fmt.Sprintf("/directories/%s", subdir.PublicID)), env.accessToken, nil, http.StatusOK, &resp)
|
|
|
|
|
|
|
|
|
|
if resp.Kind != "directory" {
|
|
|
|
|
t.Errorf("expected kind=directory, got %q", resp.Kind)
|
|
|
|
|
}
|
|
|
|
|
if resp.Name != "subdir" {
|
|
|
|
|
t.Errorf("expected name=subdir, got %q", resp.Name)
|
|
|
|
|
}
|
|
|
|
|
if resp.ID != subdir.PublicID {
|
|
|
|
|
t.Errorf("expected id=%s, got %s", subdir.PublicID, resp.ID)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("list subdir contents", func(t *testing.T) {
|
|
|
|
|
var resp struct {
|
|
|
|
|
Items []struct {
|
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
} `json:"items"`
|
|
|
|
|
}
|
|
|
|
|
doJSONAuth(t, env.app, http.MethodGet, driveURL(fmt.Sprintf("/directories/%s/content", subdir.PublicID)), env.accessToken, nil, http.StatusOK, &resp)
|
|
|
|
|
|
|
|
|
|
if len(resp.Items) != 1 {
|
|
|
|
|
t.Fatalf("expected 1 item in subdir, got %d", len(resp.Items))
|
|
|
|
|
}
|
|
|
|
|
if resp.Items[0].Name != "nested.txt" {
|
|
|
|
|
t.Errorf("expected nested.txt, got %q", resp.Items[0].Name)
|
|
|
|
|
}
|
|
|
|
|
if resp.Items[0].Kind != "file" {
|
|
|
|
|
t.Errorf("expected kind=file, got %q", resp.Items[0].Kind)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("create directory", func(t *testing.T) {
|
|
|
|
|
root, err := env.vfs.FindNode(ctx, env.db, env.rootDirID.String(), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("FindNode root: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var resp struct {
|
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
body := map[string]string{"parentID": root.PublicID, "name": "newdir"}
|
|
|
|
|
doJSONAuth(t, env.app, http.MethodPost, driveURL("/directories"), env.accessToken, body, http.StatusOK, &resp)
|
|
|
|
|
|
|
|
|
|
if resp.Kind != "directory" {
|
|
|
|
|
t.Errorf("expected kind=directory, got %q", resp.Kind)
|
|
|
|
|
}
|
|
|
|
|
if resp.Name != "newdir" {
|
|
|
|
|
t.Errorf("expected name=newdir, got %q", resp.Name)
|
|
|
|
|
}
|
|
|
|
|
if resp.ID == "" {
|
|
|
|
|
t.Errorf("expected non-empty ID")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-04 23:58:43 +00:00
|
|
|
t.Run("rename directory", func(t *testing.T) {
|
|
|
|
|
root, err := env.vfs.FindNode(ctx, env.db, env.rootDirID.String(), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("FindNode root: %v", err)
|
|
|
|
|
}
|
|
|
|
|
renameDir, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "to-rename", env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateDirectory to-rename: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var resp struct {
|
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
body := map[string]string{"name": "renamed-dir"}
|
|
|
|
|
doJSONAuth(t, env.app, http.MethodPatch, driveURL(fmt.Sprintf("/directories/%s", renameDir.PublicID)), env.accessToken, body, http.StatusOK, &resp)
|
|
|
|
|
|
|
|
|
|
if resp.Kind != "directory" {
|
|
|
|
|
t.Errorf("expected kind=directory, got %q", resp.Kind)
|
|
|
|
|
}
|
|
|
|
|
if resp.Name != "renamed-dir" {
|
|
|
|
|
t.Errorf("expected name=renamed-dir, got %q", resp.Name)
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-01-04 23:54:30 +00:00
|
|
|
|
|
|
|
|
// File tests
|
|
|
|
|
t.Run("fetch file by ID", func(t *testing.T) {
|
|
|
|
|
var resp struct {
|
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Size int64 `json:"size"`
|
|
|
|
|
MimeType string `json:"mimeType"`
|
|
|
|
|
}
|
|
|
|
|
doJSONAuth(t, env.app, http.MethodGet, driveURL(fmt.Sprintf("/files/%s", file1.PublicID)), env.accessToken, nil, http.StatusOK, &resp)
|
|
|
|
|
|
|
|
|
|
if resp.Kind != "file" {
|
|
|
|
|
t.Errorf("expected kind=file, got %q", resp.Kind)
|
|
|
|
|
}
|
|
|
|
|
if resp.Name != "file1.txt" {
|
|
|
|
|
t.Errorf("expected name=file1.txt, got %q", resp.Name)
|
|
|
|
|
}
|
|
|
|
|
if resp.Size != 8 {
|
|
|
|
|
t.Errorf("expected size=8, got %d", resp.Size)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("download file content", func(t *testing.T) {
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, driveURL(fmt.Sprintf("/files/%s/content", file1.PublicID)), nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+env.accessToken)
|
|
|
|
|
|
|
|
|
|
res, err := env.app.Test(req, 10_000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
|
|
|
body, _ := io.ReadAll(res.Body)
|
|
|
|
|
t.Fatalf("expected 200, got %d: %s", res.StatusCode, body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
content, _ := io.ReadAll(res.Body)
|
|
|
|
|
if string(content) != "content1" {
|
|
|
|
|
t.Errorf("expected content1, got %q", string(content))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("rename file", func(t *testing.T) {
|
|
|
|
|
// Create a fresh file to rename (must write content to set status to ready)
|
|
|
|
|
renameFile, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
|
|
|
|
|
ParentID: env.rootDirID,
|
|
|
|
|
Name: "to-rename.txt",
|
|
|
|
|
}, env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateFile to-rename: %v", err)
|
|
|
|
|
}
|
|
|
|
|
err = env.vfs.WriteFile(ctx, env.db, renameFile, virtualfs.FileContentFromReader(bytes.NewReader([]byte("rename me"))), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("WriteFile to-rename: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var resp struct {
|
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
body := map[string]string{"name": "renamed-file.txt"}
|
|
|
|
|
doJSONAuth(t, env.app, http.MethodPatch, driveURL(fmt.Sprintf("/files/%s", renameFile.PublicID)), env.accessToken, body, http.StatusOK, &resp)
|
|
|
|
|
|
|
|
|
|
if resp.Name != "renamed-file.txt" {
|
|
|
|
|
t.Errorf("expected name=renamed-file.txt, got %q", resp.Name)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("fetch nested file", func(t *testing.T) {
|
|
|
|
|
var resp struct {
|
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Size int64 `json:"size"`
|
|
|
|
|
}
|
|
|
|
|
doJSONAuth(t, env.app, http.MethodGet, driveURL(fmt.Sprintf("/files/%s", nestedFile.PublicID)), env.accessToken, nil, http.StatusOK, &resp)
|
|
|
|
|
|
|
|
|
|
if resp.Name != "nested.txt" {
|
|
|
|
|
t.Errorf("expected name=nested.txt, got %q", resp.Name)
|
|
|
|
|
}
|
|
|
|
|
if resp.Size != 14 {
|
|
|
|
|
t.Errorf("expected size=14, got %d", resp.Size)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("download nested file content", func(t *testing.T) {
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, driveURL(fmt.Sprintf("/files/%s/content", nestedFile.PublicID)), nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+env.accessToken)
|
|
|
|
|
|
|
|
|
|
res, err := env.app.Test(req, 10_000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
|
|
|
body, _ := io.ReadAll(res.Body)
|
|
|
|
|
t.Fatalf("expected 200, got %d: %s", res.StatusCode, body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
content, _ := io.ReadAll(res.Body)
|
|
|
|
|
if string(content) != "nested content" {
|
|
|
|
|
t.Errorf("expected 'nested content', got %q", string(content))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Delete tests
|
|
|
|
|
t.Run("trash file", func(t *testing.T) {
|
|
|
|
|
// Create a fresh file to trash (must write content to set status to ready)
|
|
|
|
|
trashFile, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
|
|
|
|
|
ParentID: env.rootDirID,
|
|
|
|
|
Name: "to-trash.txt",
|
|
|
|
|
}, env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateFile to-trash: %v", err)
|
|
|
|
|
}
|
|
|
|
|
err = env.vfs.WriteFile(ctx, env.db, trashFile, virtualfs.FileContentFromReader(bytes.NewReader([]byte("trash me"))), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("WriteFile to-trash: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var resp struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
|
}
|
|
|
|
|
doJSONAuth(t, env.app, http.MethodDelete, driveURL(fmt.Sprintf("/files/%s?trash=true", trashFile.PublicID)), env.accessToken, nil, http.StatusOK, &resp)
|
|
|
|
|
|
|
|
|
|
if resp.ID != trashFile.PublicID {
|
|
|
|
|
t.Errorf("expected id=%s, got %s", trashFile.PublicID, resp.ID)
|
|
|
|
|
}
|
|
|
|
|
if resp.Kind != "file" {
|
|
|
|
|
t.Errorf("expected kind=file, got %q", resp.Kind)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("delete file permanently", func(t *testing.T) {
|
|
|
|
|
// Create a temp file to delete (must write content to set status to ready)
|
|
|
|
|
tempFile, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
|
|
|
|
|
ParentID: env.rootDirID,
|
|
|
|
|
Name: "temp.txt",
|
|
|
|
|
}, env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateFile temp: %v", err)
|
|
|
|
|
}
|
|
|
|
|
err = env.vfs.WriteFile(ctx, env.db, tempFile, virtualfs.FileContentFromReader(bytes.NewReader([]byte("delete me"))), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("WriteFile temp: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, driveURL(fmt.Sprintf("/files/%s", tempFile.PublicID)), nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+env.accessToken)
|
|
|
|
|
|
|
|
|
|
res, err := env.app.Test(req, 10_000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusNoContent {
|
|
|
|
|
t.Errorf("expected 204, got %d", res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify file is gone
|
|
|
|
|
req = httptest.NewRequest(http.MethodGet, driveURL(fmt.Sprintf("/files/%s", tempFile.PublicID)), nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+env.accessToken)
|
|
|
|
|
|
|
|
|
|
res, err = env.app.Test(req, 10_000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusNotFound {
|
|
|
|
|
t.Errorf("expected 404 after permanent delete, got %d", res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("404 for non-existent file", func(t *testing.T) {
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, driveURL("/files/nonexistent123"), nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+env.accessToken)
|
|
|
|
|
|
|
|
|
|
res, err := env.app.Test(req, 10_000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusNotFound {
|
|
|
|
|
t.Errorf("expected 404, got %d", res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("404 for non-existent directory", func(t *testing.T) {
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, driveURL("/directories/nonexistent123"), nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+env.accessToken)
|
|
|
|
|
|
|
|
|
|
res, err := env.app.Test(req, 10_000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusNotFound {
|
|
|
|
|
t.Errorf("expected 404, got %d", res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Bulk delete tests
|
|
|
|
|
t.Run("bulk trash files", func(t *testing.T) {
|
|
|
|
|
// Create temp files for bulk delete (must write content to set status to ready)
|
|
|
|
|
bulkFile1, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
|
|
|
|
|
ParentID: env.rootDirID,
|
|
|
|
|
Name: "bulk1.txt",
|
|
|
|
|
}, env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateFile bulk1: %v", err)
|
|
|
|
|
}
|
|
|
|
|
err = env.vfs.WriteFile(ctx, env.db, bulkFile1, virtualfs.FileContentFromReader(bytes.NewReader([]byte("bulk1"))), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("WriteFile bulk1: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bulkFile2, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
|
|
|
|
|
ParentID: env.rootDirID,
|
|
|
|
|
Name: "bulk2.txt",
|
|
|
|
|
}, env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateFile bulk2: %v", err)
|
|
|
|
|
}
|
|
|
|
|
err = env.vfs.WriteFile(ctx, env.db, bulkFile2, virtualfs.FileContentFromReader(bytes.NewReader([]byte("bulk2"))), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("WriteFile bulk2: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var resp []struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
|
}
|
|
|
|
|
url := driveURL(fmt.Sprintf("/files?id=%s,%s&trash=true", bulkFile1.PublicID, bulkFile2.PublicID))
|
|
|
|
|
doJSONAuth(t, env.app, http.MethodDelete, url, env.accessToken, nil, http.StatusOK, &resp)
|
|
|
|
|
|
|
|
|
|
if len(resp) != 2 {
|
|
|
|
|
t.Fatalf("expected 2 trashed files, got %d", len(resp))
|
|
|
|
|
}
|
|
|
|
|
for _, f := range resp {
|
|
|
|
|
if f.Kind != "file" {
|
|
|
|
|
t.Errorf("expected kind=file for %s, got %q", f.Name, f.Kind)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("bulk delete files permanently", func(t *testing.T) {
|
|
|
|
|
// Create temp files for bulk delete (must write content to set status to ready)
|
|
|
|
|
bulkFile3, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
|
|
|
|
|
ParentID: env.rootDirID,
|
|
|
|
|
Name: "bulk3.txt",
|
|
|
|
|
}, env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateFile bulk3: %v", err)
|
|
|
|
|
}
|
|
|
|
|
err = env.vfs.WriteFile(ctx, env.db, bulkFile3, virtualfs.FileContentFromReader(bytes.NewReader([]byte("bulk3"))), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("WriteFile bulk3: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bulkFile4, err := env.vfs.CreateFile(ctx, env.db, virtualfs.CreateFileOptions{
|
|
|
|
|
ParentID: env.rootDirID,
|
|
|
|
|
Name: "bulk4.txt",
|
|
|
|
|
}, env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateFile bulk4: %v", err)
|
|
|
|
|
}
|
|
|
|
|
err = env.vfs.WriteFile(ctx, env.db, bulkFile4, virtualfs.FileContentFromReader(bytes.NewReader([]byte("bulk4"))), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("WriteFile bulk4: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
url := driveURL(fmt.Sprintf("/files?id=%s,%s", bulkFile3.PublicID, bulkFile4.PublicID))
|
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, url, nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+env.accessToken)
|
|
|
|
|
|
|
|
|
|
res, err := env.app.Test(req, 10_000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusNoContent {
|
|
|
|
|
t.Errorf("expected 204, got %d", res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify files are gone
|
|
|
|
|
for _, fileID := range []string{bulkFile3.PublicID, bulkFile4.PublicID} {
|
|
|
|
|
req = httptest.NewRequest(http.MethodGet, driveURL(fmt.Sprintf("/files/%s", fileID)), nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+env.accessToken)
|
|
|
|
|
|
|
|
|
|
res, err = env.app.Test(req, 10_000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusNotFound {
|
|
|
|
|
t.Errorf("expected 404 for %s after permanent delete, got %d", fileID, res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("bulk trash directories", func(t *testing.T) {
|
|
|
|
|
root, err := env.vfs.FindNode(ctx, env.db, env.rootDirID.String(), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("FindNode root: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create temp directories for bulk delete
|
|
|
|
|
bulkDir1, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "bulkdir1", env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateDirectory bulkdir1: %v", err)
|
|
|
|
|
}
|
|
|
|
|
bulkDir2, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "bulkdir2", env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateDirectory bulkdir2: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var resp []struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
DeletedAt *string `json:"deletedAt"`
|
|
|
|
|
}
|
|
|
|
|
url := driveURL(fmt.Sprintf("/directories?id=%s,%s&trash=true", bulkDir1.PublicID, bulkDir2.PublicID))
|
|
|
|
|
doJSONAuth(t, env.app, http.MethodDelete, url, env.accessToken, nil, http.StatusOK, &resp)
|
|
|
|
|
|
|
|
|
|
if len(resp) != 2 {
|
|
|
|
|
t.Fatalf("expected 2 trashed directories, got %d", len(resp))
|
|
|
|
|
}
|
|
|
|
|
for _, d := range resp {
|
|
|
|
|
if d.DeletedAt == nil {
|
|
|
|
|
t.Errorf("expected deletedAt to be set for %s", d.Name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("bulk delete directories permanently", func(t *testing.T) {
|
|
|
|
|
root, err := env.vfs.FindNode(ctx, env.db, env.rootDirID.String(), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("FindNode root: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create temp directories for bulk delete
|
|
|
|
|
bulkDir3, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "bulkdir3", env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateDirectory bulkdir3: %v", err)
|
|
|
|
|
}
|
|
|
|
|
bulkDir4, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "bulkdir4", env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateDirectory bulkdir4: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
url := driveURL(fmt.Sprintf("/directories?id=%s,%s", bulkDir3.PublicID, bulkDir4.PublicID))
|
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, url, nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+env.accessToken)
|
|
|
|
|
|
|
|
|
|
res, err := env.app.Test(req, 10_000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusNoContent {
|
|
|
|
|
t.Errorf("expected 204, got %d", res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify directories are gone
|
|
|
|
|
for _, dirID := range []string{bulkDir3.PublicID, bulkDir4.PublicID} {
|
|
|
|
|
req = httptest.NewRequest(http.MethodGet, driveURL(fmt.Sprintf("/directories/%s", dirID)), nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+env.accessToken)
|
|
|
|
|
|
|
|
|
|
res, err = env.app.Test(req, 10_000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusNotFound {
|
|
|
|
|
t.Errorf("expected 404 for %s after permanent delete, got %d", dirID, res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("delete directory with trash", func(t *testing.T) {
|
|
|
|
|
root, err := env.vfs.FindNode(ctx, env.db, env.rootDirID.String(), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("FindNode root: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trashDir, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "trashme", env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateDirectory trashme: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var resp struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
DeletedAt *string `json:"deletedAt"`
|
|
|
|
|
}
|
|
|
|
|
doJSONAuth(t, env.app, http.MethodDelete, driveURL(fmt.Sprintf("/directories/%s?trash=true", trashDir.PublicID)), env.accessToken, nil, http.StatusOK, &resp)
|
|
|
|
|
|
|
|
|
|
if resp.DeletedAt == nil {
|
|
|
|
|
t.Errorf("expected deletedAt to be set")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("delete directory permanently", func(t *testing.T) {
|
|
|
|
|
root, err := env.vfs.FindNode(ctx, env.db, env.rootDirID.String(), env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("FindNode root: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
permDir, err := env.vfs.CreateDirectory(ctx, env.db, root.ID, "deleteme", env.scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateDirectory deleteme: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, driveURL(fmt.Sprintf("/directories/%s", permDir.PublicID)), nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+env.accessToken)
|
|
|
|
|
|
|
|
|
|
res, err := env.app.Test(req, 10_000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusNoContent {
|
|
|
|
|
t.Errorf("expected 204, got %d", res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify directory is gone
|
|
|
|
|
req = httptest.NewRequest(http.MethodGet, driveURL(fmt.Sprintf("/directories/%s", permDir.PublicID)), nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+env.accessToken)
|
|
|
|
|
|
|
|
|
|
res, err = env.app.Test(req, 10_000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.StatusCode != http.StatusNotFound {
|
|
|
|
|
t.Errorf("expected 404 after permanent delete, got %d", res.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func doJSONAuth(
|
|
|
|
|
t *testing.T,
|
|
|
|
|
app *fiber.App,
|
|
|
|
|
method string,
|
|
|
|
|
path string,
|
|
|
|
|
accessToken string,
|
|
|
|
|
body any,
|
|
|
|
|
wantStatus int,
|
|
|
|
|
out any,
|
|
|
|
|
) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
var reqBody *bytes.Reader
|
|
|
|
|
if body == nil {
|
|
|
|
|
reqBody = bytes.NewReader(nil)
|
|
|
|
|
} else {
|
|
|
|
|
b, err := json.Marshal(body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("json marshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
reqBody = bytes.NewReader(b)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(method, path, reqBody)
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
if accessToken != "" {
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res, err := app.Test(req, 10_000)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("%s %s: %v", method, path, err)
|
|
|
|
|
}
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.StatusCode != wantStatus {
|
|
|
|
|
gotBody, _ := io.ReadAll(res.Body)
|
|
|
|
|
t.Fatalf("%s %s: status %d want %d body=%s", method, path, res.StatusCode, wantStatus, string(gotBody))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if out == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := json.NewDecoder(res.Body).Decode(out); err != nil {
|
|
|
|
|
t.Fatalf("%s %s: decode response: %v", method, path, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runPostgres(ctx context.Context) (_ *postgres.PostgresContainer, err error) {
|
|
|
|
|
defer func() {
|
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
|
err = fmt.Errorf("testcontainers panic: %v", r)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
return postgres.Run(
|
|
|
|
|
ctx,
|
|
|
|
|
"postgres:16-alpine",
|
|
|
|
|
postgres.WithDatabase("drexa"),
|
|
|
|
|
postgres.WithUsername("drexa"),
|
|
|
|
|
postgres.WithPassword("drexa"),
|
|
|
|
|
postgres.BasicWaitStrategies(),
|
|
|
|
|
)
|
|
|
|
|
}
|