From 1815f41124a8d9f637822efce53e06f4f756a8b0 Mon Sep 17 00:00:00 2001 From: xray33 Date: Sun, 11 Oct 2026 19:30:04 +0300 Subject: [PATCH] test: add indexer tests, WebDAV OPTIONS/PROPPATCH checks, and repository operations --- internal/indexer/indexer_test.go | 129 +++++++++++++++++++++++++ internal/repository/repository_test.go | 37 +++++++ test/functional/api_test.go | 23 ++++- 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 internal/indexer/indexer_test.go diff --git a/internal/indexer/indexer_test.go b/internal/indexer/indexer_test.go new file mode 100644 index 0000000..c79f3bf --- /dev/null +++ b/internal/indexer/indexer_test.go @@ -0,0 +1,129 @@ +package indexer_test + +import ( + "context" + "io" + "log/slog" + "os" + "path/filepath" + "testing" + "time" + + "selfcloud/internal/db" + "selfcloud/internal/domain" + "selfcloud/internal/indexer" + "selfcloud/internal/repository" +) + +func TestIndexerReindexUser(t *testing.T) { + dsn := os.Getenv("DB_DSN") + if dsn == "" { + dsn = "selfcloud:selfcloud_pass@tcp(127.0.0.1:3306)/selfcloud?parseTime=true" + } + + sqlDB, err := db.Connect("mysql", dsn) + if err != nil { + t.Skipf("Skipping indexer integration test: MariaDB connection failed: %v", err) + return + } + defer sqlDB.Close() + + if err := db.Migrate(sqlDB); err != nil { + t.Fatalf("Migrate failed: %v", err) + } + + ctx := context.Background() + userRepo := repository.NewUserRepository(sqlDB) + nodeRepo := repository.NewNodeRepository(sqlDB) + + testUsername := "indexer_test_user" + _ = userRepo.Delete(ctx, testUsername) + + hash, _ := domain.HashPassword("secret") + testUser := &domain.User{ + Username: testUsername, + PasswordHash: hash, + Email: "indexer@example.com", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + if err := userRepo.Create(ctx, testUser); err != nil { + t.Fatalf("Create user failed: %v", err) + } + defer func() { + _ = userRepo.Delete(ctx, testUsername) + }() + + // Create temp storage directory + tempDir, err := os.MkdirTemp("", "selfcloud_idx_test_*") + if err != nil { + t.Fatalf("MkdirTemp failed: %v", err) + } + defer os.RemoveAll(tempDir) + + userStorageDir := filepath.Join(tempDir, testUsername) + if err := os.MkdirAll(filepath.Join(userStorageDir, "folder1"), 0755); err != nil { + t.Fatalf("MkdirAll failed: %v", err) + } + testFilePath := filepath.Join(userStorageDir, "folder1", "file.txt") + if err := os.WriteFile(testFilePath, []byte("Hello Indexer!"), 0644); err != nil { + t.Fatalf("WriteFile failed: %v", err) + } + + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + idx := indexer.New(nodeRepo, tempDir, logger, false) + + // 1. ReindexUser creates missing folder1 and file.txt in DB + stats, err := idx.ReindexUser(ctx, testUser, true) + if err != nil { + t.Fatalf("ReindexUser failed: %v", err) + } + if stats.Created < 2 { + t.Errorf("expected at least 2 nodes created, got %d", stats.Created) + } + + // Verify nodes exist in DB + folderNode, err := nodeRepo.GetByPath(ctx, testUser.ID, "/folder1") + if err != nil || folderNode == nil { + t.Fatalf("expected /folder1 in DB, got %v (err: %v)", folderNode, err) + } + fileNode, err := nodeRepo.GetByPath(ctx, testUser.ID, "/folder1/file.txt") + if err != nil || fileNode == nil { + t.Fatalf("expected /folder1/file.txt in DB, got %v (err: %v)", fileNode, err) + } + + // 2. Remove file from disk and run reindexer with removeOrphans=true + _ = os.Remove(testFilePath) + stats2, err := idx.ReindexUser(ctx, testUser, true) + if err != nil { + t.Fatalf("ReindexUser with orphan cleanup failed: %v", err) + } + if stats2.Deleted < 1 { + t.Errorf("expected at least 1 orphan deleted, got %d", stats2.Deleted) + } + + // Verify orphan is gone from DB + orphanNode, _ := nodeRepo.GetByPath(ctx, testUser.ID, "/folder1/file.txt") + if orphanNode != nil { + t.Errorf("expected orphan node to be deleted from DB, found: %v", orphanNode) + } +} + +func TestIndexerNonExistentUserDir(t *testing.T) { + tempDir, err := os.MkdirTemp("", "selfcloud_idx_skip_*") + if err != nil { + t.Fatalf("MkdirTemp failed: %v", err) + } + defer os.RemoveAll(tempDir) + + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + idx := indexer.New(nil, tempDir, logger, true) + + stats, err := idx.ReindexUser(context.Background(), &domain.User{Username: "does_not_exist"}, false) + if err != nil { + t.Fatalf("expected nil error for missing directory, got %v", err) + } + if stats.Created != 0 { + t.Errorf("expected 0 created for missing directory, got %d", stats.Created) + } +} diff --git a/internal/repository/repository_test.go b/internal/repository/repository_test.go index 325aa0e..0721d78 100644 --- a/internal/repository/repository_test.go +++ b/internal/repository/repository_test.go @@ -145,6 +145,31 @@ func TestRepositoryOperations(t *testing.T) { t.Errorf("expected name child.txt, got %s", newFile.Name) } + // Test Node Update + newFile.Size = 999 + newFile.ETag = "updated_etag" + if err := repos.Nodes.Update(ctx, newFile); err != nil { + t.Errorf("Update node failed: %v", err) + } + + // Test Node Delete + tempNode := &domain.Node{ + UserID: fetchedUser.ID, + Path: "/folderB/temp.txt", + ParentPath: "/folderB", + Name: "temp.txt", + IsDir: false, + Size: 10, + ETag: "temp_etag", + FileID: "temp_id", + MTime: time.Now(), + ContentType: "text/plain", + } + _ = repos.Nodes.Create(ctx, tempNode) + if err := repos.Nodes.Delete(ctx, fetchedUser.ID, "/folderB/temp.txt"); err != nil { + t.Errorf("Delete node failed: %v", err) + } + // 3. Share Repository with ExpiresAt & ListByUserID expiresAt := time.Now().Add(24 * time.Hour) share := &domain.Share{ @@ -213,6 +238,18 @@ func TestRepositoryOperations(t *testing.T) { t.Errorf("DeleteByToken failed: %v", err) } + // Test DeleteByUserID + sessForUser := &domain.Session{ + Token: "sess_token_for_user", + UserID: fetchedUser.ID, + ExpiresAt: time.Now().Add(1 * time.Hour), + CreatedAt: time.Now(), + } + _ = repos.Sessions.Create(ctx, sessForUser) + if err := repos.Sessions.DeleteByUserID(ctx, fetchedUser.ID); err != nil { + t.Errorf("DeleteByUserID failed: %v", err) + } + // 5. Identity Repository identity := &domain.UserIdentity{ UserID: fetchedUser.ID, diff --git a/test/functional/api_test.go b/test/functional/api_test.go index 68afb5b..395f2e7 100644 --- a/test/functional/api_test.go +++ b/test/functional/api_test.go @@ -47,15 +47,18 @@ func setupTestServer(t *testing.T) (*httptest.Server, func()) { userRepo := repository.NewUserRepository(sqlDB) nodeRepo := repository.NewNodeRepository(sqlDB) + hash, _ := domain.HashPassword("admin") adminUser, _ := userRepo.GetByUsername(ctx, "admin") if adminUser == nil { - hash, _ := domain.HashPassword("admin") adminUser = &domain.User{Username: "admin", PasswordHash: hash, Email: "admin@selfcloud.local"} _ = userRepo.Create(ctx, adminUser) _ = nodeRepo.Create(ctx, &domain.Node{ UserID: adminUser.ID, Path: "/", ParentPath: "", Name: "", IsDir: true, ETag: "root_etag", FileID: "00000001oc_root", MTime: time.Now(), ContentType: "httpd/unix-directory", }) + } else { + adminUser.PasswordHash = hash + _ = userRepo.Update(ctx, adminUser) } tempStorage, err := os.MkdirTemp("", "selfcloud_fn_test_*") if err != nil { @@ -230,6 +233,24 @@ func TestFullSelfCloudWorkflow(t *testing.T) { } resp.Body.Close() + // 8b. OPTIONS /remote.php/webdav/ + req, _ = http.NewRequest("OPTIONS", server.URL+"/remote.php/webdav/", nil) + req.SetBasicAuth("admin", "admin") + resp, err = client.Do(req) + if err != nil || resp.StatusCode != http.StatusOK { + t.Errorf("expected 200 OK from OPTIONS, got %d", resp.StatusCode) + } + resp.Body.Close() + + // 8c. PROPPATCH /remote.php/webdav/myfolder/hello_moved.txt + req, _ = http.NewRequest("PROPPATCH", server.URL+"/remote.php/webdav/myfolder/hello_moved.txt", strings.NewReader("")) + req.SetBasicAuth("admin", "admin") + resp, err = client.Do(req) + if err != nil || resp.StatusCode != http.StatusMultiStatus { + t.Errorf("expected 207 Multi-Status from PROPPATCH, got %d", resp.StatusCode) + } + resp.Body.Close() + // 9. DELETE /remote.php/webdav/myfolder req, _ = http.NewRequest("DELETE", server.URL+"/remote.php/webdav/myfolder", nil) req.SetBasicAuth("admin", "admin")