From 23e232d5ead21450fb4d74bf13dcba552a7a9939 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Sat, 18 Jul 2026 13:10:33 -0500 Subject: [PATCH] fix: license key not showing in email when created via admin page Problem: The admin createKey action had two issues preventing the license key from appearing in the delivery email: 1. Wrong variable name: passed 'licenseKey' (camelCase) but the Resend template expects 'license_key' (snake_case). 2. Wrong template: used the default RESEND_TEMPLATE_ID (a generic template) instead of the 'license-key-delivery' template that the automation endpoint uses for license key emails. Solution: - Changed template variable from 'licenseKey' to 'license_key' - Added explicit templateId: 'license-key-delivery' to match the automation endpoint (api/v1/automations/create/account) - Updated email subject to 'Your Rhemawave License Key' for consistency Tests: Added page.server.spec.ts for the admin license-keys createKey action covering: unauthenticated rejection, missing email/level validation, user-not-found error, successful key creation with correct template ID and license_key variable, fallback to user.name when profile is missing, and graceful handling when email sending fails. --- .../admin/license-keys/+page.server.ts | 5 +- .../admin/license-keys/page.server.spec.ts | 143 ++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 src/routes/(protected)/admin/license-keys/page.server.spec.ts diff --git a/src/routes/(protected)/admin/license-keys/+page.server.ts b/src/routes/(protected)/admin/license-keys/+page.server.ts index fe221f5..3debcee 100644 --- a/src/routes/(protected)/admin/license-keys/+page.server.ts +++ b/src/routes/(protected)/admin/license-keys/+page.server.ts @@ -60,10 +60,11 @@ export const actions = { const profile = await prisma.profile.findUnique({ where: { userId: user.id } }); await sendMail({ to: email, - subject: 'Your License Key', + subject: 'Your Rhemawave License Key', + templateId: 'license-key-delivery', data: { firstName: profile?.firstName ?? user.name ?? 'Valued Customer', - licenseKey: key + license_key: key } }); } catch (e) { diff --git a/src/routes/(protected)/admin/license-keys/page.server.spec.ts b/src/routes/(protected)/admin/license-keys/page.server.spec.ts new file mode 100644 index 0000000..bb3c0db --- /dev/null +++ b/src/routes/(protected)/admin/license-keys/page.server.spec.ts @@ -0,0 +1,143 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { isHttpError } from '@sveltejs/kit'; +import { createLocals, mockUser, createFormRequest } from '$lib/test-utils'; + +const { sendMail } = vi.hoisted(() => ({ + sendMail: vi.fn() +})); + +vi.mock('$lib/server/resend', () => ({ sendMail })); + +vi.mock('$lib/svelte-admin/registry', () => ({ + getModelConfig: vi.fn(() => ({ slug: 'license-keys', listFields: [], searchFields: [] })) +})); + +vi.mock('$lib/svelte-admin/crud', () => ({ + loadRecords: vi.fn().mockResolvedValue({ records: [], relationOptionsData: {} }) +})); + +vi.mock('$lib/db', () => ({ + prisma: { + user: { findUnique: vi.fn() }, + profile: { findUnique: vi.fn() }, + licenseKey: { create: vi.fn() }, + subscriptionLevel: { findMany: vi.fn() } + } +})); + +import { prisma } from '$lib/db'; +import { actions } from './+page.server'; + +const mockFindUnique = prisma.user.findUnique as ReturnType; +const mockProfileFindUnique = prisma.profile.findUnique as ReturnType; +const mockLicenseKeyCreate = prisma.licenseKey.create as ReturnType; + +describe('admin license-keys createKey action', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('rejects unauthenticated requests', async () => { + const locals = createLocals(); + const req = createFormRequest({ email: 'jane@example.com', level: 'standard' }); + + const err = await actions.createKey({ request: req, locals } as any).catch((e) => e); + expect(isHttpError(err, 401)).toBe(true); + }); + + it('returns error when email is missing', async () => { + const locals = createLocals(mockUser() as any); + const req = createFormRequest({ level: 'standard' }); + + const result = await actions.createKey({ request: req, locals } as any); + expect(result).toEqual({ success: false, error: 'Email is required' }); + }); + + it('returns error when level is missing', async () => { + const locals = createLocals(mockUser() as any); + const req = createFormRequest({ email: 'jane@example.com' }); + + const result = await actions.createKey({ request: req, locals } as any); + expect(result).toEqual({ success: false, error: 'Subscription level is required' }); + }); + + it('returns error when user is not found', async () => { + mockFindUnique.mockResolvedValue(null); + const locals = createLocals(mockUser() as any); + const req = createFormRequest({ email: 'nobody@example.com', level: 'standard' }); + + const result = await actions.createKey({ request: req, locals } as any); + expect(result).toEqual({ success: false, error: 'No user found with that email' }); + }); + + it('creates a license key and sends email with license_key variable', async () => { + const user = mockUser(); + const profile = { firstName: 'Jane' }; + + mockFindUnique.mockResolvedValue(user); + mockProfileFindUnique.mockResolvedValue(profile); + mockLicenseKeyCreate.mockResolvedValue({}); + sendMail.mockResolvedValue({ id: 'email-1' }); + + const locals = createLocals(user as any); + const req = createFormRequest({ email: user.email, level: 'standard' }); + + const result = await actions.createKey({ request: req, locals } as any); + + expect(result).toEqual({ success: true, error: undefined }); + expect(mockLicenseKeyCreate).toHaveBeenCalledOnce(); + + const createCall = mockLicenseKeyCreate.mock.calls[0][0]; + expect(createCall.data.key).toMatch(/^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/); + expect(createCall.data.active).toBe(true); + expect(createCall.data.level).toBe('standard'); + expect(createCall.data.userId).toBe('user-1'); + + expect(sendMail).toHaveBeenCalledOnce(); + const mailCall = sendMail.mock.calls[0][0]; + expect(mailCall.to).toBe(user.email); + expect(mailCall.templateId).toBe('license-key-delivery'); + expect(mailCall.data.license_key).toBeDefined(); + expect(mailCall.data.license_key).toBe(createCall.data.key); + expect(mailCall.data.firstName).toBe('Jane'); + }); + + it('sends email with license_key even when profile is missing', async () => { + const user = mockUser(); + + mockFindUnique.mockResolvedValue(user); + mockProfileFindUnique.mockResolvedValue(null); + mockLicenseKeyCreate.mockResolvedValue({}); + sendMail.mockResolvedValue({ id: 'email-1' }); + + const locals = createLocals(user as any); + const req = createFormRequest({ email: user.email, level: 'premium' }); + + await actions.createKey({ request: req, locals } as any); + + const mailCall = sendMail.mock.calls[0][0]; + expect(mailCall.templateId).toBe('license-key-delivery'); + expect(mailCall.data.license_key).toBeDefined(); + expect(mailCall.data.firstName).toBe('Jane Doe'); + }); + + it('still returns success when email sending fails', async () => { + const user = mockUser(); + + mockFindUnique.mockResolvedValue(user); + mockProfileFindUnique.mockResolvedValue({ firstName: 'Jane' }); + mockLicenseKeyCreate.mockResolvedValue({}); + sendMail.mockRejectedValue(new Error('Resend API down')); + + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const locals = createLocals(user as any); + const req = createFormRequest({ email: user.email, level: 'standard' }); + + const result = await actions.createKey({ request: req, locals } as any); + expect(result).toEqual({ success: true, error: undefined }); + expect(mockLicenseKeyCreate).toHaveBeenCalledOnce(); + + consoleSpy.mockRestore(); + }); +});