diff --git a/package-lock.json b/package-lock.json index 330863e..6bc3c11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "jsdoc": "^4.0.2", "minimist": "^1.2.8", "mocha": "^10.4.0", - "ostensibly-typed": "^1.0.3", + "ostensibly-typed": "^1.2.0", "rollup": "^4.25.0", "sinon": "^19.0.2", "typescript": "^5.4.5" @@ -1767,9 +1767,9 @@ } }, "node_modules/ostensibly-typed": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ostensibly-typed/-/ostensibly-typed-1.0.3.tgz", - "integrity": "sha512-9xiB7kV1Uhig0vB8mwgJF62JJt/Z1ooOma+Lvg6EpG7Ni98GODi1US5aWEU7RvegafghItlsUPYyIQY6fY+B7g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ostensibly-typed/-/ostensibly-typed-1.2.0.tgz", + "integrity": "sha512-JxfyIwDZHARwW+0ebPsT2TQc4+Ev7OfAoJ3zlna455Vos5UTSyqm4DtJDC6WPsufhWM1prHWC6gTTiwoGR7A6Q==", "dev": true, "peerDependencies": { "typescript": ">=5.6" diff --git a/package.json b/package.json index ac18a70..a76f966 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,24 @@ "types": "./dist/scimmy.d.ts", "exports": { ".": { - "import": "./dist/scimmy.js", - "require": "./dist/cjs/scimmy.cjs" + "import": { + "types": "./dist/scimmy.d.ts", + "default": "./dist/scimmy.js" + }, + "require": { + "types": "./dist/scimmy.d.ts", + "default": "./dist/cjs/scimmy.cjs" + } }, "./*": { - "import": "./dist/lib/*.js", - "require": "./dist/cjs/lib/*.js" + "import": { + "types": "./dist/scimmy.d.ts", + "default": "./dist/lib/*.js" + }, + "require": { + "types": "./dist/scimmy.d.ts", + "default": "./dist/cjs/lib/*.js" + } } }, "scripts": { @@ -70,7 +82,7 @@ "jsdoc": "^4.0.2", "minimist": "^1.2.8", "mocha": "^10.4.0", - "ostensibly-typed": "^1.0.3", + "ostensibly-typed": "^1.2.0", "rollup": "^4.25.0", "sinon": "^19.0.2", "typescript": "^5.4.5" diff --git a/packager.js b/packager.js index aebe36c..5fca129 100644 --- a/packager.js +++ b/packager.js @@ -192,14 +192,6 @@ export class Packager { const input = {[entry]: path.join(src, `${entry}.js`), ...chunks.reduce((chunks, chunk) => Object.assign(chunks, {[chunk]: path.join(src, `${chunk}.js`)}), {})}; const manualChunks = chunks.reduce((chunks, chunk) => Object.assign(chunks, {[chunk]: [path.join(src, `${chunk}.js`)]}), {}); const output = []; - const config = { - exports: "named", interop: "auto", - minifyInternalExports: false, - hoistTransitiveImports: false, - manualChunks, generatedCode: { - constBindings: true - } - }; // Prepare RollupJS bundle with supplied entry points const bundle = await rollup.rollup({ @@ -210,8 +202,15 @@ export class Packager { // Construct the bundles with specified chunks in specified formats and write to destination for (let format of ["esm", "cjs"]) { const {output: results} = await bundle.write({ - ...config, format, dir: (format === "esm" ? dest : `${dest}/${format}`), - entryFileNames: fileNameConfig[format], chunkFileNames: fileNameConfig[format] + format, exports: "named", interop: "auto", + dir: (format === "esm" ? dest : `${dest}/${format}`), + entryFileNames: fileNameConfig[format], + chunkFileNames: fileNameConfig[format], + minifyInternalExports: false, + hoistTransitiveImports: false, + manualChunks, generatedCode: { + constBindings: true + } }); output.push(...results.map(file => (format === "esm" ? file.fileName : `${format}/${file.fileName}`))); @@ -234,7 +233,7 @@ export class Packager { // Prepare RollupJS with OstensiblyTyped plugin and supplied entry point const bundle = await rollup.rollup({ - external, input: path.join(src, `${entry}.js`), + external, input: path.join(src, `${entry}.js`), plugins: [generateDeclarations({moduleName: entry, defaultExport: "SCIMMY"})], onwarn: (warning, warn) => (warning.code !== "CIRCULAR_DEPENDENCY" ? warn(warning) : false) }); diff --git a/src/lib/messages/bulkrequest.js b/src/lib/messages/bulkrequest.js index a89e6b0..97458d5 100644 --- a/src/lib/messages/bulkrequest.js +++ b/src/lib/messages/bulkrequest.js @@ -34,16 +34,27 @@ export class BulkRequest { #dispatched = false; /** - * Instantiate a new SCIM BulkResponse message from the supplied BulkRequest + * BulkRequest operation details + * @typedef {Object} SCIMMY.Messages.BulkRequest~BulkOpOperation + * @property {SCIMMY.Messages.BulkRequest~ValidBulkMethods} method - the HTTP method used for the requested operation + * @property {String} [bulkId] - the transient identifier of a newly created resource, unique within a bulk request and created by the client + * @property {String} [version] - resource version after operation has been applied + * @property {String} [path] - the resource's relative path to the SCIM service provider's root + * @property {Object} [data] - the resource data as it would appear for the corresponding single SCIM HTTP request + * @inner + */ + + /** + * Instantiate a new SCIM BulkRequest message from the supplied operations * @param {Object} request - contents of the BulkRequest operation being performed - * @param {Object[]} request.Operations - list of SCIM-compliant bulk operations to apply + * @param {SCIMMY.Messages.BulkRequest~BulkOpOperation[]} request.Operations - list of SCIM-compliant bulk operations to apply * @param {Number} [request.failOnErrors] - number of error results to encounter before aborting any following operations * @param {Number} [maxOperations] - maximum number of operations supported in the request, as specified by the service provider - * @property {Object[]} Operations - list of operations in this BulkRequest instance + * @property {SCIMMY.Messages.BulkRequest~BulkOpOperation[]} Operations - list of operations in this BulkRequest instance * @property {Number} [failOnErrors] - number of error results a service provider should tolerate before aborting any following operations */ constructor(request, maxOperations = 0) { - let {schemas = [], Operations: operations = [], failOnErrors = 0} = request ?? {}; + const {schemas = [], Operations: operations = [], failOnErrors = 0} = request ?? {}; // Make sure specified schema is valid if (schemas.length !== 1 || !schemas.includes(BulkRequest.#id)) @@ -69,7 +80,7 @@ export class BulkRequest { } /** - * Apply the operations specified by the supplied BulkRequest + * Apply the operations specified by the supplied BulkRequest and return a new BulkResponse message * @param {typeof SCIMMY.Types.Resource[]} [resourceTypes] - resource type classes to be used while processing bulk operations, defaults to declared resources * @param {*} [ctx] - any additional context information to pass to the ingress, egress, and degress handlers * @returns {SCIMMY.Messages.BulkResponse} a new BulkResponse Message instance with results of the requested operations diff --git a/src/lib/messages/bulkresponse.js b/src/lib/messages/bulkresponse.js index 30a37ca..f99d283 100644 --- a/src/lib/messages/bulkresponse.js +++ b/src/lib/messages/bulkresponse.js @@ -14,15 +14,45 @@ export class BulkResponse { */ static #id = "urn:ietf:params:scim:api:messages:2.0:BulkResponse"; + /** + * BulkResponse operation response status codes + * @enum {200|201|204|307|308|400|401|403|404|409|412|500|501} SCIMMY.Messages.BulkResponse~ResponseStatusCodes + * @inner + */ + + /** + * BulkResponse operation details for a given BulkRequest operation + * @typedef {Object} SCIMMY.Messages.BulkResponse~BulkOpResponse + * @property {String} [location] - canonical URI for the target resource of the operation + * @property {SCIMMY.Messages.BulkRequest~ValidBulkMethods} method - the HTTP method used for the requested operation + * @property {String} [bulkId] - the transient identifier of a newly created resource, unique within a bulk request and created by the client + * @property {String} [version] - resource version after operation has been applied + * @property {SCIMMY.Messages.BulkResponse~ResponseStatusCodes} status - the HTTP response status code for the requested operation + * @property {Object} [response] - the HTTP response body for the specified request operation + * @inner + */ + + /** + * Instantiate a new outbound SCIM BulkResponse message from the results of performed operations + * @overload + * @param {SCIMMY.Messages.BulkResponse~BulkOpResponse[]} operations - results of performed operations + */ + /** + * Instantiate a new inbound SCIM BulkResponse message instance from the received response + * @overload + * @param {Object} request - contents of the received BulkResponse message + * @param {SCIMMY.Messages.BulkResponse~BulkOpResponse[]} request.Operations - list of SCIM-compliant bulk operation results + */ /** * Instantiate a new SCIM BulkResponse message from the supplied Operations - * @param {Object|Object[]} request - contents of the BulkResponse if object, or results of performed operations if array - * @param {Object[]} [request.Operations] - list of applied SCIM-compliant bulk operation results, if request is an object - * @property {Object[]} Operations - list of BulkResponse operation results + * @param {SCIMMY.Messages.BulkResponse~BulkOpResponse[]} request - results of performed operations if array + * @param {Object} request - contents of the received BulkResponse message if object + * @param {SCIMMY.Messages.BulkResponse~BulkOpResponse[]} request.Operations - list of SCIM-compliant bulk operation results + * @property {SCIMMY.Messages.BulkResponse~BulkOpResponse[]} Operations - list of BulkResponse operation results */ constructor(request = []) { - let outbound = Array.isArray(request), - operations = (outbound ? request : request?.Operations ?? []); + const outbound = Array.isArray(request); + const operations = (outbound ? request : request?.Operations ?? []); // Verify the BulkResponse contents are valid if (!outbound && Array.isArray(request?.schemas) && (!request.schemas.includes(BulkResponse.#id) || request.schemas.length > 1)) diff --git a/src/lib/messages/error.js b/src/lib/messages/error.js index c97be35..ca2c9aa 100644 --- a/src/lib/messages/error.js +++ b/src/lib/messages/error.js @@ -36,11 +36,11 @@ export class ErrorResponse extends Error { static #id = "urn:ietf:params:scim:api:messages:2.0:Error"; /** + * Details of the underlying cause of the error response * @typedef {Object} SCIMMY.Messages.ErrorResponse~CauseDetails * @property {SCIMMY.Messages.ErrorResponse~ValidStatusCodes} [status=500] - HTTP status code to be sent with the error * @property {SCIMMY.Messages.ErrorResponse~ValidScimTypes} [scimType] - the SCIM detail error keyword as per [RFC7644§3.12]{@link https://datatracker.ietf.org/doc/html/rfc7644#section-3.12} * @property {String} [detail] - a human-readable description of what caused the error to occur - * @internal * @inner */ diff --git a/src/lib/messages/listresponse.js b/src/lib/messages/listresponse.js index 8096d0b..be6d3ca 100644 --- a/src/lib/messages/listresponse.js +++ b/src/lib/messages/listresponse.js @@ -12,13 +12,19 @@ export class ListResponse { */ static #id = "urn:ietf:params:scim:api:messages:2.0:ListResponse"; + /** + * ListResponse sort and pagination constraints + * @typedef {Object} SCIMMY.Messages.ListResponse~ListConstraints + * @property {String} [sortBy] - the attribute to sort results by, if any + * @property {String} [sortOrder="ascending"] - the direction to sort results in, if sortBy is specified + * @property {Number} [startIndex=1] - offset index that items start from + * @property {Number} [count=20] - maximum number of items returned in this list response + */ + /** * Instantiate a new SCIM List Response Message with relevant details * @param {Object|SCIMMY.Types.Schema[]} request - contents of the ListResponse message, or items to include in the list response - * @param {Object} [params] - parameters for the list response (i.e. sort details, start index, and items per page) - * @param {String} [params.sortBy] - the attribute to sort results by, if any - * @param {String} [params.sortOrder="ascending"] - the direction to sort results in, if sortBy is specified - * @param {Number} [params.startIndex=1] - offset index that items start from + * @param {SCIMMY.Messages.ListResponse~ListConstraints} [params] - parameters for the list response (i.e. sort details, start index, and items per page) * @param {Number} [params.count=20] - alias property for itemsPerPage, used only if itemsPerPage is unset * @param {Number} [params.itemsPerPage=20] - maximum number of items returned in this list response * @property {Array} Resources - resources included in the list response diff --git a/src/lib/resources/group.js b/src/lib/resources/group.js index fe91118..654592c 100644 --- a/src/lib/resources/group.js +++ b/src/lib/resources/group.js @@ -34,6 +34,11 @@ export class Group extends Types.Resource { return Schemas.Group; } + /** @implements {SCIMMY.Types.Resource.extend} */ + static extend(...args) { + return super.extend(...args); + } + /** @private */ static #ingress = () => { throw new Types.Error(501, null, "Method 'ingress' not implemented by resource 'Group'"); diff --git a/src/lib/resources/user.js b/src/lib/resources/user.js index 7539440..c862f11 100644 --- a/src/lib/resources/user.js +++ b/src/lib/resources/user.js @@ -34,6 +34,11 @@ export class User extends Types.Resource { return Schemas.User; } + /** @implements {SCIMMY.Types.Resource.extend} */ + static extend(...args) { + return super.extend(...args); + } + /** @private */ static #ingress = () => { throw new Types.Error(501, null, "Method 'ingress' not implemented by resource 'User'"); diff --git a/src/lib/schemas/enterpriseuser.js b/src/lib/schemas/enterpriseuser.js index 69d9f17..743af66 100644 --- a/src/lib/schemas/enterpriseuser.js +++ b/src/lib/schemas/enterpriseuser.js @@ -8,6 +8,11 @@ import Types from "../types.js"; * * Can be used directly, but is typically used to extend the `SCIMMY.Schemas.User` schema definition. */ export class EnterpriseUser extends Types.Schema { + /** @type {"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"} */ + static get id() { + return EnterpriseUser.#definition.id; + } + /** @implements {SCIMMY.Types.Schema.definition} */ static get definition() { return EnterpriseUser.#definition; diff --git a/src/lib/schemas/group.js b/src/lib/schemas/group.js index 130197a..fbcccce 100644 --- a/src/lib/schemas/group.js +++ b/src/lib/schemas/group.js @@ -7,6 +7,11 @@ import Types from "../types.js"; * * Ensures a Group instance conforms to the Group schema set out in [RFC7643§4.2](https://datatracker.ietf.org/doc/html/rfc7643#section-4.2). */ export class Group extends Types.Schema { + /** @type {"urn:ietf:params:scim:schemas:core:2.0:Group"} */ + static get id() { + return Group.#definition.id; + } + /** @implements {SCIMMY.Types.Schema.definition} */ static get definition() { return Group.#definition; diff --git a/src/lib/schemas/resourcetype.js b/src/lib/schemas/resourcetype.js index c9b7acf..5ee59c1 100644 --- a/src/lib/schemas/resourcetype.js +++ b/src/lib/schemas/resourcetype.js @@ -7,6 +7,11 @@ import Types from "../types.js"; * * Ensures a ResourceType instance conforms to the ResourceType schema set out in [RFC7643§6](https://datatracker.ietf.org/doc/html/rfc7643#section-6). */ export class ResourceType extends Types.Schema { + /** @type {"urn:ietf:params:scim:schemas:core:2.0:ResourceType"} */ + static get id() { + return ResourceType.#definition.id; + } + /** @implements {SCIMMY.Types.Schema.definition} */ static get definition() { return ResourceType.#definition; diff --git a/src/lib/schemas/spconfig.js b/src/lib/schemas/spconfig.js index 9dc0276..ce6d487 100644 --- a/src/lib/schemas/spconfig.js +++ b/src/lib/schemas/spconfig.js @@ -7,6 +7,11 @@ import Types from "../types.js"; * * Ensures a ServiceProviderConfig instance conforms to the Service Provider Configuration schema set out in [RFC7643§5](https://datatracker.ietf.org/doc/html/rfc7643#section-5). */ export class ServiceProviderConfig extends Types.Schema { + /** @type {"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"} */ + static get id() { + return ServiceProviderConfig.#definition.id; + } + /** @implements {SCIMMY.Types.Schema.definition} */ static get definition() { return ServiceProviderConfig.#definition; @@ -14,8 +19,7 @@ export class ServiceProviderConfig extends Types.Schema { /** @private */ static #definition = new Types.SchemaDefinition( - "ServiceProviderConfig", "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig", - "Schema for representing the service provider's configuration", [ + "ServiceProviderConfig", "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig", "Schema for representing the service provider's configuration", [ new Types.Attribute("reference", "documentationUri", {mutable: false, referenceTypes: ["external"], description: "An HTTP-addressable URL pointing to the service provider's human-consumable help documentation."}), new Types.Attribute("complex", "patch", {required: true, mutable: false, uniqueness: false, description: "A complex type that specifies PATCH configuration options."}, [ new Types.Attribute("boolean", "supported", {required: true, mutable: false, description: "A Boolean value specifying whether or not the operation is supported."}) diff --git a/src/lib/schemas/user.js b/src/lib/schemas/user.js index 505e642..7bb18db 100644 --- a/src/lib/schemas/user.js +++ b/src/lib/schemas/user.js @@ -7,6 +7,11 @@ import Types from "../types.js"; * * Ensures a User instance conforms to the User schema set out in [RFC7643§4.1](https://datatracker.ietf.org/doc/html/rfc7643#section-4.1). */ export class User extends Types.Schema { + /** @type {"urn:ietf:params:scim:schemas:core:2.0:User"} */ + static get id() { + return User.#definition.id; + } + /** @implements {SCIMMY.Types.Schema.definition} */ static get definition() { return User.#definition; diff --git a/src/lib/types/resource.js b/src/lib/types/resource.js index d0adaef..72ff0e5 100644 --- a/src/lib/types/resource.js +++ b/src/lib/types/resource.js @@ -6,7 +6,7 @@ import {Filter} from "./filter.js"; /** * Automatically assigned attributes not required in handler return values * @enum {"schemas"|"meta"} SCIMMY.Types.Resource~ShadowAttributes - * @private + * @ignore */ /** @@ -57,9 +57,12 @@ export class Resource { /** * Register an extension to the resource's core schema + * @template {typeof SCIMMY.Types.Resource} R + * @template {SCIMMY.Types.Schema} [S=*] - type of schema instance that will be passed to handlers * @param {typeof SCIMMY.Types.Schema} extension - the schema extension to register * @param {Boolean} [required] - whether the extension is required - * @returns {typeof SCIMMY.Types.Resource} this resource type implementation for chaining + * @returns {R} this resource type implementation for chaining + * @abstract */ static extend(extension, required) { this.schema.extend(extension, required); @@ -69,7 +72,7 @@ export class Resource { /** * Handler for ingress of a resource - * @template {SCIMMY.Types.Resource} R - type of resource instance performing ingress + * @template {SCIMMY.Types.Resource} R - type of resource instance performing ingress * @template {SCIMMY.Types.Schema} S - type of schema instance that will be passed to handler * @template {Record} [V=Omit, Resource.ShadowAttributes>] - shape of return value * @callback SCIMMY.Types.Resource~IngressHandler @@ -112,7 +115,8 @@ export class Resource { * Sets the method to be called to consume a resource on create * @template {typeof SCIMMY.Types.Resource} R * @template {SCIMMY.Types.Schema} S - * @param {SCIMMY.Types.Resource~IngressHandler, S>} handler - function to invoke to consume a resource on create + * @typeParam {S} [V=S] + * @param {SCIMMY.Types.Resource~IngressHandler, V>} handler - function to invoke to consume a resource on create * @returns {R} this resource type class for chaining * @abstract */ @@ -122,7 +126,7 @@ export class Resource { /** * Handler for egress of a resource - * @template {SCIMMY.Types.Resource} R - type of resource instance performing egress + * @template {SCIMMY.Types.Resource} R - type of resource instance performing egress * @template {SCIMMY.Types.Schema} S - type of schema instance that will be passed to handler * @template {Record} [V=Omit, Resource.ShadowAttributes>] - shape of return value * @callback SCIMMY.Types.Resource~EgressHandler @@ -161,7 +165,8 @@ export class Resource { * Sets the method to be called to retrieve a resource on read * @template {typeof SCIMMY.Types.Resource} R * @template {SCIMMY.Types.Schema} S - * @param {SCIMMY.Types.Resource~EgressHandler, S>} handler - function to invoke to retrieve a resource on read + * @typeParam {S} [V=S] + * @param {SCIMMY.Types.Resource~EgressHandler, V>} handler - function to invoke to retrieve a resource on read * @returns {R} this resource type class for chaining * @abstract */ @@ -175,6 +180,7 @@ export class Resource { * @callback SCIMMY.Types.Resource~DegressHandler * @param {R} resource - the resource performing the degress * @param {*} [ctx] - external context in which the handler has been called + * @returns {void|Promise} * @example * // Handle a request to delete a specific resource * async function degress(resource, ctx) { @@ -256,7 +262,7 @@ export class Resource { * @property {String} [id] - ID of the resource instance being targeted * @property {SCIMMY.Types.Filter} [filter] - filter parsed from the supplied config * @property {SCIMMY.Types.Filter} [attributes] - attributes or excluded attributes parsed from the supplied config - * @property {Object} [constraints] - sort and pagination properties parsed from the supplied config + * @property {SCIMMY.Messages.ListResponse~ListConstraints} [constraints] - sort and pagination properties parsed from the supplied config * @property {String} [constraints.sortBy] - the attribute retrieved resources should be sorted by * @property {String} [constraints.sortOrder] - the direction retrieved resources should be sorted in * @property {Number} [constraints.startIndex] - offset index that retrieved resources should start from diff --git a/src/lib/types/schema.js b/src/lib/types/schema.js index de55fea..194889d 100644 --- a/src/lib/types/schema.js +++ b/src/lib/types/schema.js @@ -24,6 +24,21 @@ const defineToJSONProperty = (target, definition, resource) => Object.defineProp */ const hasActualValues = (target) => (Object.values(target).some((v) => typeof v === "object" ? hasActualValues(v) : v !== undefined)); +/** + * Automatically assigned attributes not required in schema extension values + * @enum {"id"|"schemas"|"meta"} SCIMMY.Types.Schema~ShadowAttributes + * @ignore + */ + +/** + * A schema instance type with an added schema extension + * @typedef {V} SCIMMY.Types.Schema~Extended + * @template {SCIMMY.Types.Schema} S + * @template {typeof SCIMMY.Types.Schema} E + * @template {S} [V=(S & {[K in keyof Pick as `${E[K]}`]?: Omit, Schema.ShadowAttributes>})] + * @ignore + */ + /** * SCIM Schema Type * @alias SCIMMY.Types.Schema @@ -32,13 +47,22 @@ const hasActualValues = (target) => (Object.values(target).some((v) => typeof v * * Once instantiated, any modifications will also be validated against the attached schema definition's matching attribute configuration (e.g. for mutability or canonical values). */ export class Schema { + /** + * SCIM schema URN namespace + * @type {String} + * @abstract + */ + static get id() { + throw new TypeError("Method 'get' for static property 'id' must be implemented by subclass"); + } + /** * Retrieves a schema's definition instance * @type {SCIMMY.Types.SchemaDefinition} * @abstract */ static get definition() { - throw new TypeError("Method 'get' for property 'definition' must be implemented by subclass"); + throw new TypeError("Method 'get' for static property 'definition' must be implemented by subclass"); } /** @@ -64,7 +88,7 @@ export class Schema { /** * Remove an attribute, schema extension, or subAttribute from the schema's definition - * @param {SCIMMY.Types.Schema|String|SCIMMY.Types.Attribute|Array} attributes - the child attributes to remove from the schema definition + * @param {SCIMMY.Types.Schema|String|SCIMMY.Types.Attribute|Array|Array} attributes - the child attributes to remove from the schema definition */ static truncate(attributes) { this.definition.truncate(attributes?.prototype instanceof Schema ? attributes.definition : attributes); diff --git a/test/hooks/resources.js b/test/hooks/resources.js index e8beef1..5c92928 100644 --- a/test/hooks/resources.js +++ b/test/hooks/resources.js @@ -123,19 +123,24 @@ export default class ResourcesHooks { extend = (supported = true) => (() => { const TargetResource = this.#target; + it("should be overridden", () => { + assert.ok(Object.getOwnPropertyNames(TargetResource).includes("extend"), + "Static method 'extend' was not overridden"); + assert.ok(typeof TargetResource.extend === "function", + "Static method 'extend' was not a function"); + }); + if (supported) { - it("should not be overridden", () => { - assert.ok(!Object.getOwnPropertyNames(TargetResource).includes("extend"), - "Static method 'extend' unexpectedly overridden by resource"); + const sandbox = this.#sandbox; + + it("should call through to the 'extend' method of the super class", () => { + const stub = sandbox.stub(Resource, "extend"); + + TargetResource.extend(); + assert.ok(stub.calledOnce, "Static method 'extend' did not call through to super class"); + stub.restore(); }); } else { - it("should be overridden", () => { - assert.ok(Object.getOwnPropertyNames(TargetResource).includes("extend"), - "Static method 'extend' was not overridden"); - assert.ok(typeof TargetResource.extend === "function", - "Static method 'extend' was not a function"); - }); - it("should throw an 'unsupported' error", () => { assert.throws(() => TargetResource.extend(), {name: "TypeError", message: `SCIM '${TargetResource.name}' resource does not support extension`}, diff --git a/test/hooks/schemas.js b/test/hooks/schemas.js index 918d3fc..f407b3b 100644 --- a/test/hooks/schemas.js +++ b/test/hooks/schemas.js @@ -220,6 +220,25 @@ export default class ResourcesHooks { }); }); + id = () => (() => { + const TargetSchema = this.#target; + + it("should be defined", () => { + assert.ok("id" in TargetSchema, + "Static member 'id' not defined"); + }); + + it("should be a string", () => { + assert.ok(typeof TargetSchema.id === "string", + "Static member 'id' was not a string"); + }); + + it("should match ID from definition instance", async () => { + assert.strictEqual(TargetSchema.id, TargetSchema.definition.id, + "Static member 'id' did not match 'id' from definition instance"); + }); + }); + definition = () => (() => { const TargetSchema = this.#target; const fixtures = this.#fixtures; diff --git a/test/lib/schemas/enterpriseuser.js b/test/lib/schemas/enterpriseuser.js index 8798d32..1f437d3 100644 --- a/test/lib/schemas/enterpriseuser.js +++ b/test/lib/schemas/enterpriseuser.js @@ -11,6 +11,7 @@ const fixtures = fs.readFile(path.join(basepath, "./enterpriseuser.json"), "utf8 describe("SCIMMY.Schemas.EnterpriseUser", () => { const hooks = new SchemasHooks(EnterpriseUser, fixtures); + describe(".id", hooks.id()); describe(".definition", hooks.definition()); describe("@constructor", hooks.construct()); }); \ No newline at end of file diff --git a/test/lib/schemas/group.js b/test/lib/schemas/group.js index d845e31..7dcd981 100644 --- a/test/lib/schemas/group.js +++ b/test/lib/schemas/group.js @@ -11,6 +11,7 @@ const fixtures = fs.readFile(path.join(basepath, "./group.json"), "utf8").then(( describe("SCIMMY.Schemas.Group", () => { const hooks = new SchemasHooks(Group, fixtures); + describe(".id", hooks.id()); describe(".definition", hooks.definition()); describe("@constructor", hooks.construct()); }); \ No newline at end of file diff --git a/test/lib/schemas/resourcetype.js b/test/lib/schemas/resourcetype.js index 67450ff..9faa625 100644 --- a/test/lib/schemas/resourcetype.js +++ b/test/lib/schemas/resourcetype.js @@ -11,6 +11,7 @@ const fixtures = fs.readFile(path.join(basepath, "./resourcetype.json"), "utf8") describe("SCIMMY.Schemas.ResourceType", () => { const hooks = new SchemasHooks(ResourceType, fixtures); + describe(".id", hooks.id()); describe(".definition", hooks.definition()); describe("@constructor", hooks.construct()); }); \ No newline at end of file diff --git a/test/lib/schemas/spconfig.js b/test/lib/schemas/spconfig.js index a834727..96a0e49 100644 --- a/test/lib/schemas/spconfig.js +++ b/test/lib/schemas/spconfig.js @@ -11,6 +11,7 @@ const fixtures = fs.readFile(path.join(basepath, "./spconfig.json"), "utf8").the describe("SCIMMY.Schemas.ServiceProviderConfig", () => { const hooks = new SchemasHooks(ServiceProviderConfig, fixtures); + describe(".id", hooks.id()); describe(".definition", hooks.definition()); describe("@constructor", hooks.construct()); }); \ No newline at end of file diff --git a/test/lib/schemas/user.js b/test/lib/schemas/user.js index 3e117d3..bc915b4 100644 --- a/test/lib/schemas/user.js +++ b/test/lib/schemas/user.js @@ -11,6 +11,7 @@ const fixtures = fs.readFile(path.join(basepath, "./user.json"), "utf8").then((f describe("SCIMMY.Schemas.User", () => { const hooks = new SchemasHooks(User, fixtures); + describe(".id", hooks.id()); describe(".definition", hooks.definition()); describe("@constructor", hooks.construct()); }); \ No newline at end of file diff --git a/test/lib/types/schema.js b/test/lib/types/schema.js index ff3b6d8..1562165 100644 --- a/test/lib/types/schema.js +++ b/test/lib/types/schema.js @@ -11,6 +11,19 @@ const basepath = path.relative(process.cwd(), path.dirname(url.fileURLToPath(imp const fixtures = fs.readFile(path.join(basepath, "./schema.json"), "utf8").then((f) => JSON.parse(f)); describe("SCIMMY.Types.Schema", () => { + describe(".id", () => { + it("should be defined", () => { + assert.ok(typeof Object.getOwnPropertyDescriptor(Schema, "id").get === "function", + "Static member 'id' was not defined"); + }); + + it("should be abstract", () => { + assert.throws(() => Schema.id, + {name: "TypeError", message: "Method 'get' for static property 'id' must be implemented by subclass"}, + "Static member 'id' was not abstract"); + }); + }); + describe(".definition", () => { it("should be defined", () => { assert.ok(typeof Object.getOwnPropertyDescriptor(Schema, "definition").get === "function", @@ -19,7 +32,7 @@ describe("SCIMMY.Types.Schema", () => { it("should be abstract", () => { assert.throws(() => Schema.definition, - {name: "TypeError", message: "Method 'get' for property 'definition' must be implemented by subclass"}, + {name: "TypeError", message: "Method 'get' for static property 'definition' must be implemented by subclass"}, "Static member 'definition' was not abstract"); }); });