Skip to content

Commit cd5cf89

Browse files
authored
Merge pull request #204 from bcgov/develop
Develop
2 parents ce1a4c5 + 9ac4bf2 commit cd5cf89

38 files changed

Lines changed: 1590 additions & 130 deletions

apps/backend-services/src/benchmark/benchmark-definition-db.service.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export class BenchmarkDefinitionDbService {
105105
async findBenchmarkProject(
106106
id: string,
107107
tx?: Prisma.TransactionClient,
108-
): Promise<{ id: string } | null> {
108+
): Promise<{ id: string; group_id: string } | null> {
109109
const client = tx ?? this.prisma;
110110
return client.benchmarkProject.findUnique({ where: { id } });
111111
}
@@ -120,11 +120,14 @@ export class BenchmarkDefinitionDbService {
120120
async findDatasetVersion(
121121
id: string,
122122
tx?: Prisma.TransactionClient,
123-
): Promise<{ id: string; dataset: { name: string } } | null> {
123+
): Promise<{
124+
id: string;
125+
dataset: { name: string; group_id: string };
126+
} | null> {
124127
const client = tx ?? this.prisma;
125128
return client.datasetVersion.findUnique({
126129
where: { id },
127-
include: { dataset: { select: { name: true } } },
130+
include: { dataset: { select: { name: true, group_id: true } } },
128131
});
129132
}
130133

apps/backend-services/src/benchmark/benchmark-definition.service.spec.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ describe("BenchmarkDefinitionService", () => {
8686
createdAt: new Date(),
8787
dataset: {
8888
name: "Test Dataset",
89+
group_id: "test-group",
8990
},
9091
};
9192

@@ -114,6 +115,7 @@ describe("BenchmarkDefinitionService", () => {
114115
lineage: {
115116
id: "workflow-1",
116117
name: "Test Workflow",
118+
group_id: "test-group",
117119
},
118120
};
119121

@@ -502,6 +504,44 @@ describe("BenchmarkDefinitionService", () => {
502504
);
503505
});
504506

507+
it("rejects a dataset version owned by another group (cross-group reference blocked)", async () => {
508+
jest
509+
.spyOn(prisma.benchmarkProject, "findUnique")
510+
.mockResolvedValue(mockProject); // group "test-group"
511+
// Dataset version exists but its dataset belongs to a different group.
512+
jest.spyOn(prisma.datasetVersion, "findUnique").mockResolvedValue({
513+
...mockDatasetVersion,
514+
dataset: { name: "Other", group_id: "other-group" },
515+
});
516+
517+
await expect(
518+
service.createDefinition("project-1", createDto),
519+
).rejects.toThrow(
520+
'Dataset version with ID "ds-version-1" does not exist',
521+
);
522+
});
523+
524+
it("rejects a workflow version owned by another group (cross-group reference blocked)", async () => {
525+
jest
526+
.spyOn(prisma.benchmarkProject, "findUnique")
527+
.mockResolvedValue(mockProject); // group "test-group"
528+
jest
529+
.spyOn(prisma.datasetVersion, "findUnique")
530+
.mockResolvedValue(mockDatasetVersion);
531+
jest.spyOn(prisma.split, "findUnique").mockResolvedValue(mockSplit);
532+
// Workflow version exists but its lineage belongs to a different group.
533+
jest.spyOn(prisma.workflowVersion, "findUnique").mockResolvedValue({
534+
...mockWorkflowVersion,
535+
lineage: { ...mockWorkflowVersion.lineage, group_id: "other-group" },
536+
});
537+
538+
await expect(
539+
service.createDefinition("project-1", createDto),
540+
).rejects.toThrow(
541+
'Workflow version with ID "wv-workflow-1" does not exist',
542+
);
543+
});
544+
505545
it("returns 400 when evaluator type is not registered", async () => {
506546
jest
507547
.spyOn(prisma.benchmarkProject, "findUnique")
@@ -701,6 +741,9 @@ describe("BenchmarkDefinitionService", () => {
701741
jest
702742
.spyOn(prisma.benchmarkDefinition, "findFirst")
703743
.mockResolvedValue(existingDefinition as never);
744+
jest
745+
.spyOn(prisma.benchmarkProject, "findUnique")
746+
.mockResolvedValue(mockProject);
704747
jest.spyOn(prisma.benchmarkDefinition, "update").mockResolvedValue({
705748
...existingDefinition,
706749
immutable: true,
@@ -770,6 +813,9 @@ describe("BenchmarkDefinitionService", () => {
770813
jest
771814
.spyOn(prisma.benchmarkDefinition, "findFirst")
772815
.mockResolvedValue(existingDefinition as never);
816+
jest
817+
.spyOn(prisma.benchmarkProject, "findUnique")
818+
.mockResolvedValue(mockProject);
773819

774820
const updatedDefinition = {
775821
...existingDefinition,
@@ -1043,16 +1089,27 @@ describe("BenchmarkDefinitionService", () => {
10431089
const baseLineageId = "base-lineage";
10441090
const newBaseVersionId = "wv-new-base";
10451091

1092+
jest
1093+
.spyOn(prisma.benchmarkProject, "findUnique")
1094+
.mockResolvedValue(mockProject);
1095+
10461096
jest.spyOn(prisma.workflowVersion, "findUnique").mockResolvedValue({
10471097
id: candidateWorkflowVersionId,
10481098
config: candidateConfig,
10491099
lineage: {
10501100
id: "cand-lineage",
1101+
group_id: "test-group",
10511102
workflow_kind: "benchmark_candidate",
10521103
source_workflow_id: baseLineageId,
10531104
},
10541105
} as never);
10551106

1107+
jest.spyOn(prisma.workflowLineage, "findUnique").mockResolvedValue({
1108+
id: baseLineageId,
1109+
group_id: "test-group",
1110+
head_version_id: "wv-old-head",
1111+
} as never);
1112+
10561113
jest.spyOn(prisma.workflowVersion, "findFirst").mockResolvedValue({
10571114
version_number: 2,
10581115
} as never);
@@ -1107,16 +1164,27 @@ describe("BenchmarkDefinitionService", () => {
11071164
const baseLineageId = "base-lineage";
11081165
const candidateLineageId = "cand-lineage";
11091166

1167+
jest
1168+
.spyOn(prisma.benchmarkProject, "findUnique")
1169+
.mockResolvedValue(mockProject);
1170+
11101171
jest.spyOn(prisma.workflowVersion, "findUnique").mockResolvedValue({
11111172
id: candidateWorkflowVersionId,
11121173
config: candidateConfig,
11131174
lineage: {
11141175
id: candidateLineageId,
1176+
group_id: "test-group",
11151177
workflow_kind: "benchmark_candidate",
11161178
source_workflow_id: baseLineageId,
11171179
},
11181180
} as never);
11191181

1182+
jest.spyOn(prisma.workflowLineage, "findUnique").mockResolvedValue({
1183+
id: baseLineageId,
1184+
group_id: "test-group",
1185+
head_version_id: "wv-old-head",
1186+
} as never);
1187+
11201188
jest.spyOn(prisma.workflowVersion, "findFirst").mockResolvedValue({
11211189
version_number: 2,
11221190
} as never);
@@ -1195,11 +1263,15 @@ describe("BenchmarkDefinitionService", () => {
11951263
});
11961264

11971265
it("rejects non-candidate workflows", async () => {
1266+
jest
1267+
.spyOn(prisma.benchmarkProject, "findUnique")
1268+
.mockResolvedValue(mockProject);
11981269
jest.spyOn(prisma.workflowVersion, "findUnique").mockResolvedValue({
11991270
id: "wv-primary",
12001271
config: candidateConfig,
12011272
lineage: {
12021273
id: "primary-lineage",
1274+
group_id: "test-group",
12031275
workflow_kind: "primary",
12041276
source_workflow_id: null,
12051277
},
@@ -1210,6 +1282,37 @@ describe("BenchmarkDefinitionService", () => {
12101282
).rejects.toThrow(/not a benchmark candidate/i);
12111283
});
12121284

1285+
it("rejects a candidate whose source points at another group's base lineage (cross-group write blocked)", async () => {
1286+
// Candidate is in the caller's group, but its source_workflow_id points at
1287+
// a lineage owned by a different group. The base-lineage group check must
1288+
// reject this before any version is written.
1289+
jest
1290+
.spyOn(prisma.benchmarkProject, "findUnique")
1291+
.mockResolvedValue(mockProject);
1292+
jest.spyOn(prisma.workflowVersion, "findUnique").mockResolvedValue({
1293+
id: "candidate-v1",
1294+
config: candidateConfig,
1295+
lineage: {
1296+
id: "cand-lineage",
1297+
group_id: "test-group",
1298+
workflow_kind: "benchmark_candidate",
1299+
source_workflow_id: "foreign-base-lineage",
1300+
},
1301+
} as never);
1302+
// Base lineage belongs to another group.
1303+
jest.spyOn(prisma.workflowLineage, "findUnique").mockResolvedValue({
1304+
id: "foreign-base-lineage",
1305+
group_id: "other-group",
1306+
head_version_id: "wv-foreign-head",
1307+
} as never);
1308+
const createSpy = jest.spyOn(prisma.workflowVersion, "create");
1309+
1310+
await expect(
1311+
service.applyToBaseWorkflow("project-1", "candidate-v1", false),
1312+
).rejects.toThrow(NotFoundException);
1313+
expect(createSpy).not.toHaveBeenCalled();
1314+
});
1315+
12131316
it("throws ConflictException when definition pin does not match lineage head inside transaction", async () => {
12141317
const projectId = "project-1";
12151318
const definitionId = "def-1";
@@ -1235,6 +1338,10 @@ describe("BenchmarkDefinitionService", () => {
12351338
edges: [],
12361339
};
12371340

1341+
jest
1342+
.spyOn(prisma.benchmarkProject, "findUnique")
1343+
.mockResolvedValue({ ...mockProject, group_id: baseLineageGroupId });
1344+
12381345
let findFirstCalls = 0;
12391346
jest
12401347
.spyOn(prisma.benchmarkDefinition, "findFirst")
@@ -1315,6 +1422,10 @@ describe("BenchmarkDefinitionService", () => {
13151422
.spyOn(service, "getDefinitionById")
13161423
.mockResolvedValue({ id: definitionId } as never);
13171424

1425+
jest
1426+
.spyOn(prisma.benchmarkProject, "findUnique")
1427+
.mockResolvedValue({ ...mockProject, group_id: baseLineageGroupId });
1428+
13181429
jest.spyOn(prisma.benchmarkDefinition, "findFirst").mockResolvedValue({
13191430
id: definitionId,
13201431
projectId,

0 commit comments

Comments
 (0)