mirror of
https://github.com/get-drexa/drive.git
synced 2026-02-02 16:11:17 +00:00
test(backend): sharing service tests
This commit is contained in:
365
apps/backend/internal/sharing/service_integration_test.go
Normal file
365
apps/backend/internal/sharing/service_integration_test.go
Normal file
@@ -0,0 +1,365 @@
|
||||
//go:build integration
|
||||
|
||||
package sharing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/get-drexa/drexa/internal/account"
|
||||
"github.com/get-drexa/drexa/internal/blob"
|
||||
"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/password"
|
||||
"github.com/get-drexa/drexa/internal/user"
|
||||
"github.com/get-drexa/drexa/internal/virtualfs"
|
||||
"github.com/testcontainers/testcontainers-go/modules/postgres"
|
||||
)
|
||||
|
||||
func TestService_SharingScopes(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
pg, err := runPostgres(ctx)
|
||||
if err != nil {
|
||||
t.Skipf("postgres testcontainer unavailable (docker not running/configured?): %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()
|
||||
|
||||
hashed, err := password.HashString("share-pass")
|
||||
if err != nil {
|
||||
t.Fatalf("HashString: %v", err)
|
||||
}
|
||||
|
||||
testUser, err := userSvc.RegisterUser(ctx, db, user.UserRegistrationOptions{
|
||||
Email: "share@example.com",
|
||||
DisplayName: "Share User",
|
||||
Password: hashed,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RegisterUser: %v", err)
|
||||
}
|
||||
|
||||
org, err := orgSvc.CreatePersonalOrganization(ctx, db, "Personal")
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePersonalOrganization: %v", err)
|
||||
}
|
||||
|
||||
acc, err := accSvc.CreateAccount(ctx, db, org.ID, testUser.ID, account.RoleAdmin, account.StatusActive)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateAccount: %v", err)
|
||||
}
|
||||
|
||||
drv, err := driveSvc.CreateDrive(ctx, db, drive.CreateDriveOptions{
|
||||
OrgID: org.ID,
|
||||
OwnerAccountID: &acc.ID,
|
||||
Name: "My Drive",
|
||||
QuotaBytes: 1024 * 1024 * 1024,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateDrive: %v", err)
|
||||
}
|
||||
|
||||
root, err := vfs.CreateRootDirectory(ctx, db, drv.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateRootDirectory: %v", err)
|
||||
}
|
||||
|
||||
scope := &virtualfs.Scope{
|
||||
DriveID: drv.ID,
|
||||
RootNodeID: root.ID,
|
||||
AllowedOps: virtualfs.AllAllowedOps,
|
||||
AllowedNodes: nil,
|
||||
ActorKind: virtualfs.ScopeActorAccount,
|
||||
ActorID: acc.ID,
|
||||
}
|
||||
|
||||
dirNode, err := vfs.CreateDirectory(ctx, db, root.ID, "Shared Folder", scope)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateDirectory: %v", err)
|
||||
}
|
||||
|
||||
fileNode, err := vfs.CreateFile(ctx, db, virtualfs.CreateFileOptions{
|
||||
ParentID: dirNode.ID,
|
||||
Name: "shared.txt",
|
||||
}, scope)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateFile: %v", err)
|
||||
}
|
||||
if err := vfs.WriteFile(ctx, db, fileNode, virtualfs.FileContentFromReader(strings.NewReader("hello")), scope); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
shareSvc, err := NewService(vfs)
|
||||
if err != nil {
|
||||
t.Fatalf("NewService: %v", err)
|
||||
}
|
||||
|
||||
t.Run("create share validation", func(t *testing.T) {
|
||||
if _, err := shareSvc.CreateShare(ctx, db, drv.ID, acc.ID, CreateShareOptions{}); err != ErrShareNoItems {
|
||||
t.Fatalf("expected ErrShareNoItems, got %v", err)
|
||||
}
|
||||
|
||||
if _, err := shareSvc.CreateShare(ctx, db, drv.ID, acc.ID, CreateShareOptions{
|
||||
Items: []*virtualfs.Node{root},
|
||||
}); err != ErrCannotShareRoot {
|
||||
t.Fatalf("expected ErrCannotShareRoot, got %v", err)
|
||||
}
|
||||
|
||||
if _, err := shareSvc.CreateShare(ctx, db, drv.ID, acc.ID, CreateShareOptions{
|
||||
Items: []*virtualfs.Node{dirNode, fileNode},
|
||||
}); err != ErrNotSameParent {
|
||||
t.Fatalf("expected ErrNotSameParent, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
dirShare, err := shareSvc.CreateShare(ctx, db, drv.ID, acc.ID, CreateShareOptions{
|
||||
Items: []*virtualfs.Node{dirNode},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateShare(dir): %v", err)
|
||||
}
|
||||
fileShare, err := shareSvc.CreateShare(ctx, db, drv.ID, acc.ID, CreateShareOptions{
|
||||
Items: []*virtualfs.Node{fileNode},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateShare(file): %v", err)
|
||||
}
|
||||
|
||||
t.Run("public scope for directory share", func(t *testing.T) {
|
||||
scope, err := shareSvc.ResolveScopeForShare(ctx, db, nil, dirShare)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveScopeForShare(dir): %v", err)
|
||||
}
|
||||
if scope.ActorKind != virtualfs.ScopeActorShare {
|
||||
t.Fatalf("unexpected actor kind: got %q want %q", scope.ActorKind, virtualfs.ScopeActorShare)
|
||||
}
|
||||
if scope.ActorID != dirShare.ID {
|
||||
t.Fatalf("unexpected actor id: got %q want %q", scope.ActorID, dirShare.ID)
|
||||
}
|
||||
if scope.RootNodeID != dirShare.SharedDirectoryID {
|
||||
t.Fatalf("unexpected root node: got %q want %q", scope.RootNodeID, dirShare.SharedDirectoryID)
|
||||
}
|
||||
if !scope.Allows(virtualfs.OperationRead) {
|
||||
t.Fatalf("expected read to be allowed")
|
||||
}
|
||||
if scope.Allows(virtualfs.OperationWrite) || scope.Allows(virtualfs.OperationUpload) {
|
||||
t.Fatalf("expected write/upload to be disallowed")
|
||||
}
|
||||
if _, ok := scope.AllowedNodes[dirNode.ID]; !ok {
|
||||
t.Fatalf("expected directory node to be allowlisted")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("public scope for file share", func(t *testing.T) {
|
||||
scope, err := shareSvc.ResolveScopeForShare(ctx, db, nil, fileShare)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveScopeForShare(file): %v", err)
|
||||
}
|
||||
if scope.ActorKind != virtualfs.ScopeActorShare {
|
||||
t.Fatalf("unexpected actor kind: got %q want %q", scope.ActorKind, virtualfs.ScopeActorShare)
|
||||
}
|
||||
if scope.ActorID != fileShare.ID {
|
||||
t.Fatalf("unexpected actor id: got %q want %q", scope.ActorID, fileShare.ID)
|
||||
}
|
||||
if scope.RootNodeID != dirNode.ID {
|
||||
t.Fatalf("unexpected root node: got %q want %q", scope.RootNodeID, dirNode.ID)
|
||||
}
|
||||
if scope.RootNodeID != fileShare.SharedDirectoryID {
|
||||
t.Fatalf("unexpected root node: got %q want %q", scope.RootNodeID, fileShare.SharedDirectoryID)
|
||||
}
|
||||
if _, ok := scope.AllowedNodes[fileNode.ID]; !ok {
|
||||
t.Fatalf("expected file node to be allowlisted")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list shares includes expired", func(t *testing.T) {
|
||||
expiredShare, err := shareSvc.CreateShare(ctx, db, drv.ID, acc.ID, CreateShareOptions{
|
||||
Items: []*virtualfs.Node{fileNode},
|
||||
ExpiresAt: time.Now().Add(-1 * time.Hour),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateShare(expired list): %v", err)
|
||||
}
|
||||
|
||||
activeShares, err := shareSvc.ListShares(ctx, db, drv.ID, ListSharesOptions{IncludesExpired: false})
|
||||
if err != nil {
|
||||
t.Fatalf("ListShares(active): %v", err)
|
||||
}
|
||||
activeIDs := make(map[string]bool, len(activeShares))
|
||||
for _, sh := range activeShares {
|
||||
activeIDs[sh.ID.String()] = true
|
||||
}
|
||||
if activeIDs[expiredShare.ID.String()] {
|
||||
t.Fatalf("expected expired share to be excluded")
|
||||
}
|
||||
if !activeIDs[dirShare.ID.String()] || !activeIDs[fileShare.ID.String()] {
|
||||
t.Fatalf("expected active shares to be listed")
|
||||
}
|
||||
|
||||
allShares, err := shareSvc.ListShares(ctx, db, drv.ID, ListSharesOptions{IncludesExpired: true})
|
||||
if err != nil {
|
||||
t.Fatalf("ListShares(all): %v", err)
|
||||
}
|
||||
allIDs := make(map[string]bool, len(allShares))
|
||||
for _, sh := range allShares {
|
||||
allIDs[sh.ID.String()] = true
|
||||
}
|
||||
if !allIDs[expiredShare.ID.String()] {
|
||||
t.Fatalf("expected expired share to be included")
|
||||
}
|
||||
if !allIDs[dirShare.ID.String()] || !allIDs[fileShare.ID.String()] {
|
||||
t.Fatalf("expected active shares to be listed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("find share not found", func(t *testing.T) {
|
||||
if _, err := shareSvc.FindShareByPublicID(ctx, db, "missing-share"); err != ErrShareNotFound {
|
||||
t.Fatalf("expected ErrShareNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("expired share", func(t *testing.T) {
|
||||
expiredShare, err := shareSvc.CreateShare(ctx, db, drv.ID, acc.ID, CreateShareOptions{
|
||||
Items: []*virtualfs.Node{fileNode},
|
||||
ExpiresAt: time.Now().Add(-1 * time.Hour),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateShare(expired): %v", err)
|
||||
}
|
||||
if _, err := shareSvc.ResolveScopeForShare(ctx, db, nil, expiredShare); err != ErrShareExpired {
|
||||
t.Fatalf("expected ErrShareExpired, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("revoked share", func(t *testing.T) {
|
||||
revokedShare, err := shareSvc.CreateShare(ctx, db, drv.ID, acc.ID, CreateShareOptions{
|
||||
Items: []*virtualfs.Node{fileNode},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateShare(revoked): %v", err)
|
||||
}
|
||||
now := time.Now()
|
||||
if _, err := db.NewUpdate().Model(revokedShare).
|
||||
Set("revoked_at = ?", now).
|
||||
WherePK().
|
||||
Exec(ctx); err != nil {
|
||||
t.Fatalf("update revoked share: %v", err)
|
||||
}
|
||||
revokedShare.RevokedAt = &now
|
||||
if _, err := shareSvc.ResolveScopeForShare(ctx, db, nil, revokedShare); err != ErrShareRevoked {
|
||||
t.Fatalf("expected ErrShareRevoked, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no permissions", func(t *testing.T) {
|
||||
noPermShare, err := shareSvc.CreateShare(ctx, db, drv.ID, acc.ID, CreateShareOptions{
|
||||
Items: []*virtualfs.Node{fileNode},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateShare(no permissions): %v", err)
|
||||
}
|
||||
if _, err := db.NewDelete().
|
||||
Model(&SharePermission{}).
|
||||
Where("share_id = ?", noPermShare.ID).
|
||||
Exec(ctx); err != nil {
|
||||
t.Fatalf("delete permissions: %v", err)
|
||||
}
|
||||
if _, err := shareSvc.ResolveScopeForShare(ctx, db, nil, noPermShare); err != ErrNoPermissions {
|
||||
t.Fatalf("expected ErrNoPermissions, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("permission expired", func(t *testing.T) {
|
||||
expiredPermShare, err := shareSvc.CreateShare(ctx, db, drv.ID, acc.ID, CreateShareOptions{
|
||||
Items: []*virtualfs.Node{fileNode},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateShare(expired perm): %v", err)
|
||||
}
|
||||
expiredAt := time.Now().Add(-1 * time.Hour)
|
||||
if _, err := db.NewUpdate().
|
||||
Model(&SharePermission{}).
|
||||
Set("expires_at = ?", expiredAt).
|
||||
Where("share_id = ?", expiredPermShare.ID).
|
||||
Exec(ctx); err != nil {
|
||||
t.Fatalf("update permission expiry: %v", err)
|
||||
}
|
||||
if _, err := shareSvc.ResolveScopeForShare(ctx, db, nil, expiredPermShare); err != ErrShareExpired {
|
||||
t.Fatalf("expected ErrShareExpired, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no items", func(t *testing.T) {
|
||||
noItemsShare, err := shareSvc.CreateShare(ctx, db, drv.ID, acc.ID, CreateShareOptions{
|
||||
Items: []*virtualfs.Node{fileNode},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateShare(no items): %v", err)
|
||||
}
|
||||
if _, err := db.NewDelete().
|
||||
Model(&ShareItem{}).
|
||||
Where("share_id = ?", noItemsShare.ID).
|
||||
Exec(ctx); err != nil {
|
||||
t.Fatalf("delete share items: %v", err)
|
||||
}
|
||||
if _, err := shareSvc.ResolveScopeForShare(ctx, db, nil, noItemsShare); err != ErrShareNoItems {
|
||||
t.Fatalf("expected ErrShareNoItems, got %v", 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(),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user