Vervangt stub met volledige implementatie: requireUser via getSession, demo-block, Zod-validatie, upsert met user_id-scoping en user-scoped deleteMany. Tests (8): idempotentie, demo-block, unauthenticated, invalid input. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
const { mockGetSession } = vi.hoisted(() => ({
|
|
mockGetSession: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/lib/auth', () => ({
|
|
getSession: mockGetSession,
|
|
}))
|
|
|
|
const { mockUpsert, mockDeleteMany } = vi.hoisted(() => ({
|
|
mockUpsert: vi.fn(),
|
|
mockDeleteMany: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
pushSubscription: {
|
|
upsert: mockUpsert,
|
|
deleteMany: mockDeleteMany,
|
|
},
|
|
},
|
|
}))
|
|
|
|
import { subscribeToPushAction, unsubscribeFromPushAction } from '@/actions/push'
|
|
|
|
const VALID_INPUT = {
|
|
endpoint: 'https://push.example.com/subscription/abc123',
|
|
keys: { p256dh: 'aBcDeFgH', auth: 'xYzAbC' },
|
|
}
|
|
|
|
const SESSION_USER = { userId: 'user-1', isDemo: false }
|
|
const SESSION_DEMO = { userId: 'demo-1', isDemo: true }
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockUpsert.mockResolvedValue({})
|
|
mockDeleteMany.mockResolvedValue({ count: 1 })
|
|
})
|
|
|
|
describe('subscribeToPushAction', () => {
|
|
it('upserts subscription for authenticated user', async () => {
|
|
mockGetSession.mockResolvedValue(SESSION_USER)
|
|
await subscribeToPushAction(VALID_INPUT)
|
|
expect(mockUpsert).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { endpoint: VALID_INPUT.endpoint },
|
|
create: expect.objectContaining({ user_id: 'user-1', endpoint: VALID_INPUT.endpoint }),
|
|
})
|
|
)
|
|
})
|
|
|
|
it('is idempotent — calling twice upserts twice without error', async () => {
|
|
mockGetSession.mockResolvedValue(SESSION_USER)
|
|
await subscribeToPushAction(VALID_INPUT)
|
|
await subscribeToPushAction(VALID_INPUT)
|
|
expect(mockUpsert).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('returns without writing for demo user', async () => {
|
|
mockGetSession.mockResolvedValue(SESSION_DEMO)
|
|
await subscribeToPushAction(VALID_INPUT)
|
|
expect(mockUpsert).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('returns without writing when not authenticated', async () => {
|
|
mockGetSession.mockResolvedValue({})
|
|
await subscribeToPushAction(VALID_INPUT)
|
|
expect(mockUpsert).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('returns without writing for invalid input', async () => {
|
|
mockGetSession.mockResolvedValue(SESSION_USER)
|
|
// @ts-expect-error intentionally invalid
|
|
await subscribeToPushAction({ endpoint: 'not-a-url', keys: {} })
|
|
expect(mockUpsert).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('unsubscribeFromPushAction', () => {
|
|
it('deletes subscription scoped to user_id', async () => {
|
|
mockGetSession.mockResolvedValue(SESSION_USER)
|
|
await unsubscribeFromPushAction({ endpoint: VALID_INPUT.endpoint })
|
|
expect(mockDeleteMany).toHaveBeenCalledWith({
|
|
where: { endpoint: VALID_INPUT.endpoint, user_id: 'user-1' },
|
|
})
|
|
})
|
|
|
|
it('does not touch subscriptions of other users', async () => {
|
|
mockGetSession.mockResolvedValue({ userId: 'other-user', isDemo: false })
|
|
await unsubscribeFromPushAction({ endpoint: VALID_INPUT.endpoint })
|
|
expect(mockDeleteMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({ where: expect.objectContaining({ user_id: 'other-user' }) })
|
|
)
|
|
})
|
|
|
|
it('returns without writing when not authenticated', async () => {
|
|
mockGetSession.mockResolvedValue({})
|
|
await unsubscribeFromPushAction({ endpoint: VALID_INPUT.endpoint })
|
|
expect(mockDeleteMany).not.toHaveBeenCalled()
|
|
})
|
|
})
|