Erster Docker-Stand
This commit is contained in:
512
_node_modules/@electric-sql/pglite-socket/tests/index.test.ts
generated
Normal file
512
_node_modules/@electric-sql/pglite-socket/tests/index.test.ts
generated
Normal file
@@ -0,0 +1,512 @@
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
vi,
|
||||
beforeAll,
|
||||
afterAll,
|
||||
} from 'vitest'
|
||||
import { PGlite } from '@electric-sql/pglite'
|
||||
import {
|
||||
PGLiteSocketHandler,
|
||||
PGLiteSocketServer,
|
||||
CONNECTION_QUEUE_TIMEOUT,
|
||||
} from '../src'
|
||||
import { Socket, createConnection } from 'net'
|
||||
import { existsSync } from 'fs'
|
||||
import { unlink } from 'fs/promises'
|
||||
|
||||
// Mock timers for testing timeouts
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
async function testSocket(
|
||||
fn: (socketOptions: {
|
||||
host?: string
|
||||
port?: number
|
||||
path?: string
|
||||
}) => Promise<void>,
|
||||
) {
|
||||
describe('TCP socket server', async () => {
|
||||
await fn({ host: '127.0.0.1', port: 5433 })
|
||||
})
|
||||
describe('unix socket server', async () => {
|
||||
await fn({ path: '/tmp/.s.PGSQL.5432' })
|
||||
})
|
||||
}
|
||||
|
||||
// Create a mock Socket for testing
|
||||
const createMockSocket = () => {
|
||||
const eventHandlers: Record<string, Array<(data: any) => void>> = {}
|
||||
|
||||
const mockSocket = {
|
||||
// Socket methods we need for testing
|
||||
removeAllListeners: vi.fn(),
|
||||
end: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
write: vi.fn(),
|
||||
writable: true,
|
||||
remoteAddress: '127.0.0.1',
|
||||
remotePort: 12345,
|
||||
|
||||
// Mock on method with tracking of handlers
|
||||
on: vi
|
||||
.fn()
|
||||
.mockImplementation((event: string, callback: (data: any) => void) => {
|
||||
if (!eventHandlers[event]) {
|
||||
eventHandlers[event] = []
|
||||
}
|
||||
eventHandlers[event].push(callback)
|
||||
return mockSocket
|
||||
}),
|
||||
|
||||
// Store event handlers for testing
|
||||
eventHandlers,
|
||||
|
||||
// Helper to emit events
|
||||
emit(event: string, data: any) {
|
||||
if (eventHandlers[event]) {
|
||||
eventHandlers[event].forEach((handler) => handler(data))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return mockSocket as unknown as Socket
|
||||
}
|
||||
|
||||
describe('PGLiteSocketHandler', () => {
|
||||
let db: PGlite
|
||||
let handler: PGLiteSocketHandler
|
||||
let mockSocket: ReturnType<typeof createMockSocket> & {
|
||||
eventHandlers: Record<string, Array<(data: any) => void>>
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a PGlite instance for testing
|
||||
db = await PGlite.create()
|
||||
handler = new PGLiteSocketHandler({ db })
|
||||
mockSocket = createMockSocket() as any
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Ensure handler is detached before closing the database
|
||||
if (handler?.isAttached) {
|
||||
handler.detach(true)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
await db.close()
|
||||
})
|
||||
|
||||
it('should attach to a socket', async () => {
|
||||
// Attach mock socket to handler
|
||||
await handler.attach(mockSocket)
|
||||
|
||||
// Check that the socket is attached
|
||||
expect(handler.isAttached).toBe(true)
|
||||
expect(mockSocket.on).toHaveBeenCalledWith('data', expect.any(Function))
|
||||
expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function))
|
||||
expect(mockSocket.on).toHaveBeenCalledWith('close', expect.any(Function))
|
||||
})
|
||||
|
||||
it('should detach from a socket', async () => {
|
||||
// First attach
|
||||
await handler.attach(mockSocket)
|
||||
expect(handler.isAttached).toBe(true)
|
||||
|
||||
// Then detach
|
||||
handler.detach(false)
|
||||
expect(handler.isAttached).toBe(false)
|
||||
expect(mockSocket.removeAllListeners).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should close socket when detaching with close option', async () => {
|
||||
// Attach mock socket to handler
|
||||
await handler.attach(mockSocket)
|
||||
|
||||
// Detach with close option
|
||||
handler.detach(true)
|
||||
expect(handler.isAttached).toBe(false)
|
||||
expect(mockSocket.end).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should reject attaching multiple sockets', async () => {
|
||||
// Attach first socket
|
||||
await handler.attach(mockSocket)
|
||||
|
||||
// Trying to attach another socket should throw an error
|
||||
const anotherMockSocket = createMockSocket()
|
||||
await expect(handler.attach(anotherMockSocket)).rejects.toThrow(
|
||||
'Socket already attached',
|
||||
)
|
||||
})
|
||||
|
||||
it('should emit error event when socket has error', async () => {
|
||||
// Set up error listener
|
||||
const errorHandler = vi.fn()
|
||||
handler.addEventListener('error', errorHandler)
|
||||
|
||||
// Attach socket
|
||||
await handler.attach(mockSocket)
|
||||
|
||||
// Mock the event handler logic directly instead of triggering actual error handlers
|
||||
const customEvent = new CustomEvent('error', {
|
||||
detail: { code: 'MOCK_ERROR', message: 'Test socket error' },
|
||||
})
|
||||
handler.dispatchEvent(customEvent)
|
||||
|
||||
// Verify error handler was called
|
||||
expect(errorHandler).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should emit close event when socket closes', async () => {
|
||||
// Set up close listener
|
||||
const closeHandler = vi.fn()
|
||||
handler.addEventListener('close', closeHandler)
|
||||
|
||||
// Attach socket
|
||||
await handler.attach(mockSocket)
|
||||
|
||||
// Mock the event handler logic directly instead of triggering actual socket handlers
|
||||
const customEvent = new CustomEvent('close')
|
||||
handler.dispatchEvent(customEvent)
|
||||
|
||||
// Verify close handler was called
|
||||
expect(closeHandler).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
testSocket(async (connOptions) => {
|
||||
describe('PGLiteSocketServer', () => {
|
||||
let db: PGlite
|
||||
let server: PGLiteSocketServer
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a PGlite instance for testing
|
||||
db = await PGlite.create()
|
||||
if (connOptions.path) {
|
||||
if (existsSync(connOptions.path)) {
|
||||
try {
|
||||
await unlink(connOptions.path)
|
||||
console.log(`Removed old socket at ${connOptions.path}`)
|
||||
} catch (err) {
|
||||
console.log('')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Stop server if running
|
||||
try {
|
||||
await server?.stop()
|
||||
} catch (e) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
|
||||
// Close database
|
||||
await db.close()
|
||||
})
|
||||
|
||||
it('should start and stop server', async () => {
|
||||
// Create server
|
||||
server = new PGLiteSocketServer({
|
||||
db,
|
||||
host: connOptions.host,
|
||||
port: connOptions.port,
|
||||
path: connOptions.path,
|
||||
})
|
||||
|
||||
// Start server
|
||||
await server.start()
|
||||
|
||||
// Try to connect to confirm server is running
|
||||
let client
|
||||
if (connOptions.path) {
|
||||
// unix socket
|
||||
client = createConnection({ path: connOptions.path })
|
||||
} else {
|
||||
if (connOptions.port) {
|
||||
// TCP socket
|
||||
client = createConnection({
|
||||
port: connOptions.port,
|
||||
host: connOptions.host,
|
||||
})
|
||||
} else {
|
||||
throw new Error(
|
||||
'need to specify connOptions.path or connOptions.port',
|
||||
)
|
||||
}
|
||||
}
|
||||
client.on('error', () => {
|
||||
// Ignore connection errors during test
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
client.on('connect', () => {
|
||||
client.end()
|
||||
resolve()
|
||||
})
|
||||
|
||||
// Set timeout to resolve in case connection fails
|
||||
setTimeout(resolve, 100)
|
||||
})
|
||||
|
||||
// Stop server
|
||||
await server.stop()
|
||||
|
||||
// Try to connect again - should fail
|
||||
await expect(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
let failClient
|
||||
if (connOptions.path) {
|
||||
// unix socket
|
||||
failClient = createConnection({ path: connOptions.path })
|
||||
} else {
|
||||
if (connOptions.port) {
|
||||
// TCP socket
|
||||
failClient = createConnection({
|
||||
port: connOptions.port,
|
||||
host: connOptions.host,
|
||||
})
|
||||
} else {
|
||||
throw new Error(
|
||||
'need to specify connOptions.path or connOptions.port',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
failClient.on('error', () => {
|
||||
// Expected error - connection should fail
|
||||
resolve()
|
||||
})
|
||||
|
||||
failClient.on('connect', () => {
|
||||
failClient.end()
|
||||
reject(new Error('Connection should have failed'))
|
||||
})
|
||||
|
||||
// Set timeout to resolve in case no events fire
|
||||
setTimeout(resolve, 100)
|
||||
}),
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
describe('Connection queuing', () => {
|
||||
// Mock implementation details
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let handleConnectionSpy: any
|
||||
let processNextInQueueSpy: any
|
||||
let attachSocketToNewHandlerSpy: any
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a server with a short timeout for testing
|
||||
server = new PGLiteSocketServer({
|
||||
db,
|
||||
host: connOptions.host,
|
||||
port: connOptions.port,
|
||||
path: connOptions.path,
|
||||
connectionQueueTimeout: 100, // Very short timeout for testing
|
||||
})
|
||||
|
||||
// Spy on internal methods
|
||||
handleConnectionSpy = vi.spyOn(server as any, 'handleConnection')
|
||||
processNextInQueueSpy = vi.spyOn(server as any, 'processNextInQueue')
|
||||
attachSocketToNewHandlerSpy = vi.spyOn(
|
||||
server as any,
|
||||
'attachSocketToNewHandler',
|
||||
)
|
||||
})
|
||||
|
||||
it('should create a handler for a new connection', async () => {
|
||||
await server.start()
|
||||
|
||||
// Create mock socket
|
||||
const socket1 = createMockSocket()
|
||||
|
||||
// Setup event listener
|
||||
const connectionHandler = vi.fn()
|
||||
server.addEventListener('connection', connectionHandler)
|
||||
|
||||
// Handle connection
|
||||
await (server as any).handleConnection(socket1)
|
||||
|
||||
// Verify handler was created
|
||||
expect(attachSocketToNewHandlerSpy).toHaveBeenCalledWith(
|
||||
socket1,
|
||||
expect.anything(),
|
||||
)
|
||||
expect(connectionHandler).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should queue a second connection when first is active', async () => {
|
||||
await server.start()
|
||||
|
||||
// Setup event listeners
|
||||
const queuedConnectionHandler = vi.fn()
|
||||
server.addEventListener('queuedConnection', queuedConnectionHandler)
|
||||
|
||||
// Create mock sockets
|
||||
const socket1 = createMockSocket()
|
||||
const socket2 = createMockSocket()
|
||||
|
||||
// Handle first connection
|
||||
await (server as any).handleConnection(socket1)
|
||||
|
||||
// The first socket should be attached directly
|
||||
expect(attachSocketToNewHandlerSpy).toHaveBeenCalledWith(
|
||||
socket1,
|
||||
expect.anything(),
|
||||
)
|
||||
|
||||
// Handle second connection - should be queued
|
||||
await (server as any).handleConnection(socket2)
|
||||
|
||||
// The second connection should be queued
|
||||
expect(queuedConnectionHandler).toHaveBeenCalledTimes(1)
|
||||
expect(queuedConnectionHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: expect.objectContaining({
|
||||
queueSize: 1,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should process next connection when current connection closes', async () => {
|
||||
await server.start()
|
||||
|
||||
// Create mock sockets
|
||||
const socket1 = createMockSocket()
|
||||
const socket2 = createMockSocket()
|
||||
|
||||
// Setup event listener
|
||||
const connectionHandler = vi.fn()
|
||||
server.addEventListener('connection', connectionHandler)
|
||||
|
||||
// Handle first connection
|
||||
await (server as any).handleConnection(socket1)
|
||||
|
||||
// Handle second connection (will be queued)
|
||||
await (server as any).handleConnection(socket2)
|
||||
|
||||
// First connection should be active, but clear the handler for next assertions
|
||||
expect(connectionHandler).toHaveBeenCalled()
|
||||
connectionHandler.mockClear()
|
||||
|
||||
// Simulate closing the first connection
|
||||
const activeHandler = (server as any).activeHandler
|
||||
activeHandler.dispatchEvent(new CustomEvent('close'))
|
||||
|
||||
// The next connection should be processed
|
||||
expect(processNextInQueueSpy).toHaveBeenCalled()
|
||||
expect(attachSocketToNewHandlerSpy).toHaveBeenCalledWith(
|
||||
socket2,
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
|
||||
it('should timeout queued connections after specified time', async () => {
|
||||
await server.start()
|
||||
|
||||
// Setup event listeners
|
||||
const queueTimeoutHandler = vi.fn()
|
||||
server.addEventListener('queueTimeout', queueTimeoutHandler)
|
||||
|
||||
// Create mock sockets
|
||||
const socket1 = createMockSocket()
|
||||
const socket2 = createMockSocket()
|
||||
|
||||
// Handle first connection
|
||||
await (server as any).handleConnection(socket1)
|
||||
|
||||
// Handle second connection (will be queued)
|
||||
await (server as any).handleConnection(socket2)
|
||||
|
||||
// Fast-forward time to trigger timeout
|
||||
vi.advanceTimersByTime(1001)
|
||||
|
||||
// The queued connection should timeout
|
||||
expect(queueTimeoutHandler).toHaveBeenCalledTimes(1)
|
||||
expect(socket2.end).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should use default timeout value from CONNECTION_QUEUE_TIMEOUT', async () => {
|
||||
// Create server without specifying timeout
|
||||
const defaultServer = new PGLiteSocketServer({
|
||||
db,
|
||||
host: connOptions.host,
|
||||
port: connOptions.port,
|
||||
path: connOptions.path,
|
||||
})
|
||||
|
||||
// Check that it's using the default timeout
|
||||
expect((defaultServer as any).connectionQueueTimeout).toBe(
|
||||
CONNECTION_QUEUE_TIMEOUT,
|
||||
)
|
||||
})
|
||||
|
||||
it('should clean up queue when stopping the server', async () => {
|
||||
await server.start()
|
||||
|
||||
// Create mock sockets
|
||||
const socket1 = createMockSocket()
|
||||
const socket2 = createMockSocket()
|
||||
|
||||
// Handle first connection
|
||||
await (server as any).handleConnection(socket1)
|
||||
|
||||
// Handle second connection (will be queued)
|
||||
await (server as any).handleConnection(socket2)
|
||||
|
||||
// Stop the server
|
||||
await server.stop()
|
||||
|
||||
// All connections should be closed
|
||||
expect(socket1.end).toHaveBeenCalled()
|
||||
expect(socket2.end).toHaveBeenCalled()
|
||||
|
||||
// Queue should be emptied
|
||||
expect((server as any).connectionQueue).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should start server with OS-assigned port when port is 0', async () => {
|
||||
server = new PGLiteSocketServer({
|
||||
db,
|
||||
host: connOptions.host,
|
||||
port: 0, // Let OS assign port
|
||||
})
|
||||
|
||||
await server.start()
|
||||
const assignedPort = (server as any).port
|
||||
expect(assignedPort).toBeGreaterThan(1024)
|
||||
|
||||
// Try to connect to confirm server is running
|
||||
const client = createConnection({
|
||||
port: assignedPort,
|
||||
host: connOptions.host,
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
client.on('error', () => {
|
||||
reject(new Error('Connection should have failed'))
|
||||
})
|
||||
client.on('connect', () => {
|
||||
client.end()
|
||||
resolve()
|
||||
})
|
||||
setTimeout(resolve, 100)
|
||||
})
|
||||
|
||||
await server.stop()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
717
_node_modules/@electric-sql/pglite-socket/tests/query-with-node-pg.test.ts
generated
Normal file
717
_node_modules/@electric-sql/pglite-socket/tests/query-with-node-pg.test.ts
generated
Normal file
@@ -0,0 +1,717 @@
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeAll,
|
||||
afterAll,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
} from 'vitest'
|
||||
import { Client } from 'pg'
|
||||
import { PGlite } from '@electric-sql/pglite'
|
||||
import { PGLiteSocketServer } from '../src'
|
||||
import { spawn, ChildProcess } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
import fs from 'fs'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
/**
|
||||
* Debug configuration for testing
|
||||
*
|
||||
* To test against a real PostgreSQL server:
|
||||
* - Set DEBUG_TESTS=true as an environment variable
|
||||
* - Optionally set DEBUG_TESTS_REAL_SERVER with a connection URL (defaults to localhost)
|
||||
*
|
||||
* Example:
|
||||
* DEBUG_TESTS=true DEBUG_TESTS_REAL_SERVER=postgres://user:pass@host:port/db npm vitest ./tests/query-with-node-pg.test.ts
|
||||
*/
|
||||
const DEBUG_TESTS = process.env.DEBUG_TESTS === 'true'
|
||||
const DEBUG_TESTS_REAL_SERVER =
|
||||
process.env.DEBUG_TESTS_REAL_SERVER ||
|
||||
'postgres://postgres:postgres@localhost:5432/postgres'
|
||||
const TEST_PORT = 5434
|
||||
|
||||
describe(`PGLite Socket Server`, () => {
|
||||
describe('with node-pg client', () => {
|
||||
let db: PGlite
|
||||
let server: PGLiteSocketServer
|
||||
let client: typeof Client.prototype
|
||||
let connectionConfig: any
|
||||
|
||||
beforeAll(async () => {
|
||||
if (DEBUG_TESTS) {
|
||||
console.log('TESTING WITH REAL POSTGRESQL SERVER')
|
||||
console.log(`Connection URL: ${DEBUG_TESTS_REAL_SERVER}`)
|
||||
} else {
|
||||
console.log('TESTING WITH PGLITE SERVER')
|
||||
|
||||
// Create a PGlite instance
|
||||
db = await PGlite.create()
|
||||
|
||||
// Wait for database to be ready
|
||||
await db.waitReady
|
||||
|
||||
console.log('PGLite database ready')
|
||||
|
||||
// Create and start the server with explicit host
|
||||
server = new PGLiteSocketServer({
|
||||
db,
|
||||
port: TEST_PORT,
|
||||
host: '127.0.0.1',
|
||||
})
|
||||
|
||||
// Add event listeners for debugging
|
||||
server.addEventListener('error', (event) => {
|
||||
console.error('Socket server error:', (event as CustomEvent).detail)
|
||||
})
|
||||
|
||||
server.addEventListener('connection', (event) => {
|
||||
console.log(
|
||||
'Socket connection received:',
|
||||
(event as CustomEvent).detail,
|
||||
)
|
||||
})
|
||||
|
||||
await server.start()
|
||||
console.log(`PGLite Socket Server started on port ${TEST_PORT}`)
|
||||
|
||||
connectionConfig = {
|
||||
host: '127.0.0.1',
|
||||
port: TEST_PORT,
|
||||
database: 'postgres',
|
||||
user: 'postgres',
|
||||
password: 'postgres',
|
||||
// Connection timeout in milliseconds
|
||||
connectionTimeoutMillis: 10000,
|
||||
// Query timeout in milliseconds
|
||||
statement_timeout: 5000,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (!DEBUG_TESTS) {
|
||||
// Stop server if running
|
||||
if (server) {
|
||||
await server.stop()
|
||||
console.log('PGLite Socket Server stopped')
|
||||
}
|
||||
|
||||
// Close database
|
||||
if (db) {
|
||||
await db.close()
|
||||
console.log('PGLite database closed')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create pg client instance before each test
|
||||
if (DEBUG_TESTS) {
|
||||
// Direct connection to real PostgreSQL server using URL
|
||||
client = new Client({
|
||||
connectionString: DEBUG_TESTS_REAL_SERVER,
|
||||
connectionTimeoutMillis: 10000,
|
||||
statement_timeout: 5000,
|
||||
})
|
||||
} else {
|
||||
// Connection to PGLite Socket Server
|
||||
client = new Client(connectionConfig)
|
||||
}
|
||||
|
||||
// Connect the client
|
||||
await client.connect()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up any tables created in tests
|
||||
try {
|
||||
await client.query('DROP TABLE IF EXISTS test_users')
|
||||
} catch (e) {
|
||||
console.error('Error cleaning up tables:', e)
|
||||
}
|
||||
|
||||
// Disconnect the client after each test
|
||||
if (client) {
|
||||
await client.end()
|
||||
}
|
||||
})
|
||||
|
||||
it('should execute a basic SELECT query', async () => {
|
||||
const result = await client.query('SELECT 1 as one')
|
||||
expect(result.rows[0].one).toBe(1)
|
||||
})
|
||||
|
||||
it('should create a table', async () => {
|
||||
await client.query(`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
|
||||
// Verify table exists by querying the schema
|
||||
const tableCheck = await client.query(`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'test_users'
|
||||
`)
|
||||
|
||||
expect(tableCheck.rows.length).toBe(1)
|
||||
expect(tableCheck.rows[0].table_name).toBe('test_users')
|
||||
})
|
||||
|
||||
it('should insert rows into a table', async () => {
|
||||
// Create table
|
||||
await client.query(`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT
|
||||
)
|
||||
`)
|
||||
|
||||
// Insert data
|
||||
const insertResult = await client.query(`
|
||||
INSERT INTO test_users (name, email)
|
||||
VALUES
|
||||
('Alice', 'alice@example.com'),
|
||||
('Bob', 'bob@example.com')
|
||||
RETURNING *
|
||||
`)
|
||||
|
||||
expect(insertResult.rows.length).toBe(2)
|
||||
expect(insertResult.rows[0].name).toBe('Alice')
|
||||
expect(insertResult.rows[1].name).toBe('Bob')
|
||||
|
||||
// Verify data is there
|
||||
const count = await client.query(
|
||||
'SELECT COUNT(*)::int as count FROM test_users',
|
||||
)
|
||||
expect(count.rows[0].count).toBe(2)
|
||||
})
|
||||
|
||||
it('should update rows in a table', async () => {
|
||||
// Create and populate table
|
||||
await client.query(`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT
|
||||
)
|
||||
`)
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO test_users (name, email)
|
||||
VALUES ('Alice', 'alice@example.com')
|
||||
`)
|
||||
|
||||
// Update
|
||||
const updateResult = await client.query(`
|
||||
UPDATE test_users
|
||||
SET email = 'alice.new@example.com'
|
||||
WHERE name = 'Alice'
|
||||
RETURNING *
|
||||
`)
|
||||
|
||||
expect(updateResult.rows.length).toBe(1)
|
||||
expect(updateResult.rows[0].email).toBe('alice.new@example.com')
|
||||
})
|
||||
|
||||
it('should delete rows from a table', async () => {
|
||||
// Create and populate table
|
||||
await client.query(`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT
|
||||
)
|
||||
`)
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO test_users (name, email)
|
||||
VALUES
|
||||
('Alice', 'alice@example.com'),
|
||||
('Bob', 'bob@example.com')
|
||||
`)
|
||||
|
||||
// Delete
|
||||
const deleteResult = await client.query(`
|
||||
DELETE FROM test_users
|
||||
WHERE name = 'Alice'
|
||||
RETURNING *
|
||||
`)
|
||||
|
||||
expect(deleteResult.rows.length).toBe(1)
|
||||
expect(deleteResult.rows[0].name).toBe('Alice')
|
||||
|
||||
// Verify only Bob remains
|
||||
const remaining = await client.query('SELECT * FROM test_users')
|
||||
expect(remaining.rows.length).toBe(1)
|
||||
expect(remaining.rows[0].name).toBe('Bob')
|
||||
})
|
||||
|
||||
it('should execute operations in a transaction', async () => {
|
||||
// Create table
|
||||
await client.query(`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
balance INTEGER DEFAULT 0
|
||||
)
|
||||
`)
|
||||
|
||||
// Insert initial data
|
||||
await client.query(`
|
||||
INSERT INTO test_users (name, balance)
|
||||
VALUES ('Alice', 100), ('Bob', 50)
|
||||
`)
|
||||
|
||||
// Start a transaction and perform operations
|
||||
await client.query('BEGIN')
|
||||
|
||||
try {
|
||||
// Deduct from Alice
|
||||
await client.query(`
|
||||
UPDATE test_users
|
||||
SET balance = balance - 30
|
||||
WHERE name = 'Alice'
|
||||
`)
|
||||
|
||||
// Add to Bob
|
||||
await client.query(`
|
||||
UPDATE test_users
|
||||
SET balance = balance + 30
|
||||
WHERE name = 'Bob'
|
||||
`)
|
||||
|
||||
// Commit the transaction
|
||||
await client.query('COMMIT')
|
||||
} catch (error) {
|
||||
// Rollback on error
|
||||
await client.query('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
|
||||
// Verify both operations succeeded
|
||||
const users = await client.query(
|
||||
'SELECT name, balance FROM test_users ORDER BY name',
|
||||
)
|
||||
|
||||
expect(users.rows.length).toBe(2)
|
||||
expect(users.rows[0].name).toBe('Alice')
|
||||
expect(users.rows[0].balance).toBe(70)
|
||||
expect(users.rows[1].name).toBe('Bob')
|
||||
expect(users.rows[1].balance).toBe(80)
|
||||
})
|
||||
|
||||
it('should rollback a transaction on ROLLBACK', async () => {
|
||||
// Create table
|
||||
await client.query(`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
balance INTEGER DEFAULT 0
|
||||
)
|
||||
`)
|
||||
|
||||
// Insert initial data
|
||||
await client.query(`
|
||||
INSERT INTO test_users (name, balance)
|
||||
VALUES ('Alice', 100), ('Bob', 50)
|
||||
`)
|
||||
|
||||
// Get initial balance
|
||||
const initialResult = await client.query(`
|
||||
SELECT balance FROM test_users WHERE name = 'Alice'
|
||||
`)
|
||||
const initialBalance = initialResult.rows[0].balance
|
||||
|
||||
// Start a transaction
|
||||
await client.query('BEGIN')
|
||||
|
||||
try {
|
||||
// Deduct from Alice
|
||||
await client.query(`
|
||||
UPDATE test_users
|
||||
SET balance = balance - 30
|
||||
WHERE name = 'Alice'
|
||||
`)
|
||||
|
||||
// Verify balance is changed within transaction
|
||||
const midResult = await client.query(`
|
||||
SELECT balance FROM test_users WHERE name = 'Alice'
|
||||
`)
|
||||
expect(midResult.rows[0].balance).toBe(70)
|
||||
|
||||
// Explicitly roll back (cancel) the transaction
|
||||
await client.query('ROLLBACK')
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
|
||||
// Verify balance wasn't changed after rollback
|
||||
const finalResult = await client.query(`
|
||||
SELECT balance FROM test_users WHERE name = 'Alice'
|
||||
`)
|
||||
expect(finalResult.rows[0].balance).toBe(initialBalance)
|
||||
})
|
||||
|
||||
it('should rollback a transaction on error', async () => {
|
||||
// Create table
|
||||
await client.query(`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
balance INTEGER DEFAULT 0
|
||||
)
|
||||
`)
|
||||
|
||||
// Insert initial data
|
||||
await client.query(`
|
||||
INSERT INTO test_users (name, balance)
|
||||
VALUES ('Alice', 100), ('Bob', 50)
|
||||
`)
|
||||
|
||||
try {
|
||||
// Start a transaction
|
||||
await client.query('BEGIN')
|
||||
|
||||
// Deduct from Alice
|
||||
await client.query(`
|
||||
UPDATE test_users
|
||||
SET balance = balance - 30
|
||||
WHERE name = 'Alice'
|
||||
`)
|
||||
|
||||
// This will trigger an error
|
||||
await client.query(`
|
||||
UPDATE test_users_nonexistent
|
||||
SET balance = balance + 30
|
||||
WHERE name = 'Bob'
|
||||
`)
|
||||
|
||||
// Should never get here
|
||||
await client.query('COMMIT')
|
||||
} catch (error) {
|
||||
// Expected to fail - rollback transaction
|
||||
await client.query('ROLLBACK').catch(() => {
|
||||
// If the client connection is in a bad state, we just ignore
|
||||
// the rollback error
|
||||
})
|
||||
}
|
||||
|
||||
// Verify Alice's balance was not changed due to rollback
|
||||
const users = await client.query(
|
||||
'SELECT name, balance FROM test_users ORDER BY name',
|
||||
)
|
||||
|
||||
expect(users.rows.length).toBe(2)
|
||||
expect(users.rows[0].name).toBe('Alice')
|
||||
expect(users.rows[0].balance).toBe(100) // Should remain 100 after rollback
|
||||
})
|
||||
|
||||
it('should handle a syntax error', async () => {
|
||||
// Expect syntax error
|
||||
let errorMessage = ''
|
||||
try {
|
||||
await client.query('THIS IS NOT VALID SQL;')
|
||||
} catch (error) {
|
||||
errorMessage = (error as Error).message
|
||||
}
|
||||
|
||||
expect(errorMessage).not.toBe('')
|
||||
expect(errorMessage.toLowerCase()).toContain('syntax error')
|
||||
})
|
||||
|
||||
it('should support cursor-based pagination', async () => {
|
||||
// Create a test table with many rows
|
||||
await client.query(`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
value INTEGER
|
||||
)
|
||||
`)
|
||||
|
||||
// Insert 100 rows using generate_series (server-side generation)
|
||||
await client.query(`
|
||||
INSERT INTO test_users (name, value)
|
||||
SELECT
|
||||
'User ' || i as name,
|
||||
i as value
|
||||
FROM generate_series(1, 100) as i
|
||||
`)
|
||||
|
||||
// Use a cursor to read data in smaller chunks
|
||||
const chunkSize = 10
|
||||
let results: any[] = []
|
||||
let page = 0
|
||||
|
||||
try {
|
||||
// Begin transaction
|
||||
await client.query('BEGIN')
|
||||
|
||||
// Declare a cursor
|
||||
await client.query(
|
||||
'DECLARE user_cursor CURSOR FOR SELECT * FROM test_users ORDER BY id',
|
||||
)
|
||||
|
||||
let hasMoreData = true
|
||||
while (hasMoreData) {
|
||||
// Fetch a batch of results
|
||||
const chunk = await client.query('FETCH 10 FROM user_cursor')
|
||||
|
||||
// If no rows returned, we're done
|
||||
if (chunk.rows.length === 0) {
|
||||
hasMoreData = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Process this chunk
|
||||
page++
|
||||
|
||||
// Add to our results array
|
||||
results = [...results, ...chunk.rows]
|
||||
|
||||
// Verify each chunk has correct data (except possibly the last one)
|
||||
if (chunk.rows.length === chunkSize) {
|
||||
expect(chunk.rows.length).toBe(chunkSize)
|
||||
expect(chunk.rows[0].id).toBe((page - 1) * chunkSize + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Close the cursor
|
||||
await client.query('CLOSE user_cursor')
|
||||
|
||||
// Commit transaction
|
||||
await client.query('COMMIT')
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
|
||||
// Verify we got all 100 records
|
||||
expect(results.length).toBe(100)
|
||||
expect(results[0].name).toBe('User 1')
|
||||
expect(results[99].name).toBe('User 100')
|
||||
|
||||
// Verify we received the expected number of pages
|
||||
expect(page).toBe(Math.ceil(100 / chunkSize))
|
||||
})
|
||||
|
||||
it('should support LISTEN/NOTIFY for pub/sub messaging', async () => {
|
||||
// Set up listener for notifications
|
||||
let receivedPayload = ''
|
||||
const notificationReceived = new Promise<void>((resolve) => {
|
||||
client.on('notification', (msg) => {
|
||||
receivedPayload = msg.payload || ''
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
// Start listening
|
||||
await client.query('LISTEN test_channel')
|
||||
|
||||
// Small delay to ensure listener is set up
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Send a notification
|
||||
await client.query("NOTIFY test_channel, 'Hello from PGlite!'")
|
||||
|
||||
// Wait for the notification to be received with an appropriate timeout
|
||||
const timeoutPromise = new Promise<void>((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Notification timeout')), 2000)
|
||||
})
|
||||
|
||||
await Promise.race([notificationReceived, timeoutPromise]).catch(
|
||||
(error) => {
|
||||
console.error('Notification error:', error)
|
||||
},
|
||||
)
|
||||
|
||||
// Verify the notification was received with the correct payload
|
||||
expect(receivedPayload).toBe('Hello from PGlite!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with extensions via CLI', () => {
|
||||
const UNIX_SOCKET_DIR_PATH = `/tmp/${Date.now().toString()}`
|
||||
fs.mkdirSync(UNIX_SOCKET_DIR_PATH)
|
||||
const UNIX_SOCKET_PATH = `${UNIX_SOCKET_DIR_PATH}/.s.PGSQL.5432`
|
||||
let serverProcess: ChildProcess | null = null
|
||||
let client: typeof Client.prototype
|
||||
|
||||
beforeAll(async () => {
|
||||
// Start the server with extensions via CLI using tsx for dev or node for dist
|
||||
const serverScript = join(__dirname, '../src/scripts/server.ts')
|
||||
serverProcess = spawn(
|
||||
'npx',
|
||||
[
|
||||
'tsx',
|
||||
serverScript,
|
||||
'--path',
|
||||
UNIX_SOCKET_PATH,
|
||||
'--extensions',
|
||||
'vector,pg_uuidv7,@electric-sql/pglite/pg_hashids:pg_hashids',
|
||||
],
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
|
||||
// Wait for server to be ready by checking for "listening" message
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Server startup timeout'))
|
||||
}, 30000)
|
||||
|
||||
const onData = (data: Buffer) => {
|
||||
const output = data.toString()
|
||||
if (output.includes('listening')) {
|
||||
clearTimeout(timeout)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
serverProcess!.stdout?.on('data', onData)
|
||||
serverProcess!.stderr?.on('data', (data) => {
|
||||
console.error('Server stderr:', data.toString())
|
||||
})
|
||||
|
||||
serverProcess!.on('error', (err) => {
|
||||
clearTimeout(timeout)
|
||||
reject(err)
|
||||
})
|
||||
|
||||
serverProcess!.on('exit', (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error(`Server exited with code ${code}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
console.log('Server with extensions started')
|
||||
|
||||
client = new Client({
|
||||
host: UNIX_SOCKET_DIR_PATH,
|
||||
database: 'postgres',
|
||||
user: 'postgres',
|
||||
password: 'postgres',
|
||||
connectionTimeoutMillis: 10000,
|
||||
})
|
||||
await client.connect()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (client) {
|
||||
await client.end().catch(() => {})
|
||||
}
|
||||
|
||||
if (serverProcess) {
|
||||
serverProcess.kill('SIGTERM')
|
||||
await new Promise<void>((resolve) => {
|
||||
serverProcess!.on('exit', () => resolve())
|
||||
setTimeout(resolve, 2000) // Force resolve after 2s
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('should load and use vector extension', async () => {
|
||||
// Create the extension
|
||||
await client.query('CREATE EXTENSION IF NOT EXISTS vector')
|
||||
|
||||
// Verify extension is loaded
|
||||
const extCheck = await client.query(`
|
||||
SELECT extname FROM pg_extension WHERE extname = 'vector'
|
||||
`)
|
||||
expect(extCheck.rows).toHaveLength(1)
|
||||
expect(extCheck.rows[0].extname).toBe('vector')
|
||||
|
||||
// Create a table with vector column
|
||||
await client.query(`
|
||||
CREATE TABLE test_vectors (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT,
|
||||
vec vector(3)
|
||||
)
|
||||
`)
|
||||
|
||||
// Insert test data
|
||||
await client.query(`
|
||||
INSERT INTO test_vectors (name, vec) VALUES
|
||||
('test1', '[1,2,3]'),
|
||||
('test2', '[4,5,6]'),
|
||||
('test3', '[7,8,9]')
|
||||
`)
|
||||
|
||||
// Query with vector distance
|
||||
const result = await client.query(`
|
||||
SELECT name, vec, vec <-> '[3,1,2]' AS distance
|
||||
FROM test_vectors
|
||||
ORDER BY distance
|
||||
`)
|
||||
|
||||
expect(result.rows).toHaveLength(3)
|
||||
expect(result.rows[0].name).toBe('test1')
|
||||
expect(result.rows[0].vec).toBe('[1,2,3]')
|
||||
expect(parseFloat(result.rows[0].distance)).toBeCloseTo(2.449, 2)
|
||||
})
|
||||
|
||||
it('should load and use pg_uuidv7 extension', async () => {
|
||||
// Create the extension
|
||||
await client.query('CREATE EXTENSION IF NOT EXISTS pg_uuidv7')
|
||||
|
||||
// Verify extension is loaded
|
||||
const extCheck = await client.query(`
|
||||
SELECT extname FROM pg_extension WHERE extname = 'pg_uuidv7'
|
||||
`)
|
||||
expect(extCheck.rows).toHaveLength(1)
|
||||
expect(extCheck.rows[0].extname).toBe('pg_uuidv7')
|
||||
|
||||
// Generate a UUIDv7
|
||||
const result = await client.query('SELECT uuid_generate_v7() as uuid')
|
||||
expect(result.rows[0].uuid).toHaveLength(36)
|
||||
|
||||
// Test uuid_v7_to_timestamptz function
|
||||
const tsResult = await client.query(`
|
||||
SELECT uuid_v7_to_timestamptz('018570bb-4a7d-7c7e-8df4-6d47afd8c8fc') as ts
|
||||
`)
|
||||
const timestamp = new Date(tsResult.rows[0].ts)
|
||||
expect(timestamp.toISOString()).toBe('2023-01-02T04:26:40.637Z')
|
||||
})
|
||||
|
||||
it('should load and use pg_hashids extension from npm package path', async () => {
|
||||
// Create the extension
|
||||
await client.query('CREATE EXTENSION IF NOT EXISTS pg_hashids')
|
||||
|
||||
// Verify extension is loaded
|
||||
const extCheck = await client.query(`
|
||||
SELECT extname FROM pg_extension WHERE extname = 'pg_hashids'
|
||||
`)
|
||||
expect(extCheck.rows).toHaveLength(1)
|
||||
expect(extCheck.rows[0].extname).toBe('pg_hashids')
|
||||
|
||||
// Test id_encode function
|
||||
const result = await client.query(`
|
||||
SELECT id_encode(1234567, 'salt', 10, 'abcdefghijABCDEFGHIJ1234567890') as hash
|
||||
`)
|
||||
expect(result.rows[0].hash).toBeTruthy()
|
||||
expect(typeof result.rows[0].hash).toBe('string')
|
||||
|
||||
// Test id_decode function (round-trip)
|
||||
const hash = result.rows[0].hash
|
||||
const decodeResult = await client.query(`
|
||||
SELECT id_decode('${hash}', 'salt', 10, 'abcdefghijABCDEFGHIJ1234567890') as id
|
||||
`)
|
||||
expect(decodeResult.rows[0].id[0]).toBe('1234567')
|
||||
})
|
||||
})
|
||||
})
|
||||
678
_node_modules/@electric-sql/pglite-socket/tests/query-with-postgres-js.test.ts
generated
Normal file
678
_node_modules/@electric-sql/pglite-socket/tests/query-with-postgres-js.test.ts
generated
Normal file
@@ -0,0 +1,678 @@
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeAll,
|
||||
afterAll,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
} from 'vitest'
|
||||
import postgres from 'postgres'
|
||||
import { PGlite } from '@electric-sql/pglite'
|
||||
import { PGLiteSocketServer } from '../src'
|
||||
import { spawn, ChildProcess } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
import fs from 'fs'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
/**
|
||||
* Debug configuration for testing
|
||||
*
|
||||
* To test against a real PostgreSQL server:
|
||||
* - Set DEBUG_TESTS=true as an environment variable
|
||||
* - Optionally set DEBUG_TESTS_REAL_SERVER with a connection URL (defaults to localhost)
|
||||
*
|
||||
* Example:
|
||||
* DEBUG_TESTS=true DEBUG_TESTS_REAL_SERVER=postgres://user:pass@host:port/db npm vitest ./tests/query-with-postgres-js.test.ts
|
||||
*/
|
||||
const DEBUG_LOCAL = process.env.DEBUG_LOCAL === 'true'
|
||||
const DEBUG_TESTS = process.env.DEBUG_TESTS === 'true'
|
||||
const DEBUG_TESTS_REAL_SERVER =
|
||||
process.env.DEBUG_TESTS_REAL_SERVER ||
|
||||
'postgres://postgres:postgres@localhost:5432/postgres'
|
||||
const TEST_PORT = 5434
|
||||
|
||||
describe(`PGLite Socket Server`, () => {
|
||||
describe('with postgres.js client', () => {
|
||||
let db: PGlite
|
||||
let server: PGLiteSocketServer
|
||||
let sql: ReturnType<typeof postgres>
|
||||
let connectionConfig: any
|
||||
|
||||
beforeAll(async () => {
|
||||
if (DEBUG_TESTS) {
|
||||
console.log('TESTING WITH REAL POSTGRESQL SERVER')
|
||||
console.log(`Connection URL: ${DEBUG_TESTS_REAL_SERVER}`)
|
||||
} else {
|
||||
console.log('TESTING WITH PGLITE SERVER')
|
||||
|
||||
// Create a PGlite instance
|
||||
if (DEBUG_LOCAL) db = await PGlite.create({ debug: '1' })
|
||||
else db = await PGlite.create()
|
||||
|
||||
// Wait for database to be ready
|
||||
await db.waitReady
|
||||
|
||||
console.log('PGLite database ready')
|
||||
|
||||
// Create and start the server with explicit host
|
||||
server = new PGLiteSocketServer({
|
||||
db,
|
||||
port: TEST_PORT,
|
||||
host: '127.0.0.1',
|
||||
inspect: DEBUG_TESTS || DEBUG_LOCAL,
|
||||
})
|
||||
|
||||
// Add event listeners for debugging
|
||||
server.addEventListener('error', (event) => {
|
||||
console.error('Socket server error:', (event as CustomEvent).detail)
|
||||
})
|
||||
|
||||
server.addEventListener('connection', (event) => {
|
||||
console.log(
|
||||
'Socket connection received:',
|
||||
(event as CustomEvent).detail,
|
||||
)
|
||||
})
|
||||
|
||||
await server.start()
|
||||
console.log(`PGLite Socket Server started on port ${TEST_PORT}`)
|
||||
|
||||
connectionConfig = {
|
||||
host: '127.0.0.1',
|
||||
port: TEST_PORT,
|
||||
database: 'postgres',
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
idle_timeout: 5,
|
||||
connect_timeout: 10,
|
||||
max: 1,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (!DEBUG_TESTS) {
|
||||
// Stop server if running
|
||||
if (server) {
|
||||
await server.stop()
|
||||
console.log('PGLite Socket Server stopped')
|
||||
}
|
||||
|
||||
// Close database
|
||||
if (db) {
|
||||
await db.close()
|
||||
console.log('PGLite database closed')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a postgres client instance before each test
|
||||
if (DEBUG_TESTS) {
|
||||
// Direct connection to real PostgreSQL server using URL
|
||||
sql = postgres(DEBUG_TESTS_REAL_SERVER, {
|
||||
idle_timeout: 5,
|
||||
connect_timeout: 10,
|
||||
max: 1,
|
||||
})
|
||||
} else {
|
||||
// Connection to PGLite Socket Server
|
||||
sql = postgres(connectionConfig)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up any tables created in tests
|
||||
try {
|
||||
await sql`DROP TABLE IF EXISTS test_users`
|
||||
} catch (e) {
|
||||
console.error('Error cleaning up tables:', e)
|
||||
}
|
||||
|
||||
// Disconnect the client after each test
|
||||
if (sql) {
|
||||
await sql.end()
|
||||
}
|
||||
})
|
||||
if (!DEBUG_LOCAL) {
|
||||
it('should execute a basic SELECT query', async () => {
|
||||
const result = await sql`SELECT 1 as one`
|
||||
expect(result[0].one).toBe(1)
|
||||
})
|
||||
|
||||
it('should create a table', async () => {
|
||||
await sql`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`
|
||||
|
||||
// Verify table exists by querying the schema
|
||||
const tableCheck = await sql`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'test_users'
|
||||
`
|
||||
|
||||
expect(tableCheck.length).toBe(1)
|
||||
expect(tableCheck[0].table_name).toBe('test_users')
|
||||
})
|
||||
|
||||
it('should insert rows into a table', async () => {
|
||||
// Create table
|
||||
await sql`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT
|
||||
)
|
||||
`
|
||||
|
||||
// Insert data
|
||||
const insertResult = await sql`
|
||||
INSERT INTO test_users (name, email)
|
||||
VALUES
|
||||
('Alice', 'alice@example.com'),
|
||||
('Bob', 'bob@example.com')
|
||||
RETURNING *
|
||||
`
|
||||
|
||||
expect(insertResult.length).toBe(2)
|
||||
expect(insertResult[0].name).toBe('Alice')
|
||||
expect(insertResult[1].name).toBe('Bob')
|
||||
|
||||
// Verify data is there
|
||||
const count = await sql`SELECT COUNT(*)::int as count FROM test_users`
|
||||
expect(count[0].count).toBe(2)
|
||||
})
|
||||
|
||||
it('should update rows in a table', async () => {
|
||||
// Create and populate table
|
||||
await sql`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
INSERT INTO test_users (name, email)
|
||||
VALUES ('Alice', 'alice@example.com')
|
||||
`
|
||||
|
||||
// Update
|
||||
const updateResult = await sql`
|
||||
UPDATE test_users
|
||||
SET email = 'alice.new@example.com'
|
||||
WHERE name = 'Alice'
|
||||
RETURNING *
|
||||
`
|
||||
|
||||
expect(updateResult.length).toBe(1)
|
||||
expect(updateResult[0].email).toBe('alice.new@example.com')
|
||||
})
|
||||
|
||||
it('should delete rows from a table', async () => {
|
||||
// Create and populate table
|
||||
await sql`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
INSERT INTO test_users (name, email)
|
||||
VALUES
|
||||
('Alice', 'alice@example.com'),
|
||||
('Bob', 'bob@example.com')
|
||||
`
|
||||
|
||||
// Delete
|
||||
const deleteResult = await sql`
|
||||
DELETE FROM test_users
|
||||
WHERE name = 'Alice'
|
||||
RETURNING *
|
||||
`
|
||||
|
||||
expect(deleteResult.length).toBe(1)
|
||||
expect(deleteResult[0].name).toBe('Alice')
|
||||
|
||||
// Verify only Bob remains
|
||||
const remaining = await sql`SELECT * FROM test_users`
|
||||
expect(remaining.length).toBe(1)
|
||||
expect(remaining[0].name).toBe('Bob')
|
||||
})
|
||||
|
||||
it('should execute operations in a transaction', async () => {
|
||||
// Create table
|
||||
await sql`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
balance INTEGER DEFAULT 0
|
||||
)
|
||||
`
|
||||
|
||||
// Insert initial data
|
||||
await sql`
|
||||
INSERT INTO test_users (name, balance)
|
||||
VALUES ('Alice', 100), ('Bob', 50)
|
||||
`
|
||||
|
||||
// Start a transaction and perform operations
|
||||
await sql.begin(async (tx) => {
|
||||
// Deduct from Alice
|
||||
await tx`
|
||||
UPDATE test_users
|
||||
SET balance = balance - 30
|
||||
WHERE name = 'Alice'
|
||||
`
|
||||
|
||||
// Add to Bob
|
||||
await tx`
|
||||
UPDATE test_users
|
||||
SET balance = balance + 30
|
||||
WHERE name = 'Bob'
|
||||
`
|
||||
})
|
||||
|
||||
// Verify both operations succeeded
|
||||
const users =
|
||||
await sql`SELECT name, balance FROM test_users ORDER BY name`
|
||||
|
||||
expect(users.length).toBe(2)
|
||||
expect(users[0].name).toBe('Alice')
|
||||
expect(users[0].balance).toBe(70)
|
||||
expect(users[1].name).toBe('Bob')
|
||||
expect(users[1].balance).toBe(80)
|
||||
})
|
||||
|
||||
it('should rollback a transaction on ROLLBACK', async () => {
|
||||
// Create table
|
||||
await sql`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
balance INTEGER DEFAULT 0
|
||||
)
|
||||
`
|
||||
|
||||
// Insert initial data
|
||||
await sql`
|
||||
INSERT INTO test_users (name, balance)
|
||||
VALUES ('Alice', 100), ('Bob', 50)
|
||||
`
|
||||
|
||||
// Get initial balance
|
||||
const initialResult = await sql`
|
||||
SELECT balance FROM test_users WHERE name = 'Alice'
|
||||
`
|
||||
const initialBalance = initialResult[0].balance
|
||||
|
||||
// Start a transaction
|
||||
await sql
|
||||
.begin(async (tx) => {
|
||||
// Deduct from Alice
|
||||
await tx`
|
||||
UPDATE test_users
|
||||
SET balance = balance - 30
|
||||
WHERE name = 'Alice'
|
||||
`
|
||||
|
||||
// Verify balance is changed within transaction
|
||||
const midResult = await tx`
|
||||
SELECT balance FROM test_users WHERE name = 'Alice'
|
||||
`
|
||||
expect(midResult[0].balance).toBe(70)
|
||||
|
||||
// Explicitly roll back (cancel) the transaction
|
||||
throw new Error('Triggering rollback')
|
||||
})
|
||||
.catch(() => {
|
||||
// Expected error to trigger rollback
|
||||
console.log('Transaction was rolled back as expected')
|
||||
})
|
||||
|
||||
// Verify balance wasn't changed after rollback
|
||||
const finalResult = await sql`
|
||||
SELECT balance FROM test_users WHERE name = 'Alice'
|
||||
`
|
||||
expect(finalResult[0].balance).toBe(initialBalance)
|
||||
})
|
||||
|
||||
it('should rollback a transaction on error', async () => {
|
||||
// Create table
|
||||
await sql`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
balance INTEGER DEFAULT 0
|
||||
)
|
||||
`
|
||||
|
||||
// Insert initial data
|
||||
await sql`
|
||||
INSERT INTO test_users (name, balance)
|
||||
VALUES ('Alice', 100), ('Bob', 50)
|
||||
`
|
||||
|
||||
// Start a transaction that will fail
|
||||
try {
|
||||
await sql.begin(async (tx) => {
|
||||
// Deduct from Alice
|
||||
await tx`
|
||||
UPDATE test_users
|
||||
SET balance = balance - 30
|
||||
WHERE name = 'Alice'
|
||||
`
|
||||
|
||||
// This will trigger an error
|
||||
await tx`
|
||||
UPDATE test_users_nonexistent
|
||||
SET balance = balance + 30
|
||||
WHERE name = 'Bob'
|
||||
`
|
||||
})
|
||||
} catch (error) {
|
||||
// Expected to fail
|
||||
}
|
||||
|
||||
// Verify Alice's balance was not changed due to rollback
|
||||
const users =
|
||||
await sql`SELECT name, balance FROM test_users ORDER BY name`
|
||||
|
||||
expect(users.length).toBe(2)
|
||||
expect(users[0].name).toBe('Alice')
|
||||
expect(users[0].balance).toBe(100) // Should remain 100 after rollback
|
||||
})
|
||||
|
||||
it('should handle a syntax error', async () => {
|
||||
// Expect syntax error
|
||||
let errorMessage = ''
|
||||
try {
|
||||
await sql`THIS IS NOT VALID SQL;`
|
||||
} catch (error) {
|
||||
errorMessage = (error as Error).message
|
||||
}
|
||||
|
||||
expect(errorMessage).not.toBe('')
|
||||
expect(errorMessage.toLowerCase()).toContain('syntax error')
|
||||
})
|
||||
|
||||
it('should support cursor-based pagination', async () => {
|
||||
// Create a test table with many rows
|
||||
await sql`
|
||||
CREATE TABLE test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
value INTEGER
|
||||
)
|
||||
`
|
||||
|
||||
// Insert 100 rows using generate_series (server-side generation)
|
||||
await sql`
|
||||
INSERT INTO test_users (name, value)
|
||||
SELECT
|
||||
'User ' || i as name,
|
||||
i as value
|
||||
FROM generate_series(1, 100) as i
|
||||
`
|
||||
|
||||
// Use a cursor to read data in smaller chunks
|
||||
const chunkSize = 10
|
||||
let results: any[] = []
|
||||
let page = 0
|
||||
|
||||
// Use a transaction for cursor operations (cursors must be in transactions)
|
||||
await sql.begin(async (tx) => {
|
||||
// Declare a cursor
|
||||
await tx`DECLARE user_cursor CURSOR FOR SELECT * FROM test_users ORDER BY id`
|
||||
|
||||
let hasMoreData = true
|
||||
while (hasMoreData) {
|
||||
// Fetch a batch of results
|
||||
const chunk = await tx`FETCH 10 FROM user_cursor`
|
||||
|
||||
// If no rows returned, we're done
|
||||
if (chunk.length === 0) {
|
||||
hasMoreData = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Process this chunk
|
||||
page++
|
||||
|
||||
// Add to our results array
|
||||
results = [...results, ...chunk]
|
||||
|
||||
// Verify each chunk has correct data (except possibly the last one)
|
||||
if (chunk.length === chunkSize) {
|
||||
expect(chunk.length).toBe(chunkSize)
|
||||
expect(chunk[0].id).toBe((page - 1) * chunkSize + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Close the cursor
|
||||
await tx`CLOSE user_cursor`
|
||||
})
|
||||
|
||||
// Verify we got all 100 records
|
||||
expect(results.length).toBe(100)
|
||||
expect(results[0].name).toBe('User 1')
|
||||
expect(results[99].name).toBe('User 100')
|
||||
|
||||
// Verify we received the expected number of pages
|
||||
expect(page).toBe(Math.ceil(100 / chunkSize))
|
||||
})
|
||||
} else {
|
||||
it('should support LISTEN/NOTIFY for pub/sub messaging', async () => {
|
||||
// Create a promise that will resolve when the notification is received
|
||||
let receivedPayload = ''
|
||||
const notificationPromise = new Promise<void>((resolve) => {
|
||||
// Set up listener for the 'test_channel' notification
|
||||
sql.listen('test_channel', (data) => {
|
||||
receivedPayload = data
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
// Small delay to ensure listener is set up
|
||||
// await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Send a notification on the same connection
|
||||
await sql`NOTIFY test_channel, 'Hello from PGlite!'`
|
||||
|
||||
// Wait for the notification to be received
|
||||
await notificationPromise
|
||||
|
||||
// Verify the notification was received with the correct payload
|
||||
expect(receivedPayload).toBe('Hello from PGlite!')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('with extensions via CLI', () => {
|
||||
const UNIX_SOCKET_DIR_PATH = `/tmp/${Date.now().toString()}`
|
||||
fs.mkdirSync(UNIX_SOCKET_DIR_PATH)
|
||||
const UNIX_SOCKET_PATH = `${UNIX_SOCKET_DIR_PATH}/.s.PGSQL.5432`
|
||||
let serverProcess: ChildProcess | null = null
|
||||
let sql: ReturnType<typeof postgres>
|
||||
|
||||
beforeAll(async () => {
|
||||
// Start the server with extensions via CLI using tsx for dev or node for dist
|
||||
const serverScript = join(__dirname, '../src/scripts/server.ts')
|
||||
serverProcess = spawn(
|
||||
'npx',
|
||||
[
|
||||
'tsx',
|
||||
serverScript,
|
||||
'--path',
|
||||
UNIX_SOCKET_PATH,
|
||||
'--extensions',
|
||||
'vector,pg_uuidv7,@electric-sql/pglite/pg_hashids:pg_hashids',
|
||||
],
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
|
||||
// Wait for server to be ready by checking for "listening" message
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Server startup timeout'))
|
||||
}, 30000)
|
||||
|
||||
const onData = (data: Buffer) => {
|
||||
const output = data.toString()
|
||||
if (output.includes('listening')) {
|
||||
clearTimeout(timeout)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
serverProcess!.stdout?.on('data', onData)
|
||||
serverProcess!.stderr?.on('data', (data) => {
|
||||
console.error('Server stderr:', data.toString())
|
||||
})
|
||||
|
||||
serverProcess!.on('error', (err) => {
|
||||
clearTimeout(timeout)
|
||||
reject(err)
|
||||
})
|
||||
|
||||
serverProcess!.on('exit', (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error(`Server exited with code ${code}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
console.log('Server with extensions started')
|
||||
|
||||
sql = postgres({
|
||||
path: UNIX_SOCKET_PATH,
|
||||
database: 'postgres',
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
idle_timeout: 5,
|
||||
connect_timeout: 10,
|
||||
max: 1,
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (sql) {
|
||||
await sql.end().catch(() => {})
|
||||
}
|
||||
|
||||
if (serverProcess) {
|
||||
serverProcess.kill('SIGTERM')
|
||||
await new Promise<void>((resolve) => {
|
||||
serverProcess!.on('exit', () => resolve())
|
||||
setTimeout(resolve, 2000) // Force resolve after 2s
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('should load and use vector extension', async () => {
|
||||
// Create the extension
|
||||
await sql`CREATE EXTENSION IF NOT EXISTS vector`
|
||||
|
||||
// Verify extension is loaded
|
||||
const extCheck = await sql`
|
||||
SELECT extname FROM pg_extension WHERE extname = 'vector'
|
||||
`
|
||||
expect(extCheck).toHaveLength(1)
|
||||
expect(extCheck[0].extname).toBe('vector')
|
||||
|
||||
// Create a table with vector column
|
||||
await sql`
|
||||
CREATE TABLE test_vectors (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT,
|
||||
vec vector(3)
|
||||
)
|
||||
`
|
||||
|
||||
// Insert test data
|
||||
await sql`
|
||||
INSERT INTO test_vectors (name, vec) VALUES
|
||||
('test1', '[1,2,3]'),
|
||||
('test2', '[4,5,6]'),
|
||||
('test3', '[7,8,9]')
|
||||
`
|
||||
|
||||
// Query with vector distance
|
||||
const result = await sql`
|
||||
SELECT name, vec, vec <-> '[3,1,2]' AS distance
|
||||
FROM test_vectors
|
||||
ORDER BY distance
|
||||
`
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].name).toBe('test1')
|
||||
expect(result[0].vec).toBe('[1,2,3]')
|
||||
expect(parseFloat(result[0].distance)).toBeCloseTo(2.449, 2)
|
||||
})
|
||||
|
||||
it('should load and use pg_uuidv7 extension', async () => {
|
||||
// Create the extension
|
||||
await sql`CREATE EXTENSION IF NOT EXISTS pg_uuidv7`
|
||||
|
||||
// Verify extension is loaded
|
||||
const extCheck = await sql`
|
||||
SELECT extname FROM pg_extension WHERE extname = 'pg_uuidv7'
|
||||
`
|
||||
expect(extCheck).toHaveLength(1)
|
||||
expect(extCheck[0].extname).toBe('pg_uuidv7')
|
||||
|
||||
// Generate a UUIDv7
|
||||
const result = await sql`SELECT uuid_generate_v7() as uuid`
|
||||
expect(result[0].uuid).toHaveLength(36)
|
||||
|
||||
// Test uuid_v7_to_timestamptz function
|
||||
const tsResult = await sql`
|
||||
SELECT uuid_v7_to_timestamptz('018570bb-4a7d-7c7e-8df4-6d47afd8c8fc') as ts
|
||||
`
|
||||
const timestamp = new Date(tsResult[0].ts)
|
||||
expect(timestamp.toISOString()).toBe('2023-01-02T04:26:40.637Z')
|
||||
})
|
||||
|
||||
it('should load and use pg_hashids extension from npm package path', async () => {
|
||||
// Create the extension
|
||||
await sql`CREATE EXTENSION IF NOT EXISTS pg_hashids`
|
||||
|
||||
// Verify extension is loaded
|
||||
const extCheck = await sql`
|
||||
SELECT extname FROM pg_extension WHERE extname = 'pg_hashids'
|
||||
`
|
||||
expect(extCheck).toHaveLength(1)
|
||||
expect(extCheck[0].extname).toBe('pg_hashids')
|
||||
|
||||
// Test id_encode function
|
||||
const result = await sql`
|
||||
SELECT id_encode(1234567, 'salt', 10, 'abcdefghijABCDEFGHIJ1234567890') as hash
|
||||
`
|
||||
expect(result[0].hash).toBeTruthy()
|
||||
expect(typeof result[0].hash).toBe('string')
|
||||
|
||||
// Test id_decode function (round-trip)
|
||||
const hash = result[0].hash
|
||||
const decodeResult = await sql`
|
||||
SELECT id_decode(${hash}, 'salt', 10, 'abcdefghijABCDEFGHIJ1234567890') as id
|
||||
`
|
||||
expect(decodeResult[0].id[0]).toBe('1234567')
|
||||
})
|
||||
})
|
||||
})
|
||||
233
_node_modules/@electric-sql/pglite-socket/tests/server.test.ts
generated
Normal file
233
_node_modules/@electric-sql/pglite-socket/tests/server.test.ts
generated
Normal file
@@ -0,0 +1,233 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest'
|
||||
import { spawn, ChildProcess } from 'node:child_process'
|
||||
import { createConnection } from 'net'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const serverScript = path.resolve(__dirname, '../src/scripts/server.ts')
|
||||
|
||||
// Helper to wait for a port to be available
|
||||
async function waitForPort(port: number, timeout = 15000): Promise<boolean> {
|
||||
const start = Date.now()
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
try {
|
||||
const socket = createConnection({ port, host: '127.0.0.1' })
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.on('connect', () => {
|
||||
socket.end()
|
||||
resolve()
|
||||
})
|
||||
socket.on('error', reject)
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
describe('Server Script Tests', () => {
|
||||
const TEST_PORT_BASE = 15500
|
||||
let currentTestPort = TEST_PORT_BASE
|
||||
|
||||
// Get a unique port for each test
|
||||
function getTestPort(): number {
|
||||
return ++currentTestPort
|
||||
}
|
||||
|
||||
describe('Help and Basic Functionality', () => {
|
||||
it('should show help when --help flag is used', async () => {
|
||||
const serverProcess = spawn('tsx', [serverScript, '--help'], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
})
|
||||
|
||||
let output = ''
|
||||
serverProcess.stdout?.on('data', (data) => {
|
||||
output += data.toString()
|
||||
})
|
||||
|
||||
serverProcess.stderr?.on('data', (data) => {
|
||||
console.error(data.toString())
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
serverProcess.on('exit', (code) => {
|
||||
expect(code).toBe(0)
|
||||
expect(output).toContain('PGlite Socket Server')
|
||||
expect(output).toContain('Usage:')
|
||||
expect(output).toContain('Options:')
|
||||
expect(output).toContain('--db')
|
||||
expect(output).toContain('--port')
|
||||
expect(output).toContain('--host')
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}, 10000)
|
||||
|
||||
it('should accept and use debug level parameter', async () => {
|
||||
const testPort = getTestPort()
|
||||
const serverProcess = spawn(
|
||||
'tsx',
|
||||
[serverScript, '--port', testPort.toString(), '--debug', '2'],
|
||||
{
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
|
||||
let output = ''
|
||||
serverProcess.stdout?.on('data', (data) => {
|
||||
output += data.toString()
|
||||
})
|
||||
|
||||
serverProcess.stderr?.on('data', (data) => {
|
||||
console.error(data.toString())
|
||||
})
|
||||
|
||||
// Wait for server to start
|
||||
await waitForPort(testPort)
|
||||
|
||||
// Kill the server
|
||||
serverProcess.kill('SIGTERM')
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
serverProcess.on('exit', () => {
|
||||
expect(output).toContain('Debug level: 2')
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}, 10000)
|
||||
})
|
||||
|
||||
describe('Server Startup and Connectivity', () => {
|
||||
let serverProcess: ChildProcess | null = null
|
||||
|
||||
afterEach(async () => {
|
||||
if (serverProcess) {
|
||||
serverProcess.kill('SIGTERM')
|
||||
await new Promise<void>((resolve) => {
|
||||
if (serverProcess) {
|
||||
serverProcess.on('exit', () => resolve())
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
serverProcess = null
|
||||
}
|
||||
})
|
||||
|
||||
it('should start server on TCP port and accept connections', async () => {
|
||||
const testPort = getTestPort()
|
||||
|
||||
serverProcess = spawn(
|
||||
'tsx',
|
||||
[serverScript, '--port', testPort.toString()],
|
||||
{
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
|
||||
let output = ''
|
||||
serverProcess.stdout?.on('data', (data) => {
|
||||
output += data.toString()
|
||||
})
|
||||
|
||||
serverProcess.stderr?.on('data', (data) => {
|
||||
console.error(data.toString())
|
||||
})
|
||||
|
||||
// Wait for server to be ready
|
||||
const isReady = await waitForPort(testPort)
|
||||
expect(isReady).toBe(true)
|
||||
|
||||
// Check that we can connect
|
||||
const socket = createConnection({ port: testPort, host: '127.0.0.1' })
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.on('connect', resolve)
|
||||
socket.on('error', reject)
|
||||
setTimeout(() => reject(new Error('Connection timeout')), 3000)
|
||||
})
|
||||
socket.end()
|
||||
|
||||
expect(output).toContain('PGlite database initialized')
|
||||
expect(output).toContain(`"port":${testPort}`)
|
||||
}, 10000)
|
||||
|
||||
it('should work with memory database', async () => {
|
||||
const testPort = getTestPort()
|
||||
|
||||
serverProcess = spawn(
|
||||
'tsx',
|
||||
[serverScript, '--port', testPort.toString(), '--db', 'memory://'],
|
||||
{
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
|
||||
let output = ''
|
||||
serverProcess.stdout?.on('data', (data) => {
|
||||
output += data.toString()
|
||||
})
|
||||
|
||||
serverProcess.stderr?.on('data', (data) => {
|
||||
console.error(data.toString())
|
||||
})
|
||||
|
||||
const isReady = await waitForPort(testPort)
|
||||
expect(isReady).toBe(true)
|
||||
expect(output).toContain('Initializing PGLite with database: memory://')
|
||||
}, 10000)
|
||||
})
|
||||
|
||||
describe('Configuration Options', () => {
|
||||
let serverProcess: ChildProcess | null = null
|
||||
|
||||
afterEach(async () => {
|
||||
if (serverProcess) {
|
||||
serverProcess.kill('SIGTERM')
|
||||
await new Promise<void>((resolve) => {
|
||||
if (serverProcess) {
|
||||
serverProcess.on('exit', () => resolve())
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
serverProcess = null
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle different hosts', async () => {
|
||||
const testPort = getTestPort()
|
||||
|
||||
serverProcess = spawn(
|
||||
'tsx',
|
||||
[serverScript, '--port', testPort.toString(), '--host', '0.0.0.0'],
|
||||
{
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
)
|
||||
|
||||
let output = ''
|
||||
serverProcess.stdout?.on('data', (data) => {
|
||||
output += data.toString()
|
||||
})
|
||||
|
||||
serverProcess.stderr?.on('data', (data) => {
|
||||
console.error(data.toString())
|
||||
})
|
||||
|
||||
const isReady = await waitForPort(testPort)
|
||||
expect(isReady).toBe(true)
|
||||
serverProcess.kill()
|
||||
await new Promise<void>((resolve) => {
|
||||
serverProcess.on('exit', () => {
|
||||
expect(output).toContain(`"host":"0.0.0.0"`)
|
||||
serverProcess = null
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}, 10000)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user