package databases import ( "encoding/json" "fmt" "net/http" "strconv" "strings" "testing" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" "databasus-backend/internal/config" "databasus-backend/internal/features/audit_logs" "databasus-backend/internal/features/databases/databases/mariadb" "databasus-backend/internal/features/databases/databases/mongodb" "databasus-backend/internal/features/databases/databases/postgresql" users_enums "databasus-backend/internal/features/users/enums" users_middleware "databasus-backend/internal/features/users/middleware" users_services "databasus-backend/internal/features/users/services" users_testing "databasus-backend/internal/features/users/testing" workspaces_controllers "databasus-backend/internal/features/workspaces/controllers" workspaces_testing "databasus-backend/internal/features/workspaces/testing" "databasus-backend/internal/util/encryption" test_utils "databasus-backend/internal/util/testing" "databasus-backend/internal/util/tools" ) func Test_CreateDatabase_PermissionsEnforced(t *testing.T) { tests := []struct { name string workspaceRole *users_enums.WorkspaceRole isGlobalAdmin bool expectSuccess bool expectedStatusCode int }{ { name: "workspace owner can create database", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleOwner; return &r }(), isGlobalAdmin: false, expectSuccess: true, expectedStatusCode: http.StatusCreated, }, { name: "workspace member can create database", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleMember; return &r }(), isGlobalAdmin: false, expectSuccess: true, expectedStatusCode: http.StatusCreated, }, { name: "workspace viewer cannot create database", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleViewer; return &r }(), isGlobalAdmin: false, expectSuccess: false, expectedStatusCode: http.StatusBadRequest, }, { name: "global admin can create database", workspaceRole: nil, isGlobalAdmin: true, expectSuccess: true, expectedStatusCode: http.StatusCreated, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) var testUserToken string if tt.isGlobalAdmin { admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin) testUserToken = admin.Token } else if tt.workspaceRole != nil && *tt.workspaceRole == users_enums.WorkspaceRoleOwner { testUserToken = owner.Token } else if tt.workspaceRole != nil { member := users_testing.CreateTestUser(users_enums.UserRoleMember) workspaces_testing.AddMemberToWorkspace( workspace, member, *tt.workspaceRole, owner.Token, router, ) testUserToken = member.Token } request := Database{ Name: "Test Database", WorkspaceID: &workspace.ID, Type: DatabaseTypePostgres, Postgresql: getTestPostgresConfig(), } var response Database testResp := test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/databases/create", "Bearer "+testUserToken, request, tt.expectedStatusCode, &response, ) if tt.expectSuccess { defer RemoveTestDatabase(&response) assert.Equal(t, "Test Database", response.Name) assert.NotEqual(t, uuid.Nil, response.ID) } else { assert.Contains(t, string(testResp.Body), "insufficient permissions") } }) } } func Test_CreateDatabase_WhenUserIsNotWorkspaceMember_ReturnsForbidden(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) nonMember := users_testing.CreateTestUser(users_enums.UserRoleMember) request := Database{ Name: "Test Database", WorkspaceID: &workspace.ID, Type: DatabaseTypePostgres, Postgresql: getTestPostgresConfig(), } testResp := test_utils.MakePostRequest( t, router, "/api/v1/databases/create", "Bearer "+nonMember.Token, request, http.StatusBadRequest, ) assert.Contains(t, string(testResp.Body), "insufficient permissions") } func Test_CreateDatabase_WalV1Type_NoConnectionFieldsRequired(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) request := Database{ Name: "Test WAL Database", WorkspaceID: &workspace.ID, Type: DatabaseTypePostgres, Postgresql: &postgresql.PostgresqlDatabase{ BackupType: postgresql.PostgresBackupTypeWalV1, CpuCount: 1, }, } var response Database test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/databases/create", "Bearer "+owner.Token, request, http.StatusCreated, &response, ) defer RemoveTestDatabase(&response) assert.Equal(t, "Test WAL Database", response.Name) assert.NotEqual(t, uuid.Nil, response.ID) } func Test_CreateDatabase_PgDumpType_ConnectionFieldsRequired(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) request := Database{ Name: "Test PG_DUMP Database", WorkspaceID: &workspace.ID, Type: DatabaseTypePostgres, Postgresql: &postgresql.PostgresqlDatabase{ BackupType: postgresql.PostgresBackupTypePgDump, CpuCount: 1, }, } testResp := test_utils.MakePostRequest( t, router, "/api/v1/databases/create", "Bearer "+owner.Token, request, http.StatusBadRequest, ) assert.Contains(t, string(testResp.Body), "host is required") } func Test_UpdateDatabase_PermissionsEnforced(t *testing.T) { tests := []struct { name string workspaceRole *users_enums.WorkspaceRole isGlobalAdmin bool expectSuccess bool expectedStatusCode int }{ { name: "workspace owner can update database", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleOwner; return &r }(), isGlobalAdmin: false, expectSuccess: true, expectedStatusCode: http.StatusOK, }, { name: "workspace member can update database", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleMember; return &r }(), isGlobalAdmin: false, expectSuccess: true, expectedStatusCode: http.StatusOK, }, { name: "workspace viewer cannot update database", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleViewer; return &r }(), isGlobalAdmin: false, expectSuccess: false, expectedStatusCode: http.StatusBadRequest, }, { name: "global admin can update database", workspaceRole: nil, isGlobalAdmin: true, expectSuccess: true, expectedStatusCode: http.StatusOK, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) database := createTestDatabaseViaAPI("Test Database", workspace.ID, owner.Token, router) defer RemoveTestDatabase(database) var testUserToken string if tt.isGlobalAdmin { admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin) testUserToken = admin.Token } else if tt.workspaceRole != nil && *tt.workspaceRole == users_enums.WorkspaceRoleOwner { testUserToken = owner.Token } else if tt.workspaceRole != nil { member := users_testing.CreateTestUser(users_enums.UserRoleMember) workspaces_testing.AddMemberToWorkspace( workspace, member, *tt.workspaceRole, owner.Token, router, ) testUserToken = member.Token } database.Name = "Updated Database" var response Database testResp := test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/databases/update", "Bearer "+testUserToken, database, tt.expectedStatusCode, &response, ) if tt.expectSuccess { assert.Equal(t, "Updated Database", response.Name) } else { assert.Contains(t, string(testResp.Body), "insufficient permissions") } }) } } func Test_UpdateDatabase_WhenUserIsNotWorkspaceMember_ReturnsForbidden(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) database := createTestDatabaseViaAPI("Test Database", workspace.ID, owner.Token, router) defer RemoveTestDatabase(database) nonMember := users_testing.CreateTestUser(users_enums.UserRoleMember) database.Name = "Hacked Name" testResp := test_utils.MakePostRequest( t, router, "/api/v1/databases/update", "Bearer "+nonMember.Token, database, http.StatusBadRequest, ) assert.Contains(t, string(testResp.Body), "insufficient permissions") } func Test_UpdateDatabase_WhenDatabaseTypeChanged_ReturnsBadRequest(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) database := createTestDatabaseViaAPI("Test Database", workspace.ID, owner.Token, router) defer RemoveTestDatabase(database) database.Type = DatabaseTypeMysql testResp := test_utils.MakePostRequest( t, router, "/api/v1/databases/update", "Bearer "+owner.Token, database, http.StatusBadRequest, ) assert.Contains(t, string(testResp.Body), "database type cannot be changed") } func Test_UpdateDatabase_WhenBackupTypeChanged_ReturnsBadRequest(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) database := createTestDatabaseViaAPI("Test Database", workspace.ID, owner.Token, router) defer RemoveTestDatabase(database) database.Postgresql.BackupType = postgresql.PostgresBackupTypeWalV1 testResp := test_utils.MakePostRequest( t, router, "/api/v1/databases/update", "Bearer "+owner.Token, database, http.StatusBadRequest, ) assert.Contains(t, string(testResp.Body), "backup type cannot be changed") } func Test_DeleteDatabase_PermissionsEnforced(t *testing.T) { tests := []struct { name string workspaceRole *users_enums.WorkspaceRole isGlobalAdmin bool expectSuccess bool expectedStatusCode int }{ { name: "workspace owner can delete database", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleOwner; return &r }(), isGlobalAdmin: false, expectSuccess: true, expectedStatusCode: http.StatusNoContent, }, { name: "workspace member can delete database", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleMember; return &r }(), isGlobalAdmin: false, expectSuccess: true, expectedStatusCode: http.StatusNoContent, }, { name: "workspace viewer cannot delete database", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleViewer; return &r }(), isGlobalAdmin: false, expectSuccess: false, expectedStatusCode: http.StatusInternalServerError, }, { name: "global admin can delete database", workspaceRole: nil, isGlobalAdmin: true, expectSuccess: true, expectedStatusCode: http.StatusNoContent, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) database := createTestDatabaseViaAPI("Test Database", workspace.ID, owner.Token, router) var testUserToken string if tt.isGlobalAdmin { admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin) testUserToken = admin.Token } else if tt.workspaceRole != nil && *tt.workspaceRole == users_enums.WorkspaceRoleOwner { testUserToken = owner.Token } else if tt.workspaceRole != nil { member := users_testing.CreateTestUser(users_enums.UserRoleMember) workspaces_testing.AddMemberToWorkspace( workspace, member, *tt.workspaceRole, owner.Token, router, ) testUserToken = member.Token } testResp := test_utils.MakeDeleteRequest( t, router, "/api/v1/databases/"+database.ID.String(), "Bearer "+testUserToken, tt.expectedStatusCode, ) if !tt.expectSuccess { defer RemoveTestDatabase(database) assert.Contains(t, string(testResp.Body), "insufficient permissions") } }) } } func Test_GetDatabase_PermissionsEnforced(t *testing.T) { memberRole := users_enums.WorkspaceRoleViewer tests := []struct { name string userRole *users_enums.WorkspaceRole isGlobalAdmin bool expectSuccess bool expectedStatusCode int }{ { name: "workspace member can get database", userRole: &memberRole, isGlobalAdmin: false, expectSuccess: true, expectedStatusCode: http.StatusOK, }, { name: "non-member cannot get database", userRole: nil, isGlobalAdmin: false, expectSuccess: false, expectedStatusCode: http.StatusBadRequest, }, { name: "global admin can get database", userRole: nil, isGlobalAdmin: true, expectSuccess: true, expectedStatusCode: http.StatusOK, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) database := createTestDatabaseViaAPI("Test Database", workspace.ID, owner.Token, router) defer RemoveTestDatabase(database) var testUser string if tt.isGlobalAdmin { admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin) testUser = admin.Token } else if tt.userRole != nil { member := users_testing.CreateTestUser(users_enums.UserRoleMember) workspaces_testing.AddMemberToWorkspace( workspace, member, *tt.userRole, owner.Token, router, ) testUser = member.Token } else { nonMember := users_testing.CreateTestUser(users_enums.UserRoleMember) testUser = nonMember.Token } var response Database testResp := test_utils.MakeGetRequestAndUnmarshal( t, router, "/api/v1/databases/"+database.ID.String(), "Bearer "+testUser, tt.expectedStatusCode, &response, ) if tt.expectSuccess { assert.Equal(t, database.ID, response.ID) assert.Equal(t, "Test Database", response.Name) } else { assert.Contains(t, string(testResp.Body), "insufficient permissions") } }) } } func Test_GetDatabasesByWorkspace_PermissionsEnforced(t *testing.T) { tests := []struct { name string isMember bool isGlobalAdmin bool expectSuccess bool expectedStatusCode int }{ { name: "workspace member can list databases", isMember: true, isGlobalAdmin: false, expectSuccess: true, expectedStatusCode: http.StatusOK, }, { name: "non-member cannot list databases", isMember: false, isGlobalAdmin: false, expectSuccess: false, expectedStatusCode: http.StatusBadRequest, }, { name: "global admin can list databases", isMember: false, isGlobalAdmin: true, expectSuccess: true, expectedStatusCode: http.StatusOK, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) db1 := createTestDatabaseViaAPI("Database 1", workspace.ID, owner.Token, router) defer RemoveTestDatabase(db1) db2 := createTestDatabaseViaAPI("Database 2", workspace.ID, owner.Token, router) defer RemoveTestDatabase(db2) var testUser string if tt.isGlobalAdmin { admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin) testUser = admin.Token } else if tt.isMember { testUser = owner.Token } else { nonMember := users_testing.CreateTestUser(users_enums.UserRoleMember) testUser = nonMember.Token } if tt.expectSuccess { var response []Database test_utils.MakeGetRequestAndUnmarshal( t, router, "/api/v1/databases?workspace_id="+workspace.ID.String(), "Bearer "+testUser, tt.expectedStatusCode, &response, ) assert.GreaterOrEqual(t, len(response), 2) } else { testResp := test_utils.MakeGetRequest( t, router, "/api/v1/databases?workspace_id="+workspace.ID.String(), "Bearer "+testUser, tt.expectedStatusCode, ) assert.Contains(t, string(testResp.Body), "insufficient permissions") } }) } } func Test_GetDatabasesByWorkspace_WhenMultipleDatabasesExist_ReturnsCorrectCount(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) db1 := createTestDatabaseViaAPI("Database 1", workspace.ID, owner.Token, router) defer RemoveTestDatabase(db1) db2 := createTestDatabaseViaAPI("Database 2", workspace.ID, owner.Token, router) defer RemoveTestDatabase(db2) db3 := createTestDatabaseViaAPI("Database 3", workspace.ID, owner.Token, router) defer RemoveTestDatabase(db3) var response []Database test_utils.MakeGetRequestAndUnmarshal( t, router, "/api/v1/databases?workspace_id="+workspace.ID.String(), "Bearer "+owner.Token, http.StatusOK, &response, ) assert.Equal(t, 3, len(response)) } func Test_GetDatabasesByWorkspace_EnsuresCrossWorkspaceIsolation(t *testing.T) { router := createTestRouter() owner1 := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace1 := workspaces_testing.CreateTestWorkspace("Workspace 1", owner1, router) defer workspaces_testing.RemoveTestWorkspace(workspace1, router) owner2 := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace2 := workspaces_testing.CreateTestWorkspace("Workspace 2", owner2, router) defer workspaces_testing.RemoveTestWorkspace(workspace2, router) workspace1Db1 := createTestDatabaseViaAPI("Workspace1 DB1", workspace1.ID, owner1.Token, router) defer RemoveTestDatabase(workspace1Db1) workspace1Db2 := createTestDatabaseViaAPI("Workspace1 DB2", workspace1.ID, owner1.Token, router) defer RemoveTestDatabase(workspace1Db2) workspace2Db1 := createTestDatabaseViaAPI("Workspace2 DB1", workspace2.ID, owner2.Token, router) defer RemoveTestDatabase(workspace2Db1) var workspace1Dbs []Database test_utils.MakeGetRequestAndUnmarshal( t, router, "/api/v1/databases?workspace_id="+workspace1.ID.String(), "Bearer "+owner1.Token, http.StatusOK, &workspace1Dbs, ) var workspace2Dbs []Database test_utils.MakeGetRequestAndUnmarshal( t, router, "/api/v1/databases?workspace_id="+workspace2.ID.String(), "Bearer "+owner2.Token, http.StatusOK, &workspace2Dbs, ) assert.Equal(t, 2, len(workspace1Dbs)) assert.Equal(t, 1, len(workspace2Dbs)) for _, db := range workspace1Dbs { assert.Equal(t, workspace1.ID, *db.WorkspaceID) } for _, db := range workspace2Dbs { assert.Equal(t, workspace2.ID, *db.WorkspaceID) } } func Test_CopyDatabase_PermissionsEnforced(t *testing.T) { tests := []struct { name string workspaceRole *users_enums.WorkspaceRole isGlobalAdmin bool expectSuccess bool expectedStatusCode int }{ { name: "workspace owner can copy database", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleOwner; return &r }(), isGlobalAdmin: false, expectSuccess: true, expectedStatusCode: http.StatusCreated, }, { name: "workspace member can copy database", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleMember; return &r }(), isGlobalAdmin: false, expectSuccess: true, expectedStatusCode: http.StatusCreated, }, { name: "workspace viewer cannot copy database", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleViewer; return &r }(), isGlobalAdmin: false, expectSuccess: false, expectedStatusCode: http.StatusBadRequest, }, { name: "global admin can copy database", workspaceRole: nil, isGlobalAdmin: true, expectSuccess: true, expectedStatusCode: http.StatusCreated, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) database := createTestDatabaseViaAPI("Test Database", workspace.ID, owner.Token, router) defer RemoveTestDatabase(database) var testUserToken string if tt.isGlobalAdmin { admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin) testUserToken = admin.Token } else if tt.workspaceRole != nil && *tt.workspaceRole == users_enums.WorkspaceRoleOwner { testUserToken = owner.Token } else if tt.workspaceRole != nil { member := users_testing.CreateTestUser(users_enums.UserRoleMember) workspaces_testing.AddMemberToWorkspace( workspace, member, *tt.workspaceRole, owner.Token, router, ) testUserToken = member.Token } var response Database testResp := test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/databases/"+database.ID.String()+"/copy", "Bearer "+testUserToken, nil, tt.expectedStatusCode, &response, ) if tt.expectSuccess { defer RemoveTestDatabase(&response) assert.NotEqual(t, database.ID, response.ID) assert.Contains(t, response.Name, "(Copy)") } else { assert.Contains(t, string(testResp.Body), "insufficient permissions") } }) } } func Test_CopyDatabase_CopyStaysInSameWorkspace(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) database := createTestDatabaseViaAPI("Test Database", workspace.ID, owner.Token, router) defer RemoveTestDatabase(database) var response Database test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/databases/"+database.ID.String()+"/copy", "Bearer "+owner.Token, nil, http.StatusCreated, &response, ) defer RemoveTestDatabase(&response) assert.NotEqual(t, database.ID, response.ID) assert.Equal(t, "Test Database (Copy)", response.Name) assert.Equal(t, workspace.ID, *response.WorkspaceID) assert.Equal(t, database.Type, response.Type) } func Test_CreateDatabase_PasswordIsEncryptedInDB(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) pgConfig := getTestPostgresConfig() plainPassword := "testpassword" pgConfig.Password = plainPassword request := Database{ Name: "Test Database", WorkspaceID: &workspace.ID, Type: DatabaseTypePostgres, Postgresql: pgConfig, } var createdDatabase Database test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/databases/create", "Bearer "+owner.Token, request, http.StatusCreated, &createdDatabase, ) repository := &DatabaseRepository{} databaseFromDB, err := repository.FindByID(createdDatabase.ID) assert.NoError(t, err) assert.NotNil(t, databaseFromDB) assert.NotNil(t, databaseFromDB.Postgresql) assert.True( t, strings.HasPrefix(databaseFromDB.Postgresql.Password, "enc:"), "Password should be encrypted in database with 'enc:' prefix, got: %s", databaseFromDB.Postgresql.Password, ) encryptor := encryption.GetFieldEncryptor() decryptedPassword, err := encryptor.Decrypt( databaseFromDB.ID, databaseFromDB.Postgresql.Password, ) assert.NoError(t, err) assert.Equal(t, plainPassword, decryptedPassword, "Decrypted password should match original plaintext password") test_utils.MakeDeleteRequest( t, router, "/api/v1/databases/"+createdDatabase.ID.String(), "Bearer "+owner.Token, http.StatusNoContent, ) workspaces_testing.RemoveTestWorkspace(workspace, router) } func Test_DatabaseSensitiveDataLifecycle_AllTypes(t *testing.T) { testCases := []struct { name string databaseType DatabaseType createDatabase func(workspaceID uuid.UUID) *Database updateDatabase func(workspaceID, databaseID uuid.UUID) *Database verifySensitiveData func(t *testing.T, database *Database) verifyHiddenData func(t *testing.T, database *Database) }{ { name: "PostgreSQL Database", databaseType: DatabaseTypePostgres, createDatabase: func(workspaceID uuid.UUID) *Database { pgConfig := getTestPostgresConfig() return &Database{ WorkspaceID: &workspaceID, Name: "Test PostgreSQL Database", Type: DatabaseTypePostgres, Postgresql: pgConfig, } }, updateDatabase: func(workspaceID, databaseID uuid.UUID) *Database { pgConfig := getTestPostgresConfig() pgConfig.Password = "" return &Database{ ID: databaseID, WorkspaceID: &workspaceID, Name: "Updated PostgreSQL Database", Type: DatabaseTypePostgres, Postgresql: pgConfig, } }, verifySensitiveData: func(t *testing.T, database *Database) { assert.True(t, strings.HasPrefix(database.Postgresql.Password, "enc:"), "Password should be encrypted in database") encryptor := encryption.GetFieldEncryptor() decrypted, err := encryptor.Decrypt(database.ID, database.Postgresql.Password) assert.NoError(t, err) assert.Equal(t, "testpassword", decrypted) }, verifyHiddenData: func(t *testing.T, database *Database) { assert.Equal(t, "", database.Postgresql.Password) }, }, { name: "MariaDB Database", databaseType: DatabaseTypeMariadb, createDatabase: func(workspaceID uuid.UUID) *Database { mariaConfig := getTestMariadbConfig() return &Database{ WorkspaceID: &workspaceID, Name: "Test MariaDB Database", Type: DatabaseTypeMariadb, Mariadb: mariaConfig, } }, updateDatabase: func(workspaceID, databaseID uuid.UUID) *Database { mariaConfig := getTestMariadbConfig() mariaConfig.Password = "" return &Database{ ID: databaseID, WorkspaceID: &workspaceID, Name: "Updated MariaDB Database", Type: DatabaseTypeMariadb, Mariadb: mariaConfig, } }, verifySensitiveData: func(t *testing.T, database *Database) { assert.True(t, strings.HasPrefix(database.Mariadb.Password, "enc:"), "Password should be encrypted in database") encryptor := encryption.GetFieldEncryptor() decrypted, err := encryptor.Decrypt(database.ID, database.Mariadb.Password) assert.NoError(t, err) assert.Equal(t, "testpassword", decrypted) }, verifyHiddenData: func(t *testing.T, database *Database) { assert.Equal(t, "", database.Mariadb.Password) }, }, { name: "MongoDB Database", databaseType: DatabaseTypeMongodb, createDatabase: func(workspaceID uuid.UUID) *Database { mongoConfig := getTestMongodbConfig() return &Database{ WorkspaceID: &workspaceID, Name: "Test MongoDB Database", Type: DatabaseTypeMongodb, Mongodb: mongoConfig, } }, updateDatabase: func(workspaceID, databaseID uuid.UUID) *Database { mongoConfig := getTestMongodbConfig() mongoConfig.Password = "" return &Database{ ID: databaseID, WorkspaceID: &workspaceID, Name: "Updated MongoDB Database", Type: DatabaseTypeMongodb, Mongodb: mongoConfig, } }, verifySensitiveData: func(t *testing.T, database *Database) { assert.True(t, strings.HasPrefix(database.Mongodb.Password, "enc:"), "Password should be encrypted in database") encryptor := encryption.GetFieldEncryptor() decrypted, err := encryptor.Decrypt(database.ID, database.Mongodb.Password) assert.NoError(t, err) assert.Equal(t, "rootpassword", decrypted) }, verifyHiddenData: func(t *testing.T, database *Database) { assert.Equal(t, "", database.Mongodb.Password) }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) // Phase 1: Create database with sensitive data initialDatabase := tc.createDatabase(workspace.ID) var createdDatabase Database test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/databases/create", "Bearer "+owner.Token, *initialDatabase, http.StatusCreated, &createdDatabase, ) assert.NotEmpty(t, createdDatabase.ID) assert.Equal(t, initialDatabase.Name, createdDatabase.Name) // Phase 2: Read via service - sensitive data should be hidden var retrievedDatabase Database test_utils.MakeGetRequestAndUnmarshal( t, router, fmt.Sprintf("/api/v1/databases/%s", createdDatabase.ID.String()), "Bearer "+owner.Token, http.StatusOK, &retrievedDatabase, ) tc.verifyHiddenData(t, &retrievedDatabase) assert.Equal(t, initialDatabase.Name, retrievedDatabase.Name) // Phase 3: Update with non-sensitive changes only (sensitive fields empty) updatedDatabase := tc.updateDatabase(workspace.ID, createdDatabase.ID) var updateResponse Database test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/databases/update", "Bearer "+owner.Token, *updatedDatabase, http.StatusOK, &updateResponse, ) // Phase 4: Retrieve directly from repository to verify sensitive data preservation repository := &DatabaseRepository{} databaseFromDB, err := repository.FindByID(createdDatabase.ID) assert.NoError(t, err) // Verify original sensitive data is still present in DB tc.verifySensitiveData(t, databaseFromDB) // Verify non-sensitive fields were updated in DB assert.Equal(t, updatedDatabase.Name, databaseFromDB.Name) // Phase 5: Additional verification - Check via GET that data is still hidden var finalRetrieved Database test_utils.MakeGetRequestAndUnmarshal( t, router, fmt.Sprintf("/api/v1/databases/%s", createdDatabase.ID.String()), "Bearer "+owner.Token, http.StatusOK, &finalRetrieved, ) tc.verifyHiddenData(t, &finalRetrieved) // Phase 6: Verify GetDatabasesByWorkspace also hides sensitive data var workspaceDatabases []Database test_utils.MakeGetRequestAndUnmarshal( t, router, fmt.Sprintf("/api/v1/databases?workspace_id=%s", workspace.ID.String()), "Bearer "+owner.Token, http.StatusOK, &workspaceDatabases, ) var foundDatabase *Database for i := range workspaceDatabases { if workspaceDatabases[i].ID == createdDatabase.ID { foundDatabase = &workspaceDatabases[i] break } } assert.NotNil(t, foundDatabase, "Database should be found in workspace databases list") tc.verifyHiddenData(t, foundDatabase) // Clean up: Delete database before removing workspace test_utils.MakeDeleteRequest( t, router, fmt.Sprintf("/api/v1/databases/%s", createdDatabase.ID.String()), "Bearer "+owner.Token, http.StatusNoContent, ) workspaces_testing.RemoveTestWorkspace(workspace, router) }) } } func Test_TestConnection_PermissionsEnforced(t *testing.T) { tests := []struct { name string isMember bool isGlobalAdmin bool expectAccessGranted bool expectedStatusCodeOnErr int }{ { name: "workspace member can test connection", isMember: true, isGlobalAdmin: false, expectAccessGranted: true, expectedStatusCodeOnErr: http.StatusBadRequest, }, { name: "non-member cannot test connection", isMember: false, isGlobalAdmin: false, expectAccessGranted: false, expectedStatusCodeOnErr: http.StatusBadRequest, }, { name: "global admin can test connection", isMember: false, isGlobalAdmin: true, expectAccessGranted: true, expectedStatusCodeOnErr: http.StatusBadRequest, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) database := createTestDatabaseViaAPI("Test Database", workspace.ID, owner.Token, router) defer RemoveTestDatabase(database) var testUser string if tt.isGlobalAdmin { admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin) testUser = admin.Token } else if tt.isMember { testUser = owner.Token } else { nonMember := users_testing.CreateTestUser(users_enums.UserRoleMember) testUser = nonMember.Token } w := workspaces_testing.MakeAPIRequest( router, "POST", "/api/v1/databases/"+database.ID.String()+"/test-connection", "Bearer "+testUser, nil, ) body := w.Body.String() if tt.expectAccessGranted { assert.True( t, w.Code == http.StatusOK || (w.Code == http.StatusBadRequest && strings.Contains(body, "connect")), "Expected 200 OK or 400 with connection error, got %d: %s", w.Code, body, ) } else { assert.Equal(t, tt.expectedStatusCodeOnErr, w.Code) assert.Contains(t, body, "insufficient permissions") } }) } } func Test_RegenerateAgentToken_ReturnsToken(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) database := createTestDatabaseViaAPI("Test Database", workspace.ID, owner.Token, router) defer RemoveTestDatabase(database) var response map[string]string test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/databases/"+database.ID.String()+"/regenerate-token", "Bearer "+owner.Token, nil, http.StatusOK, &response, ) assert.NotEmpty(t, response["token"]) assert.Len(t, response["token"], 32) var updatedDatabase Database test_utils.MakeGetRequestAndUnmarshal( t, router, "/api/v1/databases/"+database.ID.String(), "Bearer "+owner.Token, http.StatusOK, &updatedDatabase, ) assert.True(t, updatedDatabase.IsAgentTokenGenerated) } func Test_VerifyAgentToken_WithValidToken_Succeeds(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) database := createTestDatabaseViaAPI("Test Database", workspace.ID, owner.Token, router) defer RemoveTestDatabase(database) var regenerateResponse map[string]string test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/databases/"+database.ID.String()+"/regenerate-token", "Bearer "+owner.Token, nil, http.StatusOK, ®enerateResponse, ) token := regenerateResponse["token"] assert.NotEmpty(t, token) w := workspaces_testing.MakeAPIRequest( router, "POST", "/api/v1/databases/verify-token", "", VerifyAgentTokenRequest{Token: token}, ) assert.Equal(t, http.StatusOK, w.Code) } func Test_VerifyAgentToken_WithInvalidToken_Returns401(t *testing.T) { router := createTestRouter() w := workspaces_testing.MakeAPIRequest( router, "POST", "/api/v1/databases/verify-token", "", VerifyAgentTokenRequest{Token: "invalidtoken00000000000000000000"}, ) assert.Equal(t, http.StatusUnauthorized, w.Code) } func createTestDatabaseViaAPI( name string, workspaceID uuid.UUID, token string, router *gin.Engine, ) *Database { env := config.GetEnv() port, err := strconv.Atoi(env.TestPostgres16Port) if err != nil { panic(fmt.Sprintf("Failed to parse TEST_POSTGRES_16_PORT: %v", err)) } testDbName := "testdb" request := Database{ Name: name, WorkspaceID: &workspaceID, Type: DatabaseTypePostgres, Postgresql: &postgresql.PostgresqlDatabase{ Version: tools.PostgresqlVersion16, Host: config.GetEnv().TestLocalhost, Port: port, Username: "testuser", Password: "testpassword", Database: &testDbName, CpuCount: 1, }, } w := workspaces_testing.MakeAPIRequest( router, "POST", "/api/v1/databases/create", "Bearer "+token, request, ) if w.Code != http.StatusCreated { panic( fmt.Sprintf("Failed to create database. Status: %d, Body: %s", w.Code, w.Body.String()), ) } var database Database if err := json.Unmarshal(w.Body.Bytes(), &database); err != nil { panic(err) } return &database } func createTestRouter() *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() v1 := router.Group("/api/v1") protected := v1.Group("").Use(users_middleware.AuthMiddleware(users_services.GetUserService())) workspaces_controllers.GetWorkspaceController().RegisterRoutes(protected.(*gin.RouterGroup)) workspaces_controllers.GetMembershipController().RegisterRoutes(protected.(*gin.RouterGroup)) GetDatabaseController().RegisterRoutes(protected.(*gin.RouterGroup)) GetDatabaseController().RegisterPublicRoutes(v1) audit_logs.SetupDependencies() return router } func getTestPostgresConfig() *postgresql.PostgresqlDatabase { env := config.GetEnv() port, err := strconv.Atoi(env.TestPostgres16Port) if err != nil { panic(fmt.Sprintf("Failed to parse TEST_POSTGRES_16_PORT: %v", err)) } testDbName := "testdb" return &postgresql.PostgresqlDatabase{ BackupType: postgresql.PostgresBackupTypePgDump, Version: tools.PostgresqlVersion16, Host: config.GetEnv().TestLocalhost, Port: port, Username: "testuser", Password: "testpassword", Database: &testDbName, CpuCount: 1, } } func Test_CreateDatabase_WhenCloudAndUserIsNotReadOnly_ReturnsBadRequest(t *testing.T) { enableCloud(t) router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Cloud Not ReadOnly", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) request := Database{ Name: "Cloud Non-ReadOnly DB", WorkspaceID: &workspace.ID, Type: DatabaseTypePostgres, Postgresql: getTestPostgresConfig(), } resp := test_utils.MakePostRequest( t, router, "/api/v1/databases/create", "Bearer "+owner.Token, request, http.StatusBadRequest, ) assert.Contains(t, string(resp.Body), "in cloud mode, only read-only database users are allowed") } func Test_CreateDatabase_WhenCloudAndUserIsReadOnly_DatabaseCreated(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Cloud ReadOnly", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) database := createTestDatabaseViaAPI("Temp DB for RO User", workspace.ID, owner.Token, router) readOnlyUser := createReadOnlyUserViaAPI(t, router, database.ID, owner.Token) assert.NotEmpty(t, readOnlyUser.Username) assert.NotEmpty(t, readOnlyUser.Password) RemoveTestDatabase(database) enableCloud(t) pgConfig := getTestPostgresConfig() pgConfig.Username = readOnlyUser.Username pgConfig.Password = readOnlyUser.Password request := Database{ Name: "Cloud ReadOnly DB", WorkspaceID: &workspace.ID, Type: DatabaseTypePostgres, Postgresql: pgConfig, } var response Database test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/databases/create", "Bearer "+owner.Token, request, http.StatusCreated, &response, ) defer RemoveTestDatabase(&response) assert.Equal(t, "Cloud ReadOnly DB", response.Name) assert.NotEqual(t, uuid.Nil, response.ID) } func Test_CreateDatabase_WhenNotCloudAndUserIsNotReadOnly_DatabaseCreated(t *testing.T) { router := createTestRouter() owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Non-Cloud", owner, router) defer workspaces_testing.RemoveTestWorkspace(workspace, router) request := Database{ Name: "Non-Cloud DB", WorkspaceID: &workspace.ID, Type: DatabaseTypePostgres, Postgresql: getTestPostgresConfig(), } var response Database test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/databases/create", "Bearer "+owner.Token, request, http.StatusCreated, &response, ) defer RemoveTestDatabase(&response) assert.Equal(t, "Non-Cloud DB", response.Name) assert.NotEqual(t, uuid.Nil, response.ID) } func enableCloud(t *testing.T) { t.Helper() config.GetEnv().IsCloud = true t.Cleanup(func() { config.GetEnv().IsCloud = false }) } func createReadOnlyUserViaAPI( t *testing.T, router *gin.Engine, databaseID uuid.UUID, token string, ) *CreateReadOnlyUserResponse { var database Database test_utils.MakeGetRequestAndUnmarshal( t, router, fmt.Sprintf("/api/v1/databases/%s", databaseID.String()), "Bearer "+token, http.StatusOK, &database, ) var response CreateReadOnlyUserResponse test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/databases/create-readonly-user", "Bearer "+token, database, http.StatusOK, &response, ) return &response } func getTestMariadbConfig() *mariadb.MariadbDatabase { env := config.GetEnv() portStr := env.TestMariadb1011Port if portStr == "" { portStr = "33111" } port, err := strconv.Atoi(portStr) if err != nil { panic(fmt.Sprintf("Failed to parse TEST_MARIADB_1011_PORT: %v", err)) } testDbName := "testdb" return &mariadb.MariadbDatabase{ Version: tools.MariadbVersion1011, Host: config.GetEnv().TestLocalhost, Port: port, Username: "testuser", Password: "testpassword", Database: &testDbName, } } func getTestMongodbConfig() *mongodb.MongodbDatabase { env := config.GetEnv() portStr := env.TestMongodb70Port if portStr == "" { portStr = "27070" } port, err := strconv.Atoi(portStr) if err != nil { panic(fmt.Sprintf("Failed to parse TEST_MONGODB_70_PORT: %v", err)) } return &mongodb.MongodbDatabase{ Version: tools.MongodbVersion7, Host: config.GetEnv().TestLocalhost, Port: &port, Username: "root", Password: "rootpassword", Database: "testdb", AuthDatabase: "admin", IsHttps: false, IsSrv: false, CpuCount: 1, } }