Add database-backed quality gate configuration for workspaces with full CRUD operations and default gate seeding. Schema: - Add QualityGate model with workspace relation - Support for custom commands and regex patterns - Enable/disable and ordering support Service: - CRUD operations for quality gates - findEnabled: Get ordered, enabled gates - reorder: Bulk reorder with transaction - seedDefaults: Seed 4 default gates - toOrchestratorFormat: Convert to orchestrator interface Endpoints: - GET /workspaces/:id/quality-gates - List - GET /workspaces/:id/quality-gates/:gateId - Get one - POST /workspaces/:id/quality-gates - Create - PATCH /workspaces/:id/quality-gates/:gateId - Update - DELETE /workspaces/:id/quality-gates/:gateId - Delete - POST /workspaces/:id/quality-gates/reorder - POST /workspaces/:id/quality-gates/seed-defaults Default gates: Build, Lint, Test, Coverage (85%) Tests: 25 passing with 95.16% coverage Fixes #135 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
363 lines
10 KiB
TypeScript
363 lines
10 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { Test, TestingModule } from "@nestjs/testing";
|
|
import { QualityGateConfigService } from "./quality-gate-config.service";
|
|
import { PrismaService } from "../prisma/prisma.service";
|
|
import { NotFoundException, ConflictException } from "@nestjs/common";
|
|
|
|
describe("QualityGateConfigService", () => {
|
|
let service: QualityGateConfigService;
|
|
let prisma: PrismaService;
|
|
|
|
const mockPrismaService = {
|
|
qualityGate: {
|
|
create: vi.fn(),
|
|
findMany: vi.fn(),
|
|
findUnique: vi.fn(),
|
|
update: vi.fn(),
|
|
delete: vi.fn(),
|
|
count: vi.fn(),
|
|
},
|
|
$transaction: vi.fn(),
|
|
};
|
|
|
|
const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440001";
|
|
const mockGateId = "550e8400-e29b-41d4-a716-446655440002";
|
|
|
|
const mockQualityGate = {
|
|
id: mockGateId,
|
|
workspaceId: mockWorkspaceId,
|
|
name: "Build Check",
|
|
description: "Verify code compiles without errors",
|
|
type: "build",
|
|
command: "pnpm build",
|
|
expectedOutput: null,
|
|
isRegex: false,
|
|
required: true,
|
|
order: 1,
|
|
isEnabled: true,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
QualityGateConfigService,
|
|
{
|
|
provide: PrismaService,
|
|
useValue: mockPrismaService,
|
|
},
|
|
],
|
|
}).compile();
|
|
|
|
service = module.get<QualityGateConfigService>(QualityGateConfigService);
|
|
prisma = module.get<PrismaService>(PrismaService);
|
|
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("should be defined", () => {
|
|
expect(service).toBeDefined();
|
|
});
|
|
|
|
describe("create", () => {
|
|
it("should create a quality gate successfully", async () => {
|
|
const createDto = {
|
|
name: "Build Check",
|
|
description: "Verify code compiles without errors",
|
|
type: "build" as const,
|
|
command: "pnpm build",
|
|
required: true,
|
|
order: 1,
|
|
};
|
|
|
|
mockPrismaService.qualityGate.create.mockResolvedValue(mockQualityGate);
|
|
|
|
const result = await service.create(mockWorkspaceId, createDto);
|
|
|
|
expect(result).toEqual(mockQualityGate);
|
|
expect(prisma.qualityGate.create).toHaveBeenCalledWith({
|
|
data: {
|
|
workspaceId: mockWorkspaceId,
|
|
name: createDto.name,
|
|
description: createDto.description,
|
|
type: createDto.type,
|
|
command: createDto.command,
|
|
expectedOutput: null,
|
|
isRegex: false,
|
|
required: true,
|
|
order: 1,
|
|
isEnabled: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should use default values when optional fields are not provided", async () => {
|
|
const createDto = {
|
|
name: "Test Gate",
|
|
type: "test" as const,
|
|
};
|
|
|
|
mockPrismaService.qualityGate.create.mockResolvedValue({
|
|
...mockQualityGate,
|
|
name: createDto.name,
|
|
type: createDto.type,
|
|
});
|
|
|
|
await service.create(mockWorkspaceId, createDto);
|
|
|
|
expect(prisma.qualityGate.create).toHaveBeenCalledWith({
|
|
data: {
|
|
workspaceId: mockWorkspaceId,
|
|
name: createDto.name,
|
|
description: null,
|
|
type: createDto.type,
|
|
command: null,
|
|
expectedOutput: null,
|
|
isRegex: false,
|
|
required: true,
|
|
order: 0,
|
|
isEnabled: true,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("findAll", () => {
|
|
it("should return all quality gates for a workspace", async () => {
|
|
const mockGates = [mockQualityGate];
|
|
mockPrismaService.qualityGate.findMany.mockResolvedValue(mockGates);
|
|
|
|
const result = await service.findAll(mockWorkspaceId);
|
|
|
|
expect(result).toEqual(mockGates);
|
|
expect(prisma.qualityGate.findMany).toHaveBeenCalledWith({
|
|
where: { workspaceId: mockWorkspaceId },
|
|
orderBy: { order: "asc" },
|
|
});
|
|
});
|
|
|
|
it("should return empty array when no gates exist", async () => {
|
|
mockPrismaService.qualityGate.findMany.mockResolvedValue([]);
|
|
|
|
const result = await service.findAll(mockWorkspaceId);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("findEnabled", () => {
|
|
it("should return only enabled quality gates ordered by priority", async () => {
|
|
const mockGates = [mockQualityGate];
|
|
mockPrismaService.qualityGate.findMany.mockResolvedValue(mockGates);
|
|
|
|
const result = await service.findEnabled(mockWorkspaceId);
|
|
|
|
expect(result).toEqual(mockGates);
|
|
expect(prisma.qualityGate.findMany).toHaveBeenCalledWith({
|
|
where: {
|
|
workspaceId: mockWorkspaceId,
|
|
isEnabled: true,
|
|
},
|
|
orderBy: { order: "asc" },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("findOne", () => {
|
|
it("should return a quality gate by id", async () => {
|
|
mockPrismaService.qualityGate.findUnique.mockResolvedValue(mockQualityGate);
|
|
|
|
const result = await service.findOne(mockWorkspaceId, mockGateId);
|
|
|
|
expect(result).toEqual(mockQualityGate);
|
|
expect(prisma.qualityGate.findUnique).toHaveBeenCalledWith({
|
|
where: {
|
|
id: mockGateId,
|
|
workspaceId: mockWorkspaceId,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should throw NotFoundException when gate does not exist", async () => {
|
|
mockPrismaService.qualityGate.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(service.findOne(mockWorkspaceId, mockGateId)).rejects.toThrow(NotFoundException);
|
|
});
|
|
});
|
|
|
|
describe("update", () => {
|
|
it("should update a quality gate successfully", async () => {
|
|
const updateDto = {
|
|
name: "Updated Build Check",
|
|
required: false,
|
|
};
|
|
|
|
const updatedGate = {
|
|
...mockQualityGate,
|
|
...updateDto,
|
|
};
|
|
|
|
mockPrismaService.qualityGate.findUnique.mockResolvedValue(mockQualityGate);
|
|
mockPrismaService.qualityGate.update.mockResolvedValue(updatedGate);
|
|
|
|
const result = await service.update(mockWorkspaceId, mockGateId, updateDto);
|
|
|
|
expect(result).toEqual(updatedGate);
|
|
expect(prisma.qualityGate.update).toHaveBeenCalledWith({
|
|
where: { id: mockGateId },
|
|
data: updateDto,
|
|
});
|
|
});
|
|
|
|
it("should throw NotFoundException when gate does not exist", async () => {
|
|
const updateDto = { name: "Updated" };
|
|
mockPrismaService.qualityGate.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(service.update(mockWorkspaceId, mockGateId, updateDto)).rejects.toThrow(
|
|
NotFoundException
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("delete", () => {
|
|
it("should delete a quality gate successfully", async () => {
|
|
mockPrismaService.qualityGate.findUnique.mockResolvedValue(mockQualityGate);
|
|
mockPrismaService.qualityGate.delete.mockResolvedValue(mockQualityGate);
|
|
|
|
await service.delete(mockWorkspaceId, mockGateId);
|
|
|
|
expect(prisma.qualityGate.delete).toHaveBeenCalledWith({
|
|
where: { id: mockGateId },
|
|
});
|
|
});
|
|
|
|
it("should throw NotFoundException when gate does not exist", async () => {
|
|
mockPrismaService.qualityGate.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(service.delete(mockWorkspaceId, mockGateId)).rejects.toThrow(NotFoundException);
|
|
});
|
|
});
|
|
|
|
describe("reorder", () => {
|
|
it("should reorder gates successfully", async () => {
|
|
const gateIds = ["gate1", "gate2", "gate3"];
|
|
const mockGates = gateIds.map((id, index) => ({
|
|
...mockQualityGate,
|
|
id,
|
|
order: index,
|
|
}));
|
|
|
|
mockPrismaService.$transaction.mockImplementation((callback) => {
|
|
return callback(mockPrismaService);
|
|
});
|
|
|
|
mockPrismaService.qualityGate.update.mockResolvedValue(mockGates[0]);
|
|
mockPrismaService.qualityGate.findMany.mockResolvedValue(mockGates);
|
|
|
|
const result = await service.reorder(mockWorkspaceId, gateIds);
|
|
|
|
expect(result).toEqual(mockGates);
|
|
expect(prisma.$transaction).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("seedDefaults", () => {
|
|
it("should seed default quality gates for a workspace", async () => {
|
|
const mockDefaultGates = [
|
|
{
|
|
id: "1",
|
|
workspaceId: mockWorkspaceId,
|
|
name: "Build Check",
|
|
description: null,
|
|
type: "build",
|
|
command: "pnpm build",
|
|
expectedOutput: null,
|
|
isRegex: false,
|
|
required: true,
|
|
order: 1,
|
|
isEnabled: true,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
{
|
|
id: "2",
|
|
workspaceId: mockWorkspaceId,
|
|
name: "Lint Check",
|
|
description: null,
|
|
type: "lint",
|
|
command: "pnpm lint",
|
|
expectedOutput: null,
|
|
isRegex: false,
|
|
required: true,
|
|
order: 2,
|
|
isEnabled: true,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
];
|
|
|
|
mockPrismaService.$transaction.mockImplementation((callback) => {
|
|
return callback(mockPrismaService);
|
|
});
|
|
|
|
mockPrismaService.qualityGate.create.mockImplementation((args) =>
|
|
Promise.resolve({
|
|
...mockDefaultGates[0],
|
|
...args.data,
|
|
})
|
|
);
|
|
|
|
mockPrismaService.qualityGate.findMany.mockResolvedValue(mockDefaultGates);
|
|
|
|
const result = await service.seedDefaults(mockWorkspaceId);
|
|
|
|
expect(result.length).toBeGreaterThan(0);
|
|
expect(prisma.$transaction).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("toOrchestratorFormat", () => {
|
|
it("should convert database gates to orchestrator format", () => {
|
|
const gates = [mockQualityGate];
|
|
|
|
const result = service.toOrchestratorFormat(gates);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toMatchObject({
|
|
id: mockQualityGate.id,
|
|
name: mockQualityGate.name,
|
|
description: mockQualityGate.description,
|
|
type: mockQualityGate.type,
|
|
command: mockQualityGate.command,
|
|
required: mockQualityGate.required,
|
|
order: mockQualityGate.order,
|
|
});
|
|
});
|
|
|
|
it("should handle regex patterns correctly", () => {
|
|
const gateWithRegex = {
|
|
...mockQualityGate,
|
|
expectedOutput: "Coverage: (\\d+)%",
|
|
isRegex: true,
|
|
};
|
|
|
|
const result = service.toOrchestratorFormat([gateWithRegex]);
|
|
|
|
expect(result[0]?.expectedOutput).toBeInstanceOf(RegExp);
|
|
});
|
|
|
|
it("should handle string patterns correctly", () => {
|
|
const gateWithString = {
|
|
...mockQualityGate,
|
|
expectedOutput: "All tests passed",
|
|
isRegex: false,
|
|
};
|
|
|
|
const result = service.toOrchestratorFormat([gateWithString]);
|
|
|
|
expect(typeof result[0]?.expectedOutput).toBe("string");
|
|
});
|
|
});
|
|
});
|