diff --git a/server/src/__tests__/company-portability-routes.test.ts b/server/src/__tests__/company-portability-routes.test.ts index f8b763f2f2a..0c68cf255b6 100644 --- a/server/src/__tests__/company-portability-routes.test.ts +++ b/server/src/__tests__/company-portability-routes.test.ts @@ -538,6 +538,28 @@ describe.sequential("company portability routes", () => { expect(mockLogActivity).not.toHaveBeenCalled(); }); + it.sequential("accepts trusted Cloud async import jobs before validating the full import payload", async () => { + const app = await createApp(cloudTenantActor()); + + const accepted = await request(app) + .post("/api/companies/import") + .set("x-paperclip-cloud-async-import", "1") + .set(cloudHeaders) + .send({ target: { mode: "existing_company", companyId } }); + + expect(accepted.status).toBe(202); + expect(accepted.body.job.status).toBe("running"); + expect(mockCompanyPortabilityService.importBundle).not.toHaveBeenCalled(); + + const failed = await waitForImportJobStatus(app, accepted.body.statusUrl, "failed"); + + expect(failed.status).toBe(200); + expect(failed.body.job.status).toBe("failed"); + expect(failed.body.job.error.message).toEqual(expect.any(String)); + expect(mockCompanyPortabilityService.importBundle).not.toHaveBeenCalled(); + expect(mockLogActivity).not.toHaveBeenCalled(); + }); + it.sequential("keeps global import apply synchronous when Cloud async opt-in is absent", async () => { mockCompanyPortabilityService.importBundle.mockResolvedValueOnce(createImportResult("created")); const app = await createApp(cloudTenantActor()); diff --git a/server/src/routes/companies.ts b/server/src/routes/companies.ts index e057ca87742..03eaa17bd27 100644 --- a/server/src/routes/companies.ts +++ b/server/src/routes/companies.ts @@ -188,18 +188,19 @@ export function companyRoutes(db: Db, storage?: StorageService) { res.json(importJobResponse(job)); }); - router.post("/import", validate(companyPortabilityImportSchema), async (req, res) => { + router.post("/import", async (req, res) => { assertBoard(req); - assertImportTargetAccess(req, req.body.target); + const rawImportBody: unknown = req.body; const actor = getActorInfo(req); - const importBody = req.body; const boardUserId = req.actor.type === "board" ? req.actor.userId : null; - const activity = importedCompanyActivityContext(actor, importBody.include ?? null); if (req.header("x-paperclip-cloud-async-import") === "1") { assertCloudTenantCaller(req); const job = createImportJob(cloudTenantRequestKey(req)); importJobs.set(job.id, job); const operation = async () => { + const importBody = companyPortabilityImportSchema.parse(rawImportBody); + assertImportTargetAccess(req, importBody.target); + const activity = importedCompanyActivityContext(actor, importBody.include ?? null); const result = await portability.importBundle(importBody, boardUserId); await logImportedCompanyActivity(db, activity, result); return result; @@ -211,6 +212,9 @@ export function companyRoutes(db: Db, storage?: StorageService) { return; } + const importBody = companyPortabilityImportSchema.parse(rawImportBody); + assertImportTargetAccess(req, importBody.target); + const activity = importedCompanyActivityContext(actor, importBody.include ?? null); const result = await portability.importBundle(importBody, boardUserId); await logImportedCompanyActivity(db, activity, result); res.json(result);