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
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,8 @@ export class PolicyTemplateController {
@Post()
@ApiOperation({ summary: 'Create a policy template' })
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
async create(
@Body() dto: CreatePolicyTemplateDto,
@Query('frameworkId') frameworkId?: string,
) {
return this.service.create(dto, frameworkId);
async create(@Body() dto: CreatePolicyTemplateDto) {
return this.service.create(dto);
}

@Patch(':id')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
jest.mock('@db', () => ({
db: {
frameworkEditorPolicyTemplate: {
create: jest.fn(),
findUnique: jest.fn(),
findMany: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
frameworkEditorControlTemplate: {
findMany: jest.fn(),
},
},
Prisma: { PrismaClientKnownRequestError: class {} },
}));

import { db } from '@db';
import { PolicyTemplateService } from './policy-template.service';

const mockDb = db as jest.Mocked<typeof db>;

describe('PolicyTemplateService', () => {
let service: PolicyTemplateService;

beforeEach(() => {
service = new PolicyTemplateService();
jest.clearAllMocks();
(mockDb.frameworkEditorPolicyTemplate.create as jest.Mock).mockResolvedValue({
id: 'frk_pt_new',
name: 'New Policy',
});
});

describe('create', () => {
const baseDto = {
name: 'New Policy',
description: 'desc',
frequency: 'monthly',
department: 'none',
} as never;

// Regression test: previously, passing `frameworkId` caused the create
// path to query every control template in the framework and auto-connect
// the new policy to all of them. CX rarely wants that — the new policy
// should start unlinked and be attached to specific controls explicitly
// via the dedicated link endpoints. The `frameworkId` parameter is gone,
// but a legacy caller passing one anyway must still produce an unlinked
// row.
it('never queries or auto-links framework controls on create, even when a stray frameworkId is passed', async () => {
(mockDb.frameworkEditorControlTemplate.findMany as jest.Mock).mockResolvedValue([
{ id: 'frk_ct_1' },
{ id: 'frk_ct_2' },
]);

// Bypass TypeScript so we can simulate a stray legacy caller still
// passing frameworkId — the service must ignore it.
await (service.create as (dto: unknown, frameworkId?: string) => Promise<unknown>)(
baseDto,
'frk_soc2',
);

expect(mockDb.frameworkEditorControlTemplate.findMany).not.toHaveBeenCalled();
const createArgs = (mockDb.frameworkEditorPolicyTemplate.create as jest.Mock).mock
.calls[0][0];
expect(createArgs.data).not.toHaveProperty('controlTemplates');
});

it('persists name, description, frequency, department and an empty content blob', async () => {
await service.create(baseDto);
const createArgs = (mockDb.frameworkEditorPolicyTemplate.create as jest.Mock).mock
.calls[0][0];
expect(createArgs.data).toMatchObject({
name: 'New Policy',
description: 'desc',
frequency: 'monthly',
department: 'none',
content: {},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -48,26 +48,18 @@ export class PolicyTemplateService {
return pt;
}

async create(dto: CreatePolicyTemplateDto, frameworkId?: string) {
const controlIds = frameworkId
? await db.frameworkEditorControlTemplate
.findMany({
where: { requirements: { some: { frameworkId } } },
select: { id: true },
})
.then((cts) => cts.map((ct) => ({ id: ct.id })))
: [];

// New primitives are created unlinked. CX explicitly attaches them to
// controls via the dedicated link endpoints — auto-linking to every
// control in a framework was wrong (CX rarely wants the new policy on
// every control) and forced manual cleanup after each create.
async create(dto: CreatePolicyTemplateDto) {
const pt = await db.frameworkEditorPolicyTemplate.create({
data: {
name: dto.name,
description: dto.description ?? '',
frequency: dto.frequency,
department: dto.department,
content: {},
...(controlIds.length > 0 && {
controlTemplates: { connect: controlIds },
}),
},
});
this.logger.log(`Created policy template: ${pt.name} (${pt.id})`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,8 @@ export class TaskTemplateController {
transform: true,
}),
)
async createTaskTemplate(
@Body() dto: CreateTaskTemplateDto,
@Query('frameworkId') frameworkId?: string,
) {
return this.taskTemplateService.create(dto, frameworkId);
async createTaskTemplate(@Body() dto: CreateTaskTemplateDto) {
return this.taskTemplateService.create(dto);
}

@Get()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
jest.mock('@db', () => ({
db: {
frameworkEditorTaskTemplate: {
create: jest.fn(),
findUnique: jest.fn(),
findMany: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
frameworkEditorControlTemplate: {
findMany: jest.fn(),
},
},
Frequency: { monthly: 'monthly', yearly: 'yearly', daily: 'daily', weekly: 'weekly' },
Departments: { none: 'none', admin: 'admin', it: 'it' },
}));

import { db } from '@db';
import { TaskTemplateService } from './task-template.service';

const mockDb = db as jest.Mocked<typeof db>;

describe('TaskTemplateService', () => {
let service: TaskTemplateService;

beforeEach(() => {
service = new TaskTemplateService();
jest.clearAllMocks();
(mockDb.frameworkEditorTaskTemplate.create as jest.Mock).mockResolvedValue({
id: 'frk_tt_new',
name: 'New Task',
});
});

describe('create', () => {
const baseDto = {
name: 'New Task',
description: 'desc',
};

// Regression test: previously, passing `frameworkId` caused the create
// path to query every control template in the framework and auto-connect
// the new task to all of them. CX rarely wants that — the new task should
// start unlinked and be attached to specific controls explicitly via the
// dedicated link endpoints. The `frameworkId` parameter is gone, but a
// legacy caller passing one anyway must still produce an unlinked row.
it('never queries or auto-links framework controls on create, even when a stray frameworkId is passed', async () => {
(mockDb.frameworkEditorControlTemplate.findMany as jest.Mock).mockResolvedValue([
{ id: 'frk_ct_1' },
{ id: 'frk_ct_2' },
]);

// Bypass TypeScript so we can simulate a stray legacy caller still
// passing frameworkId — the service must ignore it.
await (service.create as (dto: unknown, frameworkId?: string) => Promise<unknown>)(
baseDto,
'frk_soc2',
);

expect(mockDb.frameworkEditorControlTemplate.findMany).not.toHaveBeenCalled();
const createArgs = (mockDb.frameworkEditorTaskTemplate.create as jest.Mock).mock
.calls[0][0];
expect(createArgs.data).not.toHaveProperty('controlTemplates');
});

it('persists name and description', async () => {
await service.create(baseDto);
const createArgs = (mockDb.frameworkEditorTaskTemplate.create as jest.Mock).mock
.calls[0][0];
expect(createArgs.data).toMatchObject({
name: 'New Task',
description: 'desc',
});
});

it('applies default frequency and department when not supplied', async () => {
await service.create({ name: 'X' } as never);
const createArgs = (mockDb.frameworkEditorTaskTemplate.create as jest.Mock).mock
.calls[0][0];
expect(createArgs.data.frequency).toBe('monthly');
expect(createArgs.data.department).toBe('none');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,17 @@ import { UpdateTaskTemplateDto } from './dto/update-task-template.dto';
export class TaskTemplateService {
private readonly logger = new Logger(TaskTemplateService.name);

async create(dto: CreateTaskTemplateDto, frameworkId?: string) {
const controlIds = frameworkId
? await db.frameworkEditorControlTemplate
.findMany({
where: { requirements: { some: { frameworkId } } },
select: { id: true },
})
.then((cts) => cts.map((ct) => ({ id: ct.id })))
: [];

// New primitives are created unlinked. CX explicitly attaches them to
// controls via the dedicated link endpoints — auto-linking to every
// control in a framework was wrong (CX rarely wants the new task on
// every control) and forced manual cleanup after each create.
async create(dto: CreateTaskTemplateDto) {
const taskTemplate = await db.frameworkEditorTaskTemplate.create({
data: {
name: dto.name,
description: dto.description ?? '',
frequency: dto.frequency ?? Frequency.monthly,
department: dto.department ?? Departments.none,
...(controlIds.length > 0 && {
controlTemplates: { connect: controlIds },
}),
},
});

Expand Down
Loading
Loading