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
141 changes: 138 additions & 3 deletions apps/api/src/policies/policies.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { PoliciesService } from './policies.service';
import { AttachmentsService } from '../attachments/attachments.service';
import { PolicyPdfRendererService } from '../trust-portal/policy-pdf-renderer.service';
Expand All @@ -8,10 +9,18 @@ jest.mock('@db', () => ({
policy: {
findMany: jest.fn(),
findFirst: jest.fn(),
findUnique: jest.fn(),
update: jest.fn(),
},
policyVersion: {
findUnique: jest.fn(),
findFirst: jest.fn(),
create: jest.fn(),
update: jest.fn(),
},
member: {
findMany: jest.fn(),
findFirst: jest.fn(),
},
auditLog: {
createMany: jest.fn(),
Expand Down Expand Up @@ -46,8 +55,19 @@ jest.mock('../utils/compliance-filters', () => ({
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { db } = require('@db') as {
db: {
policy: { findMany: jest.Mock; findFirst: jest.Mock; update: jest.Mock };
member: { findMany: jest.Mock };
policy: {
findMany: jest.Mock;
findFirst: jest.Mock;
findUnique: jest.Mock;
update: jest.Mock;
};
policyVersion: {
findUnique: jest.Mock;
findFirst: jest.Mock;
create: jest.Mock;
update: jest.Mock;
};
member: { findMany: jest.Mock; findFirst: jest.Mock };
auditLog: { createMany: jest.Mock };
$transaction: jest.Mock;
};
Expand All @@ -61,12 +81,17 @@ const { filterComplianceMembers: mockedFilterComplianceMembers } = require('../u
describe('PoliciesService', () => {
let service: PoliciesService;

const mockAttachmentsService = {
copyPolicyVersionPdf: jest.fn(),
deletePolicyVersionPdf: jest.fn(),
};

beforeEach(async () => {
jest.clearAllMocks();
const module: TestingModule = await Test.createTestingModule({
providers: [
PoliciesService,
{ provide: AttachmentsService, useValue: {} },
{ provide: AttachmentsService, useValue: mockAttachmentsService },
{ provide: PolicyPdfRendererService, useValue: {} },
],
}).compile();
Expand Down Expand Up @@ -216,4 +241,114 @@ describe('PoliciesService', () => {
]);
});
});

describe('createVersion', () => {
const organizationId = 'org_123';
const policyId = 'pol_1';
const userId = 'usr_1';

const setupHappyPath = ({
policyContent,
currentVersionContent,
policyPdfUrl,
currentVersionPdfUrl,
}: {
policyContent: unknown[];
currentVersionContent: unknown[] | null;
policyPdfUrl: string | null;
currentVersionPdfUrl: string | null;
}) => {
db.member.findFirst.mockResolvedValue({ id: 'mem_1' });
db.policy.findUnique.mockResolvedValue({
id: policyId,
organizationId,
content: policyContent,
pdfUrl: policyPdfUrl,
currentVersion: currentVersionContent
? {
id: 'pv_1',
content: currentVersionContent,
pdfUrl: currentVersionPdfUrl,
}
: null,
versions: [],
});
db.$transaction.mockImplementation(async (cb: (tx: unknown) => unknown) =>
cb({
policyVersion: {
findFirst: jest.fn().mockResolvedValue({ version: 1 }),
create: jest.fn().mockResolvedValue({ id: 'pv_2' }),
},
}),
);
};

it('creates a version when editor content is empty but a PDF exists', async () => {
setupHappyPath({
policyContent: [],
currentVersionContent: [],
policyPdfUrl: 's3://bucket/policy.pdf',
currentVersionPdfUrl: 's3://bucket/policy.pdf',
});
mockAttachmentsService.copyPolicyVersionPdf.mockResolvedValue(
's3://bucket/new.pdf',
);

const result = await service.createVersion(
policyId,
organizationId,
{},
userId,
);

expect(result).toEqual({ versionId: 'pv_2', version: 2 });
expect(mockAttachmentsService.copyPolicyVersionPdf).toHaveBeenCalled();
});

it('creates a version when both editor content is empty and no PDF exists', async () => {
setupHappyPath({
policyContent: [],
currentVersionContent: [],
policyPdfUrl: null,
currentVersionPdfUrl: null,
});

const result = await service.createVersion(
policyId,
organizationId,
{},
userId,
);

expect(result).toEqual({ versionId: 'pv_2', version: 2 });
expect(mockAttachmentsService.copyPolicyVersionPdf).not.toHaveBeenCalled();
});

it('creates a version with non-empty editor content', async () => {
setupHappyPath({
policyContent: [{ type: 'paragraph' }],
currentVersionContent: [{ type: 'paragraph' }],
policyPdfUrl: null,
currentVersionPdfUrl: null,
});

const result = await service.createVersion(
policyId,
organizationId,
{},
userId,
);

expect(result).toEqual({ versionId: 'pv_2', version: 2 });
});

it('throws NotFound when the policy does not exist', async () => {
db.member.findFirst.mockResolvedValue({ id: 'mem_1' });
db.policy.findUnique.mockResolvedValue(null);

await expect(
service.createVersion(policyId, organizationId, {}, userId),
).rejects.toBeInstanceOf(NotFoundException);
});
});
});
8 changes: 2 additions & 6 deletions apps/api/src/policies/policies.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,15 +599,11 @@ export class PoliciesService {
sourceVersion = requestedVersion;
}

const contentForVersion = sourceVersion
const contentForVersion = (sourceVersion
? (sourceVersion.content as Prisma.InputJsonValue[])
: (policy.content as Prisma.InputJsonValue[]);
: (policy.content as Prisma.InputJsonValue[])) ?? [];
const sourcePdfUrl = sourceVersion?.pdfUrl ?? policy.pdfUrl;

if (!contentForVersion || contentForVersion.length === 0) {
throw new BadRequestException('No content to create version from');
}

// S3 copy is done AFTER the transaction to prevent orphaned files on retry
let createdVersion: { versionId: string; version: number } | null = null;

Expand Down
Loading