Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Test Coverage #74

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"dependencies": {
"@libsql/client": "^0.14.0",
"@outerbase/sdk": "2.0.0-rc.3",
"form-data": "^4.0.1",
"hono": "^4.6.14",
"jose": "^5.9.6",
"mongodb": "^6.11.0",
Expand Down
156 changes: 156 additions & 0 deletions plugins/cdc/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { ChangeDataCapturePlugin } from './index'
import { StarbaseDBConfiguration } from '../../src/handler'
import { DataSource } from '../../src/types'
import type { DurableObjectStub } from '@cloudflare/workers-types'

const parser = new (require('node-sql-parser').Parser)()

let cdcPlugin: ChangeDataCapturePlugin
let mockDurableObjectStub: DurableObjectStub<any>
let mockConfig: StarbaseDBConfiguration

beforeEach(() => {
vi.clearAllMocks()
mockDurableObjectStub = {
fetch: vi.fn().mockResolvedValue(new Response('OK', { status: 200 })),
} as unknown as DurableObjectStub

mockConfig = {
role: 'admin',
} as any

cdcPlugin = new ChangeDataCapturePlugin({
stub: mockDurableObjectStub,
broadcastAllEvents: false,
events: [
{ action: 'INSERT', schema: 'public', table: 'users' },
{ action: 'DELETE', schema: 'public', table: 'orders' },
],
})
})

beforeEach(() => {
vi.clearAllMocks()
mockDurableObjectStub = {
fetch: vi.fn(),
} as any

mockConfig = {
role: 'admin',
} as any

cdcPlugin = new ChangeDataCapturePlugin({
stub: mockDurableObjectStub,
broadcastAllEvents: false,
events: [
{ action: 'INSERT', schema: 'public', table: 'users' },
{ action: 'DELETE', schema: 'public', table: 'orders' },
],
})
})

describe('ChangeDataCapturePlugin - Initialization', () => {
it('should initialize correctly with given options', () => {
expect(cdcPlugin.prefix).toBe('/cdc')
expect(cdcPlugin.broadcastAllEvents).toBe(false)
expect(cdcPlugin.listeningEvents).toHaveLength(2)
})

it('should allow all events when broadcastAllEvents is true', () => {
const plugin = new ChangeDataCapturePlugin({
stub: mockDurableObjectStub,
broadcastAllEvents: true,
})

expect(plugin.broadcastAllEvents).toBe(true)
expect(plugin.listeningEvents).toBeUndefined()
})
})

describe('ChangeDataCapturePlugin - isEventMatch', () => {
it('should return true for matching event', () => {
expect(cdcPlugin.isEventMatch('INSERT', 'public', 'users')).toBe(true)
expect(cdcPlugin.isEventMatch('DELETE', 'public', 'orders')).toBe(true)
})

it('should return false for non-matching event', () => {
expect(cdcPlugin.isEventMatch('UPDATE', 'public', 'users')).toBe(false)
expect(cdcPlugin.isEventMatch('INSERT', 'public', 'products')).toBe(
false
)
})

it('should return true for any event if broadcastAllEvents is enabled', () => {
cdcPlugin.broadcastAllEvents = true
expect(cdcPlugin.isEventMatch('INSERT', 'any', 'table')).toBe(true)
})
})

describe('ChangeDataCapturePlugin - extractValuesFromQuery', () => {
it('should extract values from INSERT queries', () => {
const ast = parser.astify(
`INSERT INTO users (id, name) VALUES (1, 'Alice')`
)
const extracted = cdcPlugin.extractValuesFromQuery(ast, [])
expect(extracted).toEqual({ id: 1, name: 'Alice' })
})

it('should extract values from UPDATE queries', () => {
const ast = parser.astify(`UPDATE users SET name = 'Bob' WHERE id = 2`)
const extracted = cdcPlugin.extractValuesFromQuery(ast, [])
expect(extracted).toEqual({ name: 'Bob', id: 2 })
})

it('should extract values from DELETE queries', () => {
const ast = parser.astify(`DELETE FROM users WHERE id = 3`)
const extracted = cdcPlugin.extractValuesFromQuery(ast, [])
expect(extracted).toEqual({ id: 3 })
})

it('should use result data when available', () => {
const result = { id: 4, name: 'Charlie' }
const extracted = cdcPlugin.extractValuesFromQuery({}, result)
expect(extracted).toEqual(result)
})
})

describe('ChangeDataCapturePlugin - queryEventDetected', () => {
it('should not trigger CDC event for unmatched actions', () => {
const mockCallback = vi.fn()
cdcPlugin.onEvent(mockCallback)

const ast = parser.astify(`UPDATE users SET name = 'Emma' WHERE id = 6`)
cdcPlugin.queryEventDetected('UPDATE', ast, [])

expect(mockCallback).not.toHaveBeenCalled()
})
})

describe('ChangeDataCapturePlugin - onEvent', () => {
it('should register event callbacks', () => {
const mockCallback = vi.fn()
cdcPlugin.onEvent(mockCallback)

const registeredCallbacks = cdcPlugin['eventCallbacks']

expect(registeredCallbacks).toHaveLength(1)
expect(registeredCallbacks[0]).toBeInstanceOf(Function)
})

it('should call registered callbacks when event occurs', () => {
const mockCallback = vi.fn()
cdcPlugin.onEvent(mockCallback)

const eventPayload = {
action: 'INSERT',
schema: 'public',
table: 'users',
data: { id: 8, name: 'Frank' },
}

cdcPlugin['eventCallbacks'].forEach((cb) => cb(eventPayload))

expect(mockCallback).toHaveBeenCalledWith(eventPayload)
})
})
4 changes: 2 additions & 2 deletions plugins/cdc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ export class ChangeDataCapturePlugin extends StarbasePlugin {
// Stub of the Durable Object class for us to access the web socket
private durableObjectStub
// If all events should be broadcasted,
private broadcastAllEvents?: boolean
public broadcastAllEvents?: boolean
// A list of events that the user is listening to
private listeningEvents?: ChangeEvent[] = []
public listeningEvents?: ChangeEvent[] = []
// Configuration details about the request and user
private config?: StarbaseDBConfiguration
// Add this new property
Expand Down
138 changes: 138 additions & 0 deletions plugins/query-log/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { QueryLogPlugin } from './index'
import { StarbaseApp, StarbaseDBConfiguration } from '../../src/handler'
import { DataSource } from '../../src/types'

let queryLogPlugin: QueryLogPlugin
let mockDataSource: DataSource
let mockExecutionContext: ExecutionContext

beforeEach(() => {
vi.clearAllMocks()

mockExecutionContext = {
waitUntil: vi.fn(),
} as unknown as ExecutionContext

mockDataSource = {
rpc: {
executeQuery: vi.fn().mockResolvedValue([]),
},
} as unknown as DataSource

queryLogPlugin = new QueryLogPlugin({ ctx: mockExecutionContext })
})

describe('QueryLogPlugin - Initialization', () => {
it('should initialize with default values', () => {
expect(queryLogPlugin).toBeInstanceOf(QueryLogPlugin)
expect(queryLogPlugin['ttl']).toBe(1)
expect(queryLogPlugin['state'].totalTime).toBe(0)
})
})

describe('QueryLogPlugin - register()', () => {
it('should execute the query to create the log table', async () => {
const mockApp = {
use: vi.fn((middleware) =>
middleware({ get: vi.fn(() => mockDataSource) }, vi.fn())
),
} as unknown as StarbaseApp

await queryLogPlugin.register(mockApp)

expect(mockDataSource.rpc.executeQuery).toHaveBeenCalledTimes(1)
expect(mockDataSource.rpc.executeQuery).toHaveBeenCalledWith({
sql: expect.stringContaining(
'CREATE TABLE IF NOT EXISTS tmp_query_log'
),
params: [],
})
})
})

describe('QueryLogPlugin - beforeQuery()', () => {
it('should set the query state before execution', async () => {
const sql = 'SELECT * FROM users WHERE id = ?'
const params = [1]

const result = await queryLogPlugin.beforeQuery({
sql,
params,
dataSource: mockDataSource,
})

expect(queryLogPlugin['state'].query).toBe(sql)
expect(queryLogPlugin['state'].startTime).toBeInstanceOf(Date)
expect(result).toEqual({ sql, params })
})
})

describe('QueryLogPlugin - afterQuery()', () => {
it('should calculate query duration and insert log', async () => {
const sql = 'SELECT * FROM users WHERE id = ?'

await queryLogPlugin.beforeQuery({ sql, dataSource: mockDataSource })
await new Promise((resolve) => setTimeout(resolve, 10))
await queryLogPlugin.afterQuery({
sql,
result: [],
isRaw: false,
dataSource: mockDataSource,
})

expect(queryLogPlugin['state'].totalTime).toBeGreaterThan(0)
expect(mockDataSource.rpc.executeQuery).toHaveBeenCalledWith({
sql: expect.stringContaining('INSERT INTO tmp_query_log'),
params: [sql, expect.any(Number)],
})
})

it('should schedule log expiration using executionContext.waitUntil()', async () => {
const sql = 'SELECT * FROM users WHERE id = ?'

await queryLogPlugin.beforeQuery({ sql, dataSource: mockDataSource })
await queryLogPlugin.afterQuery({
sql,
result: [],
isRaw: false,
dataSource: mockDataSource,
})

expect(mockExecutionContext.waitUntil).toHaveBeenCalledTimes(1)
})
})

describe('QueryLogPlugin - addQuery()', () => {
it('should insert query execution details into the log table', async () => {
queryLogPlugin['state'].query = 'SELECT * FROM test'
queryLogPlugin['state'].totalTime = 50

await queryLogPlugin['addQuery'](mockDataSource)

expect(mockDataSource.rpc.executeQuery).toHaveBeenCalledWith({
sql: expect.stringContaining('INSERT INTO tmp_query_log'),
params: ['SELECT * FROM test', 50],
})
})
})

describe('QueryLogPlugin - expireLog()', () => {
it('should delete old logs based on TTL', async () => {
queryLogPlugin['dataSource'] = mockDataSource

await queryLogPlugin['expireLog']()

expect(mockDataSource.rpc.executeQuery).toHaveBeenCalledWith({
sql: expect.stringContaining('DELETE FROM tmp_query_log'),
params: [1],
})
})

it('should return false if no dataSource is available', async () => {
queryLogPlugin['dataSource'] = undefined

const result = await queryLogPlugin['expireLog']()
expect(result).toBe(false)
})
})
Loading
Loading