feat(api): add admin bulk import endpoints (MS21-MIG-004) (#567)
All checks were successful
ci/woodpecker/push/api Pipeline was successful
All checks were successful
ci/woodpecker/push/api Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #567.
This commit is contained in:
251
apps/api/src/import/import.service.spec.ts
Normal file
251
apps/api/src/import/import.service.spec.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { ProjectStatus, TaskPriority, TaskStatus } from "@prisma/client";
|
||||
import { ImportService } from "./import.service";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
|
||||
describe("ImportService", () => {
|
||||
let service: ImportService;
|
||||
|
||||
const mockPrismaService = {
|
||||
withWorkspaceContext: vi.fn(),
|
||||
domain: {
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
project: {
|
||||
findFirst: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
task: {
|
||||
findFirst: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const workspaceId = "550e8400-e29b-41d4-a716-446655440001";
|
||||
const userId = "550e8400-e29b-41d4-a716-446655440002";
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ImportService,
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: mockPrismaService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ImportService>(ImportService);
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockPrismaService.withWorkspaceContext.mockImplementation(
|
||||
async (_userId: string, _workspaceId: string, fn: (client: unknown) => Promise<unknown>) => {
|
||||
return fn(mockPrismaService);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe("importTasks", () => {
|
||||
it("maps status/priority/domain and imports a task", async () => {
|
||||
mockPrismaService.task.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.domain.findUnique.mockResolvedValue(null);
|
||||
mockPrismaService.domain.create.mockResolvedValue({ id: "domain-id" });
|
||||
mockPrismaService.project.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.task.create.mockResolvedValue({ id: "task-id" });
|
||||
|
||||
const result = await service.importTasks(workspaceId, userId, [
|
||||
{
|
||||
id: "task-1",
|
||||
title: "Import me",
|
||||
domain: "Platform Ops",
|
||||
status: "in-progress",
|
||||
priority: "critical",
|
||||
project: null,
|
||||
related: [],
|
||||
blocks: [],
|
||||
blocked_by: [],
|
||||
progress: 42,
|
||||
due: "2026-03-15",
|
||||
created: "2026-03-01T10:00:00.000Z",
|
||||
updated: "2026-03-05T12:00:00.000Z",
|
||||
assignee: null,
|
||||
notes: "notes",
|
||||
notes_nontechnical: "non technical",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toEqual({ imported: 1, skipped: 0, errors: [] });
|
||||
expect(mockPrismaService.task.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
title: "Import me",
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
priority: TaskPriority.HIGH,
|
||||
domainId: "domain-id",
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("skips existing task by brainId", async () => {
|
||||
mockPrismaService.task.findFirst.mockResolvedValue({ id: "existing-task-id" });
|
||||
|
||||
const result = await service.importTasks(workspaceId, userId, [
|
||||
{
|
||||
id: "task-1",
|
||||
title: "Existing",
|
||||
domain: null,
|
||||
status: "pending",
|
||||
priority: "medium",
|
||||
project: null,
|
||||
related: [],
|
||||
blocks: [],
|
||||
blocked_by: [],
|
||||
progress: null,
|
||||
due: null,
|
||||
created: null,
|
||||
updated: null,
|
||||
assignee: null,
|
||||
notes: null,
|
||||
notes_nontechnical: null,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result.imported).toBe(0);
|
||||
expect(result.skipped).toBe(1);
|
||||
expect(mockPrismaService.task.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("collects mapping/missing-project errors while importing", async () => {
|
||||
mockPrismaService.task.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.project.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.task.create.mockResolvedValue({ id: "task-id" });
|
||||
|
||||
const result = await service.importTasks(workspaceId, userId, [
|
||||
{
|
||||
id: "task-1",
|
||||
title: "Needs project",
|
||||
domain: null,
|
||||
status: "mystery-status",
|
||||
priority: "mystery-priority",
|
||||
project: "brain-project-1",
|
||||
related: [],
|
||||
blocks: [],
|
||||
blocked_by: [],
|
||||
progress: null,
|
||||
due: null,
|
||||
created: null,
|
||||
updated: null,
|
||||
assignee: null,
|
||||
notes: null,
|
||||
notes_nontechnical: null,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result.imported).toBe(1);
|
||||
expect(result.errors).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Unknown task status "mystery-status"'),
|
||||
expect.stringContaining('Unknown task priority "mystery-priority"'),
|
||||
expect.stringContaining('referenced project "brain-project-1" not found'),
|
||||
])
|
||||
);
|
||||
expect(mockPrismaService.task.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
status: TaskStatus.NOT_STARTED,
|
||||
priority: TaskPriority.MEDIUM,
|
||||
projectId: null,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("importProjects", () => {
|
||||
it("maps status/domain and imports a project", async () => {
|
||||
mockPrismaService.project.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.domain.findUnique.mockResolvedValue(null);
|
||||
mockPrismaService.domain.create.mockResolvedValue({ id: "domain-id" });
|
||||
mockPrismaService.project.create.mockResolvedValue({ id: "project-id" });
|
||||
|
||||
const result = await service.importProjects(workspaceId, userId, [
|
||||
{
|
||||
id: "project-1",
|
||||
name: "Project One",
|
||||
description: "desc",
|
||||
domain: "Backend",
|
||||
status: "in-progress",
|
||||
priority: "high",
|
||||
progress: 50,
|
||||
repo: "git@example.com/repo",
|
||||
branch: "main",
|
||||
current_milestone: "MS21",
|
||||
next_milestone: "MS22",
|
||||
blocker: null,
|
||||
owner: "owner",
|
||||
docs_path: "docs/PRD.md",
|
||||
created: "2026-03-01",
|
||||
updated: "2026-03-05",
|
||||
target_date: "2026-04-01",
|
||||
notes: "notes",
|
||||
notes_nontechnical: "non tech",
|
||||
parent: null,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toEqual({ imported: 1, skipped: 0, errors: [] });
|
||||
expect(mockPrismaService.project.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
name: "Project One",
|
||||
status: ProjectStatus.ACTIVE,
|
||||
domainId: "domain-id",
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("captures create failures as errors", async () => {
|
||||
mockPrismaService.project.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.project.create.mockRejectedValue(new Error("db failed"));
|
||||
|
||||
const result = await service.importProjects(workspaceId, userId, [
|
||||
{
|
||||
id: "project-1",
|
||||
name: "Project One",
|
||||
description: null,
|
||||
domain: null,
|
||||
status: "planning",
|
||||
priority: null,
|
||||
progress: null,
|
||||
repo: null,
|
||||
branch: null,
|
||||
current_milestone: null,
|
||||
next_milestone: null,
|
||||
blocker: null,
|
||||
owner: null,
|
||||
docs_path: null,
|
||||
created: null,
|
||||
updated: null,
|
||||
target_date: null,
|
||||
notes: null,
|
||||
notes_nontechnical: null,
|
||||
parent: null,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result.imported).toBe(0);
|
||||
expect(result.skipped).toBe(1);
|
||||
expect(result.errors).toEqual([
|
||||
expect.stringContaining("project project-1: failed to import: db failed"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user