Skip to content

Commit b9832f8

Browse files
adityachoudhari26github-actions[bot]claude
authored
fix: wrap job status update request body in an object (#1159)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Aditya Choudhari <adityachoudhari26@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1433cb9 commit b9832f8

7 files changed

Lines changed: 194 additions & 6 deletions

File tree

‎apps/api/openapi/openapi.json‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6704,7 +6704,15 @@
67046704
"content": {
67056705
"application/json": {
67066706
"schema": {
6707-
"$ref": "#/components/schemas/JobStatus"
6707+
"type": "object",
6708+
"required": [
6709+
"status"
6710+
],
6711+
"properties": {
6712+
"status": {
6713+
"$ref": "#/components/schemas/JobStatus"
6714+
}
6715+
}
67086716
}
67096717
}
67106718
},

‎apps/api/openapi/paths/jobs.jsonnet‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,13 @@ local openapi = import '../lib/openapi.libsonnet';
5757
required: true,
5858
content: {
5959
'application/json': {
60-
schema: openapi.schemaRef('JobStatus'),
60+
schema: {
61+
type: 'object',
62+
required: ['status'],
63+
properties: {
64+
status: openapi.schemaRef('JobStatus'),
65+
},
66+
},
6167
},
6268
},
6369
},

‎apps/api/src/routes/v1/workspaces/jobs.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,9 @@ const updateJobStatus: AsyncTypedHandler<
261261
"put"
262262
> = async (req, res) => {
263263
const { workspaceId, jobId } = req.params;
264-
const { body: oapiStatus } = req;
264+
const { status: oapiStatus } = req.body;
265265

266-
const dbStatus = oapiToDbStatus[oapiStatus as string];
266+
const dbStatus = oapiToDbStatus[oapiStatus];
267267
if (dbStatus == null) throw new ApiError("Invalid job status", 400);
268268

269269
const existing = await db

‎apps/api/src/types/openapi.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4517,7 +4517,9 @@ export interface operations {
45174517
};
45184518
requestBody: {
45194519
content: {
4520-
"application/json": components["schemas"]["JobStatus"];
4520+
"application/json": {
4521+
status: components["schemas"]["JobStatus"];
4522+
};
45214523
};
45224524
};
45234525
responses: {

‎e2e/api/schema.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4517,7 +4517,9 @@ export interface operations {
45174517
};
45184518
requestBody: {
45194519
content: {
4520-
"application/json": components["schemas"]["JobStatus"];
4520+
"application/json": {
4521+
status: components["schemas"]["JobStatus"];
4522+
};
45214523
};
45224524
};
45234525
responses: {

‎e2e/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"test:systems": "pnpm exec playwright test tests/systems.spec.ts",
2929
"test:workflows": "pnpm exec playwright test tests/api/workflows.spec.ts",
3030
"test:job-agent-pull": "pnpm exec playwright test --project=api-tests tests/api/job-agent-pull.spec.ts",
31+
"test:job-status-update": "pnpm exec playwright test --project=api-tests tests/api/job-status-update.spec.ts",
3132
"seed": "tsx seed.ts",
3233
"clean": "rm -rf .turbo node_modules",
3334
"show-report": "pnpm exec playwright show-report",
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { expect } from "@playwright/test";
2+
import { faker } from "@faker-js/faker";
3+
import { v4 as uuidv4 } from "uuid";
4+
5+
import { inArray } from "@ctrlplane/db";
6+
import { db } from "@ctrlplane/db/client";
7+
import * as schema from "@ctrlplane/db/schema";
8+
9+
import { test } from "../fixtures";
10+
11+
// Exercises PUT /v1/workspaces/{workspaceId}/jobs/{jobId}/status. The request
12+
// body is an object `{ status }` — a bare string is rejected by the strict JSON
13+
// body parser (the bug this endpoint had). Jobs are seeded directly with the
14+
// release linkage the handler requires: job -> release_job -> release ->
15+
// deployment(in workspace).
16+
test.describe("Job Status Update API", () => {
17+
const created = {
18+
jobIds: [] as string[],
19+
releaseIds: [] as string[],
20+
versionIds: [] as string[],
21+
deploymentIds: [] as string[],
22+
environmentIds: [] as string[],
23+
resourceIds: [] as string[],
24+
};
25+
26+
const seedJob = async (
27+
workspaceId: string,
28+
status: "in_progress" | "pending" = "in_progress",
29+
) => {
30+
const suffix = faker.string.alphanumeric(8);
31+
32+
const deployment = await db
33+
.insert(schema.deployment)
34+
.values({ name: `status-deploy-${suffix}`, description: "", workspaceId })
35+
.returning()
36+
.then((rows) => rows[0]!);
37+
const resource = await db
38+
.insert(schema.resource)
39+
.values({
40+
name: `status-res-${suffix}`,
41+
kind: "TestKind",
42+
identifier: `status-res-${suffix}`,
43+
version: "1.0.0",
44+
workspaceId,
45+
})
46+
.returning()
47+
.then((rows) => rows[0]!);
48+
const environment = await db
49+
.insert(schema.environment)
50+
.values({ name: `status-env-${suffix}`, workspaceId })
51+
.returning()
52+
.then((rows) => rows[0]!);
53+
const version = await db
54+
.insert(schema.deploymentVersion)
55+
.values({
56+
name: suffix,
57+
tag: suffix,
58+
deploymentId: deployment.id,
59+
workspaceId,
60+
})
61+
.returning()
62+
.then((rows) => rows[0]!);
63+
const release = await db
64+
.insert(schema.release)
65+
.values({
66+
resourceId: resource.id,
67+
environmentId: environment.id,
68+
deploymentId: deployment.id,
69+
versionId: version.id,
70+
})
71+
.returning()
72+
.then((rows) => rows[0]!);
73+
const job = await db
74+
.insert(schema.job)
75+
.values({ status })
76+
.returning()
77+
.then((rows) => rows[0]!);
78+
await db
79+
.insert(schema.releaseJob)
80+
.values({ jobId: job.id, releaseId: release.id });
81+
82+
created.jobIds.push(job.id);
83+
created.releaseIds.push(release.id);
84+
created.versionIds.push(version.id);
85+
created.deploymentIds.push(deployment.id);
86+
created.environmentIds.push(environment.id);
87+
created.resourceIds.push(resource.id);
88+
return job.id;
89+
};
90+
91+
test.afterAll(async () => {
92+
if (created.jobIds.length > 0) {
93+
await db
94+
.delete(schema.releaseJob)
95+
.where(inArray(schema.releaseJob.jobId, created.jobIds));
96+
await db.delete(schema.job).where(inArray(schema.job.id, created.jobIds));
97+
}
98+
if (created.releaseIds.length > 0)
99+
await db
100+
.delete(schema.release)
101+
.where(inArray(schema.release.id, created.releaseIds));
102+
if (created.versionIds.length > 0)
103+
await db
104+
.delete(schema.deploymentVersion)
105+
.where(inArray(schema.deploymentVersion.id, created.versionIds));
106+
if (created.deploymentIds.length > 0)
107+
await db
108+
.delete(schema.deployment)
109+
.where(inArray(schema.deployment.id, created.deploymentIds));
110+
if (created.environmentIds.length > 0)
111+
await db
112+
.delete(schema.environment)
113+
.where(inArray(schema.environment.id, created.environmentIds));
114+
if (created.resourceIds.length > 0)
115+
await db
116+
.delete(schema.resource)
117+
.where(inArray(schema.resource.id, created.resourceIds));
118+
});
119+
120+
test("updates a job's status via the { status } body", async ({
121+
api,
122+
workspace,
123+
}) => {
124+
const jobId = await seedJob(workspace.id, "in_progress");
125+
126+
const res = await api.PUT(
127+
"/v1/workspaces/{workspaceId}/jobs/{jobId}/status",
128+
{
129+
params: { path: { workspaceId: workspace.id, jobId } },
130+
body: { status: "successful" },
131+
},
132+
);
133+
expect(res.response.status).toBe(202);
134+
135+
const get = await api.GET("/v1/workspaces/{workspaceId}/jobs/{jobId}", {
136+
params: { path: { workspaceId: workspace.id, jobId } },
137+
});
138+
expect(get.response.status).toBe(200);
139+
expect(get.data!.status).toBe("successful");
140+
expect(get.data!.completedAt).toBeDefined();
141+
});
142+
143+
test("returns 400 for an invalid status value", async ({
144+
api,
145+
workspace,
146+
}) => {
147+
const jobId = await seedJob(workspace.id);
148+
149+
const res = await api.PUT(
150+
"/v1/workspaces/{workspaceId}/jobs/{jobId}/status",
151+
{
152+
params: { path: { workspaceId: workspace.id, jobId } },
153+
body: { status: "bogus" } as { status: "successful" },
154+
},
155+
);
156+
expect(res.response.status).toBe(400);
157+
});
158+
159+
test("returns 404 for a non-existent job", async ({ api, workspace }) => {
160+
const res = await api.PUT(
161+
"/v1/workspaces/{workspaceId}/jobs/{jobId}/status",
162+
{
163+
params: { path: { workspaceId: workspace.id, jobId: uuidv4() } },
164+
body: { status: "successful" },
165+
},
166+
);
167+
expect(res.response.status).toBe(404);
168+
});
169+
});

0 commit comments

Comments
 (0)