Skip to content

Commit fd3200d

Browse files
committed
fixup
Signed-off-by: Paolo Insogna <paolo@cowtech.it>
1 parent 2cde905 commit fd3200d

1 file changed

Lines changed: 141 additions & 88 deletions

File tree

acceptance/specs/database-watt.test.ts

Lines changed: 141 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { randomUUID } from 'node:crypto'
2-
import { describeAcceptance, encodePathSegments, getAcceptanceConfig } from '../support/config'
3-
import { createRestClient } from '../support/http'
4-
import {
5-
cleanupRestResources,
6-
createRestBucket,
7-
requireServiceKey,
8-
uniqueBucketName,
9-
uniqueObjectKey,
10-
uploadRestObject,
11-
} from '../support/resources'
2+
import { setupLoopbackMessaging } from '@platformatic/runtime'
3+
import dotenv from 'dotenv'
4+
import { describeAcceptance, getAcceptanceConfig } from '../support/config'
5+
import { uniqueBucketName } from '../support/resources'
6+
7+
dotenv.config({ path: '.env.test', override: false })
8+
dotenv.config({ path: '.env', override: false })
9+
10+
type ApplicationContext = {
11+
close: () => Promise<void>
12+
isBackgroundApplication: boolean
13+
}
1214

1315
type DatabaseWattStats = {
1416
acquire: number
@@ -40,6 +42,10 @@ type QueryRowsResponse = {
4042
rows: Array<{ value: number }>
4143
}
4244

45+
type BucketRowsResponse = {
46+
rows: BucketResponse[]
47+
}
48+
4349
type LockResponse = {
4450
lockId: string
4551
}
@@ -48,16 +54,29 @@ type ErrorResponse = {
4854
code: string
4955
destination?: string
5056
message: string
57+
sqlState?: string
5158
}
5259

5360
const describeDatabaseWattAcceptance = process.env.ACCEPTANCE_DATABASE_WATT === 'true' ? describeAcceptance : describe.skip
61+
let app: ApplicationContext | undefined
62+
let messaging: ReturnType<typeof setupLoopbackMessaging> | undefined
63+
64+
async function createDatabaseWattApp(): Promise<ApplicationContext> {
65+
const moduleUrl = new URL('../../src/database/index.ts', import.meta.url).href
66+
const databaseApp = await import(moduleUrl)
67+
return databaseApp.create()
68+
}
5469

5570
function testDestination(): string {
5671
return process.env.ACCEPTANCE_TENANT_ID || process.env.TENANT_ID || 'default'
5772
}
5873

5974
function sendDatabaseMessage<T = unknown>(message: string, payload: unknown): Promise<T> {
60-
return sendWattMessage<T>('database', message, payload)
75+
if (!messaging) {
76+
throw new Error('Database Watt loopback messaging is not initialized')
77+
}
78+
79+
return messaging.send('database', message, payload) as Promise<T>
6180
}
6281

6382
function isDatabaseError(result: unknown): result is ErrorResponse {
@@ -89,6 +108,75 @@ async function queryDatabaseWatt(destination = testDestination()): Promise<Query
89108
})
90109
}
91110

111+
async function cleanupBucket(bucketName: string, destination = testDestination()): Promise<void> {
112+
const tx = await beginTransaction(destination)
113+
114+
try {
115+
await checkedResult(
116+
sendDatabaseMessage('database.lockedQuery', {
117+
lockId: tx.lockId,
118+
requestId: randomUUID(),
119+
sql: `SELECT set_config('storage.allow_delete_query', 'true', true)`,
120+
})
121+
)
122+
await checkedResult(
123+
sendDatabaseMessage('database.lockedQuery', {
124+
lockId: tx.lockId,
125+
requestId: randomUUID(),
126+
sql: 'DELETE FROM storage.buckets WHERE id = $1',
127+
values: [bucketName],
128+
})
129+
)
130+
await checkedResult(sendDatabaseMessage('database.commitTransaction', { lockId: tx.lockId }))
131+
} catch (error) {
132+
await sendDatabaseMessage('database.rollbackTransaction', { lockId: tx.lockId }).catch(() => undefined)
133+
throw error
134+
}
135+
}
136+
137+
async function getBucket(bucketName: string, destination = testDestination()): Promise<BucketResponse | undefined> {
138+
const result = await checkedResult<BucketRowsResponse>(
139+
sendDatabaseMessage('database.query', {
140+
destination,
141+
requestId: randomUUID(),
142+
sql: 'SELECT id, name, public FROM storage.buckets WHERE id = $1',
143+
values: [bucketName],
144+
})
145+
)
146+
147+
return result.rows[0]
148+
}
149+
150+
async function insertBucket(bucketName: string, destination = testDestination()): Promise<unknown> {
151+
return sendDatabaseMessage('database.query', {
152+
destination,
153+
requestId: randomUUID(),
154+
sql: `INSERT INTO storage.buckets (id, name, owner, public) VALUES ($1, $1, $2, false)`,
155+
values: [bucketName, randomUUID()],
156+
})
157+
}
158+
159+
async function commitBucketDatabaseWatt(): Promise<{ bucketName: string }> {
160+
const bucketName = uniqueBucketName('dbwatt-commit')
161+
const tx = await beginTransaction()
162+
163+
try {
164+
await checkedResult(
165+
sendDatabaseMessage('database.lockedQuery', {
166+
lockId: tx.lockId,
167+
requestId: randomUUID(),
168+
sql: `INSERT INTO storage.buckets (id, name, owner, public) VALUES ($1, $1, $2, false)`,
169+
values: [bucketName, randomUUID()],
170+
})
171+
)
172+
await checkedResult(sendDatabaseMessage('database.commitTransaction', { lockId: tx.lockId }))
173+
return { bucketName }
174+
} catch (error) {
175+
await sendDatabaseMessage('database.rollbackTransaction', { lockId: tx.lockId }).catch(() => undefined)
176+
throw error
177+
}
178+
}
179+
92180
async function masterTransaction(): Promise<{ value: number | undefined }> {
93181
const tx = await beginTransaction('master')
94182

@@ -224,15 +312,28 @@ async function resetDatabaseWattStats(): Promise<void> {
224312
}
225313

226314
describeDatabaseWattAcceptance(
227-
'Database Watt E2E',
315+
'Database Watt PostgreSQL integration',
228316
{
229317
destructive: true,
230318
profiles: ['core'],
231319
},
232320
() => {
233-
it('executes stateless queries in the Database Watt worker', async () => {
321+
beforeAll(async () => {
322+
messaging = setupLoopbackMessaging('database-watt-acceptance')
323+
app = await createDatabaseWattApp()
324+
})
325+
326+
beforeEach(async () => {
234327
await resetDatabaseWattStats()
328+
})
329+
330+
afterAll(async () => {
331+
await app?.close()
332+
app = undefined
333+
messaging = undefined
334+
})
235335

336+
it('executes stateless queries in the Database Watt worker', async () => {
236337
const response = await queryDatabaseWatt()
237338
const stats = await getDatabaseWattStats()
238339

@@ -248,7 +349,6 @@ describeDatabaseWattAcceptance(
248349
return
249350
}
250351

251-
await resetDatabaseWattStats()
252352
const response = await queryDatabaseWatt('master')
253353
const stats = await getDatabaseWattStats()
254354

@@ -264,7 +364,6 @@ describeDatabaseWattAcceptance(
264364
return
265365
}
266366

267-
await resetDatabaseWattStats()
268367
const response = await masterTransaction()
269368
const stats = await getDatabaseWattStats()
270369

@@ -274,56 +373,38 @@ describeDatabaseWattAcceptance(
274373
expect(stats.commitTransaction).toBeGreaterThanOrEqual(1)
275374
})
276375

277-
it('commits REST object changes through Database Watt transactions', async () => {
278-
const client = createRestClient()
279-
const token = requireServiceKey()
280-
const bucketName = uniqueBucketName('dbwatt-commit')
281-
const objectKey = uniqueObjectKey('dbwatt-commit')
282-
await resetDatabaseWattStats()
283-
376+
it('commits bucket changes through Database Watt transactions', async () => {
377+
let bucketName: string | undefined
284378
try {
285-
await createRestBucket(bucketName, { isPublic: false })
286-
await uploadRestObject(bucketName, objectKey, 'database-watt-commit')
287-
288-
const read = await client.request('GET', `/object/${bucketName}/${encodePathSegments(objectKey)}`, {
289-
expectedStatus: 200,
290-
token,
291-
})
379+
const committed = await commitBucketDatabaseWatt()
380+
bucketName = committed.bucketName
381+
const bucket = await getBucket(bucketName)
292382
const stats = await getDatabaseWattStats()
293383

294-
expect(read.body).toBe('database-watt-commit')
384+
expect(bucket).toMatchObject({ id: bucketName, name: bucketName, public: false })
295385
expect(stats.beginTransaction).toBeGreaterThanOrEqual(1)
296386
expect(stats.lockedQuery).toBeGreaterThanOrEqual(1)
297387
expect(stats.commitTransaction).toBeGreaterThanOrEqual(1)
298388
} finally {
299-
await cleanupRestResources(bucketName, [objectKey], client)
389+
if (bucketName) {
390+
await cleanupBucket(bucketName)
391+
}
300392
}
301393
})
302394

303395
it('rolls back failed transaction work and leaves no partial bucket state', async () => {
304-
const client = createRestClient()
305-
const token = requireServiceKey()
306-
await resetDatabaseWattStats()
307-
308396
const rollback = await rollbackDatabaseWatt()
309397
const bucketName = rollback.bucketName
310398
expect(bucketName).toBeTruthy()
311399

312-
const lookup = await client.request('GET', `/bucket/${bucketName}`, {
313-
expectedStatus: 400,
314-
token,
315-
})
400+
const bucket = await getBucket(bucketName)
316401
const stats = await getDatabaseWattStats()
317402

318-
expect(lookup.status).toBe(400)
403+
expect(bucket).toBeUndefined()
319404
expect(stats.rollbackTransaction).toBeGreaterThanOrEqual(1)
320405
})
321406

322407
it('preserves nested savepoint semantics in Database Watt transactions', async () => {
323-
const client = createRestClient()
324-
const token = requireServiceKey()
325-
await resetDatabaseWattStats()
326-
327408
const savepoint = await savepointDatabaseWatt()
328409
const outerBucket = savepoint.outerBucket
329410
const innerBucket = savepoint.innerBucket
@@ -332,56 +413,37 @@ describeDatabaseWattAcceptance(
332413
expect(outerBucket).toBeTruthy()
333414
expect(innerBucket).toBeTruthy()
334415

335-
const outer = await client.request<BucketResponse>('GET', `/bucket/${outerBucket}`, {
336-
expectedStatus: 200,
337-
token,
338-
})
339-
const inner = await client.request('GET', `/bucket/${innerBucket}`, {
340-
expectedStatus: 400,
341-
token,
342-
})
416+
const outer = await getBucket(outerBucket)
417+
const inner = await getBucket(innerBucket)
343418
const stats = await getDatabaseWattStats()
344419

345-
expect(outer.json?.id).toBe(outerBucket)
346-
expect(inner.status).toBe(400)
420+
expect(outer?.id).toBe(outerBucket)
421+
expect(inner).toBeUndefined()
347422
expect(stats.lockedQuery).toBeGreaterThanOrEqual(3)
348423
expect(stats.commitTransaction).toBeGreaterThanOrEqual(1)
349424
} finally {
350425
if (outerBucket) {
351-
await cleanupRestResources(outerBucket, [], client)
426+
await cleanupBucket(outerBucket)
352427
}
353428
}
354429
})
355430

356-
it('preserves storage error mapping when Database Watt returns PostgreSQL errors', async () => {
357-
const client = createRestClient()
358-
const token = requireServiceKey()
431+
it('preserves PostgreSQL error mapping when Database Watt returns PostgreSQL errors', async () => {
359432
const bucketName = uniqueBucketName('dbwatt-error')
360-
await resetDatabaseWattStats()
361433

362434
try {
363-
await createRestBucket(bucketName, { isPublic: false })
364-
const duplicate = await client.request('POST', '/bucket', {
365-
body: {
366-
id: bucketName,
367-
name: bucketName,
368-
public: false,
369-
},
370-
expectedStatus: 400,
371-
token,
372-
})
435+
await checkedResult(insertBucket(bucketName))
436+
const duplicate = await insertBucket(bucketName) as ErrorResponse
373437
const stats = await getDatabaseWattStats()
374438

375-
expect(duplicate.status).toBe(400)
376-
expect(stats.lockedQuery).toBeGreaterThanOrEqual(1)
439+
expect(duplicate).toMatchObject({ code: 'POSTGRES_ERROR', sqlState: '23505' })
440+
expect(stats.query).toBeGreaterThanOrEqual(2)
377441
} finally {
378-
await cleanupRestResources(bucketName, [], client)
442+
await cleanupBucket(bucketName)
379443
}
380444
})
381445

382446
it('translates request aborts into Database Watt cancellation', async () => {
383-
await resetDatabaseWattStats()
384-
385447
const response = await sleepDatabaseWatt()
386448
const stats = await getDatabaseWattStats()
387449

@@ -397,16 +459,13 @@ describeDatabaseWattAcceptance(
397459
return
398460
}
399461

400-
await resetDatabaseWattStats()
401462
const response = await missingDestinationDatabaseWatt()
402463

403464
expect(response).toMatchObject({ code: 'DESTINATION_UNKNOWN' })
404465
expect(response.destination).toEqual(expect.stringMatching(/^missing-/))
405466
})
406467

407468
it('handles concurrent Database Watt query load', async () => {
408-
await resetDatabaseWattStats()
409-
410469
const response = await concurrentQueriesDatabaseWatt()
411470
const stats = await getDatabaseWattStats()
412471

@@ -416,28 +475,22 @@ describeDatabaseWattAcceptance(
416475

417476
it('exercises multitenant destination resolution when the target is multitenant', async () => {
418477
const config = getAcceptanceConfig()
419-
const client = createRestClient()
420-
const token = requireServiceKey(config)
421478
const bucketName = uniqueBucketName('dbwatt-tenant')
422479

423480
if (!config.tenantId) {
424481
expect(config.tenantId).toBeUndefined()
425482
return
426483
}
427484

428-
await resetDatabaseWattStats()
429485
try {
430-
await createRestBucket(bucketName, { isPublic: false })
431-
const bucket = await client.request<BucketResponse>('GET', `/bucket/${bucketName}`, {
432-
expectedStatus: 200,
433-
token,
434-
})
486+
await checkedResult(insertBucket(bucketName, config.tenantId))
487+
const bucket = await getBucket(bucketName, config.tenantId)
435488
const stats = await getDatabaseWattStats()
436489

437-
expect(bucket.json?.id).toBe(bucketName)
438-
expect(stats.beginTransaction).toBeGreaterThanOrEqual(1)
490+
expect(bucket?.id).toBe(bucketName)
491+
expect(stats.query).toBeGreaterThanOrEqual(2)
439492
} finally {
440-
await cleanupRestResources(bucketName, [], client)
493+
await cleanupBucket(bucketName, config.tenantId)
441494
}
442495
})
443496
}

0 commit comments

Comments
 (0)