Skip to content
Open
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
135 changes: 125 additions & 10 deletions src/export/dump.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ vi.mock('../utils', () => ({

let mockDataSource: DataSource
let mockConfig: StarbaseDBConfiguration
let mockR2Bucket: any
let mockFetch: any

beforeEach(() => {
vi.clearAllMocks()
Expand All @@ -36,6 +38,17 @@ beforeEach(() => {
role: 'admin',
features: { allowlist: true, rls: true, rest: true },
}

mockR2Bucket = {
createMultipartUpload: vi.fn().mockResolvedValue({
uploadPart: vi.fn().mockResolvedValue({ etag: 'mock-etag' }),
complete: vi.fn().mockResolvedValue(undefined),
abort: vi.fn().mockResolvedValue(undefined),
}),
}

mockFetch = vi.fn().mockResolvedValue(new Response('OK', { status: 200 }))
vi.stubGlobal('fetch', mockFetch)
})

describe('Database Dump Module', () => {
Expand All @@ -57,7 +70,12 @@ describe('Database Dump Module', () => {
{ id: 2, total: 49.5 },
])

const response = await dumpDatabaseRoute(mockDataSource, mockConfig)
const req = new Request('http://localhost/export/dump')
const response = await dumpDatabaseRoute(
req,
mockDataSource,
mockConfig
)

expect(response).toBeInstanceOf(Response)
expect(response.headers.get('Content-Type')).toBe(
Expand All @@ -71,19 +89,24 @@ describe('Database Dump Module', () => {
expect(dumpText).toContain(
'CREATE TABLE users (id INTEGER, name TEXT);'
)
expect(dumpText).toContain("INSERT INTO users VALUES (1, 'Alice');")
expect(dumpText).toContain("INSERT INTO users VALUES (2, 'Bob');")
expect(dumpText).toContain('INSERT INTO "users" VALUES (1, \'Alice\');')
expect(dumpText).toContain('INSERT INTO "users" VALUES (2, \'Bob\');')
expect(dumpText).toContain(
'CREATE TABLE orders (id INTEGER, total REAL);'
)
expect(dumpText).toContain('INSERT INTO orders VALUES (1, 99.99);')
expect(dumpText).toContain('INSERT INTO orders VALUES (2, 49.5);')
expect(dumpText).toContain('INSERT INTO "orders" VALUES (1, 99.99);')
expect(dumpText).toContain('INSERT INTO "orders" VALUES (2, 49.5);')
})

it('should handle empty databases (no tables)', async () => {
vi.mocked(executeOperation).mockResolvedValueOnce([])

const response = await dumpDatabaseRoute(mockDataSource, mockConfig)
const req = new Request('http://localhost/export/dump')
const response = await dumpDatabaseRoute(
req,
mockDataSource,
mockConfig
)

expect(response).toBeInstanceOf(Response)
expect(response.headers.get('Content-Type')).toBe(
Expand All @@ -101,7 +124,12 @@ describe('Database Dump Module', () => {
])
.mockResolvedValueOnce([])

const response = await dumpDatabaseRoute(mockDataSource, mockConfig)
const req = new Request('http://localhost/export/dump')
const response = await dumpDatabaseRoute(
req,
mockDataSource,
mockConfig
)

expect(response).toBeInstanceOf(Response)
const dumpText = await response.text()
Expand All @@ -119,12 +147,17 @@ describe('Database Dump Module', () => {
])
.mockResolvedValueOnce([{ id: 1, bio: "Alice's adventure" }])

const response = await dumpDatabaseRoute(mockDataSource, mockConfig)
const req = new Request('http://localhost/export/dump')
const response = await dumpDatabaseRoute(
req,
mockDataSource,
mockConfig
)

expect(response).toBeInstanceOf(Response)
const dumpText = await response.text()
expect(dumpText).toContain(
"INSERT INTO users VALUES (1, 'Alice''s adventure');"
"INSERT INTO \"users\" VALUES (1, 'Alice''s adventure');"
)
})

Expand All @@ -136,10 +169,92 @@ describe('Database Dump Module', () => {
new Error('Database Error')
)

const response = await dumpDatabaseRoute(mockDataSource, mockConfig)
const req = new Request('http://localhost/export/dump')
const response = await dumpDatabaseRoute(
req,
mockDataSource,
mockConfig
)

expect(response.status).toBe(500)
const jsonResponse: { error: string } = await response.json()
expect(jsonResponse.error).toBe('Failed to create database dump')
consoleErrorMock.mockRestore()
})

it('should return a 400 response when async is requested but bucket is missing', async () => {
const req = new Request('http://localhost/export/dump?async=true')
const response = await dumpDatabaseRoute(
req,
mockDataSource,
mockConfig
)

expect(response.status).toBe(400)
const jsonResponse = (await response.json()) as any
expect(jsonResponse.error).toContain(
'require an EXPORT_BUCKET R2 binding'
)
})

it('should trigger background R2 dump when async is requested', async () => {
vi.mocked(executeOperation)
.mockResolvedValueOnce([{ name: 'users' }])
.mockResolvedValueOnce([
{ sql: 'CREATE TABLE users (id INTEGER, name TEXT);' },
])
.mockResolvedValueOnce([{ id: 1, name: 'Alice' }])

const configWithBucket: StarbaseDBConfiguration = {
...mockConfig,
export: {
bucket: mockR2Bucket,
callbackUrl: 'http://callback.url/notify',
chunkSize: 100,
},
}

const mockExecutionContext = {
waitUntil: vi.fn(),
} as any

const req = new Request(
'http://localhost/export/dump?async=true&filename=test-dump.sql'
)
const response = await dumpDatabaseRoute(
req,
mockDataSource,
configWithBucket,
mockExecutionContext
)

expect(response.status).toBe(202)
const jsonResponse = (await response.json()) as any
expect(jsonResponse.result.status).toBe('running')
expect(jsonResponse.result.filename).toBe('test-dump.sql')

expect(mockExecutionContext.waitUntil).toHaveBeenCalled()
const backgroundPromise =
mockExecutionContext.waitUntil.mock.calls[0][0]

// Await the background process
await backgroundPromise

// Verify R2 was used
expect(mockR2Bucket.createMultipartUpload).toHaveBeenCalledWith(
'test-dump.sql',
{
httpMetadata: { contentType: 'application/sql' },
}
)

// Verify callback url notified
expect(mockFetch).toHaveBeenCalledWith(
'http://callback.url/notify',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"status":"completed"'),
})
)
})
})
Loading