ST-cmovs7ut4: actions/push.ts subscribeToPushAction + unsubscribeFromPushAction
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>
This commit is contained in:
parent
ba298a0ba6
commit
353d2dff8a
2 changed files with 148 additions and 5 deletions
102
__tests__/actions/push.test.ts
Normal file
102
__tests__/actions/push.test.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
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()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,11 +1,52 @@
|
|||
'use server'
|
||||
|
||||
// Stub — full implementation added by ST-cmovs7t590009 (subscribeToPushAction + unsubscribeFromPushAction)
|
||||
import { z } from 'zod'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getSession } from '@/lib/auth'
|
||||
|
||||
export async function subscribeToPushAction(_sub: unknown): Promise<void> {
|
||||
throw new Error('Not implemented')
|
||||
const subscribeSchema = z.object({
|
||||
endpoint: z.string().url(),
|
||||
keys: z.object({
|
||||
p256dh: z.string().min(1),
|
||||
auth: z.string().min(1),
|
||||
}),
|
||||
userAgent: z.string().optional(),
|
||||
})
|
||||
|
||||
export type SubscribeToPushInput = z.infer<typeof subscribeSchema>
|
||||
|
||||
export async function subscribeToPushAction(input: SubscribeToPushInput): Promise<void> {
|
||||
const session = await getSession()
|
||||
if (!session.userId) return
|
||||
if (session.isDemo) return
|
||||
|
||||
const parsed = subscribeSchema.safeParse(input)
|
||||
if (!parsed.success) return
|
||||
|
||||
const { endpoint, keys, userAgent } = parsed.data
|
||||
await prisma.pushSubscription.upsert({
|
||||
where: { endpoint },
|
||||
create: {
|
||||
user_id: session.userId,
|
||||
endpoint,
|
||||
p256dh: keys.p256dh,
|
||||
auth: keys.auth,
|
||||
user_agent: userAgent ?? null,
|
||||
},
|
||||
update: {
|
||||
user_id: session.userId,
|
||||
p256dh: keys.p256dh,
|
||||
auth: keys.auth,
|
||||
last_used_at: new Date(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function unsubscribeFromPushAction(_args: { endpoint: string }): Promise<void> {
|
||||
throw new Error('Not implemented')
|
||||
export async function unsubscribeFromPushAction(args: { endpoint: string }): Promise<void> {
|
||||
const session = await getSession()
|
||||
if (!session.userId) return
|
||||
|
||||
await prisma.pushSubscription.deleteMany({
|
||||
where: { endpoint: args.endpoint, user_id: session.userId },
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue