Skip to content
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
143 changes: 143 additions & 0 deletions plugins/cron/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { CronPlugin } from './index'
import { parseCronExpression, getNextExecutionTime } from './utils'
import type { DataSource } from '../../src/types'

describe('Cron Utilities', () => {
it('should parse valid cron expression', () => {
const expr = parseCronExpression('*/5 * * * *')
expect(expr).toBeDefined()
})

it('should calculate next execution time in the future', () => {
const now = 1700000000000
const nextTime = getNextExecutionTime('*/10 * * * *', now)
expect(nextTime).toBeGreaterThan(now)
})
})

describe('CronPlugin', () => {
let plugin: CronPlugin
let mockRpc: any
let mockDataSource: DataSource

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

mockRpc = {
executeQuery: vi.fn(),
setAlarm: vi.fn(),
deleteAlarm: vi.fn(),
}

mockDataSource = {
source: 'internal',
internal: {} as any,
rpc: mockRpc,
}

plugin = new CronPlugin()
})

it('should initialize with correct plugin name and auth requirement', () => {
expect(plugin.name).toBe('starbasedb:cron')
expect(plugin.opts.requiresAuth).toBe(true)
expect(plugin.pathPrefix).toBe('/cron')
})

it('should throw an error on addEvent if plugin is not initialized with dataSource', async () => {
await expect(
plugin.addEvent(
'*/5 * * * *',
'sample_task',
{},
'http://localhost'
)
).rejects.toThrow('CronPlugin not properly initialized')
})

it('should register middleware and callback route on app', async () => {
const mockApp: any = {
use: vi.fn(),
post: vi.fn(),
}

await plugin.register(mockApp)

expect(mockApp.use).toHaveBeenCalledTimes(1)
expect(mockApp.post).toHaveBeenCalledWith(
'/cron/callback',
expect.any(Function)
)
})

it('should invoke event callbacks when callback endpoint receives payloads', async () => {
const mockApp: any = {
use: vi.fn(),
post: vi.fn(),
}

await plugin.register(mockApp)

const callbackHandler = mockApp.post.mock.calls.find(
(call: any[]) => call[0] === '/cron/callback'
)[1]

const eventCallback = vi.fn()
plugin.onEvent(eventCallback)

const mockContext: any = {
req: {
json: vi.fn().mockResolvedValue([
{
name: 'daily_sync',
cron_tab: '0 0 * * *',
payload: { action: 'sync' },
},
]),
},
}

const res = await callbackHandler(mockContext)
expect(res.status).toBe(200)
expect(eventCallback).toHaveBeenCalledWith({
name: 'daily_sync',
cron_tab: '0 0 * * *',
payload: { action: 'sync' },
})
})

it('should handle ctx.waitUntil when onEvent callback returns a Promise', async () => {
const mockCtx: any = {
waitUntil: vi.fn(),
}

const asyncCallback = vi.fn().mockResolvedValue(true)
plugin.onEvent(asyncCallback, mockCtx)

const mockApp: any = {
use: vi.fn(),
post: vi.fn(),
}

await plugin.register(mockApp)
const callbackHandler = mockApp.post.mock.calls.find(
(call: any[]) => call[0] === '/cron/callback'
)[1]

const mockContext: any = {
req: {
json: vi.fn().mockResolvedValue([
{
name: 'backup',
cron_tab: '0 2 * * *',
payload: {},
},
]),
},
}

await callbackHandler(mockContext)
expect(mockCtx.waitUntil).toHaveBeenCalledTimes(1)
})
})
85 changes: 85 additions & 0 deletions plugins/resend/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { ResendPlugin } from './index'

describe('ResendPlugin', () => {
beforeEach(() => {
vi.restoreAllMocks()
})

it('should initialize with plugin name and API key', () => {
const plugin = new ResendPlugin({ apiKey: 're_test_key_123' })
expect(plugin.name).toBe('starbasedb:resend')
expect(plugin.apiKey).toBe('re_test_key_123')
})

it('should successfully send an email via Resend API', async () => {
const plugin = new ResendPlugin({ apiKey: 're_valid_key' })
const mockResponse = { id: 'email_abc123' }

globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockResponse,
}) as any

const result = await plugin.sendEmail(
'onboarding@example.com',
['user@example.com'],
'Welcome!',
'<p>Hello World</p>'
)

expect(result).toEqual(mockResponse)
expect(globalThis.fetch).toHaveBeenCalledWith(
'https://api.resend.com/emails',
{
method: 'POST',
headers: {
Authorization: 'Bearer re_valid_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'onboarding@example.com',
to: ['user@example.com'],
subject: 'Welcome!',
html: '<p>Hello World</p>',
}),
}
)
})

it('should throw an error when Resend API returns non-ok response', async () => {
const plugin = new ResendPlugin({ apiKey: 're_invalid_key' })

globalThis.fetch = vi.fn().mockResolvedValue({
ok: false,
json: async () => ({ message: 'Invalid API Key' }),
}) as any

await expect(
plugin.sendEmail(
'test@example.com',
['test@example.com'],
'Test',
'<p>Test</p>'
)
).rejects.toThrow('Invalid API Key')
})

it('should fallback to default error message when response message is missing', async () => {
const plugin = new ResendPlugin()

globalThis.fetch = vi.fn().mockResolvedValue({
ok: false,
json: async () => ({}),
}) as any

await expect(
plugin.sendEmail(
'test@example.com',
['test@example.com'],
'Test',
'<p>Test</p>'
)
).rejects.toThrow('Failed to send email')
})
})
113 changes: 113 additions & 0 deletions plugins/stripe/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { StripeSubscriptionPlugin } from './index'

describe('StripeSubscriptionPlugin', () => {
beforeEach(() => {
vi.restoreAllMocks()
})

it('should throw an error if stripeSecretKey is missing', () => {
expect(() => {
new StripeSubscriptionPlugin({
stripeSecretKey: '',
stripeWebhookSecret: 'whsec_test',
})
}).toThrow('Stripe API key is required for this plugin.')
})

it('should initialize with correct name, keys and non-auth setting', () => {
const plugin = new StripeSubscriptionPlugin({
stripeSecretKey: 'sk_test_123',
stripeWebhookSecret: 'whsec_123',
})

expect(plugin.name).toBe('starbasedb:stripe-subscriptions')
expect(plugin.opts.requiresAuth).toBe(false)
expect(plugin.pathPrefix).toBe('/stripe')
expect(plugin.stripeSecretKey).toBe('sk_test_123')
expect(plugin.stripeWebhookSecret).toBe('whsec_123')
})

it('should register routes and execute table creation in middleware', async () => {
const plugin = new StripeSubscriptionPlugin({
stripeSecretKey: 'sk_test_123',
stripeWebhookSecret: 'whsec_123',
})

const mockApp: any = {
use: vi.fn(),
post: vi.fn(),
}

await plugin.register(mockApp)

expect(mockApp.use).toHaveBeenCalledTimes(1)
expect(mockApp.post).toHaveBeenCalledWith(
'/stripe/subscribe',
expect.any(Function)
)
expect(mockApp.post).toHaveBeenCalledWith(
'/stripe/unsubscribe',
expect.any(Function)
)
})

it('should reject subscribe requests missing required fields', async () => {
const plugin = new StripeSubscriptionPlugin({
stripeSecretKey: 'sk_test_123',
stripeWebhookSecret: 'whsec_123',
})

const mockApp: any = {
use: vi.fn(),
post: vi.fn(),
}

await plugin.register(mockApp)

const subscribeHandler = mockApp.post.mock.calls.find(
(call: any[]) => call[0] === '/stripe/subscribe'
)[1]

const mockContext: any = {
req: {
json: vi.fn().mockResolvedValue({
userId: '',
stripeProductId: '',
}),
},
}

const response = await subscribeHandler(mockContext)
expect(response.status).toBe(400)
})

it('should reject unsubscribe requests missing userId', async () => {
const plugin = new StripeSubscriptionPlugin({
stripeSecretKey: 'sk_test_123',
stripeWebhookSecret: 'whsec_123',
})

const mockApp: any = {
use: vi.fn(),
post: vi.fn(),
}

await plugin.register(mockApp)

const unsubscribeHandler = mockApp.post.mock.calls.find(
(call: any[]) => call[0] === '/stripe/unsubscribe'
)[1]

const mockContext: any = {
req: {
json: vi.fn().mockResolvedValue({
userId: '',
}),
},
}

const response = await unsubscribeHandler(mockContext)
expect(response.status).toBe(400)
})
})
Loading