Skip to content

Commit

Permalink
Merge pull request #74 from varshith257/test-coverage
Browse files Browse the repository at this point in the history
Improve Test Coverage
  • Loading branch information
Brayden authored Feb 17, 2025
2 parents 3554b6f + 6f2eae0 commit 0bcea81
Show file tree
Hide file tree
Showing 30 changed files with 3,657 additions and 51 deletions.
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

0 comments on commit 0bcea81

Please sign in to comment.