-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #74 from varshith257/test-coverage
Improve Test Coverage
- Loading branch information
Showing
30 changed files
with
3,657 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) |
Oops, something went wrong.