From e8868dde398eeab8337ace9d28e7a9cf81d7c89c Mon Sep 17 00:00:00 2001 From: LordMathis Date: Mon, 25 Nov 2024 21:58:16 +0100 Subject: [PATCH] Test users and workspaces --- server/internal/db/users_test.go | 413 +++++++++++++++++++++++++ server/internal/db/workspaces_test.go | 430 ++++++++++++++++++++++++++ 2 files changed, 843 insertions(+) create mode 100644 server/internal/db/users_test.go create mode 100644 server/internal/db/workspaces_test.go diff --git a/server/internal/db/users_test.go b/server/internal/db/users_test.go new file mode 100644 index 0000000..e0683ea --- /dev/null +++ b/server/internal/db/users_test.go @@ -0,0 +1,413 @@ +package db_test + +import ( + "strings" + "testing" + + "novamd/internal/db" + "novamd/internal/models" +) + +func TestUserOperations(t *testing.T) { + database, err := db.NewTestDB(":memory:", &mockSecrets{}) + if err != nil { + t.Fatalf("failed to create test database: %v", err) + } + defer database.Close() + + if err := database.Migrate(); err != nil { + t.Fatalf("failed to run migrations: %v", err) + } + + t.Run("CreateUser", func(t *testing.T) { + testCases := []struct { + name string + user *models.User + wantErr bool + errContains string + }{ + { + name: "valid user", + user: &models.User{ + Email: "test@example.com", + DisplayName: "Test User", + PasswordHash: "hashed_password", + Role: models.RoleEditor, + }, + wantErr: false, + }, + { + name: "duplicate email", + user: &models.User{ + Email: "test@example.com", // Same as above + DisplayName: "Another User", + PasswordHash: "different_hash", + Role: models.RoleViewer, + }, + wantErr: true, + errContains: "UNIQUE constraint failed", + }, + { + name: "invalid role", + user: &models.User{ + Email: "invalid@example.com", + DisplayName: "Invalid Role User", + PasswordHash: "hash", + Role: "invalid_role", + }, + wantErr: true, + errContains: "CHECK constraint failed", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + user, err := database.CreateUser(tc.user) + + if tc.wantErr { + if err == nil { + t.Error("expected error, got nil") + } else if !strings.Contains(err.Error(), tc.errContains) { + t.Errorf("error = %v, want error containing %v", err, tc.errContains) + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Verify user was created properly + if user.ID == 0 { + t.Error("expected non-zero user ID") + } + if user.Email != tc.user.Email { + t.Errorf("Email = %v, want %v", user.Email, tc.user.Email) + } + if user.DisplayName != tc.user.DisplayName { + t.Errorf("DisplayName = %v, want %v", user.DisplayName, tc.user.DisplayName) + } + if user.Role != tc.user.Role { + t.Errorf("Role = %v, want %v", user.Role, tc.user.Role) + } + if user.CreatedAt.IsZero() { + t.Error("CreatedAt should not be zero") + } + if user.LastWorkspaceID == 0 { + t.Error("expected non-zero LastWorkspaceID (default workspace)") + } + }) + } + }) + + t.Run("GetUserByID", func(t *testing.T) { + // Create a test user first + createdUser, err := database.CreateUser(&models.User{ + Email: "getbyid@example.com", + DisplayName: "Get By ID User", + PasswordHash: "hash", + Role: models.RoleEditor, + }) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + + testCases := []struct { + name string + userID int + wantErr bool + }{ + { + name: "existing user", + userID: createdUser.ID, + wantErr: false, + }, + { + name: "non-existent user", + userID: 99999, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + user, err := database.GetUserByID(tc.userID) + + if tc.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if user.ID != tc.userID { + t.Errorf("ID = %v, want %v", user.ID, tc.userID) + } + }) + } + }) + + t.Run("GetUserByEmail", func(t *testing.T) { + // Create a test user first + createdUser, err := database.CreateUser(&models.User{ + Email: "getbyemail@example.com", + DisplayName: "Get By Email User", + PasswordHash: "hash", + Role: models.RoleEditor, + }) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + + testCases := []struct { + name string + email string + wantErr bool + }{ + { + name: "existing user", + email: createdUser.Email, + wantErr: false, + }, + { + name: "non-existent user", + email: "nonexistent@example.com", + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + user, err := database.GetUserByEmail(tc.email) + + if tc.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if user.Email != tc.email { + t.Errorf("Email = %v, want %v", user.Email, tc.email) + } + }) + } + }) + + t.Run("UpdateUser", func(t *testing.T) { + // Create a test user first + user, err := database.CreateUser(&models.User{ + Email: "update@example.com", + DisplayName: "Original Name", + PasswordHash: "original_hash", + Role: models.RoleEditor, + }) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + + // Update user details + user.DisplayName = "Updated Name" + user.PasswordHash = "new_hash" + user.Role = models.RoleAdmin + + if err := database.UpdateUser(user); err != nil { + t.Fatalf("failed to update user: %v", err) + } + + // Verify updates + updated, err := database.GetUserByID(user.ID) + if err != nil { + t.Fatalf("failed to get updated user: %v", err) + } + + if updated.DisplayName != "Updated Name" { + t.Errorf("DisplayName = %v, want %v", updated.DisplayName, "Updated Name") + } + if updated.PasswordHash != "new_hash" { + t.Errorf("PasswordHash = %v, want %v", updated.PasswordHash, "new_hash") + } + if updated.Role != models.RoleAdmin { + t.Errorf("Role = %v, want %v", updated.Role, models.RoleAdmin) + } + }) + + t.Run("GetAllUsers", func(t *testing.T) { + // Create several test users + testUsers := []*models.User{ + { + Email: "user1@example.com", + DisplayName: "User One", + PasswordHash: "hash1", + Role: models.RoleEditor, + }, + { + Email: "user2@example.com", + DisplayName: "User Two", + PasswordHash: "hash2", + Role: models.RoleViewer, + }, + } + + for _, u := range testUsers { + _, err := database.CreateUser(u) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + } + + // Get all users + users, err := database.GetAllUsers() + if err != nil { + t.Fatalf("failed to get all users: %v", err) + } + + // We should have at least as many users as we just created + // (there might be more from previous tests) + if len(users) < len(testUsers) { + t.Errorf("got %d users, want at least %d", len(users), len(testUsers)) + } + + // Verify each test user exists in the result + for _, expected := range testUsers { + found := false + for _, u := range users { + if u.Email == expected.Email { + found = true + if u.DisplayName != expected.DisplayName { + t.Errorf("DisplayName = %v, want %v", u.DisplayName, expected.DisplayName) + } + if u.Role != expected.Role { + t.Errorf("Role = %v, want %v", u.Role, expected.Role) + } + break + } + } + if !found { + t.Errorf("user with email %s not found in results", expected.Email) + } + } + }) + + t.Run("UpdateLastWorkspace", func(t *testing.T) { + // Create a test user with multiple workspaces + user, err := database.CreateUser(&models.User{ + Email: "workspace@example.com", + DisplayName: "Workspace User", + PasswordHash: "hash", + Role: models.RoleEditor, + }) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + + // Create additional workspace + workspace := &models.Workspace{ + UserID: user.ID, + Name: "Second Workspace", + } + if err := database.CreateWorkspace(workspace); err != nil { + t.Fatalf("failed to create additional workspace: %v", err) + } + + // Update last workspace + err = database.UpdateLastWorkspace(user.ID, workspace.Name) + if err != nil { + t.Fatalf("failed to update last workspace: %v", err) + } + + // Verify update + lastWorkspace, err := database.GetLastWorkspaceName(user.ID) + if err != nil { + t.Fatalf("failed to get last workspace: %v", err) + } + + if lastWorkspace != workspace.Name { + t.Errorf("LastWorkspace = %v, want %v", lastWorkspace, workspace.Name) + } + }) + + t.Run("DeleteUser", func(t *testing.T) { + // Create a test user + user, err := database.CreateUser(&models.User{ + Email: "delete@example.com", + DisplayName: "Delete User", + PasswordHash: "hash", + Role: models.RoleEditor, + }) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + + // Delete the user + if err := database.DeleteUser(user.ID); err != nil { + t.Fatalf("failed to delete user: %v", err) + } + + // Verify user is gone + _, err = database.GetUserByID(user.ID) + if err == nil { + t.Error("expected error getting deleted user, got nil") + } + + // Verify workspaces are gone + workspaces, err := database.GetWorkspacesByUserID(user.ID) + if err != nil { + t.Fatalf("unexpected error checking workspaces: %v", err) + } + if len(workspaces) > 0 { + t.Error("expected no workspaces for deleted user") + } + }) + + t.Run("CountAdminUsers", func(t *testing.T) { + // Create users with different roles + testUsers := []*models.User{ + { + Email: "admin1@example.com", + DisplayName: "Admin One", + PasswordHash: "hash1", + Role: models.RoleAdmin, + }, + { + Email: "admin2@example.com", + DisplayName: "Admin Two", + PasswordHash: "hash2", + Role: models.RoleAdmin, + }, + { + Email: "editor@example.com", + DisplayName: "Editor", + PasswordHash: "hash3", + Role: models.RoleEditor, + }, + } + + for _, u := range testUsers { + _, err := database.CreateUser(u) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + } + + // Count admin users + count, err := database.CountAdminUsers() + if err != nil { + t.Fatalf("failed to count admin users: %v", err) + } + + // We should have at least 2 admin users (from our test cases) + // There might be more from previous tests + if count < 2 { + t.Errorf("AdminCount = %d, want at least 2", count) + } + }) +} diff --git a/server/internal/db/workspaces_test.go b/server/internal/db/workspaces_test.go new file mode 100644 index 0000000..924d4a9 --- /dev/null +++ b/server/internal/db/workspaces_test.go @@ -0,0 +1,430 @@ +package db_test + +import ( + "database/sql" + "strings" + "testing" + + "novamd/internal/db" + "novamd/internal/models" +) + +func TestWorkspaceOperations(t *testing.T) { + database, err := db.NewTestDB(":memory:", &mockSecrets{}) + if err != nil { + t.Fatalf("failed to create test database: %v", err) + } + defer database.Close() + + if err := database.Migrate(); err != nil { + t.Fatalf("failed to run migrations: %v", err) + } + + // Create a test user first + user, err := database.CreateUser(&models.User{ + Email: "test@example.com", + DisplayName: "Test User", + PasswordHash: "hash", + Role: models.RoleEditor, + }) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + + t.Run("CreateWorkspace", func(t *testing.T) { + testCases := []struct { + name string + workspace *models.Workspace + wantErr bool + errContains string + }{ + { + name: "valid workspace", + workspace: &models.Workspace{ + UserID: user.ID, + Name: "Test Workspace", + }, + wantErr: false, + }, + { + name: "non-existent user", + workspace: &models.Workspace{ + UserID: 99999, + Name: "Invalid User", + }, + wantErr: true, + errContains: "FOREIGN KEY constraint failed", + }, + { + name: "with git settings", + workspace: &models.Workspace{ + UserID: user.ID, + Name: "Git Workspace", + Theme: "dark", + AutoSave: true, + ShowHiddenFiles: true, + GitEnabled: true, + GitURL: "https://github.com/user/repo", + GitUser: "username", + GitToken: "secret-token", + GitAutoCommit: true, + GitCommitMsgTemplate: "${action} ${filename}", + }, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.workspace.Theme == "" { + tc.workspace.GetDefaultSettings() + } + + err := database.CreateWorkspace(tc.workspace) + + if tc.wantErr { + if err == nil { + t.Error("expected error, got nil") + } else if !strings.Contains(err.Error(), tc.errContains) { + t.Errorf("error = %v, want error containing %v", err, tc.errContains) + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Verify workspace was created properly + if tc.workspace.ID == 0 { + t.Error("expected non-zero workspace ID") + } + + // Retrieve and verify workspace + stored, err := database.GetWorkspaceByID(tc.workspace.ID) + if err != nil { + t.Fatalf("failed to retrieve workspace: %v", err) + } + + verifyWorkspace(t, stored, tc.workspace) + }) + } + }) + + t.Run("GetWorkspaceByID", func(t *testing.T) { + // Create a test workspace first + workspace := &models.Workspace{ + UserID: user.ID, + Name: "Get By ID Workspace", + } + workspace.GetDefaultSettings() + if err := database.CreateWorkspace(workspace); err != nil { + t.Fatalf("failed to create test workspace: %v", err) + } + + testCases := []struct { + name string + workspaceID int + wantErr bool + }{ + { + name: "existing workspace", + workspaceID: workspace.ID, + wantErr: false, + }, + { + name: "non-existent workspace", + workspaceID: 99999, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := database.GetWorkspaceByID(tc.workspaceID) + + if tc.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result.ID != tc.workspaceID { + t.Errorf("ID = %v, want %v", result.ID, tc.workspaceID) + } + }) + } + }) + + t.Run("GetWorkspaceByName", func(t *testing.T) { + // Create a test workspace first + workspace := &models.Workspace{ + UserID: user.ID, + Name: "Get By Name Workspace", + } + workspace.GetDefaultSettings() + if err := database.CreateWorkspace(workspace); err != nil { + t.Fatalf("failed to create test workspace: %v", err) + } + + testCases := []struct { + name string + userID int + workspaceName string + wantErr bool + }{ + { + name: "existing workspace", + userID: user.ID, + workspaceName: workspace.Name, + wantErr: false, + }, + { + name: "wrong user ID", + userID: 99999, + workspaceName: workspace.Name, + wantErr: true, + }, + { + name: "non-existent workspace", + userID: user.ID, + workspaceName: "Non-existent", + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := database.GetWorkspaceByName(tc.userID, tc.workspaceName) + + if tc.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result.Name != tc.workspaceName { + t.Errorf("Name = %v, want %v", result.Name, tc.workspaceName) + } + if result.UserID != tc.userID { + t.Errorf("UserID = %v, want %v", result.UserID, tc.userID) + } + }) + } + }) + + t.Run("UpdateWorkspace", func(t *testing.T) { + // Create a test workspace first + workspace := &models.Workspace{ + UserID: user.ID, + Name: "Update Workspace", + } + workspace.GetDefaultSettings() + if err := database.CreateWorkspace(workspace); err != nil { + t.Fatalf("failed to create test workspace: %v", err) + } + + // Update workspace settings + workspace.Theme = "dark" + workspace.AutoSave = true + workspace.ShowHiddenFiles = true + workspace.GitEnabled = true + workspace.GitURL = "https://github.com/user/repo" + workspace.GitUser = "username" + workspace.GitToken = "new-token" + workspace.GitAutoCommit = true + workspace.GitCommitMsgTemplate = "custom ${filename}" + + if err := database.UpdateWorkspace(workspace); err != nil { + t.Fatalf("failed to update workspace: %v", err) + } + + // Verify updates + updated, err := database.GetWorkspaceByID(workspace.ID) + if err != nil { + t.Fatalf("failed to get updated workspace: %v", err) + } + + verifyWorkspace(t, updated, workspace) + }) + + t.Run("GetWorkspacesByUserID", func(t *testing.T) { + // Create several test workspaces + testWorkspaces := []*models.Workspace{ + { + UserID: user.ID, + Name: "User Workspace 1", + }, + { + UserID: user.ID, + Name: "User Workspace 2", + }, + } + + for _, w := range testWorkspaces { + w.GetDefaultSettings() + if err := database.CreateWorkspace(w); err != nil { + t.Fatalf("failed to create test workspace: %v", err) + } + } + + // Get all workspaces for user + workspaces, err := database.GetWorkspacesByUserID(user.ID) + if err != nil { + t.Fatalf("failed to get workspaces: %v", err) + } + + // We should have at least as many workspaces as we just created + // (there might be more from previous tests) + if len(workspaces) < len(testWorkspaces) { + t.Errorf("got %d workspaces, want at least %d", len(workspaces), len(testWorkspaces)) + } + + // Verify each test workspace exists in the result + for _, expected := range testWorkspaces { + found := false + for _, w := range workspaces { + if w.Name == expected.Name { + found = true + if w.UserID != expected.UserID { + t.Errorf("UserID = %v, want %v", w.UserID, expected.UserID) + } + break + } + } + if !found { + t.Errorf("workspace %s not found in results", expected.Name) + } + } + }) + + t.Run("UpdateLastOpenedFile", func(t *testing.T) { + // Create a test workspace + workspace := &models.Workspace{ + UserID: user.ID, + Name: "Last File Workspace", + } + workspace.GetDefaultSettings() + if err := database.CreateWorkspace(workspace); err != nil { + t.Fatalf("failed to create test workspace: %v", err) + } + + testCases := []struct { + name string + filePath string + wantErr bool + }{ + { + name: "valid file path", + filePath: "docs/test.md", + wantErr: false, + }, + { + name: "empty file path", + filePath: "", + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := database.UpdateLastOpenedFile(workspace.ID, tc.filePath) + if tc.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Verify update + path, err := database.GetLastOpenedFile(workspace.ID) + if err != nil { + t.Fatalf("failed to get last opened file: %v", err) + } + + if path != tc.filePath { + t.Errorf("LastOpenedFile = %v, want %v", path, tc.filePath) + } + }) + } + }) + + t.Run("DeleteWorkspace", func(t *testing.T) { + // Create a test workspace + workspace := &models.Workspace{ + UserID: user.ID, + Name: "Delete Workspace", + } + workspace.GetDefaultSettings() + if err := database.CreateWorkspace(workspace); err != nil { + t.Fatalf("failed to create test workspace: %v", err) + } + + // Delete the workspace + if err := database.DeleteWorkspace(workspace.ID); err != nil { + t.Fatalf("failed to delete workspace: %v", err) + } + + // Verify workspace is gone + _, err = database.GetWorkspaceByID(workspace.ID) + if err != sql.ErrNoRows { + t.Errorf("expected sql.ErrNoRows, got %v", err) + } + }) +} + +// Helper function to verify workspace fields +func verifyWorkspace(t *testing.T, actual, expected *models.Workspace) { + t.Helper() + + if actual.Name != expected.Name { + t.Errorf("Name = %v, want %v", actual.Name, expected.Name) + } + if actual.UserID != expected.UserID { + t.Errorf("UserID = %v, want %v", actual.UserID, expected.UserID) + } + if actual.Theme != expected.Theme { + t.Errorf("Theme = %v, want %v", actual.Theme, expected.Theme) + } + if actual.AutoSave != expected.AutoSave { + t.Errorf("AutoSave = %v, want %v", actual.AutoSave, expected.AutoSave) + } + if actual.ShowHiddenFiles != expected.ShowHiddenFiles { + t.Errorf("ShowHiddenFiles = %v, want %v", actual.ShowHiddenFiles, expected.ShowHiddenFiles) + } + if actual.GitEnabled != expected.GitEnabled { + t.Errorf("GitEnabled = %v, want %v", actual.GitEnabled, expected.GitEnabled) + } + if actual.GitURL != expected.GitURL { + t.Errorf("GitURL = %v, want %v", actual.GitURL, expected.GitURL) + } + if actual.GitUser != expected.GitUser { + t.Errorf("GitUser = %v, want %v", actual.GitUser, expected.GitUser) + } + if actual.GitToken != expected.GitToken { + t.Errorf("GitToken = %v, want %v", actual.GitToken, expected.GitToken) + } + if actual.GitAutoCommit != expected.GitAutoCommit { + t.Errorf("GitAutoCommit = %v, want %v", actual.GitAutoCommit, expected.GitAutoCommit) + } + if actual.GitCommitMsgTemplate != expected.GitCommitMsgTemplate { + t.Errorf("GitCommitMsgTemplate = %v, want %v", actual.GitCommitMsgTemplate, expected.GitCommitMsgTemplate) + } + if actual.CreatedAt.IsZero() { + t.Error("CreatedAt should not be zero") + } +}