Repository: solid/community-server (ts)

Issue:
Server returns 501 Not Implemented on empty sparql updates
As discussed yesterday with @joachimvh 

When sending a sparql-update PATCH request with an empty body, the server responds with `501 Not Implemented - NotImplementedHttpError: No handler supports the given input: [Can only determine permissions of acl resources, Cannot determine permissions of PATCH, Can only determine permissions of a PATCH with DELETE/INSERT operations.]`.

Full request:
```
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Authorization: DPoP eyJ...veA
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 1
Content-Type: application/sparql-update
Cookie: _session.legacy=FCIJ_F1CkGvfXWJSgGEQW; _session.legacy.sig=LQ4rrS91PNrKJHGgP0hWgJeMn9I
DPoP: eyJ...RWg
Host: localhost:3000
Origin: http://localhost:3002
Pragma: no-cache
Referer: http://localhost:3002/
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"
sec-ch-ua-mobile: ?0
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36

(Empty request payload)
```

Pull request:
fix: Support empty PATCH requests
Closes #759

Patch:
diff --git a/package-lock.json b/package-lock.json
index b6b676e9c..b558f7e0a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1432,6 +1432,29 @@
         "asynciterator": "^3.1.0",
         "immutable": "^3.8.2",
         "sparqlalgebrajs": "^2.4.0"
+      },
+      "dependencies": {
+        "sparqlalgebrajs": {
+          "version": "2.5.5",
+          "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-2.5.5.tgz",
+          "integrity": "sha512-sG9XI5311mS+JPDaeZUwtwYaYDRiTZDzxtHVS1GSrnfcZ2aiK1fa1PX9z16l7dtS35X3z1j1qyHEElzZO5OM3A==",
+          "requires": {
+            "fast-deep-equal": "^3.1.3",
+            "minimist": "^1.2.5",
+            "rdf-data-factory": "^1.0.4",
+            "rdf-isomorphic": "^1.2.0",
+            "rdf-string": "^1.5.0",
+            "sparqljs": "^3.3.0"
+          }
+        },
+        "sparqljs": {
+          "version": "3.4.2",
+          "resolved": "https://registry.npmjs.org/sparqljs/-/sparqljs-3.4.2.tgz",
+          "integrity": "sha512-MmmZ6cMuvhf4Eh2FXX21dalgADUiZ9WN8XKMedwhTFg0r7W09/o8wvoZ8C4yA6FptnjjAjm+mGnxAEpkSRY3QQ==",
+          "requires": {
+            "rdf-data-factory": "^1.0.4"
+          }
+        }
       }
     },
     "@dabh/diagnostics": {
@@ -2368,6 +2391,14 @@
       "resolved": "https://registry.npmjs.org/@rdfjs/to-ntriples/-/to-ntriples-1.0.2.tgz",
       "integrity": "sha512-ngw5XAaGHjgGiwWWBPGlfdCclHftonmbje5lMys4G2j4NvfExraPIuRZgjSnd5lg4dnulRVUll8tRbgKO+7EDA=="
     },
+    "@rdfjs/types": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-1.0.1.tgz",
+      "integrity": "sha512-YxVkH0XrCNG3MWeZxfg596GFe+oorTVusmNxRP6ZHTsGczZ8AGvG3UchRNkg3Fy4MyysI7vBAA5YZbESL+VmHQ==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "@sindresorhus/is": {
       "version": "0.14.0",
       "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@@ -6569,6 +6600,15 @@
       "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
       "dev": true
     },
+    "hash.js": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "requires": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
     "hosted-git-info": {
       "version": "2.8.8",
       "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
@@ -9628,6 +9668,11 @@
       "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
       "dev": true
     },
+    "minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+    },
     "minimatch": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -10769,6 +10814,14 @@
         "rdf-terms": "^1.6.2"
       }
     },
+    "rdf-js": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/rdf-js/-/rdf-js-4.0.2.tgz",
+      "integrity": "sha512-ApvlFa/WsQh8LpPK/6hctQwG06Z9ztQQGWVtrcrf9L6+sejHNXLPOqL+w7q3hF+iL0C4sv3AX1PUtGkLNzyZ0Q==",
+      "requires": {
+        "@rdfjs/types": "*"
+      }
+    },
     "rdf-literal": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/rdf-literal/-/rdf-literal-1.2.0.tgz",
@@ -11505,22 +11558,62 @@
       }
     },
     "sparqlalgebrajs": {
-      "version": "2.5.1",
-      "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-2.5.1.tgz",
-      "integrity": "sha512-J682nUANGeaaJexN5gcjw7fS507Vxoiz78wIRgFZ2Zv0nu4ObPZQDPgzYK01V0/FXd7sboXkWiHyG8BUeXnimQ==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-3.0.0.tgz",
+      "integrity": "sha512-3QbDE78l170TiyzzTNkW4ug0p9rCRpNHK5vTN67O9LO2yrwnqI3v3Hp+8RW7ZQZAswZCPQ3T3o8TFDR6s3AZYA==",
       "requires": {
+        "@types/minimist": "^1.2.1",
+        "@types/node": "^15.12.2",
+        "@types/rdf-js": "^4.0.2",
+        "@types/sparqljs": "^3.1.2",
         "fast-deep-equal": "^3.1.3",
         "minimist": "^1.2.5",
         "rdf-data-factory": "^1.0.4",
-        "rdf-isomorphic": "^1.2.0",
+        "rdf-isomorphic": "^1.2.1",
         "rdf-string": "^1.5.0",
-        "sparqljs": "^3.3.0"
+        "sparqljs": "^3.4.2"
       },
       "dependencies": {
+        "@types/minimist": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz",
+          "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg=="
+        },
+        "@types/node": {
+          "version": "15.12.2",
+          "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
+          "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww=="
+        },
+        "@types/rdf-js": {
+          "version": "4.0.2",
+          "resolved": "https://registry.npmjs.org/@types/rdf-js/-/rdf-js-4.0.2.tgz",
+          "integrity": "sha512-soR/+RMogGiDU1lrpuQl5ZL55/L1eq/JlR2dWx052Uh/RYs9okh3XZHFlIJXHZqjqyjEn4WdbOMfBj7vvc2WVQ==",
+          "requires": {
+            "rdf-js": "*"
+          }
+        },
+        "@types/sparqljs": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/@types/sparqljs/-/sparqljs-3.1.2.tgz",
+          "integrity": "sha512-tLfrnBuK37P2Bn8Fo7Qik95sBXYHw5D+gq3MMq1HVyoTpCWivwPnP0Mmd7Apamdc9eH3mLJwIZIETHCQ6HxMUw==",
+          "requires": {
+            "rdf-js": "^4.0.2"
+          }
+        },
+        "rdf-isomorphic": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/rdf-isomorphic/-/rdf-isomorphic-1.2.1.tgz",
+          "integrity": "sha512-kIKlQYoizNqp8zhbca1zV3mYngisoD/KNt/xBRjagp7R3F8niI3b1vxvqcWlSkNXgPD6MsXpP2E/uXZ8oGTIcA==",
+          "requires": {
+            "hash.js": "^1.1.7",
+            "rdf-string": "^1.5.0",
+            "rdf-terms": "^1.6.2"
+          }
+        },
         "sparqljs": {
-          "version": "3.3.0",
-          "resolved": "https://registry.npmjs.org/sparqljs/-/sparqljs-3.3.0.tgz",
-          "integrity": "sha512-2WnOX1b72pmf8yHyhoaPKnqqCBMn22qa59H/aiIruQ4lkv9ngaSYGOos5ZqW3o6AcUvCj/ObLQw4RnfQKXRdww==",
+          "version": "3.4.2",
+          "resolved": "https://registry.npmjs.org/sparqljs/-/sparqljs-3.4.2.tgz",
+          "integrity": "sha512-MmmZ6cMuvhf4Eh2FXX21dalgADUiZ9WN8XKMedwhTFg0r7W09/o8wvoZ8C4yA6FptnjjAjm+mGnxAEpkSRY3QQ==",
           "requires": {
             "rdf-data-factory": "^1.0.4"
           }
diff --git a/package.json b/package.json
index 282c63916..c2ce4f113 100644
--- a/package.json
+++ b/package.json
@@ -122,7 +122,7 @@
     "rdf-terms": "^1.5.1",
     "redis": "^3.0.2",
     "redlock": "^4.2.0",
-    "sparqlalgebrajs": "^2.5.1",
+    "sparqlalgebrajs": "^3.0.0",
     "sparqljs": "^3.1.2",
     "streamify-array": "^1.0.1",
     "url-join": "^4.0.1",
diff --git a/src/ldp/permissions/SparqlPatchPermissionsExtractor.ts b/src/ldp/permissions/SparqlPatchPermissionsExtractor.ts
index 695978d1b..6a29e064f 100644
--- a/src/ldp/permissions/SparqlPatchPermissionsExtractor.ts
+++ b/src/ldp/permissions/SparqlPatchPermissionsExtractor.ts
@@ -44,7 +44,7 @@ export class SparqlPatchPermissionsExtractor extends PermissionsExtractor {
   }
 
   private isSupported(op: Algebra.Operation): boolean {
-    if (op.type === Algebra.types.DELETE_INSERT) {
+    if (this.isDeleteInsert(op) || this.isNop(op)) {
       return true;
     }
     if (op.type === Algebra.types.COMPOSITE_UPDATE) {
@@ -57,7 +57,14 @@ export class SparqlPatchPermissionsExtractor extends PermissionsExtractor {
     return op.type === Algebra.types.DELETE_INSERT;
   }
 
+  private isNop(op: Algebra.Operation): op is Algebra.Nop {
+    return op.type === Algebra.types.NOP;
+  }
+
   private needsAppend(update: Algebra.Operation): boolean {
+    if (this.isNop(update)) {
+      return false;
+    }
     if (this.isDeleteInsert(update)) {
       return Boolean(update.insert && update.insert.length > 0);
     }
@@ -66,6 +73,9 @@ export class SparqlPatchPermissionsExtractor extends PermissionsExtractor {
   }
 
   private needsWrite(update: Algebra.Operation): boolean {
+    if (this.isNop(update)) {
+      return false;
+    }
     if (this.isDeleteInsert(update)) {
       return Boolean(update.delete && update.delete.length > 0);
     }
diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts
index de07bcf5b..48d17ed80 100644
--- a/src/storage/patch/SparqlUpdatePatchHandler.ts
+++ b/src/storage/patch/SparqlUpdatePatchHandler.ts
@@ -47,6 +47,12 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
     // Verify the patch
     const { source, identifier, patch } = input;
     const op = (patch as SparqlUpdatePatch).algebra;
+
+    // In case of a NOP we can skip everything
+    if (op.type === Algebra.types.NOP) {
+      return [];
+    }
+
     this.validateUpdate(op);
 
     return this.applyPatch(source, identifier, op);


Test patch:
diff --git a/test/unit/ldp/permissions/SparqlPatchPermissionsExtractor.test.ts b/test/unit/ldp/permissions/SparqlPatchPermissionsExtractor.test.ts
index 2605b0e13..6f9e8672d 100644
--- a/test/unit/ldp/permissions/SparqlPatchPermissionsExtractor.test.ts
+++ b/test/unit/ldp/permissions/SparqlPatchPermissionsExtractor.test.ts
@@ -32,6 +32,19 @@ describe('A SparqlPatchPermissionsExtractor', (): void => {
     await expect(result).rejects.toThrow('Can only determine permissions of a PATCH with DELETE/INSERT operations.');
   });
 
+  it('requires nothing for NOP operations.', async(): Promise<void> => {
+    const operation = {
+      method: 'PATCH',
+      body: { algebra: factory.createNop() },
+    } as unknown as Operation;
+    await expect(extractor.handle(operation)).resolves.toEqual({
+      read: false,
+      append: false,
+      write: false,
+      control: false,
+    });
+  });
+
   it('requires append for INSERT operations.', async(): Promise<void> => {
     const operation = {
       method: 'PATCH',
diff --git a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
index 69c62150c..03a9eb5cc 100644
--- a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
+++ b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
@@ -93,6 +93,13 @@ describe('A SparqlUpdatePatchHandler', (): void => {
     await expect(handler.canHandle(input)).rejects.toThrow(NotImplementedHttpError);
   });
 
+  it('handles NOP operations by not doing anything.', async(): Promise<void> => {
+    await handle('');
+    expect(source.getRepresentation).toHaveBeenCalledTimes(0);
+    expect(converter.handleSafe).toHaveBeenCalledTimes(0);
+    expect(source.setRepresentation).toHaveBeenCalledTimes(0);
+  });
+
   it('handles INSERT DATA updates.', async(): Promise<void> => {
     await handle(fullfilledDataInsert);
     expect(await basicChecks(startQuads.concat(