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