package domain import ( "context" "os" "path/filepath" "testing" "time" archivalStore "git.nakama.town/fmartingr/hako/internal/archival/store" authDomain "git.nakama.town/fmartingr/hako/internal/auth/domain" authStore "git.nakama.town/fmartingr/hako/internal/auth/store" "git.nakama.town/fmartingr/hako/internal/database" "git.nakama.town/fmartingr/hako/internal/model" "git.nakama.town/fmartingr/hako/internal/testutil" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTestLinkService(t *testing.T) (*LinkDomain, *archivalStore.ArchiveStore, *archivalStore.ArchiveFileStore, *archivalStore.LinkCategoryStore, *database.Connections, string, string, func()) { t.Helper() ctx := context.Background() testDeps := testutil.GetTestConfigurationAndDependencies(t, ctx) // Initialize domains (must be done in test to avoid import cycle) testDeps.Dependencies.Domains().SetLinks(NewLinkDomain(testDeps.Dependencies)) testDeps.Dependencies.Domains().SetArchives(NewArchiveDomain(testDeps.Dependencies)) testDeps.Dependencies.Domains().SetCategories(NewCategoryDomain(testDeps.Dependencies)) testDeps.Dependencies.Domains().SetAuth(authDomain.NewAuthDomain(testDeps.Dependencies)) // Create a test user userUUID := uuid.New() user := &authStore.User{ ID: userUUID, Email: "test@example.com", PasswordHash: "test-hash", CreatedAt: time.Now(), UpdatedAt: time.Now(), } err := testDeps.Dependencies.UserStore.Create(user) require.NoError(t, err) linkService := testDeps.Dependencies.Domains().Links().(*LinkDomain) return linkService, testDeps.ArchiveStore, testDeps.ArchiveFileStore, testDeps.LinkCategoryStore, testDeps.DBConnections, userUUID.String(), testDeps.Config.ArchiveStoragePath, func() {} } func TestLinkService_DeleteLink_CascadeDelete(t *testing.T) { ctx := context.Background() linkService, archiveStore, archiveFileStore, linkCategoryStore, conns, userID, storageDir, cleanup := setupTestLinkService(t) defer cleanup() // Create a link link, err := linkService.CreateLink(ctx, "https://example.com/test", userID) require.NoError(t, err) // Wait a bit for the archive job to be created, then manually create a completed archive // For testing, let's create a completed archive manually archive := &model.Archive{ ID: uuid.New().String(), LinkID: link.ID, UserID: userID, Status: model.ArchiveStatusCompleted, Title: "Test Archive", CreatedAt: time.Now(), } err = archiveStore.Create(ctx, archive) require.NoError(t, err) // Create archive files file1 := &model.ArchiveFile{ ID: uuid.New().String(), ArchiveID: archive.ID, ArchiverKey: "direct_download", Filename: "test1.txt", MimeType: "text/plain", FileSize: 100, StoragePath: model.GetPathForLinkFile(link.ID, "test1.txt"), HashSha256: "abc123", CreatedAt: time.Now(), } err = archiveFileStore.Create(ctx, file1) require.NoError(t, err) file2 := &model.ArchiveFile{ ID: uuid.New().String(), ArchiveID: archive.ID, ArchiverKey: "direct_download", Filename: "test2.txt", MimeType: "text/plain", FileSize: 200, StoragePath: model.GetPathForLinkFile(link.ID, "test2.txt"), HashSha256: "def456", CreatedAt: time.Now(), } err = archiveFileStore.Create(ctx, file2) require.NoError(t, err) // Create storage files file1Path := filepath.Join(storageDir, file1.StoragePath) file2Path := filepath.Join(storageDir, file2.StoragePath) err = os.MkdirAll(filepath.Dir(file1Path), 0755) require.NoError(t, err) err = os.WriteFile(file1Path, []byte("test content 1"), 0644) require.NoError(t, err) err = os.WriteFile(file2Path, []byte("test content 2"), 0644) require.NoError(t, err) // Create a category and link it (insert directly into database) categoryID := uuid.New().String() now := time.Now().Format("2006-01-02 15:04:05") _, err = conns.Write.ExecContext(ctx, ` INSERT INTO categories (id, name, icon, created_at, updated_at) VALUES (?, ?, ?, ?, ?) `, categoryID, "Test Category", "test-icon", now, now) require.NoError(t, err) err = linkCategoryStore.AddCategory(ctx, link.ID, categoryID) require.NoError(t, err) // Verify everything exists before deletion // Note: CreateLink creates an archive automatically, so we have 2 archives total archives, err := archiveStore.ListByLinkID(ctx, archivalStore.ArchiveListOptions{LinkID: link.ID}) require.NoError(t, err) assert.Len(t, archives, 2) // One from CreateLink + one we created manually files, err := archiveFileStore.ListByArchiveID(ctx, archivalStore.ArchiveFileListOptions{ArchiveID: archive.ID}) require.NoError(t, err) assert.Len(t, files, 2) categories, err := linkCategoryStore.GetCategoriesByLinkID(ctx, link.ID) require.NoError(t, err) assert.Len(t, categories, 1) // Verify storage files exist _, err = os.Stat(file1Path) require.NoError(t, err) _, err = os.Stat(file2Path) require.NoError(t, err) // Delete the link err = linkService.DeleteLink(ctx, link.ID) require.NoError(t, err) // Verify link is deleted _, err = linkService.GetLink(ctx, link.ID) assert.Error(t, err) assert.Contains(t, err.Error(), "not found") // Verify archives are deleted archives, err = archiveStore.ListByLinkID(ctx, archivalStore.ArchiveListOptions{LinkID: link.ID}) require.NoError(t, err) assert.Len(t, archives, 0) // Verify archive files are deleted files, err = archiveFileStore.ListByArchiveID(ctx, archivalStore.ArchiveFileListOptions{ArchiveID: archive.ID}) require.NoError(t, err) assert.Len(t, files, 0) // Verify link categories are deleted categories, err = linkCategoryStore.GetCategoriesByLinkID(ctx, link.ID) require.NoError(t, err) assert.Len(t, categories, 0) // Verify storage files are deleted _, err = os.Stat(file1Path) assert.True(t, os.IsNotExist(err)) _, err = os.Stat(file2Path) assert.True(t, os.IsNotExist(err)) } func TestLinkService_DeleteLink_MultipleArchives(t *testing.T) { ctx := context.Background() linkService, archiveStore, archiveFileStore, _, _, userID, storageDir, cleanup := setupTestLinkService(t) defer cleanup() // Create a link link, err := linkService.CreateLink(ctx, "https://example.com/test", userID) require.NoError(t, err) // Create multiple archives archive1 := &model.Archive{ ID: uuid.New().String(), LinkID: link.ID, UserID: userID, Status: model.ArchiveStatusCompleted, Title: "Archive 1", CreatedAt: time.Now(), } err = archiveStore.Create(ctx, archive1) require.NoError(t, err) archive2 := &model.Archive{ ID: uuid.New().String(), LinkID: link.ID, UserID: userID, Status: model.ArchiveStatusCompleted, Title: "Archive 2", CreatedAt: time.Now(), } err = archiveStore.Create(ctx, archive2) require.NoError(t, err) // Create files for each archive file1 := &model.ArchiveFile{ ID: uuid.New().String(), ArchiveID: archive1.ID, ArchiverKey: "direct_download", Filename: "file1.txt", MimeType: "text/plain", FileSize: 100, StoragePath: model.GetPathForLinkFile(link.ID, "file1.txt"), HashSha256: "hash1", CreatedAt: time.Now(), } err = archiveFileStore.Create(ctx, file1) require.NoError(t, err) file2 := &model.ArchiveFile{ ID: uuid.New().String(), ArchiveID: archive2.ID, ArchiverKey: "direct_download", Filename: "file2.txt", MimeType: "text/plain", FileSize: 200, StoragePath: model.GetPathForLinkFile(link.ID, "file2.txt"), HashSha256: "hash2", CreatedAt: time.Now(), } err = archiveFileStore.Create(ctx, file2) require.NoError(t, err) // Create storage files file1Path := filepath.Join(storageDir, file1.StoragePath) file2Path := filepath.Join(storageDir, file2.StoragePath) err = os.MkdirAll(filepath.Dir(file1Path), 0755) require.NoError(t, err) err = os.WriteFile(file1Path, []byte("content 1"), 0644) require.NoError(t, err) err = os.WriteFile(file2Path, []byte("content 2"), 0644) require.NoError(t, err) // Verify both archives exist // Note: CreateLink creates an archive automatically, so we have 3 archives total archives, err := archiveStore.ListByLinkID(ctx, archivalStore.ArchiveListOptions{LinkID: link.ID}) require.NoError(t, err) assert.Len(t, archives, 3) // One from CreateLink + two we created manually // Delete the link err = linkService.DeleteLink(ctx, link.ID) require.NoError(t, err) // Verify all archives are deleted archives, err = archiveStore.ListByLinkID(ctx, archivalStore.ArchiveListOptions{LinkID: link.ID}) require.NoError(t, err) assert.Len(t, archives, 0) // Verify all files are deleted files1, err := archiveFileStore.ListByArchiveID(ctx, archivalStore.ArchiveFileListOptions{ArchiveID: archive1.ID}) require.NoError(t, err) assert.Len(t, files1, 0) files2, err := archiveFileStore.ListByArchiveID(ctx, archivalStore.ArchiveFileListOptions{ArchiveID: archive2.ID}) require.NoError(t, err) assert.Len(t, files2, 0) // Verify storage files are deleted _, err = os.Stat(file1Path) assert.True(t, os.IsNotExist(err)) _, err = os.Stat(file2Path) assert.True(t, os.IsNotExist(err)) } func TestLinkService_DeleteLink_NoArchives(t *testing.T) { ctx := context.Background() linkService, _, _, _, _, userID, _, cleanup := setupTestLinkService(t) defer cleanup() // Create a link link, err := linkService.CreateLink(ctx, "https://example.com/test", userID) require.NoError(t, err) // Delete the link (should work even with no archives) err = linkService.DeleteLink(ctx, link.ID) require.NoError(t, err) // Verify link is deleted _, err = linkService.GetLink(ctx, link.ID) assert.Error(t, err) } func TestLinkService_DeleteLink_StorageFileNotFound(t *testing.T) { ctx := context.Background() linkService, archiveStore, archiveFileStore, _, _, userID, _, cleanup := setupTestLinkService(t) defer cleanup() // Create a link link, err := linkService.CreateLink(ctx, "https://example.com/test", userID) require.NoError(t, err) // Create an archive archive := &model.Archive{ ID: uuid.New().String(), LinkID: link.ID, UserID: userID, Status: model.ArchiveStatusCompleted, Title: "Test Archive", CreatedAt: time.Now(), } err = archiveStore.Create(ctx, archive) require.NoError(t, err) // Create archive file with storage path, but don't create the actual file file := &model.ArchiveFile{ ID: uuid.New().String(), ArchiveID: archive.ID, ArchiverKey: "direct_download", Filename: "missing.txt", MimeType: "text/plain", FileSize: 100, StoragePath: model.GetPathForLinkFile(link.ID, "missing.txt"), HashSha256: "hash", CreatedAt: time.Now(), } err = archiveFileStore.Create(ctx, file) require.NoError(t, err) // Delete the link - should succeed even if storage file doesn't exist err = linkService.DeleteLink(ctx, link.ID) require.NoError(t, err) // Verify everything is deleted archives, err := archiveStore.ListByLinkID(ctx, archivalStore.ArchiveListOptions{LinkID: link.ID}) require.NoError(t, err) assert.Len(t, archives, 0) }