Skip to content

Commit 0bcea81

Browse files
authored
Merge pull request #74 from varshith257/test-coverage
Improve Test Coverage
2 parents 3554b6f + 6f2eae0 commit 0bcea81

30 files changed

+3657
-51
lines changed

plugins/cdc/index.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { ChangeDataCapturePlugin } from './index'
3+
import { StarbaseDBConfiguration } from '../../src/handler'
4+
import { DataSource } from '../../src/types'
5+
import type { DurableObjectStub } from '@cloudflare/workers-types'
6+
7+
const parser = new (require('node-sql-parser').Parser)()
8+
9+
let cdcPlugin: ChangeDataCapturePlugin
10+
let mockDurableObjectStub: DurableObjectStub<any>
11+
let mockConfig: StarbaseDBConfiguration
12+
13+
beforeEach(() => {
14+
vi.clearAllMocks()
15+
mockDurableObjectStub = {
16+
fetch: vi.fn().mockResolvedValue(new Response('OK', { status: 200 })),
17+
} as unknown as DurableObjectStub
18+
19+
mockConfig = {
20+
role: 'admin',
21+
} as any
22+
23+
cdcPlugin = new ChangeDataCapturePlugin({
24+
stub: mockDurableObjectStub,
25+
broadcastAllEvents: false,
26+
events: [
27+
{ action: 'INSERT', schema: 'public', table: 'users' },
28+
{ action: 'DELETE', schema: 'public', table: 'orders' },
29+
],
30+
})
31+
})
32+
33+
beforeEach(() => {
34+
vi.clearAllMocks()
35+
mockDurableObjectStub = {
36+
fetch: vi.fn(),
37+
} as any
38+
39+
mockConfig = {
40+
role: 'admin',
41+
} as any
42+
43+
cdcPlugin = new ChangeDataCapturePlugin({
44+
stub: mockDurableObjectStub,
45+
broadcastAllEvents: false,
46+
events: [
47+
{ action: 'INSERT', schema: 'public', table: 'users' },
48+
{ action: 'DELETE', schema: 'public', table: 'orders' },
49+
],
50+
})
51+
})
52+
53+
describe('ChangeDataCapturePlugin - Initialization', () => {
54+
it('should initialize correctly with given options', () => {
55+
expect(cdcPlugin.prefix).toBe('/cdc')
56+
expect(cdcPlugin.broadcastAllEvents).toBe(false)
57+
expect(cdcPlugin.listeningEvents).toHaveLength(2)
58+
})
59+
60+
it('should allow all events when broadcastAllEvents is true', () => {
61+
const plugin = new ChangeDataCapturePlugin({
62+
stub: mockDurableObjectStub,
63+
broadcastAllEvents: true,
64+
})
65+
66+
expect(plugin.broadcastAllEvents).toBe(true)
67+
expect(plugin.listeningEvents).toBeUndefined()
68+
})
69+
})
70+
71+
describe('ChangeDataCapturePlugin - isEventMatch', () => {
72+
it('should return true for matching event', () => {
73+
expect(cdcPlugin.isEventMatch('INSERT', 'public', 'users')).toBe(true)
74+
expect(cdcPlugin.isEventMatch('DELETE', 'public', 'orders')).toBe(true)
75+
})
76+
77+
it('should return false for non-matching event', () => {
78+
expect(cdcPlugin.isEventMatch('UPDATE', 'public', 'users')).toBe(false)
79+
expect(cdcPlugin.isEventMatch('INSERT', 'public', 'products')).toBe(
80+
false
81+
)
82+
})
83+
84+
it('should return true for any event if broadcastAllEvents is enabled', () => {
85+
cdcPlugin.broadcastAllEvents = true
86+
expect(cdcPlugin.isEventMatch('INSERT', 'any', 'table')).toBe(true)
87+
})
88+
})
89+
90+
describe('ChangeDataCapturePlugin - extractValuesFromQuery', () => {
91+
it('should extract values from INSERT queries', () => {
92+
const ast = parser.astify(
93+
`INSERT INTO users (id, name) VALUES (1, 'Alice')`
94+
)
95+
const extracted = cdcPlugin.extractValuesFromQuery(ast, [])
96+
expect(extracted).toEqual({ id: 1, name: 'Alice' })
97+
})
98+
99+
it('should extract values from UPDATE queries', () => {
100+
const ast = parser.astify(`UPDATE users SET name = 'Bob' WHERE id = 2`)
101+
const extracted = cdcPlugin.extractValuesFromQuery(ast, [])
102+
expect(extracted).toEqual({ name: 'Bob', id: 2 })
103+
})
104+
105+
it('should extract values from DELETE queries', () => {
106+
const ast = parser.astify(`DELETE FROM users WHERE id = 3`)
107+
const extracted = cdcPlugin.extractValuesFromQuery(ast, [])
108+
expect(extracted).toEqual({ id: 3 })
109+
})
110+
111+
it('should use result data when available', () => {
112+
const result = { id: 4, name: 'Charlie' }
113+
const extracted = cdcPlugin.extractValuesFromQuery({}, result)
114+
expect(extracted).toEqual(result)
115+
})
116+
})
117+
118+
describe('ChangeDataCapturePlugin - queryEventDetected', () => {
119+
it('should not trigger CDC event for unmatched actions', () => {
120+
const mockCallback = vi.fn()
121+
cdcPlugin.onEvent(mockCallback)
122+
123+
const ast = parser.astify(`UPDATE users SET name = 'Emma' WHERE id = 6`)
124+
cdcPlugin.queryEventDetected('UPDATE', ast, [])
125+
126+
expect(mockCallback).not.toHaveBeenCalled()
127+
})
128+
})
129+
130+
describe('ChangeDataCapturePlugin - onEvent', () => {
131+
it('should register event callbacks', () => {
132+
const mockCallback = vi.fn()
133+
cdcPlugin.onEvent(mockCallback)
134+
135+
const registeredCallbacks = cdcPlugin['eventCallbacks']
136+
137+
expect(registeredCallbacks).toHaveLength(1)
138+
expect(registeredCallbacks[0]).toBeInstanceOf(Function)
139+
})
140+
141+
it('should call registered callbacks when event occurs', () => {
142+
const mockCallback = vi.fn()
143+
cdcPlugin.onEvent(mockCallback)
144+
145+
const eventPayload = {
146+
action: 'INSERT',
147+
schema: 'public',
148+
table: 'users',
149+
data: { id: 8, name: 'Frank' },
150+
}
151+
152+
cdcPlugin['eventCallbacks'].forEach((cb) => cb(eventPayload))
153+
154+
expect(mockCallback).toHaveBeenCalledWith(eventPayload)
155+
})
156+
})

plugins/cdc/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ export class ChangeDataCapturePlugin extends StarbasePlugin {
2626
// Stub of the Durable Object class for us to access the web socket
2727
private durableObjectStub
2828
// If all events should be broadcasted,
29-
private broadcastAllEvents?: boolean
29+
public broadcastAllEvents?: boolean
3030
// A list of events that the user is listening to
31-
private listeningEvents?: ChangeEvent[] = []
31+
public listeningEvents?: ChangeEvent[] = []
3232
// Configuration details about the request and user
3333
private config?: StarbaseDBConfiguration
3434
// Add this new property

plugins/query-log/index.test.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { QueryLogPlugin } from './index'
3+
import { StarbaseApp, StarbaseDBConfiguration } from '../../src/handler'
4+
import { DataSource } from '../../src/types'
5+
6+
let queryLogPlugin: QueryLogPlugin
7+
let mockDataSource: DataSource
8+
let mockExecutionContext: ExecutionContext
9+
10+
beforeEach(() => {
11+
vi.clearAllMocks()
12+
13+
mockExecutionContext = {
14+
waitUntil: vi.fn(),
15+
} as unknown as ExecutionContext
16+
17+
mockDataSource = {
18+
rpc: {
19+
executeQuery: vi.fn().mockResolvedValue([]),
20+
},
21+
} as unknown as DataSource
22+
23+
queryLogPlugin = new QueryLogPlugin({ ctx: mockExecutionContext })
24+
})
25+
26+
describe('QueryLogPlugin - Initialization', () => {
27+
it('should initialize with default values', () => {
28+
expect(queryLogPlugin).toBeInstanceOf(QueryLogPlugin)
29+
expect(queryLogPlugin['ttl']).toBe(1)
30+
expect(queryLogPlugin['state'].totalTime).toBe(0)
31+
})
32+
})
33+
34+
describe('QueryLogPlugin - register()', () => {
35+
it('should execute the query to create the log table', async () => {
36+
const mockApp = {
37+
use: vi.fn((middleware) =>
38+
middleware({ get: vi.fn(() => mockDataSource) }, vi.fn())
39+
),
40+
} as unknown as StarbaseApp
41+
42+
await queryLogPlugin.register(mockApp)
43+
44+
expect(mockDataSource.rpc.executeQuery).toHaveBeenCalledTimes(1)
45+
expect(mockDataSource.rpc.executeQuery).toHaveBeenCalledWith({
46+
sql: expect.stringContaining(
47+
'CREATE TABLE IF NOT EXISTS tmp_query_log'
48+
),
49+
params: [],
50+
})
51+
})
52+
})
53+
54+
describe('QueryLogPlugin - beforeQuery()', () => {
55+
it('should set the query state before execution', async () => {
56+
const sql = 'SELECT * FROM users WHERE id = ?'
57+
const params = [1]
58+
59+
const result = await queryLogPlugin.beforeQuery({
60+
sql,
61+
params,
62+
dataSource: mockDataSource,
63+
})
64+
65+
expect(queryLogPlugin['state'].query).toBe(sql)
66+
expect(queryLogPlugin['state'].startTime).toBeInstanceOf(Date)
67+
expect(result).toEqual({ sql, params })
68+
})
69+
})
70+
71+
describe('QueryLogPlugin - afterQuery()', () => {
72+
it('should calculate query duration and insert log', async () => {
73+
const sql = 'SELECT * FROM users WHERE id = ?'
74+
75+
await queryLogPlugin.beforeQuery({ sql, dataSource: mockDataSource })
76+
await new Promise((resolve) => setTimeout(resolve, 10))
77+
await queryLogPlugin.afterQuery({
78+
sql,
79+
result: [],
80+
isRaw: false,
81+
dataSource: mockDataSource,
82+
})
83+
84+
expect(queryLogPlugin['state'].totalTime).toBeGreaterThan(0)
85+
expect(mockDataSource.rpc.executeQuery).toHaveBeenCalledWith({
86+
sql: expect.stringContaining('INSERT INTO tmp_query_log'),
87+
params: [sql, expect.any(Number)],
88+
})
89+
})
90+
91+
it('should schedule log expiration using executionContext.waitUntil()', async () => {
92+
const sql = 'SELECT * FROM users WHERE id = ?'
93+
94+
await queryLogPlugin.beforeQuery({ sql, dataSource: mockDataSource })
95+
await queryLogPlugin.afterQuery({
96+
sql,
97+
result: [],
98+
isRaw: false,
99+
dataSource: mockDataSource,
100+
})
101+
102+
expect(mockExecutionContext.waitUntil).toHaveBeenCalledTimes(1)
103+
})
104+
})
105+
106+
describe('QueryLogPlugin - addQuery()', () => {
107+
it('should insert query execution details into the log table', async () => {
108+
queryLogPlugin['state'].query = 'SELECT * FROM test'
109+
queryLogPlugin['state'].totalTime = 50
110+
111+
await queryLogPlugin['addQuery'](mockDataSource)
112+
113+
expect(mockDataSource.rpc.executeQuery).toHaveBeenCalledWith({
114+
sql: expect.stringContaining('INSERT INTO tmp_query_log'),
115+
params: ['SELECT * FROM test', 50],
116+
})
117+
})
118+
})
119+
120+
describe('QueryLogPlugin - expireLog()', () => {
121+
it('should delete old logs based on TTL', async () => {
122+
queryLogPlugin['dataSource'] = mockDataSource
123+
124+
await queryLogPlugin['expireLog']()
125+
126+
expect(mockDataSource.rpc.executeQuery).toHaveBeenCalledWith({
127+
sql: expect.stringContaining('DELETE FROM tmp_query_log'),
128+
params: [1],
129+
})
130+
})
131+
132+
it('should return false if no dataSource is available', async () => {
133+
queryLogPlugin['dataSource'] = undefined
134+
135+
const result = await queryLogPlugin['expireLog']()
136+
expect(result).toBe(false)
137+
})
138+
})

0 commit comments

Comments
 (0)