diff --git a/test/integration/json-api/v1/base-format/create/index.js b/test/integration/json-api/v1/base-format/create/index.js index a15e0ed..03fde12 100644 --- a/test/integration/json-api/v1/base-format/create/index.js +++ b/test/integration/json-api/v1/base-format/create/index.js @@ -24,6 +24,99 @@ describe('creatingResources', function() { return Fixture.reset(); }); + // NOTE: JSON-API does not require an error here. + // See https://github.com/json-api/json-api/issues/766 + it('must require a single resource object as primary data', function() { + return Agent.request('POST', '/v1/books') + .send({ data: [bookData] }) + .promise() + .then(function(res) { + expect(res.status).to.equal(400); + }); + }); + + it('The resource object **MUST** contain at least a type member.'); + + it('If a relationship is provided in the `relationships` member of the resource object, its value **MUST** be a relationship object with a `data` member.'); + + describe('clientGeneratedIds', function() { + + it('may accept a client-generated ID along with a request to create a resource.'); + + it('An ID **MUST** be specified with an `id` key, the value of which **MUST** be a universally unique identifier.'); + + it('The client **SHOULD** use a properly generated and formatted *UUID* as described in RFC 4122 [RFC4122].'); + + it('must return `403 Forbidden` in response to an unsupported request to create a resource with a client-generated ID.'); + }); + + describe('responses', function() { + + describe('201Created', function() { + + it('If a `POST` request did not include a Client-Generated ID and the requested resource has been created successfully, the server **MUST** return a `201 Created` status code.'); + + it('The response **SHOULD** include a `Location` header identifying the location of the newly created resource.'); + + it('The response **MUST** also include a document that contains the primary resource created.'); + + it('If the resource object returned by the response contains a `self` key in its `links` member and a `Location` header is provided, the value of the `self` member **MUST** match the value of the `Location` header.'); + }); + + describe('202Accepted', function() { + + it('If a request to create a resource has been accepted for processing, but the processing has not been completed by the time the server responds, the server **MUST** return a `202 Accepted` status code.'); + }); + + describe('204NoContent', function() { + + it('If a `POST` request did include a Client-Generated ID and the requested resource has been created successfully, the server **MUST** return either a `201 Created` status code and response document (as described above) or a `204 No Content` status code with no response document.'); + }); + + describe('403Forbidden', function() { + + it('may return `403 Forbidden` in response to an unsupported request to create a resource.'); + }); + + describe('409Conflict', function() { + + it('must return `409 Conflict` when processing a `POST` request to create a resource with a client-generated ID that already exists.', function() { + bookData.id = 1; + return Agent.request('POST', '/v1/books') + .send({ data: bookData }) + .promise() + .then(function(res) { + expect(res.status).to.equal(409); + }); + }); + + it('must return `409 Conflict` when processing a `POST` request in which the resource object\'s type is not among the type(s) that constitute the collection represented by the endpoint.', function() { + bookData.type = 'authors'; + return Agent.request('POST', '/v1/books') + .send({ data: bookData }) + .promise() + .then(function(res) { + expect(res.status).to.equal(409); + }); + }); + + it('should include error details and provide enough information to recognize the source of the conflict.'); + }); + + describe('OtherResponses', function() { + + it('may respond with other HTTP status codes.'); + + it('may include error details with error responses.'); + + it('must prepare responses, and a client **MUST** interpret responses, in accordance with HTTP semantics.'); + }); + }); +}); + +/* OLD CREATE TESTS +describe('creatingResources', function() { + it('must respond to a successful request with an object', function() { return Agent.request('POST', '/v1/books') .send({ data: bookData }) @@ -55,18 +148,6 @@ describe('creatingResources', function() { }); }); - // TODO: Source/DB test: verify rollback on error - // it('must not allow partial updates'); - - it('must require a single resource object as primary data', function() { - return Agent.request('POST', '/v1/books') - .send({ data: [bookData] }) - .promise() - .then(function(res) { - expect(res.status).to.equal(400); - }); - }); - it('must require primary data to have a type member', function() { delete bookData.type; return Agent.request('POST', '/v1/books') @@ -169,46 +250,6 @@ describe('creatingResources', function() { }); }); }); - - // Endpoints will respond with a 201 on all create requests - // Tested above. - // describe('204NoContent', function() { - // it('must respond with either 201 or 204 if the request included a client-generated ID'); - // }); - - // API decision to not create the route - endpoints will always support creation - // describe('403Forbidden', function() { - // it('should return 403 Forbidden in response to an unsupported creation request'); - // }); - - describe.skip('409Conflict', function() { - it('must return 409 Conflict when processing a request to create a resource with an existing client-generated ID', function() { - bookData.id = 1; - return Agent.request('POST', '/v1/books') - .send({ data: bookData }) - .promise() - .then(function(res) { - expect(res.status).to.equal(409); - }); - }); - - it('must return 409 Conflict when processing a request where the type does not match the endpoint', function() { - bookData.type = 'authors'; - return Agent.request('POST', '/v1/books') - .send({ data: bookData }) - .promise() - .then(function(res) { - expect(res.status).to.equal(409); - }); - }); - }); - - // Not testable as written. Each error handling branch should be - // unit-tested for proper HTTP semantics. - // describe('otherResponses', function() { - // it('should use other HTTP codes to represent errors'); - // it('must interpret errors in accordance with HTTP semantics'); - // it('should return error details'); - // }); }); }); +*/ diff --git a/test/integration/json-api/v1/base-format/delete/index.js b/test/integration/json-api/v1/base-format/delete/index.js index e0c6eab..48af55f 100644 --- a/test/integration/json-api/v1/base-format/delete/index.js +++ b/test/integration/json-api/v1/base-format/delete/index.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import _ from 'lodash'; +// import _ from 'lodash'; import Agent from '../../../../../app/agent'; import Fixture from '../../../../../app/fixture'; @@ -10,20 +10,49 @@ describe('deletingResources', function() { return Fixture.reset(); }); - it('must not require a content-type header of application/vnd.api+json', function() { - return Agent.request('DELETE', '/v1/chapters/1') - .promise() - .then(function(res) { - expect(res.status).to.equal(204); + describe('responses', function() { + + describe('202Accepted', function() { + + it('If a deletion request has been accepted for processing, but the processing has not been completed by the time the server responds, the server **MUST** return a `202 Accepted` status code.'); + }); + + describe('204NoContent', function() { + + it('A server **MUST** return a `204 No Content` status code if a deletion request is successful and no content is returned.', function() { + return Agent.request('DELETE', '/v1/chapters/1') + .promise() + .then(function (res) { + expect(res.status).to.equal(204); + expect(res.body).to.deep.equal({}); + }); }); + }); + + describe('200OK', function() { + + it('A server **MUST** return a `200 OK` status code if a deletion request is successful and the server responds with only top-level meta data.'); + }); + + describe('otherResponses', function() { + + it('A server **MAY** respond with other HTTP status codes.'); + + it('A server **MAY** include error details with error responses.'); + + it('A server **MUST** prepare responses, and a client **MUST** interpret responses, in accordance with HTTP semantics.'); + }); }); +}); + +/* OLD DELETE TESTS +describe('deletingResources', function() { - it('must respond to a successful request with an empty body', function() { + it('must not require a content-type header of application/vnd.api+json', function() { return Agent.request('DELETE', '/v1/chapters/1') .promise() - .then(function (res) { - expect(res.status).to.be.within(200, 299); - expect(res.body).to.deep.equal({}); + .then(function(res) { + expect(res.status).to.equal(204); }); }); @@ -35,9 +64,6 @@ describe('deletingResources', function() { }); }); - // TODO: Source/DB test: verify rollback on error - it('must not allow partial updates'); - it('should delete resources when a DELETE request is made to the resource URL', function() { var first, second, deleteId; @@ -60,16 +86,6 @@ describe('deletingResources', function() { }); describe('responses', function() { - - describe('204NoContent', function() { - it('must return 204 No Content on a successful DELETE request', function() { - return Agent.request('DELETE', '/v1/chapters/1') - .promise() - .then(function(res) { - expect(res.status).to.equal(204); - }); - }); - it('must return 204 No Content when processing a request to delete a resource that does not exist', function() { return Agent.request('DELETE', '/v1/chapters/9999') .promise() @@ -78,13 +94,6 @@ describe('deletingResources', function() { }); }); }); - - // Not testable as written. Each error handling branch should be - // unit-tested for proper HTTP semantics. - // describe('otherResponses', function() { - // it('should use other HTTP codes to represent errors'); - // it('must interpret errors in accordance with HTTP semantics'); - // it('should return error details'); - // }); }); }); +*/ diff --git a/test/integration/json-api/v1/base-format/errors/index.js b/test/integration/json-api/v1/base-format/errors/index.js index 7361734..2a8a0f4 100644 --- a/test/integration/json-api/v1/base-format/errors/index.js +++ b/test/integration/json-api/v1/base-format/errors/index.js @@ -1,13 +1,11 @@ // TODO: Implement describe('errors', function() { - it('should return error objects that include additional information about problems encountered'); - it('should not return errors with primary data'); - it('should have an id member'); - it('should have a href member with further details of the problem'); - it('should have a status member representing the string value of an HTTP status code'); - it('should have a code member representing an application-specific error code'); - it('should have a title member representing a short summary of the problem'); - it('should have a detail member representing a human-readable explanation of the problem'); - it('should have a links member representing an array of pointers to the associated resources'); - it('should have a paths member representing an array of pointers to the relevant attributes within the resource'); + + it('may choose to stop processing as soon as a problem is encountered, or it **MAY** continue processing and encounter multiple problems.'); + + it('When a server encounters multiple problems for a single request, the most generally applicable HTTP error code **SHOULD** be used in the response.'); + + it('Error objects **MUST** be returned as an array keyed by `errors` in the top level of a JSON API document.'); + + it('An error object **MAY** have the following members:\\n\\n- `id`: a unique identifier for this particular occurrence of the problem.\\n- `links`: a links object containing the following members:\\n - `about`: a link that leads to further details about this particular occurrence of the problem.\\n- `status`: the HTTP status code applicable to this problem, expressed as a string value.\\n- `code`: an application-specific error code, expressed as a string value.\\n- `title`: a short, human-readable summary of the problem that **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.\\n- `detail`: a human-readable explanation specific to this occurrence of the problem.\\n- `source`: an object containing references to the source of the error, optionally including any of the following members:\\n - `pointer`: a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].\\n - `parameter`: a string indicating which query parameter caused the error.\\n- `meta`: a meta object containing non-standard meta-information about the error.'); }); diff --git a/test/integration/json-api/v1/base-format/index.js b/test/integration/json-api/v1/base-format/index.js index 380620e..0144fbd 100644 --- a/test/integration/json-api/v1/base-format/index.js +++ b/test/integration/json-api/v1/base-format/index.js @@ -1,7 +1,456 @@ describe('baseFormat', function() { + + describe('contentNegotiation', function() { + + it('must send all JSON API data in response documents with the header `Content-Type: application/vnd.api+json` without any media type parameters'); + + it('must respond with a `415 Unsupported Media Type` status code if a request specifies the header `Content-Type: application/vnd.api+json` with any media type parameters'); + + it('respond with a `406 Not Acceptable` status code if a request\'s `Accept` header contains the JSON API media type and all instances of that media type are modified with media type parameters'); + + }); + + describe('documentStructure', function() { + + it('Unless otherwise noted, objects defined by this specification **MUST NOT** contain any additional members.'); + + it('Client and server implementations **MUST** ignore members not recognized by this specification.'); + + it('A JSON object MUST be at the root of every JSON API request and response containing data. This object defines a document\'s "top level".'); + + it('A document **MUST** contain at least one of the following top-level members:\\n\\n- `data`: the document\'s "primary data"\\n- `errors`: an array of error objects\\n- meta`: a meta object that contains non-standard meta-information.'); + + it('The members `data` and `errors` **MUST NOT** coexist in the same document.'); + + it('A document **MAY** contain any of these top-level members:\\n\\n- `jsonapi`: an object describing the server\'s implementation\\n- `links`: a links object related to the primary data.\\n- `included`: an array of resource objects that are related to the primary data and/or each other ("included resources").'); + + it('If a document does not contain a top-level `data` key, the `included` member **MUST NOT** be present either.'); + + it('The top-level links object **MAY** contain the following members:\\n\\n- `self`: the link that generated the current response document.\\n- `related`: a related resource link when the primary data represents a resource relationship.\\n- pagination links for the primary data.'); + + it('Primary data **MUST** be either:\\n\\n- a single resource object, a single resource identifier object, or `null`, for requests that target single resources\\n- an array of resource objects, an array of resource identifier objects, or an empty array (`[]`), for requests that target resource collections'); + + it('A logical collection of resources **MUST** be represented as an array, even if it only contains one item or is empty.'); + + it('A resource object MUST contain at least the following top-level members:\\n\\n- `id`\\n- `type`\\n\\nException: The `id` member is not required when the resource object originates at the client and represents a new resource to be created on the server.'); + + it('In addition, a resource object MAY contain any of these top-level members:\\n\\n- `attributes`: an attributes object representing some of the resource\'s data.\\n- `relationships`: a relationships object describing relationships between the resource and other JSON API resources.\\n -`links`: a links object containing links related to the resource.\\n- `meta`: a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship.'); + + it('Every resource object **MUST** contain an `id` member and a `type` member.'); + + it('The values of the `id` and `type` members **MUST** be strings.'); + + it('Within a given API, each resource object\'s `type` and `id` pair **MUST** identify a single, unique resource. (The set of URIs controlled by a server, or multiple servers acting as one, constitute an API.)'); + + it('The values of `type` members **MUST** adhere to the same constraints as member names.'); + + it('Fields for a resource object **MUST** share a common namespace with each other and with `type` and `id`. In other words, a resource can not have an attribute and relationship with the same name, nor can it have an attribute or relationship named `type` or id.'); + + it('The value of the `attributes` key **MUST** be an object (an "attributes object").'); + + it('any object that constitutes or is contained in an attribute **MUST** reserve the `relationships` and `links` members for future use.'); + + it('Although has-one foreign keys (e.g. author_id) are often stored internally alongside other information to be represented in a resource object, these keys **SHOULD NOT** appear as attributes.'); + + it('The value of the `relationships` key **MUST** be an object (a "relationships object").'); + + it('A "relationship object" **MUST** contain at least one of the following:]\\n\\n- `links`: a links object containing at least one of the following:\\n - `self`: a link for the relationship itself (a "relationship link"). This link allows the client to directly manipulate the relationship. For example, it would allow a client to remove an author from an article without deleting the people resource itself.\\n - `related`: a related resource link\\n- `data`: resource linkage\\n- `meta`: a meta object that contains non-standard meta-information about the relationship.'); + + it('A relationship object that represents a to-many relationship **MAY** also contain pagination links under the links member'); + + it('If present, a related resource link **MUST** reference a valid URL, even if the relationship isn\'t currently associated with any target resources.'); + + it('a related resource link **MUST NOT** change because its relationship\'s content changes.'); + + it('Resource linkage MUST be represented as one of the following:\\n\\n- `null` for empty to-one relationships.\\n- an empty array (`[]`) for empty to-many relationships.\\n- a single resource identifier object for non-empty to-one relationships.\\n- an array of resource identifier objects for non-empty to-many relationships.'); + + it('If present, this links object **MAY** contain a `self` link that identifies the resource represented by the resource object.'); + + it('A server **MUST** respond to a `GET` request to the specified URL with a response that includes the resource as the primary data.'); + + it('A "resource identifier object" **MUST** contain type and id members.'); + + it('A "resource identifier object" **MAY** also include a meta member, whose value is a meta object that contains non-standard meta-information.'); + + it('To reduce the number of HTTP requests, servers **MAY** allow responses that include related resources along with the requested primary resources. Such responses are called "compound documents".'); + + it('In a compound document, all included resources **MUST** be represented as an array of resource objects in a top-level `included` member.'); + + it('Compound documents require "full linkage", meaning that every included resource **MUST** be identified by at least one resource identifier object in the same document. These resource identifier objects could either be primary data or represent resource linkage contained within primary or included resources. The only exception to the full linkage requirement is when relationship fields that would otherwise contain linkage data are excluded via sparse fieldsets.'); + + it('A compound document **MUST NOT** include more than one resource object for each type and id pair.'); + + it('The value of each meta member **MUST** be an object (a "meta object").'); + + it('Any members **MAY** be specified within meta objects.'); + + it('The value of each links member **MUST** be an object (a "links object").'); + + it('Each member of a links object is a "link". A link **MUST** be represented as either:\\n\\n- a string containing the link\'s URL.\\n- an object ("link object") which can contain the following members:\\n - `href`: a string containing the link\'s URL.\\n - `meta`: a meta object containing non-standard meta-information about the link.'); + + it('A JSON API document **MAY** include information about its implementation under a top level `jsonapi` member.'); + + it('If present, the value of the `jsonapi` member **MUST** be an object (a "jsonapi object")'); + + it('The jsonapi object **MAY** contain a `version` member whose value is a string indicating the highest JSON API version supported.'); + + it('The jsonapi object **MAY** also contain a `meta` member, whose value is a meta object that contains non-standard meta-information.'); + + it('All member names used in a JSON API document **MUST** be treated as case sensitive by clients and servers'); + + it('Member names **MUST** contain at least one character.'); + + it('Member names **MUST** contain only the allowed characters listed below.'); + + it('Member names MUST start and end with a "globally allowed character", as defined below.'); + + it('it is **RECOMMENDED** that member names use only non-reserved, URL safe characters specified in RFC 3986.'); + + it('The following "globally allowed characters" **MAY** be used anywhere in a member name:\\n\\n- U+0061 to U+007A, "a-z"\\n- U+0041 to U+005A, "A-Z"\\n- U+0030 to U+0039, "0-9"\\n- any UNICODE character except U+0000 to U+007F (not recommended, not URL safe)'); + + it('The following characters **MUST NOT** be used in member names:\\n\\n- U+002B PLUS SIGN, "+" (used for ordering)\\n- U+002C COMMA, "," (used separator for multiple relationship paths)\\n- U+002E PERIOD, "." (used as relationship path separators)\\n- U+005B LEFT SQUARE BRACKET, "[" (use in sparse fieldsets)\\n- U+005D RIGHT SQUARE BRACKET, "]" (used in sparse fieldsets)\\n- U+0021 EXCLAMATION MARK, "!"\\n- U+0022 QUOTATION MARK, '"'\\n- U+0023 NUMBER SIGN, "#"\\n- U+0024 DOLLAR SIGN, "$"\\n- U+0025 PERCENT SIGN, "%"\\n- U+0026 AMPERSAND, "&"\\n- U+0027 APOSTROPHE, "'"\\n- U+0028 LEFT PARENTHESIS, "("\\n- U+0029 RIGHT PARENTHESIS, ")"\\n- U+002A ASTERISK, "*"\\n- U+002F SOLIDUS, "/"\\n- U+003A COLON, ":"\\n- U+003B SEMICOLON, ";"\\n- U+003C LESS-THAN SIGN, "<"\\n- U+003D EQUALS SIGN, "="\\n- U+003E GREATER-THAN SIGN, ">"\\n- U+003F QUESTION MARK, "?"\\n- U+0040 COMMERCIAL AT, "@"\\n- U+005C REVERSE SOLIDUS, "\\"\\n- U+005E CIRCUMFLEX ACCENT, "^"\\n- U+0060 GRAVE ACCENT, "`"\\n- U+007B LEFT CURLY BRACKET, "{"\\n- U+007C VERTICAL LINE, "|"\\n- U+007D RIGHT CURLY BRACKET, "}"\\n- U+007E TILDE, "~"'); + }); + require('./read'); - require('./create'); - require('./update'); - require('./delete'); + + describe('creatingUpdatingAndDeletingResources', function() { + + it('may allow resources of a given type to be created.'); + + it('may also allow existing resources to be modified or deleted.'); + + it('A request **MUST** completely succeed or fail (in a single "transaction"). No partial updates are allowed.'); + + require('./create'); + require('./update'); + require('./delete'); + }); + + require('./query-parameters'); require('./errors'); }); + + +/* OLD DOCUMENT STRUCTURE TESTS + TODO: SHOULD TEST ALL METHODS, NOT JUST GET + describe('documentStructure', function() { + + describe('topLevel', function() { + it('must respond to an unsuccessful request with a JSON object', function() { + return Fixture.dropTables() + .then(function() { + return Agent.request('GET', '/v1/books/1') + .promise(); + }) + .then(function(res) { + expect(res.status).to.equal(400); + expect(res.body).to.be.an('object'); + expect(res.body.errors).to.be.an('array'); + }); + }); + }); + + describe('resourceObjects', function() { + + describe('resourceAttributes', function() { + it('should not contain a foreign key as an attribute', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + expect(dataObj).to.be.an('object'); + expect(dataObj.attributes).to.not.have.property('author_id'); + expect(dataObj.attributes).to.not.have.property('series_id'); + }); + }); + + it('must include relations as included resources', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + expect(dataObj).to.be.an('object'); + expect(dataObj.relationships).to.have.property('author'); + expect(dataObj.relationships).to.have.property('series'); + }); + }); + }); + + describe('resourceTypes', function() { + it('must contain a type', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + expect(dataObj) + .to.have.property('type') + // must have a string value for type + .that.is.a('string'); + }); + }); + }); + + describe('resourceIds', function() { + it('must contain an id', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + expect(dataObj) + .to.have.property('id') + // must have a string value for type + .that.is.a('string'); + }); + }); + }); + + describe('relationships', function() { + it('must have an object as the value of any relationships key', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + expect(dataObj) + .to.have.property('relationships') + // must have a string value for type + .that.is.a('object'); + }); + }); + }); + + describe('resourceURLs', function() { + // OPTIONAL + // A resource object **MAY** include a URL in its links object, + // keyed by "self", that identifies the resource represented by + // the resource object. + it('may include a string in its links object keyed by "self"', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + expect(dataObj.links) + .to.have.property('self') + // must have a string value for type + .that.is.a('string'); + }); + }); + + it('must set the value of "self" to a URL that identifies the resource represented by this object', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + expect(dataObj.links.self).to.equal('/v1/books/1'); + }); + }); + + }); + + describe('resourceRelationships', function() { + // OPTIONAL + // A resource object MAY contain references to other resource + // objects ("relationships"). Relationships may be to-one or + // to-many. Relationships can be specified by including a member + // in a resource's links object. + it('may contain references to related objects in the links object', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + var relationships = res.body.data.relationships; + expect(res.status).to.equal(200); + expect(relationships).to.have.property('author'); + expect(relationships).to.have.property('series'); + expect(relationships).to.have.property('stores'); + }); + }); + + // https://github.com/json-api/json-api/commit/6b18a4685692ae260f0ef1e10522b81725f83219 + it('may include a related resource URL in its links object keyed by "related"', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + expect(dataObj.relationships.chapters.links).to.have.property('related'); + }); + }); + + it('may include a "data" member whose value represents resource identifier objects', function() { + return Agent.request('GET', '/v1/books/1?include=author') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + expect(dataObj.relationships.author).to.have.property('data'); + }); + }); + + // The value of a relationship **MUST** be either a string URL + // or a link object. + // + // Endpoints takes the view that to-many relationships may + // contain numerous records. By default, it returns link objects + // for to-one references and string URLs for to-many references. + it('should make to-one references in a relationship object', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + var relationships = res.body.data.relationships; + expect(res.status).to.equal(200); + expect(relationships.author).to.be.an('Object'); + expect(relationships.series).to.be.an('Object'); + }); + }); + + it('should make to-many references in a relationships object', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + var relationships = res.body.data.relationships; + expect(res.status).to.equal(200); + expect(relationships.stores).to.be.an('Object'); + }); + }); + + it('should return related resources as the response primary data when a to-One string URL is fetched', function() { + return Agent.request('GET', '/v1/books/1/author') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + expect(dataObj.id).to.equal('1'); + expect(dataObj.type).to.equal('authors'); + }); + }); + + it('should return related resources as the response primary data when a to-Many string URL is fetched', function() { + return Agent.request('GET', '/v1/books/1/stores') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + expect(dataObj.length).to.equal(1); + expect(dataObj[0].type).to.equal('stores'); + }); + }); + + it('should return related resources as the response primary data when a nested string URL through a to-One is fetched', function() { + return Agent.request('GET', '/v1/chapters/1/book.author') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + expect(dataObj.id).to.equal('1'); + expect(dataObj.type).to.equal('authors'); + }); + }); + + it('should return related resources as the response primary data when a nested string URL through a to-Many is fetched', function() { + return Agent.request('GET', '/v1/books/1/stores.books') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + expect(dataObj.length).to.equal(11); + expect(dataObj[0].type).to.equal('books'); + }); + }); + + describe('relationshipObject', function() { + it('must contain either a "links,", "data", or "meta" property', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + expect(res.status).to.equal(200); + var dataObj = res.body.data; + var includedAuthor = dataObj.relationships.author; + var minProp = + includedAuthor.links || + includedAuthor.data || + includedAuthor.meta; + expect(minProp).to.exist; + }); + }); + + it('must include object linkage to resource objects included in the same compound document', function() { + return Agent.request('GET', '/v1/books/1?include=author') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + var includedAuthor = dataObj.relationships.author; + + expect(includedAuthor.data).to.exist; + }); + }); + + it('must express object linkages as type and id for all relationship types', function() { + return Agent.request('GET', '/v1/books/1?include=author,series') + .promise() + .then(function(res) { + var dataObj = res.body.data; + var links = dataObj.relationships; + expect(res.status).to.equal(200); + expect(links.author.data).to.have.property('type'); + expect(links.author.data).to.have.property('id'); + expect(links.series.data).to.have.property('type'); + expect(links.series.data).to.have.property('id'); + }); + }); + }); + }); + }); + + describe('compoundDocuments', function() { + // An endpoint **MAY** return resources included to the primary data + // by default. + // + // Endpoints handles this by allowing the API implementer to set + // default includes in the router. Endpoints will not include + // included resources by default. + // + // An endpoint MAY also support custom inclusion of included + // resources based upon an include request parameter. + it('must include included resources as an array of resource objects in a top level `included` member', function() { + return Agent.request('GET', '/v1/books/1?include=author') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + var includedAuthorLinkage = dataObj.relationships.author.data; + expect(res.body.included).to.be.a('array'); + expect(res.body.included[0].type).to.equal(includedAuthorLinkage.type); + expect(res.body.included[0].id).to.equal(includedAuthorLinkage.id); + }); + }); + + it('must not include more than one resource object for each type and id pair', function() { + return Agent.request('GET', '/v1/books?include=author') + .promise() + .then(function(res) { + expect(res.status).to.equal(200); + expect(res.body.included.length).to.equal(2); + }); + }); + }); + + it('must have the identical relationship name as the key in the relationships section of the parent resource object', function() { + return Agent.request('GET', '/v1/books/1?include=author,series,stores') + .promise() + .then(function(res) { + var relationships = Object.keys(res.body.data.relationships); + expect(res.status).to.equal(200); + expect(relationships.indexOf('author')).to.be.at.least(0); + expect(relationships.indexOf('series')).to.be.at.least(0); + expect(relationships.indexOf('stores')).to.be.at.least(0); + }); + }); + }); +*/ diff --git a/test/integration/json-api/v1/base-format/query-parameters/index.js b/test/integration/json-api/v1/base-format/query-parameters/index.js new file mode 100644 index 0000000..f66a52f --- /dev/null +++ b/test/integration/json-api/v1/base-format/query-parameters/index.js @@ -0,0 +1,9 @@ +// TODO: Implement +describe('errors', function() { + + it('Implementation specific query parameters **MUST** adhere to the same constraints as member names with the additional requirement that they **MUST** contain at least one non a-z character (U+0061 to U+007A).'); + + it('It is RECOMMENDED that a U+002D HYPHEN-MINUS, \"-\", U+005F LOW LINE, \"_\", or capital letter is used (e.g. camelCasing).'); + + it('If a server encounters a query parameter that does not follow the naming conventions above, and the server does not know how to process it as a query parameter from this specification, it **MUST** return `400 Bad Request`.'); +}); diff --git a/test/integration/json-api/v1/base-format/read/index.js b/test/integration/json-api/v1/base-format/read/index.js index cf4b540..7b13cd4 100644 --- a/test/integration/json-api/v1/base-format/read/index.js +++ b/test/integration/json-api/v1/base-format/read/index.js @@ -4,362 +4,174 @@ import {expect} from 'chai'; import Agent from '../../../../../app/agent'; import Fixture from '../../../../../app/fixture'; -describe('read', function() { +describe('fetchingData', function() { beforeEach(function() { return Fixture.reset(); }); - describe('documentStructure', function() { + describe('fetchingResources', function() { - describe('topLevel', function() { - it('must respond to a successful request with an object and respond with 200 OK', function() { - return Agent.request('GET', '/v1/books/1') - .promise() - .then(function(res) { - expect(res.status).to.equal(200); - expect(res.body) - // must place primary data under a top-level key named "data" - .to.have.property('data') - // must make primary data for a single record an object - .that.is.a('object'); - }); - }); + it('must support fetching resource data for every URL provided as a self link as part of the top-level links object', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + var links = res.body.data.links; + expect(res.status).to.equal(200); + return Agent.request('GET', links.self).promise(); + }) + .then(function(res) { + expect(res.status).to.equal(200); + expect(res.body.data).to.have.property('id'); + expect(res.body.data).to.have.property('type'); + expect(res.body.data).to.have.property('attributes'); + }); + }); - it('must respond to an unsuccessful request with a JSON object', function() { - return Fixture.dropTables() - .then(function() { - return Agent.request('GET', '/v1/books/1') - .promise(); - }) - .then(function(res) { - expect(res.status).to.equal(400); - expect(res.body).to.be.an('object'); - expect(res.body.errors).to.be.an('array'); - }); - }); + it('must support fetching resource data for every URL provided as a self link as part of a resource-level links object'); - it('must make primary data for multiple records an array and respond with 200 OK', function() { - return Agent.request('GET', '/v1/books') - .promise() - .then(function(res) { - expect(res.status).to.equal(200); - expect(res.body) - .to.have.property('data') - .that.is.a('array'); - }); - }); - }); + it('must support fetching resource data for every URL provided as a related link as part of a relationship-level links object'); - describe('resourceObjects', function() { + describe('responses', function() { - describe('resourceAttributes', function() { - it('should not contain a foreign key as an attribute', function() { + describe('200OK', function() { + + it('must respond to a successful request to fetch an individual resource with a `200 OK` response', function() { return Agent.request('GET', '/v1/books/1') .promise() .then(function(res) { - var dataObj = res.body.data; expect(res.status).to.equal(200); - expect(dataObj).to.be.an('object'); - expect(dataObj.attributes).to.not.have.property('author_id'); - expect(dataObj.attributes).to.not.have.property('series_id'); }); }); - it('must include relations as included resources', function() { - return Agent.request('GET', '/v1/books/1') + it('must respond to a successful request to fetch a resource collection with a `200 OK` response', function() { + return Agent.request('GET', '/v1/books') .promise() .then(function(res) { - var dataObj = res.body.data; expect(res.status).to.equal(200); - expect(dataObj).to.be.an('object'); - expect(dataObj.relationships).to.have.property('author'); - expect(dataObj.relationships).to.have.property('series'); }); }); - }); - - // TODO: DB/ORM test - // describe('resourceIdentification', function() { - // it('must have a unique type and id pair'); - // }); - describe('resourceTypes', function() { - it('must contain a type', function() { - return Agent.request('GET', '/v1/books/1') + it('must respond to a successful request to fetch that resource with a resource object if an individual resource exists', function() { + return Agent.request('GET', '/v1/books') .promise() .then(function(res) { - var dataObj = res.body.data; expect(res.status).to.equal(200); - expect(dataObj) - .to.have.property('type') - // must have a string value for type - .that.is.a('string'); + expect(res.body).to.have.property('data').that.is.a('array'); }); }); - }); - describe('resourceIds', function() { - it('must contain an id', function() { - return Agent.request('GET', '/v1/books/1') - .promise() - .then(function(res) { - var dataObj = res.body.data; - expect(res.status).to.equal(200); - expect(dataObj) - .to.have.property('id') - // must have a string value for type - .that.is.a('string'); - }); + it('must respond to a successful request to fetch that resource with `null` provided as primary data for the response document if the requested URL is one that might correspond to a single resource but does not currently', function() { + return Agent.request('GET', '/v1/books/11/series') + .promise() + .then(function(res) { + expect(res.status).to.equal(200); + expect(res.body) + .to.have.property('data') + .that.is.null; + }); }); }); - describe('relationships', function() { - it('must have an object as the value of any relationships key', function() { - return Agent.request('GET', '/v1/books/1') + describe('404NotFound', function() { + + it('must respond with `404 Not Found` when processing a request to fetch a single resource that does not exist, except when the request warrants a `200 OK` response with `null` as the primary data (as described above).', function() { + return Agent.request('GET', '/v1/books/9999') .promise() .then(function(res) { - var dataObj = res.body.data; - expect(res.status).to.equal(200); - expect(dataObj) - .to.have.property('relationships') - // must have a string value for type - .that.is.a('object'); + expect(res.status).to.equal(404); }); }); + }); - describe('resourceURLs', function() { - // OPTIONAL - // A resource object **MAY** include a URL in its links object, - // keyed by "self", that identifies the resource represented by - // the resource object. - it('may include a string in its links object keyed by "self"', function() { - return Agent.request('GET', '/v1/books/1') - .promise() - .then(function(res) { - var dataObj = res.body.data; - expect(res.status).to.equal(200); - expect(dataObj.links) - .to.have.property('self') - // must have a string value for type - .that.is.a('string'); - }); - }); + describe('otherResponses', function() { - it('must set the value of "self" to a URL that identifies the resource represented by this object', function() { - return Agent.request('GET', '/v1/books/1') - .promise() - .then(function(res) { - var dataObj = res.body.data; - expect(res.status).to.equal(200); - expect(dataObj.links.self).to.equal('/v1/books/1'); - }); - }); + it('may respond with other HTTP status codes'); + + it('may include error details with error responses'); + it('must prepare responses, and a client **MUST** interpret responses, in accordance with HTTP semantics'); }); + }); + }); - describe('resourceRelationships', function() { - // OPTIONAL - // A resource object MAY contain references to other resource - // objects ("relationships"). Relationships may be to-one or - // to-many. Relationships can be specified by including a member - // in a resource's links object. - it('may contain references to related objects in the links object', function() { - return Agent.request('GET', '/v1/books/1') - .promise() - .then(function(res) { - var relationships = res.body.data.relationships; - expect(res.status).to.equal(200); - expect(relationships).to.have.property('author'); - expect(relationships).to.have.property('series'); - expect(relationships).to.have.property('stores'); - }); - }); + describe('fetchingRelationships', function() { - // https://github.com/json-api/json-api/commit/6b18a4685692ae260f0ef1e10522b81725f83219 - it('may include a related resource URL in its links object keyed by "related"', function() { - return Agent.request('GET', '/v1/books/1') - .promise() - .then(function(res) { - var dataObj = res.body.data; - expect(res.status).to.equal(200); - expect(dataObj.relationships.chapters.links).to.have.property('related'); - }); + it('must support fetching relationship data for every relationship URL provided as a self link as part of a relationship\'s links object', function() { + return Agent.request('GET', '/v1/books/1') + .promise() + .then(function(res) { + var relationships = res.body.data.relationships; + expect(res.status).to.equal(200); + return Agent.request('GET', relationships.chapters.links.self).promise(); + }) + .then(function(res) { + expect(res.status).to.equal(200); + expect(res.body.data.length).to.equal(22); + expect(res.body.data[0]).to.have.property('id'); + expect(res.body.data[0]).to.have.property('type'); + expect(res.body.data[0].attributes).to.have.property('title'); + expect(res.body.data[0].attributes).to.have.property('ordering'); }); + }); - it('may include a "data" member whose value represents resource identifier objects', function() { - return Agent.request('GET', '/v1/books/1?include=author') - .promise() - .then(function(res) { - var dataObj = res.body.data; - expect(res.status).to.equal(200); - expect(dataObj.relationships.author).to.have.property('data'); - }); - }); + describe('responses', function() { - // TODO: unit test - Endpoints should throw if a model has a - // relation named 'self' - // it('shall not have a relationship to another object keyed as "self"'); - - // The value of a relationship **MUST** be either a string URL - // or a link object. - // - // Endpoints takes the view that to-many relationships may - // contain numerous records. By default, it returns link objects - // for to-one references and string URLs for to-many references. - it('should make to-one references in a relationship object', function() { - return Agent.request('GET', '/v1/books/1') - .promise() - .then(function(res) { - var relationships = res.body.data.relationships; - expect(res.status).to.equal(200); - expect(relationships.author).to.be.an('Object'); - expect(relationships.series).to.be.an('Object'); - }); - }); + describe('200OK', function() { - it('should make to-many references in a relationships object', function() { - return Agent.request('GET', '/v1/books/1') - .promise() - .then(function(res) { - var relationships = res.body.data.relationships; - expect(res.status).to.equal(200); - expect(relationships.stores).to.be.an('Object'); - }); - }); + it('must respond to a successful request to fetch a relationship with a `200 OK` response'); - it('should return related resources as the response primary data when a to-One string URL is fetched', function() { - return Agent.request('GET', '/v1/books/1/author') - .promise() - .then(function(res) { - var dataObj = res.body.data; - expect(res.status).to.equal(200); - expect(dataObj.id).to.equal('1'); - expect(dataObj.type).to.equal('authors'); - }); - }); + it('The primary data in the response document **MUST** match the appropriate value for resource linkage, as described above for relationship objects'); - it('should return related resources as the response primary data when a to-Many string URL is fetched', function() { - return Agent.request('GET', '/v1/books/1/stores') - .promise() - .then(function(res) { - var dataObj = res.body.data; - expect(res.status).to.equal(200); - expect(dataObj.length).to.equal(1); - expect(dataObj[0].type).to.equal('stores'); - }); - }); + it('The top-level links object **MAY** contain self and related links, as described above for relationship objects'); + }); - it('should return related resources as the response primary data when a nested string URL through a to-One is fetched', function() { - return Agent.request('GET', '/v1/chapters/1/book.author') - .promise() - .then(function(res) { - var dataObj = res.body.data; - expect(res.status).to.equal(200); - expect(dataObj.id).to.equal('1'); - expect(dataObj.type).to.equal('authors'); - }); - }); + describe('404NotFound', function() { - it('should return related resources as the response primary data when a nested string URL through a to-Many is fetched', function() { - return Agent.request('GET', '/v1/books/1/stores.books') + it('must return 404 Not Found when processing a request to fetch a relationship link URL that does not exist.', function() { + return Agent.request('GET', '/v1/books/1/relationships/bees') .promise() .then(function(res) { - var dataObj = res.body.data; - expect(res.status).to.equal(200); - expect(dataObj.length).to.equal(11); - expect(dataObj[0].type).to.equal('books'); + expect(res.status).to.equal(404); }); }); - // TODO: implement - describe('stringURLRelationship', function() { - it('must not change related URL even when the resource changes'); - }); - - describe('relationshipObject', function() { - it('must contain either a "links,", "data", or "meta" property', function() { - return Agent.request('GET', '/v1/books/1') - .promise() - .then(function(res) { - expect(res.status).to.equal(200); - var dataObj = res.body.data; - var includedAuthor = dataObj.relationships.author; - var minProp = - includedAuthor.links || - includedAuthor.data || - includedAuthor.meta; - expect(minProp).to.exist; - }); - }); + it('If a relationship link URL exists but the relationship is empty, then `200 OK` **MUST** be returned, as described above'); + }); - it('must include object linkage to resource objects included in the same compound document', function() { - return Agent.request('GET', '/v1/books/1?include=author') - .promise() - .then(function(res) { - var dataObj = res.body.data; - expect(res.status).to.equal(200); - var includedAuthor = dataObj.relationships.author; + describe('otherResponses', function() { - expect(includedAuthor.data).to.exist; - }); - }); + it('may respond with other HTTP status codes'); - it('must express object linkages as type and id for all relationship types', function() { - return Agent.request('GET', '/v1/books/1?include=author,series') - .promise() - .then(function(res) { - var dataObj = res.body.data; - var links = dataObj.relationships; - expect(res.status).to.equal(200); - expect(links.author.data).to.have.property('type'); - expect(links.author.data).to.have.property('id'); - expect(links.series.data).to.have.property('type'); - expect(links.series.data).to.have.property('id'); - }); - }); + it('may include error details with error responses'); - // We don't do heterogeneous to-many relationships - // it('must express object linkages as a data member whose value is an array of objects containing type and id for heterogeneous to-many relationships'); - }); + it('must prepare responses, and a client **MUST** interpret responses, in accordance with HTTP semantics'); }); }); + }); - describe('compoundDocuments', function() { - // An endpoint **MAY** return resources included to the primary data - // by default. - // - // Endpoints handles this by allowing the API implementer to set - // default includes in the router. Endpoints will not include - // included resources by default. - // - // An endpoint MAY also support custom inclusion of included - // resources based upon an include request parameter. - it('must include included resources as an array of resource objects in a top level `included` member', function() { - return Agent.request('GET', '/v1/books/1?include=author') - .promise() - .then(function(res) { - var dataObj = res.body.data; - expect(res.status).to.equal(200); - var includedAuthorLinkage = dataObj.relationships.author.data; - expect(res.body.included).to.be.a('array'); - expect(res.body.included[0].type).to.equal(includedAuthorLinkage.type); - expect(res.body.included[0].id).to.equal(includedAuthorLinkage.id); - }); - }); + describe.skip('inclusionOfRelatedResources', function() { - it('must not include more than one resource object for each type and id pair', function() { - return Agent.request('GET', '/v1/books?include=author') - .promise() - .then(function(res) { - expect(res.status).to.equal(200); - expect(res.body.included.length).to.equal(2); - }); - }); + // Endpoints handles this by allowing the API implementer to set + // default includes in the router. Endpoints will not include + // included resources by default. + it('may return resources related to the primary data by default'); + + it('may support an `include` request parameter to allow the client to customize which related resources should be returned', function() { + return Agent.request('GET', '/v1/books/1?include=author') + .promise() + .then(function(res) { + var dataObj = res.body.data; + expect(res.status).to.equal(200); + var includedAuthorLinkage = dataObj.relationships.author.data; + expect(res.body.included).to.be.a('array'); + expect(res.body.included[0].type).to.equal(includedAuthorLinkage.type); + expect(res.body.included[0].id).to.equal(includedAuthorLinkage.id); + }); }); - it('must not include other resource objects in the included section when the client specifies an include parameter', function() { + it('must not include unrequested resource objects in the included section of the compound document if it supports the include parameter and a client supplies it', function() { return Agent.request('GET', '/v1/books/1?include=series') .promise() .then(function(res) { @@ -370,142 +182,104 @@ describe('read', function() { }); }); - it('must have the identical relationship name as the key in the relationships section of the parent resource object', function() { - return Agent.request('GET', '/v1/books/1?include=author,series,stores') + it('must require the value of the `include` parameter be a comma-separated (U+002C COMMA, ",") list of relationship paths. A relationship path is a dot-separated (U+002E FULL-STOP, ".") list of relationship names'); + + it('must respond with 400 Bad Request if a server is unable to identify a relationship path or does not support inclusion of resources from a path'); + }); + + describe.skip('sparseFieldsets', function() { + + it('may support a `fields[TYPE]` request parameter to allow the client to customize which related resources should be returned'); + + it('must require the value of the `fields` parameter to be a comma-separated (U+002C COMMA, ",") list that refers to the name(s) of the fields to be returned'); + + it('it must not include additional fields in the response if a client requests a restricted set of fields', function() { + return Agent.request('GET', '/v1/books/?fields[books]=id,title') .promise() .then(function(res) { - var relationships = Object.keys(res.body.data.relationships); - expect(res.status).to.equal(200); - expect(relationships.indexOf('author')).to.be.at.least(0); - expect(relationships.indexOf('series')).to.be.at.least(0); - expect(relationships.indexOf('stores')).to.be.at.least(0); + var dataObj = res.body.data[0]; + expect(dataObj).to.have.property('id'); + expect(dataObj.attributes).to.have.property('title'); + expect(dataObj.attributes).to.not.have.property('date_published'); + }); + }); + }); + + describe.skip('sorting', function() { + + it('may support requests to sort resource collections according to one or more criteria ("sort fields")'); + + it('may support requests to sort the primary data with a `sort` query parameter', function() { + return Agent.request('GET', '/v1/books/?sort=+title') + .promise() + .then(function(res) { + expect(res.body.data[0].attributes.title).to.equal('Harry Potter and the Chamber of Secrets'); }); }); - // TODO: Meta object not currently used by endpoints - describe('metaInformation', function() { - it('must be an object value'); + it('must use `sort` to represent sort fields'); + + it('may support multiple sort fields by allowing comma-separated (U+002C COMMA, ",") sort fields and should apply sort fields in the order specified', function() { + return Agent.request('GET', '/v1/books/?sort=-date_published,title') + .promise() + .then(function(res) { + expect(res.body.data[0].attributes.title).to.equal('Harry Potter and the Deathly Hallows'); + }); }); - describe('topLevelLinks', function() { - it('should not include members other than self, resource, and pagination links if necessary'); + it('must default to ascending sort order for each sort field'); + + it('must return results in descending order if the field name is prefixed with a minus (U+002D HYPHEN-MINUS, "-")', function() { + return Agent.request('GET', '/v1/books/?sort=-title') + .promise() + .then(function(res) { + expect(res.body.data[0].attributes.title).to.equal('The Two Towers'); + }); }); + + it('must return `400 Bad Request` if the server does not support sorting as specified in the query parameter `sort`'); + + it('If sorting is supported by the server and requested by the client via query parameter sort, the server **MUST** return elements of the top-level data array of the response ordered according to the criteria specified'); + + it('may apply default sorting rules to top-level data if request parameter `sort` is not specified'); }); - // These tests have been moved above to 'compoundDocuments' - // describe('inclusionOfincludedResources', function() { - // }); + describe.skip('pagination', function() { - describe('fetchingData', function() { + it('may limit the number of resources returned in a response to a subset ("page") of the whole set available'); - describe('fetchingResources', function() { - it('must support fetching resource for URLs provided as a `self` link in a links object', function() { - return Agent.request('GET', '/v1/books/1') - .promise() - .then(function(res) { - var links = res.body.data.links; - expect(res.status).to.equal(200); - return Agent.request('GET', links.self).promise(); - }) - .then(function(res) { - expect(res.status).to.equal(200); - expect(res.body.data).to.have.property('id'); - expect(res.body.data).to.have.property('type'); - expect(res.body.data).to.have.property('attributes'); - }); - }); + it('may provide links to traverse a paginated data set ("pagination links")'); + it('must place pagination links (if present) in the links object that corresponds to a collection'); - describe('responses', function() { - describe('200Ok', function() { - // TESTED ABOVE under "Top Level" - // it('must respond to a successful request to fetch an individual resource or collection with a 200 OK response'); - // it('must respond to a successful request to fetch a resource collection with an array as the document\'s primary data'); - it('must respond to a request to fetch a resource that does not exist with null as the document\'s primary data', function() { - return Agent.request('GET', '/v1/books/11/series') - .promise() - .then(function(res) { - expect(res.status).to.equal(200); - expect(res.body) - .to.have.property('data') - .that.is.null; - }); - }); - }); + it('must not use the following keys for pagination links:\n\n- `first`: the first page of data\n- `last`: the last page of data\n- `prev`: the previous page of data\n- `next`: the next page of data'); - describe('404NotFound', function() { - it('must return 404 Not Found when processing a request to fetch a resource that does not exist', function() { - return Agent.request('GET', '/v1/books/9999') - .promise() - .then(function(res) { - expect(res.status).to.equal(404); - }); - }); - }); - }); - }); + it('must either omit keys or set a `null` value to indicate that a particular link is unavailable'); - describe('fetchingRelationships', function() { - it('must support fetching resource for URLs provided as a `self` link as part of a relationship object', function() { - return Agent.request('GET', '/v1/books/1') - .promise() - .then(function(res) { - var relationships = res.body.data.relationships; - expect(res.status).to.equal(200); - return Agent.request('GET', relationships.chapters.links.self).promise(); - }) - .then(function(res) { - expect(res.status).to.equal(200); - expect(res.body.data.length).to.equal(22); - expect(res.body.data[0]).to.have.property('id'); - expect(res.body.data[0]).to.have.property('type'); - expect(res.body.data[0].attributes).to.have.property('title'); - expect(res.body.data[0].attributes).to.have.property('ordering'); - }); - }); + it('must force concepts of order, as expressed in the naming of pagination links, to remain consistent with JSON API\'s sorting rules'); - describe('responses', function() { - describe('200Ok', function() { - // TESTED ABOVE - // it('must respond to a successful request to fetch a relationship with a 200OK response'); - // it('must have a primary data consisting of null, an object of type and id members, an array'); - }); + it('should use the `page` query parameter for pagination operations'); + }); - describe('404NotFound', function() { - it('must return 404 Not Found when processing a request to fetch a relationship URL that does not exist', function() { - return Agent.request('GET', '/v1/books/1/relationships/bees') - .promise() - .then(function(res) { - expect(res.status).to.equal(404); - } - ); - }); + describe.skip('filtering', function() { + + it('should use the `filter` query parameter for filtering operations', function() { + return Agent.request('GET', '/v1/books/?filter[date_published]=2000-07-08,1937-09-21') + .promise() + .then(function(res) { + expect(res.body.data.length).to.equal(2); + expect(res.body.data[0].id).to.equal('7'); + expect(res.body.data[1].id).to.equal('11'); }); - }); }); + }); +}); - describe('sparseFieldsets', function() { - it('should support returning **only** specific fields in the response on a per-type basis by including a fields[TYPE] parameter', function() { - return Agent.request('GET', '/v1/books/?fields[books]=id,title') - .promise() - .then(function(res) { - var dataObj = res.body.data[0]; - expect(dataObj).to.have.property('id'); - expect(dataObj.attributes).to.have.property('title'); - expect(dataObj.attributes).to.not.have.property('date_published'); - }); - }); - }); +/* OLD READ TESTS - describe('sorting', function() { - it('should support requests to sort collections with a sort query parameter', function() { - return Agent.request('GET', '/v1/books/?sort=+title') - .promise() - .then(function(res) { - expect(res.body.data[0].attributes.title).to.equal('Harry Potter and the Chamber of Secrets'); - }); - }); + describe('fetchingData', function() { + describe('sorting', function() { // TODO: Support sorting by nested relations // https://github.com/endpoints/endpoints/issues/63 it.skip('should support sorting by nested relationship attributes', function() { @@ -515,44 +289,7 @@ describe('read', function() { expect(res.body.data[0].attributes.title).to.equal('Harry Potter and the Philosopher\'s Stone'); }); }); - - it('should sort multiple criteria using comma-separated fields in the order specified', function() { - return Agent.request('GET', '/v1/books/?sort=-date_published,+title') - .promise() - .then(function(res) { - expect(res.body.data[0].attributes.title).to.equal('Harry Potter and the Deathly Hallows'); - }); - }); - - it('must sort ascending or descending based on explicit sort order using "+" or "-"', function() { - return Agent.request('GET', '/v1/books/?sort=-title') - .promise() - .then(function(res) { - expect(res.body.data[0].attributes.title).to.equal('The Two Towers'); - }); - }); - }); - - // TODO: Pagination - describe('pagination', function() { - it('should limit the number of resources returned in a response to a subset of the whole set available'); - it('should provide links to traverse a paginated data set'); - it('must put any pagination links on the object that corresponds to a collection'); - it('must only use "first," "last," "prev," and "next" as keys for pagination links'); - it('must omit or set values to null for links that are unavailable'); - it('must remain consistent with the sorting rules'); - }); - - describe('filtering', function() { - it('must only use the filter query parameter for filtering data', function() { - return Agent.request('GET', '/v1/books/?filter[date_published]=2000-07-08,1937-09-21') - .promise() - .then(function(res) { - expect(res.body.data.length).to.equal(2); - expect(res.body.data[0].id).to.equal('7'); - expect(res.body.data[1].id).to.equal('11'); - }); - }); }); }); -}); + +*/ diff --git a/test/integration/json-api/v1/base-format/update/index.js b/test/integration/json-api/v1/base-format/update/index.js index 656748f..b054cb9 100644 --- a/test/integration/json-api/v1/base-format/update/index.js +++ b/test/integration/json-api/v1/base-format/update/index.js @@ -18,6 +18,162 @@ beforeEach(function() { return Fixture.reset(); }); +describe('updatingResources', function() { + + it('The `PATCH` request **MUST** include a single resource object as primary data.'); + + it('The resource object **MUST** contain `type` and `id` members.'); + + describe('updatingAResourcesAttributes', function() { + + it('Any or all of a resource\'s attributes **MAY** be included in the resource object included in a `PATCH` request.'); + + it('If a request does not include all of the attributes for a resource, the server **MUST** interpret the missing attributes as if they were included with their current values. It **MUST NOT** interpret them as `null` values.'); + }); + + describe('updatingAResourcesRelationships', function() { + + it('Any or all of a resource\'s relationships **MAY** be included in the resource object included in a `PATCH` request.'); + + it('If a request does not include all of the relationships for a resource, the server **MUST** interpret the missing relationships as if they were included with their current values. It **MUST NOT** interpret them as `null` or empty values.'); + + it('If a relationship is provided in the relationships member of a resource object in a `PATCH` request, its value **MUST** be a relationship object with a data member.'); + + it('may reject an attempt to do a full replacement of a to-many relationship.'); + + it('In such a case, the server **MUST** reject the entire update, and return a `403 Forbidden` response.'); + + it('If an update request has been accepted for processing, but the processing has not been completed by the time the server responds, the server **MUST** return a `202 Accepted` status code.'); + }); + + describe('responses', function() { + + describe('202Accepted', function() { + + it('If a server accepts an update but also changes the resource(s) in ways other than those specified by the request (for example, updating the `updated-at` attribute or a computed `sha`), it **MUST** return a `200 OK` response.'); + }); + + describe('200OK', function() { + + it('The response document **MUST** include a representation of the updated resource(s) as if a `GET` request was made to the request URL.'); + + it('must return a `200 OK` status code if an update is successful, the client\'s current attributes remain up to date, and the server responds only with top-level meta data.'); + + it('In this case the server **MUST NOT** include a representation of the updated resource(s).'); + }); + + describe('204NoContent', function() { + + it('If an update is successful and the server doesn\'t update any attributes besides those provided, the server **MUST** return either a `200 OK` status code and response document (as described above) or a `204 No Content` status code with no response document.'); + }); + + describe('403Forbidden', function() { + + it('must return `403 Forbidden` in response to an unsupported request to update a resource or relationship.'); + }); + + describe('404NotFound', function() { + + it('must return `404 Not Found` when processing a request to modify a resource that does not exist.'); + + it('must return `404 Not Found` when processing a request that references a related resource that does not exist.'); + }); + + describe('409Conflict', function() { + + it('may return `409 Conflict` when processing a `PATCH` request to update a resource if that update would violate other server-enforced constraints (such as a uniqueness constraint on a property other than `id`).'); + + it('must return `409 Conflict` when processing a `PATCH` request in which the resource object's `type` and `id` do not match the server's endpoint.'); + + it('A server **SHOULD** include error details and provide enough information to recognize the source of the conflict.'); + }); + + describe('otherResponses', function() { + + it('may respond with other `HTTP` status codes.'); + + it('may include error details with error responses.'); + + it('must prepare responses, and a client **MUST** interpret responses, in accordance with HTTP semantics.'); + }); + }); +}); + +describe('updatingRelationships', function() { + + describe('updatingToOnRelationships', function() { + + it('must respond to `PATCH` requests to a URL from a to-one relationship link as described below.'); + + it('The PATCH request MUST include a top-level member named data containing one of:\\n\\n- a resource identifier object corresponding to the new related resource.\\n- `null`, to remove the relationship.'); + + it('If the relationship is updated successfully then the server **MUST** return a successful response.'); + }); + + describe('updatingToManyRelationships', function() { + + it('must respond to `PATCH`, `POST`, and `DELETE` requests to a URL from a to-many relationship link as described below.'); + + it('For all request types, the body **MUST** contain a `data` member whose value is an empty array or an array of resource identifier objects.'); + + it('If a client makes a `PATCH` request to a URL from a to-many relationship link, the server **MUST** either completely replace every member of the relationship, return an appropriate error response if some resources can not be found or accessed, or return a `403 Forbidden` response if complete replacement is not allowed by the server.'); + + it('If a client makes a `POST` request to a URL from a relationship link, the server **MUST** add the specified members to the relationship unless they are already present.'); + + it('If a given `type` and `id` is already in the relationship, the server **MUST NOT** add it again.'); + + it('If a given `type` and `id` is already in the relationship, the server **MUST NOT** add it again.'); + + it('If all of the specified resources can be added to, or are already present in, the relationship then the server **MUST** return a successful response.'); + + it('If the client makes a `DELETE` request to a URL from a relationship link the server **MUST** delete the specified members from the relationship or return a `403 Forbidden` response.'); + + it('If the client makes a `DELETE` request to a URL from a relationship link the server **MUST** delete the specified members from the relationship or return a `403 Forbidden` response.'); + + it('If all of the specified resources are able to be removed from, or are already missing from, the relationship then the server **MUST** return a successful response.'); + }); + + describe('responses', function() { + + describe('202Accepted', function() { + + it('If a relationship update request has been accepted for processing, but the processing has not been completed by the time the server responds, the server **MUST** return a `202 Accepted` status code.'); + }); + + describe('204NoContent', function() { + + it('must return a `204 No Content` status code if an update is successful and the representation of the resource in the request matches the result.'); + }); + + describe('200OK', function() { + + it('If a server accepts an update but also changes the targeted relationship(s) in other ways than those specified by the request, it **MUST** return a `200 OK` response.'); + + it('The response document **MUST** include a representation of the updated relationship(s).'); + + it('must return a `200 OK` status code if an update is successful, the client\'s current data remain up to date, and the server responds only with top-level meta data.'); + + it('In this case the server **MUST NOT** include a representation of the updated relationship(s).'); + }); + + describe('403Forbidden', function() { + + it('must return `403 Forbidden` in response to an unsupported request to update a relationship.'); + }); + + describe('otherResponses', function() { + + it('may respond with other HTTP status codes.'); + + it('may include error details with error responses.'); + + it('must prepare responses, and a client **MUST** interpret responses, in accordance with HTTP semantics.'); + }); + }); +}); + + +/* OLD UPDATE TESTS describe('updatingResources', function() { it('must respond to a successful request with an object', function() { @@ -620,3 +776,4 @@ describe('updatingRelationships', function() { // }); }); }); +*/