Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:
types: [opened, synchronize, reopened]

env:
NODE_VERSION: "22.9.0"
NODE_VERSION: "22.12.0"
PNPM_VERSION: "10.20.0"

jobs:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ For technical details, see [Architecture Documentation](./docs/architecture.md).

### Prerequisites

- Node.js 22.9.0 or higher
- Node.js 22.12.0 or higher
- PostgreSQL database
- Kubernetes cluster with KubeBlocks installed
- GitHub OAuth application credentials
Expand Down
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This document provides guidance for local development and code patterns.

## Prerequisites

- **Node.js**: 22.9.0 or higher
- **Node.js**: 22.12.0 or higher
- **pnpm**: 9.x or higher
- **PostgreSQL**: 14.x or higher
- **Kubernetes cluster**: For full integration testing (optional)
Expand Down
183 changes: 183 additions & 0 deletions lib/platform/control/commands/database/create-database.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const {
prisma,
getK8sServiceForUser,
toK8sProjectName,
generateK8sRandomString,
} = vi.hoisted(() => ({
prisma: {
project: {
findUnique: vi.fn(),
},
database: {
create: vi.fn(),
},
},
getK8sServiceForUser: vi.fn(),
toK8sProjectName: vi.fn(),
generateK8sRandomString: vi.fn(),
}))

vi.mock('@/lib/db', () => ({
prisma,
}))

vi.mock('@/lib/k8s/k8s-service-helper', () => ({
getK8sServiceForUser,
}))

vi.mock('@/lib/k8s/kubernetes-utils', () => ({
KubernetesUtils: {
toK8sProjectName,
generateRandomString: generateK8sRandomString,
},
}))

vi.mock('@/lib/k8s/versions', () => ({
VERSIONS: {
STORAGE: {
DATABASE_SIZE: '3Gi',
},
RESOURCES: {
DATABASE: {
requests: {
cpu: '100m',
memory: '102Mi',
},
limits: {
cpu: '1000m',
memory: '1024Mi',
},
},
},
},
}))

import { createDatabaseCommand } from '@/lib/platform/control/commands/database/create-database'

describe('createDatabaseCommand', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('returns an error when the project does not exist', async () => {
prisma.project.findUnique.mockResolvedValue(null)

const result = await createDatabaseCommand({
userId: 'user-1',
projectId: 'project-1',
})

expect(result).toEqual({
success: false,
error: 'Project not found',
})
expect(prisma.database.create).not.toHaveBeenCalled()
})

it('returns an error when the project belongs to another user', async () => {
prisma.project.findUnique.mockResolvedValue({
id: 'project-1',
userId: 'other-user',
databases: [],
})

const result = await createDatabaseCommand({
userId: 'user-1',
projectId: 'project-1',
})

expect(result).toEqual({
success: false,
error: 'Unauthorized',
})
})

it('returns an error when a database already exists', async () => {
prisma.project.findUnique.mockResolvedValue({
id: 'project-1',
userId: 'user-1',
databases: [{ id: 'database-1' }],
})

const result = await createDatabaseCommand({
userId: 'user-1',
projectId: 'project-1',
})

expect(result).toEqual({
success: false,
error: 'Database already exists for this project',
})
})

it('returns a friendly error when kubeconfig is missing', async () => {
prisma.project.findUnique.mockResolvedValue({
id: 'project-1',
userId: 'user-1',
name: 'Project Alpha',
databases: [],
})
getK8sServiceForUser.mockRejectedValue(
new Error('User [user-1] does not have KUBECONFIG configured')
)

const result = await createDatabaseCommand({
userId: 'user-1',
projectId: 'project-1',
})

expect(result).toEqual({
success: false,
error: 'Please configure your kubeconfig before creating a database',
})
expect(prisma.database.create).not.toHaveBeenCalled()
})

it('creates the database with the generated default name and expected resource config', async () => {
prisma.project.findUnique.mockResolvedValue({
id: 'project-1',
userId: 'user-1',
name: 'Project Alpha',
databases: [],
})
prisma.database.create.mockResolvedValue({
id: 'database-1',
name: 'projectalpha-db-suffix',
})
getK8sServiceForUser.mockResolvedValue({
getDefaultNamespace: vi.fn().mockReturnValue('ns-user-1'),
})
toK8sProjectName.mockReturnValue('projectalpha')
generateK8sRandomString.mockReturnValue('suffix')

const result = await createDatabaseCommand({
userId: 'user-1',
projectId: 'project-1',
})

expect(result).toEqual({
success: true,
data: {
id: 'database-1',
name: 'projectalpha-db-suffix',
},
})
expect(prisma.database.create).toHaveBeenCalledWith({
data: {
projectId: 'project-1',
name: 'projectalpha-db-suffix',
k8sNamespace: 'ns-user-1',
databaseName: 'projectalpha-db-suffix',
status: 'CREATING',
lockedUntil: null,
storageSize: '3Gi',
cpuRequest: '100m',
cpuLimit: '1000m',
memoryRequest: '102Mi',
memoryLimit: '1024Mi',
},
})
})
})
86 changes: 86 additions & 0 deletions lib/platform/control/commands/database/delete-database.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { prisma } = vi.hoisted(() => ({
prisma: {
database: {
findUnique: vi.fn(),
update: vi.fn(),
},
},
}))

vi.mock('@/lib/db', () => ({
prisma,
}))

import { deleteDatabaseCommand } from '@/lib/platform/control/commands/database/delete-database'

describe('deleteDatabaseCommand', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('returns an error when the database does not exist', async () => {
prisma.database.findUnique.mockResolvedValue(null)

const result = await deleteDatabaseCommand({
userId: 'user-1',
databaseId: 'database-1',
})

expect(result).toEqual({
success: false,
error: 'Database not found',
})
expect(prisma.database.update).not.toHaveBeenCalled()
})

it('returns an error when the database belongs to another user', async () => {
prisma.database.findUnique.mockResolvedValue({
id: 'database-1',
project: {
userId: 'other-user',
},
})

const result = await deleteDatabaseCommand({
userId: 'user-1',
databaseId: 'database-1',
})

expect(result).toEqual({
success: false,
error: 'Unauthorized',
})
expect(prisma.database.update).not.toHaveBeenCalled()
})

it('marks the database as terminating and clears the lock', async () => {
prisma.database.findUnique.mockResolvedValue({
id: 'database-1',
project: {
userId: 'user-1',
},
})
prisma.database.update.mockResolvedValue({
id: 'database-1',
})

const result = await deleteDatabaseCommand({
userId: 'user-1',
databaseId: 'database-1',
})

expect(result).toEqual({
success: true,
data: undefined,
})
expect(prisma.database.update).toHaveBeenCalledWith({
where: { id: 'database-1' },
data: {
status: 'TERMINATING',
lockedUntil: null,
},
})
})
})
Loading
Loading