db management fix

This commit is contained in:
root
2026-05-22 17:21:52 -04:00
parent bae4942276
commit 48ecde2a51
13 changed files with 294 additions and 7 deletions
@@ -0,0 +1,31 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({
prisma: {
adminUser: {
findFirst: vi.fn(),
},
},
}))
import { prisma } from '../../lib/prisma'
import { findAdminByEmail } from './admin.repo'
describe('admin.repo', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('looks up admin email case-insensitively', async () => {
await findAdminByEmail('Admin@Example.com')
expect(prisma.adminUser.findFirst).toHaveBeenCalledWith({
where: {
email: {
equals: 'Admin@Example.com',
mode: 'insensitive',
},
},
})
})
})
+8 -1
View File
@@ -31,7 +31,14 @@ function parseOptionalDate(value?: string | null) {
}
export function findAdminByEmail(email: string) {
return prisma.adminUser.findUnique({ where: { email } })
return prisma.adminUser.findFirst({
where: {
email: {
equals: email,
mode: 'insensitive',
},
},
})
}
export function findAdminByIdOrThrow(id: string) {
@@ -0,0 +1,49 @@
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('./admin.repo', () => ({
findAdminByEmail: vi.fn(),
setAdminPasswordReset: vi.fn(),
}))
vi.mock('../../services/notificationService', () => ({
sendTransactionalEmail: vi.fn().mockResolvedValue(undefined),
}))
import * as repo from './admin.repo'
import { sendTransactionalEmail } from '../../services/notificationService'
import { forgotPassword } from './admin.service'
describe('admin.service forgotPassword', () => {
const originalAdminUrl = process.env.ADMIN_URL
beforeEach(() => {
vi.clearAllMocks()
process.env.ADMIN_URL = 'http://localhost:3002'
})
afterAll(() => {
process.env.ADMIN_URL = originalAdminUrl
})
it('sends the reset email to the canonical stored admin address', async () => {
vi.mocked(repo.findAdminByEmail).mockResolvedValue({
id: 'admin_1',
email: 'admin@rentaldrivego.com',
firstName: 'Samir',
isActive: true,
} as any)
await forgotPassword('Admin@RentalDriveGo.com')
expect(repo.setAdminPasswordReset).toHaveBeenCalledWith(
'admin_1',
expect.any(String),
expect.any(Date),
)
expect(sendTransactionalEmail).toHaveBeenCalledWith(
expect.objectContaining({
to: 'admin@rentaldrivego.com',
}),
)
})
})
+1 -1
View File
@@ -93,7 +93,7 @@ export async function forgotPassword(email: string) {
const resetUrl = `${adminUrl}/reset-password?token=${rawToken}`
await sendTransactionalEmail({
to: email,
to: admin.email,
subject: 'Reset your RentalDriveGo admin password',
html: `<p>Hi ${admin.firstName},</p><p><a href="${resetUrl}">Reset password</a></p><p>Expires in ${ADMIN_RESET_TTL_MINUTES} minutes.</p>`,
text: `Hi ${admin.firstName},\n\nReset here: ${resetUrl}\n\nExpires in ${ADMIN_RESET_TTL_MINUTES} minutes.`,
@@ -0,0 +1,46 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({
prisma: {
employee: {
findFirst: vi.fn(),
},
},
}))
import { prisma } from '../../lib/prisma'
import { findActiveEmployeeByEmail, findEmployeeWithCompanyByEmail } from './auth.employee.repo'
describe('auth.employee.repo', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('looks up employee login email case-insensitively', async () => {
await findEmployeeWithCompanyByEmail('Owner@Example.com')
expect(prisma.employee.findFirst).toHaveBeenCalledWith({
where: {
email: {
equals: 'Owner@Example.com',
mode: 'insensitive',
},
},
include: { company: true },
})
})
it('looks up active employee reset email case-insensitively', async () => {
await findActiveEmployeeByEmail('Owner@Example.com')
expect(prisma.employee.findFirst).toHaveBeenCalledWith({
where: {
email: {
equals: 'Owner@Example.com',
mode: 'insensitive',
},
isActive: true,
},
})
})
})
@@ -9,14 +9,25 @@ export function findEmployeeWithCompanyById(id: string) {
export function findEmployeeWithCompanyByEmail(email: string) {
return prisma.employee.findFirst({
where: { email },
where: {
email: {
equals: email,
mode: 'insensitive',
},
},
include: { company: true },
})
}
export function findActiveEmployeeByEmail(email: string) {
return prisma.employee.findFirst({
where: { email, isActive: true },
where: {
email: {
equals: email,
mode: 'insensitive',
},
isActive: true,
},
})
}
@@ -0,0 +1,49 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('./auth.employee.repo', () => ({
findActiveEmployeeByEmail: vi.fn(),
setPasswordResetToken: vi.fn(),
}))
vi.mock('../../services/notificationService', () => ({
sendTransactionalEmail: vi.fn().mockResolvedValue(undefined),
}))
import * as repo from './auth.employee.repo'
import { sendTransactionalEmail } from '../../services/notificationService'
import { forgotPassword } from './auth.employee.service'
describe('auth.employee.service forgotPassword', () => {
const originalDashboardUrl = process.env.DASHBOARD_URL
beforeEach(() => {
vi.clearAllMocks()
process.env.DASHBOARD_URL = 'http://localhost:3001'
})
afterAll(() => {
process.env.DASHBOARD_URL = originalDashboardUrl
})
it('sends the reset email to the canonical stored address', async () => {
vi.mocked(repo.findActiveEmployeeByEmail).mockResolvedValue({
id: 'emp_1',
email: 'owner@company.com',
firstName: 'Aya',
preferredLanguage: 'en',
} as any)
await forgotPassword('Owner@Company.com')
expect(repo.setPasswordResetToken).toHaveBeenCalledWith(
'emp_1',
expect.any(String),
expect.any(Date),
)
expect(sendTransactionalEmail).toHaveBeenCalledWith(
expect.objectContaining({
to: 'owner@company.com',
}),
)
})
})
@@ -78,7 +78,7 @@ export async function forgotPassword(email: string) {
const lang = (employee.preferredLanguage as Lang) ?? 'fr'
await sendTransactionalEmail({
to: email,
to: employee.email,
subject: resetPasswordEmail.subject(lang),
html: resetPasswordEmail.html(resetUrl, employee.firstName, RESET_TOKEN_TTL_MINUTES, lang),
text: resetPasswordEmail.text(resetUrl, employee.firstName, RESET_TOKEN_TTL_MINUTES, lang),
+70
View File
@@ -0,0 +1,70 @@
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../lib/prisma', () => ({
prisma: {
employee: {
findFirst: vi.fn(),
create: vi.fn(),
},
company: {
findUniqueOrThrow: vi.fn(),
},
},
}))
vi.mock('./notificationService', () => ({
sendTransactionalEmail: vi.fn().mockResolvedValue(undefined),
}))
import crypto from 'crypto'
import { prisma } from '../lib/prisma'
import { sendTransactionalEmail } from './notificationService'
import { inviteEmployee } from './teamService'
describe('teamService inviteEmployee', () => {
const originalDashboardUrl = process.env.DASHBOARD_URL
const originalPublicDashboardUrl = process.env.NEXT_PUBLIC_DASHBOARD_URL
beforeEach(() => {
vi.clearAllMocks()
process.env.DASHBOARD_URL = 'http://localhost:3001'
process.env.NEXT_PUBLIC_DASHBOARD_URL = ''
vi.spyOn(crypto, 'randomBytes').mockReturnValue(Buffer.from('token-123'))
vi.spyOn(crypto, 'randomUUID').mockReturnValue('uuid-123')
vi.mocked(prisma.employee.findFirst).mockResolvedValue(null as any)
vi.mocked(prisma.company.findUniqueOrThrow).mockResolvedValue({ name: 'Atlas Cars' } as any)
vi.mocked(prisma.employee.create).mockResolvedValue({
id: 'emp_1',
firstName: 'Aya',
lastName: 'Benali',
email: 'aya@example.com',
role: 'AGENT',
isActive: true,
} as any)
})
afterAll(() => {
process.env.DASHBOARD_URL = originalDashboardUrl
process.env.NEXT_PUBLIC_DASHBOARD_URL = originalPublicDashboardUrl
vi.restoreAllMocks()
})
it('builds invite reset links under the dashboard base path', async () => {
await inviteEmployee('company_1', 'owner_1', {
firstName: 'Aya',
lastName: 'Benali',
email: 'aya@example.com',
role: 'AGENT',
})
expect(sendTransactionalEmail).toHaveBeenCalledWith(
expect.objectContaining({
to: 'aya@example.com',
html: expect.stringContaining('http://localhost:3001/dashboard/reset-password?token=746f6b656e2d313233'),
text: expect.stringContaining('http://localhost:3001/dashboard/reset-password?token=746f6b656e2d313233'),
}),
)
})
})
+15 -1
View File
@@ -27,6 +27,18 @@ export interface TeamMember {
invitationStatus?: 'accepted' | 'pending' | 'revoked'
}
function ensureDashboardBasePath(baseUrl: string) {
try {
const url = new URL(baseUrl)
const pathname = url.pathname.replace(/\/$/, '')
if (!pathname.endsWith('/dashboard')) url.pathname = `${pathname}/dashboard`
return url.toString().replace(/\/$/, '')
} catch {
const trimmed = baseUrl.replace(/\/$/, '')
return trimmed.endsWith('/dashboard') ? trimmed : `${trimmed}/dashboard`
}
}
function buildInviteEmail(resetUrl: string, companyName: string, firstName: string, role: EmployeeRole) {
const greetingName = firstName.trim() || 'there'
@@ -84,7 +96,9 @@ export async function inviteEmployee(companyId: string, inviterId: string, paylo
},
})
const dashboardUrl = process.env.DASHBOARD_URL ?? 'http://localhost:3001'
const dashboardUrl = ensureDashboardBasePath(
process.env.DASHBOARD_URL ?? process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard',
)
const resetUrl = `${dashboardUrl}/reset-password?token=${rawToken}`
const message = buildInviteEmail(resetUrl, company.name, payload.firstName, payload.role)