Skip to content

Commit 866bc2e

Browse files
test(integration): restore compression roundtrip scenario
1 parent a543663 commit 866bc2e

2 files changed

Lines changed: 242 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@compression-roundtrip
2+
Feature: Compressed import/export roundtrip
3+
Scenario Outline: roundtrip events with <format> compression
4+
Given a seeded compression roundtrip dataset
5+
When I export events using "<format>" compression
6+
And I remove the seeded roundtrip events from the database
7+
And I import the compressed roundtrip file
8+
Then the seeded roundtrip events are restored
9+
10+
Examples:
11+
| format |
12+
| gzip |
13+
| xz |
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import { After, Given, Then, When, World } from '@cucumber/cucumber'
2+
import { randomUUID } from 'crypto'
3+
import { spawn } from 'child_process'
4+
import { expect } from 'chai'
5+
import fs from 'fs'
6+
import os from 'os'
7+
import { join } from 'path'
8+
9+
import { getMasterDbClient } from '../../../../src/database/client'
10+
import { EventRepository } from '../../../../src/repositories/event-repository'
11+
import { createEvent, createIdentity } from '../helpers'
12+
13+
type ScriptResult = {
14+
exitCode: number
15+
stderr: string
16+
stdout: string
17+
}
18+
19+
type CompressionRoundtripState = {
20+
expectedContents: string[]
21+
expectedIds: string[]
22+
identityName: string
23+
outputFilePath: string
24+
pubkey: string
25+
tempDir: string
26+
}
27+
28+
const SCRIPT_TIMEOUT_MS = 60_000
29+
const ROUNDTRIP_KEY = 'compressionRoundtrip'
30+
31+
const runCliScript = async (
32+
scriptPath: string,
33+
args: string[],
34+
): Promise<ScriptResult> => {
35+
return new Promise<ScriptResult>((resolve, reject) => {
36+
const commandArgs = ['--env-file-if-exists=.env', '-r', 'ts-node/register', scriptPath, ...args]
37+
const child = spawn(process.execPath, commandArgs, {
38+
cwd: process.cwd(),
39+
env: process.env,
40+
stdio: ['ignore', 'pipe', 'pipe'],
41+
})
42+
43+
let stdout = ''
44+
let stderr = ''
45+
46+
const timeout = setTimeout(() => {
47+
child.kill('SIGKILL')
48+
reject(new Error(`Timed out while running ${scriptPath}`))
49+
}, SCRIPT_TIMEOUT_MS)
50+
51+
child.stdout.on('data', (chunk: Buffer | string) => {
52+
stdout += chunk.toString()
53+
})
54+
55+
child.stderr.on('data', (chunk: Buffer | string) => {
56+
stderr += chunk.toString()
57+
})
58+
59+
child.on('error', (error) => {
60+
clearTimeout(timeout)
61+
reject(error)
62+
})
63+
64+
child.on('close', (code) => {
65+
clearTimeout(timeout)
66+
resolve({
67+
exitCode: code ?? 1,
68+
stderr,
69+
stdout,
70+
})
71+
})
72+
})
73+
}
74+
75+
const assertCommandSuccess = (
76+
result: ScriptResult,
77+
label: string,
78+
): void => {
79+
if (result.exitCode === 0) {
80+
return
81+
}
82+
83+
throw new Error(
84+
`${label} failed with exit code ${result.exitCode}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
85+
)
86+
}
87+
88+
const getRoundtripState = (
89+
world: World<Record<string, unknown>>,
90+
): CompressionRoundtripState => {
91+
const state = world.parameters[ROUNDTRIP_KEY]
92+
93+
if (!state) {
94+
throw new Error('Compression roundtrip state is not initialized')
95+
}
96+
97+
return state as CompressionRoundtripState
98+
}
99+
100+
Given('a seeded compression roundtrip dataset', async function (this: World<Record<string, unknown>>) {
101+
const dbClient = getMasterDbClient()
102+
const repository = new EventRepository(dbClient, dbClient)
103+
104+
const identityName = `RoundtripUser${Date.now()}`
105+
const identity = createIdentity(identityName)
106+
107+
const identities = this.parameters.identities as Record<string, { privkey: string; pubkey: string }>
108+
identities[identityName] = identity
109+
110+
const token = randomUUID()
111+
const firstContent = `compression-roundtrip-${token}-1`
112+
const secondContent = `compression-roundtrip-${token}-2`
113+
114+
const firstEvent = await createEvent(
115+
{
116+
content: firstContent,
117+
kind: 1,
118+
pubkey: identity.pubkey,
119+
tags: [['t', 'compression-roundtrip']],
120+
},
121+
identity.privkey,
122+
)
123+
124+
const secondEvent = await createEvent(
125+
{
126+
content: secondContent,
127+
kind: 1,
128+
pubkey: identity.pubkey,
129+
tags: [['t', 'compression-roundtrip']],
130+
},
131+
identity.privkey,
132+
)
133+
134+
const inserted = await repository.createMany([firstEvent, secondEvent])
135+
expect(inserted).to.equal(2)
136+
137+
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'nostream-compression-roundtrip-'))
138+
139+
this.parameters[ROUNDTRIP_KEY] = {
140+
expectedContents: [firstContent, secondContent].sort(),
141+
expectedIds: [firstEvent.id, secondEvent.id].sort(),
142+
identityName,
143+
outputFilePath: '',
144+
pubkey: identity.pubkey,
145+
tempDir,
146+
} as CompressionRoundtripState
147+
})
148+
149+
When(
150+
'I export events using {string} compression',
151+
async function (this: World<Record<string, unknown>>, format: string) {
152+
if (format !== 'gzip' && format !== 'xz') {
153+
throw new Error(`Unsupported test format: ${format}`)
154+
}
155+
156+
const state = getRoundtripState(this)
157+
const extension = format === 'gzip' ? '.jsonl.gz' : '.jsonl.xz'
158+
const outputFilePath = join(state.tempDir, `events${extension}`)
159+
160+
const result = await runCliScript('src/scripts/export-events.ts', [
161+
outputFilePath,
162+
'--compress',
163+
'--format',
164+
format,
165+
])
166+
167+
assertCommandSuccess(result, 'export script')
168+
169+
expect(fs.existsSync(outputFilePath)).to.equal(true)
170+
expect(fs.statSync(outputFilePath).size).to.be.greaterThan(0)
171+
172+
state.outputFilePath = outputFilePath
173+
this.parameters[ROUNDTRIP_KEY] = state
174+
},
175+
)
176+
177+
When('I remove the seeded roundtrip events from the database', async function (this: World<Record<string, unknown>>) {
178+
const state = getRoundtripState(this)
179+
const dbClient = getMasterDbClient()
180+
181+
await dbClient('events')
182+
.where('event_pubkey', Buffer.from(state.pubkey, 'hex'))
183+
.delete()
184+
})
185+
186+
When('I import the compressed roundtrip file', async function (this: World<Record<string, unknown>>) {
187+
const state = getRoundtripState(this)
188+
189+
const result = await runCliScript('src/import-events.ts', [
190+
state.outputFilePath,
191+
'--batch-size',
192+
'2',
193+
])
194+
195+
assertCommandSuccess(result, 'import script')
196+
})
197+
198+
Then('the seeded roundtrip events are restored', async function (this: World<Record<string, unknown>>) {
199+
const state = getRoundtripState(this)
200+
const dbClient = getMasterDbClient()
201+
202+
const rows = await dbClient('events')
203+
.select('event_id', 'event_content')
204+
.where('event_pubkey', Buffer.from(state.pubkey, 'hex'))
205+
206+
const actualIds = rows
207+
.map((row: { event_id: Buffer }) => row.event_id.toString('hex'))
208+
.sort()
209+
210+
const actualContents = rows
211+
.map((row: { event_content: string }) => row.event_content)
212+
.sort()
213+
214+
expect(actualIds).to.deep.equal(state.expectedIds)
215+
expect(actualContents).to.deep.equal(state.expectedContents)
216+
})
217+
218+
After({ tags: '@compression-roundtrip' }, async function (this: World<Record<string, unknown>>) {
219+
const state = this.parameters[ROUNDTRIP_KEY] as CompressionRoundtripState | undefined
220+
221+
if (state?.tempDir) {
222+
fs.rmSync(state.tempDir, {
223+
force: true,
224+
recursive: true,
225+
})
226+
}
227+
228+
this.parameters[ROUNDTRIP_KEY] = undefined
229+
})

0 commit comments

Comments
 (0)