diff --git a/src/rulesets/rest/2022-05-25/json-api-rules/__tests__/resource-object-rules.test.ts b/src/rulesets/rest/2022-05-25/json-api-rules/__tests__/resource-object-rules.test.ts index 21381514..b8fbc9a5 100644 --- a/src/rulesets/rest/2022-05-25/json-api-rules/__tests__/resource-object-rules.test.ts +++ b/src/rulesets/rest/2022-05-25/json-api-rules/__tests__/resource-object-rules.test.ts @@ -167,6 +167,53 @@ describe("resource object rules", () => { }, }, } as OpenAPIV3.Document; + const ruleRunner = new RuleRunner([resourceObjectRules]); + const ruleInputs = { + ...TestHelpers.createRuleInputs(baseJson, afterJson), + context, + }; + const results = await ruleRunner.runRulesWithFacts(ruleInputs); + expect(results.length).toBeGreaterThan(0); + expect(results.every((result) => result.passed)).toBe(true); + }, + ); + + test.each(["uuid", "uri", "ulid"])( + "passes when PATCH request body for a relationship is of the correct form (data is a single resource object with id and type)", + async (format) => { + const afterJson = { + ...baseJson, + paths: { + "/api/example/relationships/example": { + patch: { + responses: {}, // not tested here + requestBody: { + content: { + "application/vnd.api+json": { + schema: { + type: "object", + properties: { + data: { + type: "object", + properties: { + type: { + type: "string", + }, + id: { + type: "string", + format: format, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } as OpenAPIV3.Document; const ruleRunner = new RuleRunner([resourceObjectRules]); const ruleInputs = { @@ -227,7 +274,7 @@ describe("resource object rules", () => { where: "PATCH /api/example/relationships/example request body: application/vnd.api+json", name: "request body for relationship post/patch/delete", - error: "Expected a partial match", + error: "Expected at least one partial match", }), ]), ); @@ -335,6 +382,54 @@ describe("resource object rules", () => { }, ); + test.each(["uuid", "uri", "ulid"])( + "passes when POST request body for a relationship is of the correct form (data is a single resource object with type and id)", + async (format) => { + const afterJson = { + ...baseJson, + paths: { + "/api/example/relationships/example": { + post: { + responses: {}, // not tested here + requestBody: { + content: { + "application/vnd.api+json": { + schema: { + type: "object", + properties: { + data: { + type: "object", + properties: { + type: { + type: "string", + }, + id: { + type: "string", + format: format, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } as OpenAPIV3.Document; + + const ruleRunner = new RuleRunner([resourceObjectRules]); + const ruleInputs = { + ...TestHelpers.createRuleInputs(baseJson, afterJson), + context, + }; + const results = await ruleRunner.runRulesWithFacts(ruleInputs); + expect(results.length).toBeGreaterThan(0); + expect(results.every((result) => result.passed)).toBe(true); + }, + ); + test("fails when POST request body for a relationship is of incorrect form (missing id from resource objects in data array)", async () => { const afterJson = { ...baseJson, @@ -383,7 +478,7 @@ describe("resource object rules", () => { where: "POST /api/example/relationships/example request body: application/vnd.api+json", name: "request body for relationship post/patch/delete", - error: "Expected a partial match", + error: "Expected at least one partial match", }), ]), ); @@ -1168,6 +1263,54 @@ describe("resource object rules", () => { }, ); + test.each(["uuid", "uri", "ulid"])( + "passes when DELETE request body for a relationship is of the correct form (data is a resource object)", + async (format) => { + const afterJson = { + ...baseJson, + paths: { + "/api/example/relationships/example": { + delete: { + responses: {}, // not tested here + requestBody: { + content: { + "application/vnd.api+json": { + schema: { + type: "object", + properties: { + data: { + type: "object", + properties: { + type: { + type: "string", + }, + id: { + type: "string", + format: format, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } as OpenAPIV3.Document; + + const ruleRunner = new RuleRunner([resourceObjectRules]); + const ruleInputs = { + ...TestHelpers.createRuleInputs(baseJson, afterJson), + context, + }; + const results = await ruleRunner.runRulesWithFacts(ruleInputs); + expect(results.length).toBeGreaterThan(0); + expect(results.every((result) => result.passed)).toBe(true); + }, + ); + test("fails when DELETE request body for a relationship is of incorrect form (resource objects in collection missing id)", async () => { const afterJson = { ...baseJson, @@ -1216,7 +1359,7 @@ describe("resource object rules", () => { where: "DELETE /api/example/relationships/example request body: application/vnd.api+json", name: "request body for relationship post/patch/delete", - error: "Expected a partial match", + error: "Expected at least one partial match", }), ]), ); diff --git a/src/rulesets/rest/2022-05-25/json-api-rules/resource-object-rules.ts b/src/rulesets/rest/2022-05-25/json-api-rules/resource-object-rules.ts index 40e37d46..715f1f1a 100644 --- a/src/rulesets/rest/2022-05-25/json-api-rules/resource-object-rules.ts +++ b/src/rulesets/rest/2022-05-25/json-api-rules/resource-object-rules.ts @@ -121,10 +121,10 @@ const requestDataForPost = new RequestRule({ }, }); -// Relationship POST, PATCH, and DELETE requests must have +// Relationship POST, PATCH, and DELETE requests can have // a request body with resource objects for the relationships // to be added/patched/deleted. -const matchRelationshipModificationRequestData = { +const matchRelationshipModificationRequestArrayData = { data: { type: "array", items: { @@ -142,6 +142,24 @@ const matchRelationshipModificationRequestData = { }, }; +// Relationship POST, PATCH, and DELETE requests can have +// a request body with a resource object for the single relationship +// to be added (set)/patched/deleted. +const matchRelationshipModificationRequestSingleData = { + data: { + type: "object", + properties: { + type: { + type: Matchers.string, + }, + id: { + type: "string", + format: resourceIDFormat, + }, + }, + }, +}; + const requestDataForRelationshipModification = new RequestRule({ name: "request body for relationship post/patch/delete", docsLink: links.jsonApi.postRequests, @@ -150,18 +168,34 @@ const requestDataForRelationshipModification = new RequestRule({ request.contentType === "application/vnd.api+json" && ["patch", "delete", "post"].includes(rulesContext.operation.method), rule: (requestAssertions) => { - requestAssertions.body.added.matches({ - schema: { - type: "object", - properties: matchRelationshipModificationRequestData, + requestAssertions.body.added.matchesOneOf([ + { + schema: { + type: "object", + properties: matchRelationshipModificationRequestArrayData, + }, }, - }); - requestAssertions.body.changed.matches({ - schema: { - type: "object", - properties: matchRelationshipModificationRequestData, + { + schema: { + type: "object", + properties: matchRelationshipModificationRequestSingleData, + }, }, - }); + ]); + requestAssertions.body.changed.matchesOneOf([ + { + schema: { + type: "object", + properties: matchRelationshipModificationRequestArrayData, + }, + }, + { + schema: { + type: "object", + properties: matchRelationshipModificationRequestSingleData, + }, + }, + ]); }, });