Repository: scimmyjs/scimmy (js)

Issue:
`User` and `Group` resources throwing `400 Bad Request` for missing or invalid handler implementations
Currently, the read, write, and patch methods of the `SCIMMY.Resources.User` and `SCIMMY.Resources.Group` classes pass return values from declared ingress/egress/degress handlers directly to their corresponding schema classes for instantiation. When these handlers are either missing, or do not return expected values, the schema class constructor will throw a `TypeError` exception, as it expects the supplied value to be an object. This `TypeError` exception is then rethrown as a `SCIMError` exception with properties indicating an issue with the request, which is technically incorrect.

**Steps to Reproduce:**
Instantiate a resource and call one of the listed methods without first declaring the ingress/egress/degress handler implementations (either directly or at resource declaration), or after declaring a handler implementation that does not return a value where it is expected to
```js
// This, or any other read/write/patch variation that expects return values from handlers
await new SCIMMY.Resources.User("any value").read();
```

**Expected Behaviour:**
Missing handler implementations should throw `501 Not Implemented` exceptions
```js
SCIMError {
   status: 501,
   scimType: null,
   message: "Method 'egress' not implemented by resource 'User'"
}
```

Handler implementations that do not return a value when expected should throw `500 Internal Server Error` exceptions
```js
SCIMError {
   status: 500,
   scimType: null,
   message: "Unexpected empty value returned by handler"
}
```

**Actual Behaviour:**
A `400 Bad Request` exception is thrown, incorrectly indicating an issue with the request format
```js
SCIMError {
   status: 400,
   scimType: "invalidValue",
   message: "Expected 'data' parameter to be an object in SchemaDefinition instance"
}
```

Pull request description:
Throw `500` and `501` errors for missing or invalid handlers in `SCIMMY.Resources.User` and `SCIMMY.Resources.Group`
Currently, the read, write, and patch methods of the `SCIMMY.Resources.User` and `SCIMMY.Resources.Group` classes pass return values from declared ingress/egress/degress handlers directly to their corresponding schema classes for instantiation. When these handlers are either missing, or do not return expected values, the schema class constructor will throw a `TypeError` exception, as it expects the supplied value to be an object. This `TypeError` exception is then rethrown as a `SCIMError` exception with `status` and `scimType` properties of values `400` and `invalidValue` respectively, indicating an issue with the request, which is technically incorrect.

This change updates the default `ingress`, `egress`, and `degress` handlers for the `User` and `Group` resource classes to throw a `501 Not Implemented` exception (fixes #39). It also updates the the `read` and `write` methods of these classes to throw a `500 Internal Server Error` when handler implementations return invalid or empty values that cannot be coerced into schema-compliant values (fixes #39). Additionally, the `patch` method of these classes have been updated to call the class `read` and `write` methods to leverage the aforementioned exception handling, instead of calling ingress/egress handlers themselves (fixes #39). The `patch` methods have also been updated to require they are only able to be called for a specific resource, instead of for a list of resources. The test fixtures in the resources hooks have been updated to verify the correct exceptions are being thrown, and that the `patch` method now requires a specific resource.

Patch:
diff --git a/src/lib/messages/patchop.js b/src/lib/messages/patchop.js
index 1506181..08aaee9 100644
--- a/src/lib/messages/patchop.js
+++ b/src/lib/messages/patchop.js
@@ -130,14 +130,14 @@ export class PatchOp {
     
     /**
      * Original SCIM Schema resource instance being patched
-     * @type {SCIMMY.Types.Schema|SCIMMY.Types.Schema[]}
+     * @type {SCIMMY.Types.Schema}
      * @private
      */
     #source;
     
     /**
      * Target SCIM Schema resource instance to apply patches to
-     * @type {SCIMMY.Types.Schema|SCIMMY.Types.Schema[]}
+     * @type {SCIMMY.Types.Schema}
      * @private
      */
     #target;
@@ -146,7 +146,7 @@ export class PatchOp {
      * Apply patch operations to a resource as defined by the PatchOp instance
      * @param {SCIMMY.Types.Schema} resource - the schema instance the patch operation will be performed on
      * @param {Function} [finalise] - method to call when all operations are complete, to feed target back through model
-     * @returns {SCIMMY.Types.Schema|SCIMMY.Types.Schema[]} an instance of the resource modified as per the included patch operations
+     * @returns {Promise<SCIMMY.Types.Schema>} an instance of the resource modified as per the included patch operations
      */
     async apply(resource, finalise) {
         // Bail out if message has not been dispatched (i.e. it's not ready yet)
diff --git a/src/lib/resources/group.js b/src/lib/resources/group.js
index de49ce7..7ef66fe 100644
--- a/src/lib/resources/group.js
+++ b/src/lib/resources/group.js
@@ -31,7 +31,10 @@ export class Group extends Types.Resource {
     }
     
     /** @private */
-    static #ingress = () => {};
+    static #ingress = () => {
+        throw new Types.Error(501, null, "Method 'ingress' not implemented by resource 'Group'");
+    };
+    
     /** @implements {SCIMMY.Types.Resource.ingress} */
     static ingress(handler) {
         Group.#ingress = handler;
@@ -39,7 +42,10 @@ export class Group extends Types.Resource {
     }
     
     /** @private */
-    static #egress = () => {};
+    static #egress = () => {
+        throw new Types.Error(501, null, "Method 'egress' not implemented by resource 'Group'");
+    };
+    
     /** @implements {SCIMMY.Types.Resource.egress} */
     static egress(handler) {
         Group.#egress = handler;
@@ -47,7 +53,10 @@ export class Group extends Types.Resource {
     }
     
     /** @private */
-    static #degress = () => {};
+    static #degress = () => {
+        throw new Types.Error(501, null, "Method 'degress' not implemented by resource 'Group'");
+    };
+    
     /** @implements {SCIMMY.Types.Resource.degress} */
     static degress(handler) {
         Group.#degress = handler;
@@ -67,10 +76,10 @@ export class Group extends Types.Resource {
      * @returns {SCIMMY.Messages.ListResponse|SCIMMY.Schemas.Group}
      * @example
      * // Retrieve group with ID "1234"
-     * await (new SCIMMY.Resources.Group("1234")).read();
+     * await new SCIMMY.Resources.Group("1234").read();
      * @example
      * // Retrieve groups with a group name starting with "A"
-     * await (new SCIMMY.Resources.Group({filter: 'displayName -sw "A"'})).read();
+     * await new SCIMMY.Resources.Group({filter: 'displayName sw "A"'}).read();
      */
     async read(ctx) {
         if (!this.id) {
@@ -78,7 +87,9 @@ export class Group extends Types.Resource {
                 .map(u => new Schemas.Group(u, "out", Group.basepath(), this.attributes)), this.constraints);
         } else {
             try {
-                return new Schemas.Group([await Group.#egress(this, ctx)].flat().shift(), "out", Group.basepath(), this.attributes);
+                const source = [await Group.#egress(this, ctx)].flat().shift();
+                if (!(source instanceof Object)) throw new Types.Error(500, null, `Unexpected ${source === undefined ? "empty" : "invalid"} value returned by handler`);
+                else return new Schemas.Group(source, "out", Group.basepath(), this.attributes);
             } catch (ex) {
                 if (ex instanceof Types.Error) throw ex;
                 else if (ex instanceof TypeError) throw new Types.Error(400, "invalidValue", ex.message);
@@ -92,10 +103,10 @@ export class Group extends Types.Resource {
      * @returns {SCIMMY.Schemas.Group}
      * @example
      * // Create a new group with displayName "A Group"
-     * await (new SCIMMY.Resources.Group()).write({displayName: "A Group"});
+     * await new SCIMMY.Resources.Group().write({displayName: "A Group"});
      * @example
      * // Set members attribute for group with ID "1234"
-     * await (new SCIMMY.Resources.Group("1234")).write({members: [{value: "5678"}]});
+     * await new SCIMMY.Resources.Group("1234").write({members: [{value: "5678"}]});
      */
     async write(instance, ctx) {
         if (instance === undefined)
@@ -104,10 +115,9 @@ export class Group extends Types.Resource {
             throw new Types.Error(400, "invalidSyntax", `Operation ${!!this.id ? "PUT" : "POST"} expected request body payload to be single complex value`);
         
         try {
-            return new Schemas.Group(
-                await Group.#ingress(this, new Schemas.Group(instance, "in"), ctx),
-                "out", Group.basepath(), this.attributes
-            );
+            const target = await Group.#ingress(this, new Schemas.Group(instance, "in"), ctx);
+            if (!(target instanceof Object)) throw new Types.Error(500, null, `Unexpected ${target === undefined ? "empty" : "invalid"} value returned by handler`);
+            else return new Schemas.Group(target, "out", Group.basepath(), this.attributes);
         } catch (ex) {
             if (ex instanceof Types.Error) throw ex;
             else if (ex instanceof TypeError) throw new Types.Error(400, "invalidValue", ex.message);
@@ -121,31 +131,26 @@ export class Group extends Types.Resource {
      * @returns {SCIMMY.Schemas.Group}
      * @example
      * // Add member to group with ID "1234" with a patch operation (see SCIMMY.Messages.PatchOp)
-     * await (new SCIMMY.Resources.Group("1234")).patch({Operations: [{op: "add", path: "members", value: {value: "5678"}}]});
+     * await new SCIMMY.Resources.Group("1234").patch({Operations: [{op: "add", path: "members", value: {value: "5678"}}]});
      */
     async patch(message, ctx) {
+        if (!this.id)
+            throw new Types.Error(404, null, "PATCH operation must target a specific resource");
         if (message === undefined)
             throw new Types.Error(400, "invalidSyntax", "Missing message body from PatchOp request");
         if (Object(message) !== message || Array.isArray(message))
             throw new Types.Error(400, "invalidSyntax", "PatchOp request expected message body to be single complex value");
         
-        try {
-            return await Promise.resolve(new Messages.PatchOp(message)
-                .apply(new Schemas.Group([await Group.#egress(this, ctx)].flat().shift()), 
-                    async (instance) => await Group.#ingress(this, instance, ctx)))
-                .then(instance => !instance ? undefined : new Schemas.Group(instance, "out", Group.basepath(), this.attributes));
-        } catch (ex) {
-            if (ex instanceof Types.Error) throw ex;
-            else if (ex instanceof TypeError) throw new Types.Error(400, "invalidValue", ex.message);
-            else throw new Types.Error(404, null, `Resource ${this.id} not found`);
-        }
+        return await new Messages.PatchOp(message)
+            .apply(await this.read(ctx), async (instance) => await this.write(instance, ctx))
+            .then(instance => !instance ? undefined : new Schemas.Group(instance, "out", Group.basepath(), this.attributes));
     }
     
     /** 
      * @implements {SCIMMY.Types.Resource#dispose}
      * @example
      * // Delete group with ID "1234"
-     * await (new SCIMMY.Resources.Group("1234")).dispose();
+     * await new SCIMMY.Resources.Group("1234").dispose();
      */
     async dispose(ctx) {
         if (!this.id)
diff --git a/src/lib/resources/user.js b/src/lib/resources/user.js
index 61d3825..6f8baa1 100644
--- a/src/lib/resources/user.js
+++ b/src/lib/resources/user.js
@@ -31,7 +31,10 @@ export class User extends Types.Resource {
     }
     
     /** @private */
-    static #ingress = () => {};
+    static #ingress = () => {
+        throw new Types.Error(501, null, "Method 'ingress' not implemented by resource 'User'");
+    };
+    
     /** @implements {SCIMMY.Types.Resource.ingress} */
     static ingress(handler) {
         User.#ingress = handler;
@@ -39,7 +42,10 @@ export class User extends Types.Resource {
     }
     
     /** @private */
-    static #egress = () => {};
+    static #egress = () => {
+        throw new Types.Error(501, null, "Method 'egress' not implemented by resource 'User'");
+    };
+    
     /** @implements {SCIMMY.Types.Resource.egress} */
     static egress(handler) {
         User.#egress = handler;
@@ -47,7 +53,10 @@ export class User extends Types.Resource {
     }
     
     /** @private */
-    static #degress = () => {};
+    static #degress = () => {
+        throw new Types.Error(501, null, "Method 'degress' not implemented by resource 'User'");
+    };
+    
     /** @implements {SCIMMY.Types.Resource.degress} */
     static degress(handler) {
         User.#degress = handler;
@@ -67,10 +76,10 @@ export class User extends Types.Resource {
      * @returns {SCIMMY.Messages.ListResponse|SCIMMY.Schemas.User}
      * @example
      * // Retrieve user with ID "1234"
-     * await (new SCIMMY.Resources.User("1234")).read();
+     * await new SCIMMY.Resources.User("1234").read();
      * @example
      * // Retrieve users with an email ending in "@example.com"
-     * await (new SCIMMY.Resources.User({filter: 'email.value -ew "@example.com"'})).read();
+     * await new SCIMMY.Resources.User({filter: 'email.value ew "@example.com"'}).read();
      */
     async read(ctx) {
         if (!this.id) {
@@ -78,7 +87,9 @@ export class User extends Types.Resource {
                 .map(u => new Schemas.User(u, "out", User.basepath(), this.attributes)), this.constraints);
         } else {
             try {
-                return new Schemas.User([await User.#egress(this, ctx)].flat().shift(), "out", User.basepath(), this.attributes);
+                const source = [await User.#egress(this, ctx)].flat().shift();
+                if (!(source instanceof Object)) throw new Types.Error(500, null, `Unexpected ${source === undefined ? "empty" : "invalid"} value returned by handler`);
+                else return new Schemas.User(source, "out", User.basepath(), this.attributes);
             } catch (ex) {
                 if (ex instanceof Types.Error) throw ex;
                 else if (ex instanceof TypeError) throw new Types.Error(400, "invalidValue", ex.message);
@@ -92,10 +103,10 @@ export class User extends Types.Resource {
      * @returns {SCIMMY.Schemas.User}
      * @example
      * // Create a new user with userName "someGuy"
-     * await (new SCIMMY.Resources.User()).write({userName: "someGuy"});
+     * await new SCIMMY.Resources.User().write({userName: "someGuy"});
      * @example
      * // Set userName attribute to "someGuy" for user with ID "1234"
-     * await (new SCIMMY.Resources.User("1234")).write({userName: "someGuy"});
+     * await new SCIMMY.Resources.User("1234").write({userName: "someGuy"});
      */
     async write(instance, ctx) {
         if (instance === undefined)
@@ -104,11 +115,9 @@ export class User extends Types.Resource {
             throw new Types.Error(400, "invalidSyntax", `Operation ${!!this.id ? "PUT" : "POST"} expected request body payload to be single complex value`);
         
         try {
-            // TODO: handle incoming read-only and immutable attribute tests
-            return new Schemas.User(
-                await User.#ingress(this, new Schemas.User(instance, "in"), ctx),
-                "out", User.basepath(), this.attributes
-            );
+            const target = await User.#ingress(this, new Schemas.User(instance, "in"), ctx);
+            if (!(target instanceof Object)) throw new Types.Error(500, null, `Unexpected ${target === undefined ? "empty" : "invalid"} value returned by handler`);
+            else return new Schemas.User(target, "out", User.basepath(), this.attributes);
         } catch (ex) {
             if (ex instanceof Types.Error) throw ex;
             else if (ex instanceof TypeError) throw new Types.Error(400, "invalidValue", ex.message);
@@ -122,31 +131,26 @@ export class User extends Types.Resource {
      * @returns {SCIMMY.Schemas.User}
      * @example
      * // Set userName to "someGuy" for user with ID "1234" with a patch operation (see SCIMMY.Messages.PatchOp)
-     * await (new SCIMMY.Resources.User("1234")).patch({Operations: [{op: "add", value: {userName: "someGuy"}}]});
+     * await new SCIMMY.Resources.User("1234").patch({Operations: [{op: "add", value: {userName: "someGuy"}}]});
      */
     async patch(message, ctx) {
+        if (!this.id)
+            throw new Types.Error(404, null, "PATCH operation must target a specific resource");
         if (message === undefined)
             throw new Types.Error(400, "invalidSyntax", "Missing message body from PatchOp request");
         if (Object(message) !== message || Array.isArray(message))
             throw new Types.Error(400, "invalidSyntax", "PatchOp request expected message body to be single complex value");
         
-        try {
-            return await Promise.resolve(new Messages.PatchOp(message)
-                .apply(new Schemas.User([await User.#egress(this, ctx)].flat().shift()), 
-                    async (instance) => await User.#ingress(this, instance, ctx)))
-                .then(instance => !instance ? undefined : new Schemas.User(instance, "out", User.basepath(), this.attributes));
-        } catch (ex) {
-            if (ex instanceof Types.Error) throw ex;
-            else if (ex instanceof TypeError) throw new Types.Error(400, "invalidValue", ex.message);
-            else throw new Types.Error(404, null, `Resource ${this.id} not found`);
-        }
+        return await new Messages.PatchOp(message)
+            .apply(await this.read(ctx), async (instance) => await this.write(instance, ctx))
+            .then(instance => !instance ? undefined : new Schemas.User(instance, "out", User.basepath(), this.attributes));
     }
     
     /**
      * @implements {SCIMMY.Types.Resource#dispose}
      * @example
      * // Delete user with ID "1234"
-     * await (new SCIMMY.Resources.User("1234")).dispose();
+     * await new SCIMMY.Resources.User("1234").dispose();
      */
     async dispose(ctx) {
         if (!this.id)


Test patch:
diff --git a/test/hooks/resources.js b/test/hooks/resources.js
index c1645af..1497adc 100644
--- a/test/hooks/resources.js
+++ b/test/hooks/resources.js
@@ -5,7 +5,6 @@ import {Resource} from "#@/lib/types/resource.js";
 import {SCIMError} from "#@/lib/types/error.js";
 import {ListResponse} from "#@/lib/messages/listresponse.js";
 import {createSchemaClass} from "./schemas.js";
-import exp from "node:constants";
 
 /**
  * Create a class that extends SCIMMY.Types.Resource, for use in tests
@@ -165,6 +164,14 @@ export default class ResourcesHooks {
                     "Static method 'ingress' was not a function");
             });
             
+            it("should include a fallback private ingress handler", async () => {
+                const {egress: [source]} = await fixtures;
+                
+                await assert.rejects(() => new TargetResource().write(source),
+                    {name: "SCIMError", status: 501, scimType: null, message: /not implemented by resource/},
+                    "Static method 'ingress' did not include fallback private handler");
+            });
+            
             it("should set private ingress handler", async () => {
                 const {egress: [source]} = await fixtures;
                 const spy = sandbox.spy(TargetResource, "ingress");
@@ -206,6 +213,12 @@ export default class ResourcesHooks {
                     "Static method 'egress' was not a function");
             });
             
+            it("should include a fallback private egress handler", async () => {
+                await assert.rejects(() => new TargetResource("placeholder").read(),
+                    {name: "SCIMError", status: 501, scimType: null, message: /not implemented by resource/},
+                    "Static method 'egress' did not include fallback private handler");
+            });
+            
             it("should set private egress handler", async () => {
                 const spy = sandbox.spy(TargetResource, "egress");
                 const error = new Error("Handler Stubbed");
@@ -246,6 +259,12 @@ export default class ResourcesHooks {
                     "Static method 'degress' was not a function");
             });
             
+            it("should include a fallback private degress handler", async () => {
+                await assert.rejects(() => new TargetResource("Error").dispose(),
+                    {name: "SCIMError", status: 501, scimType: null, message: /not implemented by resource/},
+                    "Static method 'degress' did not include fallback private handler");
+            });
+            
             it("should set private degress handler", async () => {
                 const spy = sandbox.spy(TargetResource, "degress");
                 const error = new Error("Handler Stubbed");
@@ -403,6 +422,32 @@ export default class ResourcesHooks {
             });
             
             if (callsEgress) {
+                (skip ? it.skip : it)("should throw exception for invalid values returned by handler", async () => {
+                    handler.reset();
+                    
+                    for (let value of [true, false, "invalid", null]) {
+                        handler.returns(value);
+                        
+                        await assert.rejects(() => new TargetResource("placeholder").read(),
+                            {name: "SCIMError", status: 500, scimType: null,
+                                message: "Unexpected invalid value returned by handler"},
+                            "Instance method 'read' did not throw exception for invalid values returned by handler");
+                    }
+                });
+                
+                (skip ? it.skip : it)("should throw exception for empty values returned by handler", async () => {
+                    handler.reset();
+                    
+                    for (let value of [undefined, []]) {
+                        handler.returns(value);
+                        
+                        await assert.rejects(() => new TargetResource("placeholder").read(),
+                            {name: "SCIMError", status: 500, scimType: null,
+                                message: "Unexpected empty value returned by handler"},
+                            "Instance method 'read' did not throw exception for empty values returned by handler");
+                    }
+                });
+                
                 (skip ? it.skip : it)("should call egress handler method with originating resource instance as an argument", async () => {
                     const resource = new TargetResource();
                     
@@ -536,6 +581,30 @@ export default class ResourcesHooks {
                 }
             });
             
+            (skip ? it.skip : it)("should throw exception for invalid values returned by handler", async () => {
+                const {ingress: source} = await fixtures;
+                
+                handler.reset();
+                handler.returns(true);
+                
+                await assert.rejects(() => new TargetResource().write(source),
+                    {name: "SCIMError", status: 500, scimType: null,
+                        message: "Unexpected invalid value returned by handler"},
+                    "Instance method 'write' did not throw exception for invalid values returned by handler");
+            });
+            
+            (skip ? it.skip : it)("should throw exception for empty values returned by handler", async () => {
+                const {ingress: source} = await fixtures;
+                
+                handler.reset();
+                handler.returns(undefined);
+                
+                await assert.rejects(() => new TargetResource().write(source),
+                    {name: "SCIMError", status: 500, scimType: null,
+                        message: "Unexpected empty value returned by handler"},
+                    "Instance method 'write' did not throw exception for empty values returned by handler");
+            });
+            
             (skip ? it.skip : it)("should call ingress to create new resources when resource instantiated without ID", async () => {
                 const {ingress: source, egress: resources} = await fixtures;
                 const actual = await (new TargetResource()).write(source);
@@ -638,6 +707,13 @@ export default class ResourcesHooks {
                     "Instance method 'patch' was not a function");
             });
             
+            it("should expect resource instances to have 'id' property", async () => {
+                await assert.rejects(() => new TargetResource().patch(),
+                    {name: "SCIMError", status: 404, scimType: null,
+                        message: "PATCH operation must target a specific resource"},
+                    "Instance method 'patch' did not expect resource instance to have 'id' property");
+            });
+            
             it("should expect 'message' argument to be an object", async () => {
                 const fixtures = [
                     ["string value 'a string'", "a string"],
@@ -645,13 +721,13 @@ export default class ResourcesHooks {
                     ["array value", []]
                 ];
                 
-                await assert.rejects(() => new TargetResource().patch(),
+                await assert.rejects(() => new TargetResource("test").patch(),
                     {name: "SCIMError", status: 400, scimType: "invalidSyntax",
                         message: "Missing message body from PatchOp request"},
                     "Instance method 'patch' did not expect 'message' parameter to exist");
                 
                 for (let [label, value] of fixtures) {
-                    await assert.rejects(() => new TargetResource().patch(value),
+                    await assert.rejects(() => new TargetResource("test").patch(value),
                         {name: "SCIMError", status: 400, scimType: "invalidSyntax",
                             message: "PatchOp request expected message body to be single complex value"},
                         `Instance method 'patch' did not reject 'message' parameter ${label}`);
@@ -674,7 +750,7 @@ export default class ResourcesHooks {
                 const {egress: [fixture]} = await fixtures;
                 const [, target] = Object.keys(fixture);
                 const expected = {...fixture, [target]: "Test"};
-                const actual = await (new TargetResource(fixture.id)).patch({
+                const actual = await new TargetResource(fixture.id).patch({
                     schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
                     Operations: [{op: "add", path: target, value: "Test"}]
                 });
@@ -700,7 +776,7 @@ export default class ResourcesHooks {
                     const {egress: [fixture]} = await fixtures;
                     const [, target] = Object.keys(fixture);
                     
-                    await new TargetResource().patch({
+                    await new TargetResource(fixture.id).patch({
                         schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
                         Operations: [{op: "add", path: target, value: "Test"}]
                     });
@@ -709,10 +785,26 @@ export default class ResourcesHooks {
                         `Instance method 'patch' did not call ${method} handler exactly once`);
                 });
                 
+                (skip ? it.skip : it)(`should rethrow exception for empty values returned by ${method} handler`, async () => {
+                    const {ingress: source, egress: [fixture]} = await fixtures;
+                    const message = {
+                        schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
+                        Operations: [{op: "add", value: source}]
+                    };
+                    
+                    handlers[method].reset();
+                    handlers[method].returns(undefined);
+                    
+                    await assert.rejects(() => new TargetResource(fixture.id).patch(message),
+                        {name: "SCIMError", status: 500, scimType: null,
+                            message: "Unexpected empty value returned by handler"},
+                        `Instance method 'patch' did not rethrow exception for empty values returned by ${method} handler`);
+                });
+                
                 (skip ? it.skip : it)(`should call ${method} handler method with originating resource instance as an argument`, async () => {
                     const {egress: [fixture]} = await fixtures;
                     const [, target] = Object.keys(fixture);
-                    const resource = new TargetResource();
+                    const resource = new TargetResource(fixture.id);
                     
                     await resource.patch({
                         schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
@@ -726,7 +818,7 @@ export default class ResourcesHooks {
                 (skip ? it.skip : it)(`should call ${method} handler method with supplied context as an argument`, async () => {
                     const {egress: [fixture]} = await fixtures;
                     const [, target] = Object.keys(fixture);
-                    const resource = new TargetResource();
+                    const resource = new TargetResource(fixture.id);
                     const context = {};
                     const message = {
                         schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
@@ -749,7 +841,7 @@ export default class ResourcesHooks {
             (skip ? it.skip : it)("should call egress handler before ingress handler", async () => {
                 const {egress: [fixture]} = await fixtures;
                 const [, target] = Object.keys(fixture);
-                const resource = new TargetResource();
+                const resource = new TargetResource(fixture.id);
                 const spy = sandbox.spy(resource, "patch");
                 
                 await resource.patch({
diff --git a/test/lib/messages/searchrequest.js b/test/lib/messages/searchrequest.js
index 0efaaf6..bd02b12 100644
--- a/test/lib/messages/searchrequest.js
+++ b/test/lib/messages/searchrequest.js
@@ -246,7 +246,7 @@ describe("SCIMMY.Messages.SearchRequest", () => {
         });
         
         it("should return a ListResponse message instance", async () => {
-            assert.ok(await (new SearchRequest()).apply() instanceof ListResponse,
+            assert.ok(await (new SearchRequest()).apply([]) instanceof ListResponse,
                 "Instance method 'apply' did not return an instance of ListResponse");
         });