Skip to content
Merged
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
64 changes: 60 additions & 4 deletions src/email-digest/email-digest.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
jest.mock('@prisma/client', () => ({
...jest.requireActual('@prisma/client'),
DigestFrequency: {
DAILY: 'DAILY',
WEEKLY: 'WEEKLY',
},
}));

import { EmailDigestService } from './email-digest.service';
import { PrismaService } from '../database/prisma.service';
import { EmailService } from '../email/email.service';
Expand All @@ -6,18 +14,38 @@ import { ConfigService } from '@nestjs/config';
describe('EmailDigestService', () => {
let service: EmailDigestService;
let prisma: jest.Mocked<Partial<PrismaService>>;
let emailService: { sendEmail: jest.Mock };
let configService: { get: jest.Mock };

beforeEach(() => {
prisma = {
digestPreference: {
upsert: jest.fn().mockResolvedValue({ userId: 'u1', enabled: true, frequency: 'DAILY', unsubscribeToken: 'tok' }),
upsert: jest.fn().mockResolvedValue({
userId: 'u1',
enabled: true,
frequency: 'DAILY',
unsubscribeToken: 'tok',
}),
findUnique: jest.fn().mockResolvedValue(null),
} as any,
notification: {
findMany: jest.fn().mockResolvedValue([
{
title: 'New property update',
message: 'A property has new activity',
type: 'INFO',
createdAt: new Date('2026-08-31T12:00:00.000Z'),
},
]),
} as any,
};
emailService = { sendEmail: jest.fn().mockResolvedValue(undefined) };
configService = { get: jest.fn().mockReturnValue('https://api.propchain.example/api') };

service = new EmailDigestService(
prisma as unknown as PrismaService,
{ sendEmail: jest.fn() } as unknown as EmailService,
{ get: jest.fn().mockReturnValue('') } as unknown as ConfigService,
emailService as unknown as EmailService,
configService as unknown as ConfigService,
);
});

Expand All @@ -29,4 +57,32 @@ describe('EmailDigestService', () => {
);
expect(result.userId).toBe('u1');
});
});

it('uses configured API_URL for digest unsubscribe links', async () => {
await service['sendDigestForUser'](
{ id: 'u1', email: 'user@example.com', firstName: 'User' },
new Date('2026-08-30T12:00:00.000Z'),
'token-123',
);

expect(emailService.sendEmail).toHaveBeenCalledWith(
expect.objectContaining({
html: expect.stringContaining(
'https://api.propchain.example/api/email-digest/unsubscribe?token=token-123',
),
}),
);
});

it('fails when API_URL is missing for digest unsubscribe links', async () => {
configService.get.mockReturnValue(undefined);

await expect(
service['sendDigestForUser'](
{ id: 'u1', email: 'user@example.com', firstName: 'User' },
new Date('2026-08-30T12:00:00.000Z'),
'token-123',
),
).rejects.toThrow('API_URL environment variable is not set');
});
});
8 changes: 7 additions & 1 deletion src/email-digest/email-digest.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,13 @@ export class EmailDigestService {

if (notifications.length === 0) return;

const apiUrl = this.configService.get<string>('API_URL', 'http://localhost:3000/api');
const apiUrl = this.configService.get<string>('API_URL');
if (!apiUrl) {
throw new Error(
'API_URL environment variable is not set. Cannot generate digest unsubscribe link.',
);
}

const unsubscribeUrl = `${apiUrl}/email-digest/unsubscribe?token=${unsubscribeToken}`;

const html = this.buildDigestHtml(user.firstName, notifications, unsubscribeUrl);
Expand Down
Loading