Files
wehub-resource-sync 75f3dd141c
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CI and Release / lint-frontend (push) Has been cancelled
CI and Release / dockerfile-scan (push) Has been cancelled
CI and Release / test-frontend (push) Has been cancelled
CI and Release / lint-verification-agent (push) Has been cancelled
CI and Release / test-verification-agent (push) Has been cancelled
CI and Release / e2e-verification-agent (push) Has been cancelled
CI and Release / test-backend (push) Has been cancelled
CI and Release / build-dev-image (push) Has been cancelled
CI and Release / push-dev-image (push) Has been cancelled
CI and Release / build-image (push) Has been cancelled
CI and Release / build-verification-image (push) Has been cancelled
CI and Release / determine-version (push) Has been cancelled
CI and Release / push-image (push) Has been cancelled
CI and Release / push-verification-image (12) (push) Has been cancelled
CI and Release / lint-backend (push) Has been cancelled
CI and Release / push-verification-image (17) (push) Has been cancelled
CI and Release / push-verification-image (18) (push) Has been cancelled
CI and Release / release (push) Has been cancelled
CI and Release / publish-helm-chart (push) Has been cancelled
CI and Release / push-verification-image (13) (push) Has been cancelled
CI and Release / push-verification-image (14) (push) Has been cancelled
CI and Release / push-verification-image (15) (push) Has been cancelled
CI and Release / push-verification-image (16) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:11:07 +08:00

1424 lines
44 KiB
Go

package databases
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"databasus-backend/internal/config"
"databasus-backend/internal/features/audit_logs"
physical_testing "databasus-backend/internal/features/backups/backups/core/physical/testing"
"databasus-backend/internal/features/databases/databases/mariadb"
"databasus-backend/internal/features/databases/databases/mongodb"
postgresql_logical "databasus-backend/internal/features/databases/databases/postgresql/logical"
postgresql_physical "databasus-backend/internal/features/databases/databases/postgresql/physical"
postgresql_shared "databasus-backend/internal/features/databases/databases/postgresql/shared"
"databasus-backend/internal/features/notifiers"
"databasus-backend/internal/features/storages"
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/testing/containers"
"databasus-backend/internal/util/tools"
"databasus-backend/internal/util/walmath"
)
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: DatabaseTypePostgresLogical,
PostgresqlLogical: 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: DatabaseTypePostgresLogical,
PostgresqlLogical: 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_WithoutConnectionFields_ValidationFails(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 Database",
WorkspaceID: &workspace.ID,
Type: DatabaseTypePostgresLogical,
PostgresqlLogical: &postgresql_logical.PostgresqlLogicalDatabase{
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_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_WithPhysicalBackups_ReturnsNewestBackupTimeAcrossTypes(t *testing.T) {
const walSegmentBytes = 16 * 1024 * 1024
router := createTestRouter()
owner := users_testing.CreateTestUser(users_enums.UserRoleMember)
workspace := workspaces_testing.CreateTestWorkspace("Phys Last Backup "+uuid.NewString(), owner, router)
defer workspaces_testing.RemoveTestWorkspace(workspace, router)
storage := storages.CreateTestStorage(workspace.ID)
defer storages.RemoveTestStorage(storage.ID)
notifier := notifiers.CreateTestNotifier(workspace.ID)
defer notifiers.RemoveTestNotifier(notifier)
backedUp := CreateTestPhysicalPostgresDatabase(workspace.ID, notifier, "17")
defer func() {
physical_testing.DeleteAllPhysicalCatalogForDatabase(t, backedUp.ID)
RemoveTestDatabase(backedUp)
}()
noBackups := CreateTestPhysicalPostgresDatabase(workspace.ID, notifier, "17")
defer RemoveTestDatabase(noBackups)
base := time.Now().UTC().Add(-time.Hour)
fullBackup := physical_testing.NewTestCompletedFullBackup(
backedUp.ID, storage.ID, 1, walmath.LSN(0), walmath.LSN(walSegmentBytes))
fullBackup.CreatedAt = base
fullBackup.CompletedAt = new(base)
physical_testing.CreateTestFullBackup(t, fullBackup)
incrementalBackup := physical_testing.NewTestCompletedIncrementalBackup(
backedUp.ID, storage.ID, fullBackup.ID, nil, 1, walmath.LSN(walSegmentBytes), walmath.LSN(2*walSegmentBytes))
incrementalBackup.CreatedAt = base.Add(10 * time.Minute)
physical_testing.CreateTestIncrementalBackup(t, incrementalBackup)
// Newest of the three - the value the card must show.
walSegment := physical_testing.NewTestWalSegment(
backedUp.ID, storage.ID, 1, "000000010000000000000001",
walmath.LSN(2*walSegmentBytes), walmath.LSN(3*walSegmentBytes))
walSegment.ReceivedAt = base.Add(20 * time.Minute)
physical_testing.CreateTestWalSegment(t, walSegment)
var response []Database
test_utils.MakeGetRequestAndUnmarshal(
t,
router,
"/api/v1/databases?workspace_id="+workspace.ID.String(),
"Bearer "+owner.Token,
http.StatusOK,
&response,
)
backedUpListed := findDatabaseByID(t, response, backedUp.ID)
require.NotNil(t, backedUpListed.LastBackupTime, "physical database with backups must expose a last backup time")
assert.WithinDuration(t, walSegment.ReceivedAt, *backedUpListed.LastBackupTime, time.Second)
noBackupsListed := findDatabaseByID(t, response, noBackups.ID)
assert.Nil(t, noBackupsListed.LastBackupTime, "physical database without backups must have no last backup time")
}
func findDatabaseByID(t *testing.T, databases []Database, databaseID uuid.UUID) Database {
t.Helper()
for _, database := range databases {
if database.ID == databaseID {
return database
}
}
t.Fatalf("database %s not found in response", databaseID)
return Database{}
}
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: DatabaseTypePostgresLogical,
PostgresqlLogical: 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.PostgresqlLogical)
assert.True(
t,
strings.HasPrefix(databaseFromDB.PostgresqlLogical.Password, "enc:"),
"Password should be encrypted in database with 'enc:' prefix, got: %s",
databaseFromDB.PostgresqlLogical.Password,
)
encryptor := encryption.GetFieldEncryptor()
decryptedPassword, err := encryptor.Decrypt(databaseFromDB.PostgresqlLogical.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(t *testing.T, workspaceID uuid.UUID) *Database
updateDatabase func(t *testing.T, workspaceID, databaseID uuid.UUID) *Database
verifySensitiveData func(t *testing.T, database *Database)
verifyHiddenData func(t *testing.T, database *Database)
}{
{
name: "PostgreSQL Database",
databaseType: DatabaseTypePostgresLogical,
createDatabase: func(_ *testing.T, workspaceID uuid.UUID) *Database {
pgConfig := getTestPostgresConfig()
return &Database{
WorkspaceID: &workspaceID,
Name: "Test PostgreSQL Database",
Type: DatabaseTypePostgresLogical,
PostgresqlLogical: pgConfig,
}
},
updateDatabase: func(_ *testing.T, workspaceID, databaseID uuid.UUID) *Database {
pgConfig := getTestPostgresConfig()
pgConfig.Password = ""
return &Database{
ID: databaseID,
WorkspaceID: &workspaceID,
Name: "Updated PostgreSQL Database",
Type: DatabaseTypePostgresLogical,
PostgresqlLogical: pgConfig,
}
},
verifySensitiveData: func(t *testing.T, database *Database) {
assert.True(t, strings.HasPrefix(database.PostgresqlLogical.Password, "enc:"),
"Password should be encrypted in database")
encryptor := encryption.GetFieldEncryptor()
decrypted, err := encryptor.Decrypt(database.PostgresqlLogical.Password)
assert.NoError(t, err)
assert.Equal(t, "testpassword", decrypted)
},
verifyHiddenData: func(t *testing.T, database *Database) {
assert.Equal(t, "", database.PostgresqlLogical.Password)
},
},
{
name: "MariaDB Database",
databaseType: DatabaseTypeMariadb,
createDatabase: func(t *testing.T, workspaceID uuid.UUID) *Database {
mariaConfig := getTestMariadbConfig(t)
return &Database{
WorkspaceID: &workspaceID,
Name: "Test MariaDB Database",
Type: DatabaseTypeMariadb,
Mariadb: mariaConfig,
}
},
updateDatabase: func(t *testing.T, workspaceID, databaseID uuid.UUID) *Database {
mariaConfig := getTestMariadbConfig(t)
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.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(t *testing.T, workspaceID uuid.UUID) *Database {
mongoConfig := getTestMongodbConfig(t)
return &Database{
WorkspaceID: &workspaceID,
Name: "Test MongoDB Database",
Type: DatabaseTypeMongodb,
Mongodb: mongoConfig,
}
},
updateDatabase: func(t *testing.T, workspaceID, databaseID uuid.UUID) *Database {
mongoConfig := getTestMongodbConfig(t)
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.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(t, 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(t, 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 createTestDatabaseViaAPI(
name string,
workspaceID uuid.UUID,
token string,
router *gin.Engine,
) *Database {
env := config.GetEnv()
port, err := strconv.Atoi(env.TestLogicalPostgres16Port)
if err != nil {
panic(fmt.Sprintf("Failed to parse TEST_LOGICAL_POSTGRES_16_PORT: %v", err))
}
testDbName := "testdb"
request := Database{
Name: name,
WorkspaceID: &workspaceID,
Type: DatabaseTypePostgresLogical,
PostgresqlLogical: &postgresql_logical.PostgresqlLogicalDatabase{
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_logical.PostgresqlLogicalDatabase {
env := config.GetEnv()
port, err := strconv.Atoi(env.TestLogicalPostgres16Port)
if err != nil {
panic(fmt.Sprintf("Failed to parse TEST_LOGICAL_POSTGRES_16_PORT: %v", err))
}
testDbName := "testdb"
return &postgresql_logical.PostgresqlLogicalDatabase{
Version: tools.PostgresqlVersion16,
Host: config.GetEnv().TestLocalhost,
Port: port,
Username: "testuser",
Password: "testpassword",
Database: &testDbName,
CpuCount: 1,
}
}
func Test_CreateDatabase_WhenUserIsNotReadOnly_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: DatabaseTypePostgresLogical,
PostgresqlLogical: 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 getTestMariadbConfig(t *testing.T) *mariadb.MariadbDatabase {
endpoint := containers.StartMariadb(t, "mariadb:10.11")
return GetTestMariadbConfig(endpoint.Host, endpoint.Port)
}
func getTestMongodbConfig(t *testing.T) *mongodb.MongodbDatabase {
endpoint := containers.StartMongodb(t, "mongo:7.0")
return GetTestMongodbConfig(endpoint.Host, endpoint.Port)
}
// physicalNoSummaryVersion pairs a version tag with its image for the summarize_wal=off tests,
// which boot a throwaway no-summary source per version via containers.StartPhysicalPostgres.
type physicalNoSummaryVersion struct {
tag string
image string
}
var physicalNoSummaryVersions = []physicalNoSummaryVersion{
{"17", "postgres:17"},
{"18", "postgres:18"},
}
func Test_CreateDatabase_FailsForPhysicalIncrementalWhenSummarizeWalOff(t *testing.T) {
incrementalBackupTypes := []postgresql_physical.BackupType{
postgresql_physical.BackupTypeFullAndIncremental,
postgresql_physical.BackupTypeFullIncrementalAndWalStream,
}
for _, dbVersion := range physicalNoSummaryVersions {
for _, backupType := range incrementalBackupTypes {
t.Run(fmt.Sprintf("pg%s_%s", dbVersion.tag, backupType), 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)
source := containers.StartPhysicalPostgres(t, dbVersion.image, containers.WithoutSummarizer())
physicalConfig := GetTestPhysicalPostgresConfigNoSummary(source.Host, source.Port, dbVersion.tag)
physicalConfig.BackupType = backupType
request := Database{
Name: "Physical Incremental NoSummary",
WorkspaceID: &workspace.ID,
Type: DatabaseTypePostgresPhysical,
PostgresqlPhysical: physicalConfig,
}
resp := test_utils.MakePostRequest(
t,
router,
"/api/v1/databases/create",
"Bearer "+owner.Token,
request,
http.StatusBadRequest,
)
assert.Contains(t, string(resp.Body), string(postgresql_shared.ConnErrWalSummaryDisabled))
})
}
}
}
func Test_UpdateDatabase_FailsForSwitchToPhysicalIncrementalWhenSummarizeWalOff(t *testing.T) {
for _, dbVersion := range physicalNoSummaryVersions {
t.Run("pg"+dbVersion.tag, 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)
source := containers.StartPhysicalPostgres(t, dbVersion.image, containers.WithoutSummarizer())
createRequest := Database{
Name: "Physical Full Only NoSummary",
WorkspaceID: &workspace.ID,
Type: DatabaseTypePostgresPhysical,
PostgresqlPhysical: GetTestPhysicalPostgresConfigNoSummary(source.Host, source.Port, dbVersion.tag),
}
var createdDatabase Database
test_utils.MakePostRequestAndUnmarshal(
t,
router,
"/api/v1/databases/create",
"Bearer "+owner.Token,
createRequest,
http.StatusCreated,
&createdDatabase,
)
defer RemoveTestDatabase(&createdDatabase)
createdDatabase.PostgresqlPhysical.BackupType = postgresql_physical.BackupTypeFullAndIncremental
updateResponse := test_utils.MakePostRequest(
t,
router,
"/api/v1/databases/update",
"Bearer "+owner.Token,
createdDatabase,
http.StatusBadRequest,
)
assert.Contains(t, string(updateResponse.Body), string(postgresql_shared.ConnErrWalSummaryDisabled))
var refetchedDatabase Database
test_utils.MakeGetRequestAndUnmarshal(
t,
router,
"/api/v1/databases/"+createdDatabase.ID.String(),
"Bearer "+owner.Token,
http.StatusOK,
&refetchedDatabase,
)
assert.Equal(
t,
postgresql_physical.BackupTypeFullOnly,
refetchedDatabase.PostgresqlPhysical.BackupType,
)
})
}
}