Repository: solid/community-server (ts)

Issue:
Create abstract conversion PatchHandler
This would be a PatchHandler that uses a converter that converts a representation to a specific type (`internal/quads` for our current patch handler), calls an abstract function, and then converts the result back to the original type. The `SparqlUpdatePatchHandler` would then implement the abstract function to do the actual patch.

See https://github.com/solid/community-server/pull/732#issuecomment-840051121

Pull request description:
feat: Create base ConvertingPatchHandler abstract class
Closes #739 

Patch:
diff --git a/src/identity/IdentityProviderFactory.ts b/src/identity/IdentityProviderFactory.ts
index 35d2e5f52..57fec0d98 100644
--- a/src/identity/IdentityProviderFactory.ts
+++ b/src/identity/IdentityProviderFactory.ts
@@ -84,6 +84,8 @@ export class IdentityProviderFactory {
       },
       renderError:
         async(ctx: KoaContextWithOIDC, out: ErrorOut, error: Error): Promise<void> => {
+          // This allows us to stream directly to to the response object, see https://github.com/koajs/koa/issues/944
+          ctx.respond = false;
           const preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
           const result = await this.errorHandler.handleSafe({ error, preferences });
           await this.responseWriter.handleSafe({ response: ctx.res, result });
diff --git a/src/index.ts b/src/index.ts
index 5e0ad9aad..9601b9e77 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -250,6 +250,7 @@ export * from './storage/mapping/FixedContentTypeMapper';
 export * from './storage/mapping/SubdomainExtensionBasedMapper';
 
 // Storage/Patch
+export * from './storage/patch/ConvertingPatchHandler';
 export * from './storage/patch/PatchHandler';
 export * from './storage/patch/SparqlUpdatePatchHandler';
 
diff --git a/src/storage/patch/ConvertingPatchHandler.ts b/src/storage/patch/ConvertingPatchHandler.ts
new file mode 100644
index 000000000..69afb0c36
--- /dev/null
+++ b/src/storage/patch/ConvertingPatchHandler.ts
@@ -0,0 +1,89 @@
+import type { Representation } from '../../ldp/representation/Representation';
+import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
+import { getLoggerFor } from '../../logging/LogUtil';
+import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
+import type { RepresentationConverter } from '../conversion/RepresentationConverter';
+import type { ResourceStore } from '../ResourceStore';
+import type { PatchHandlerArgs } from './PatchHandler';
+import { PatchHandler } from './PatchHandler';
+
+/**
+ * An abstract patch handler.
+ *
+ * A `ConvertingPatchHandler` converts a document to its `intermediateType`,
+ * handles the patch operation, and then converts back to its original type.
+ * This abstract class covers all of the above except of handling the patch operation,
+ * for which the abstract `patch` function has to be implemented.
+ *
+ * In case there is no resource yet and a new one needs to be created,
+ * the `patch` function will be called without a Representation
+ * and the result will be converted to the `defaultType`.
+ */
+export abstract class ConvertingPatchHandler extends PatchHandler {
+  protected readonly logger = getLoggerFor(this);
+
+  private readonly converter: RepresentationConverter;
+  protected readonly intermediateType: string;
+  protected readonly defaultType: string;
+
+  /**
+   * @param converter - Converter that will be used to generate intermediate Representation.
+   * @param intermediateType - Content-type of the intermediate Representation.
+   * @param defaultType - Content-type in case a new resource gets created.
+   */
+  protected constructor(converter: RepresentationConverter, intermediateType: string, defaultType: string) {
+    super();
+    this.converter = converter;
+    this.intermediateType = intermediateType;
+    this.defaultType = defaultType;
+  }
+
+  public async handle(input: PatchHandlerArgs): Promise<ResourceIdentifier[]> {
+    const { source, identifier } = input;
+    const { representation, contentType } = await this.toIntermediate(source, identifier);
+
+    const patched = await this.patch(input, representation);
+
+    // Convert back to the original type and write the result
+    const converted = await this.converter.handleSafe({
+      representation: patched,
+      identifier,
+      preferences: { type: { [contentType]: 1 }},
+    });
+    return source.setRepresentation(identifier, converted);
+  }
+
+  /**
+   * Acquires the resource from the source and converts it to the intermediate type if it was found.
+   * Also returns the contentType that should be used when converting back before setting the representation.
+   */
+  protected async toIntermediate(source: ResourceStore, identifier: ResourceIdentifier):
+  Promise<{ representation?: Representation; contentType: string }> {
+    let converted: Representation | undefined;
+    let contentType: string;
+    try {
+      const representation = await source.getRepresentation(identifier, {});
+      contentType = representation.metadata.contentType!;
+      const preferences = { type: { [this.intermediateType]: 1 }};
+      converted = await this.converter.handleSafe({ representation, identifier, preferences });
+    } catch (error: unknown) {
+      // Solid, §5.1: "When a successful PUT or PATCH request creates a resource,
+      // the server MUST use the effective request URI to assign the URI to that resource."
+      // https://solid.github.io/specification/protocol#resource-type-heuristics
+      if (!NotFoundHttpError.isInstance(error)) {
+        throw error;
+      }
+      contentType = this.defaultType;
+      this.logger.debug(`Patching new resource ${identifier.path}`);
+    }
+    return { representation: converted, contentType };
+  }
+
+  /**
+   * Patch the given representation based on the patch arguments.
+   * In case representation is not defined a new Representation should be created.
+   * @param input - Arguments that were passed to the initial `handle` call.
+   * @param representation - Representation acquired from the source and converted to the intermediate type.
+   */
+  protected abstract patch(input: PatchHandlerArgs, representation?: Representation): Promise<Representation>;
+}
diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts
index 48d17ed80..45d1f45f2 100644
--- a/src/storage/patch/SparqlUpdatePatchHandler.ts
+++ b/src/storage/patch/SparqlUpdatePatchHandler.ts
@@ -7,34 +7,26 @@ import { Algebra } from 'sparqlalgebrajs';
 import type { Patch } from '../../ldp/http/Patch';
 import type { SparqlUpdatePatch } from '../../ldp/http/SparqlUpdatePatch';
 import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
+import type { Representation } from '../../ldp/representation/Representation';
 import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
 import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
 import { getLoggerFor } from '../../logging/LogUtil';
 import { INTERNAL_QUADS } from '../../util/ContentTypes';
-import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
 import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
 import type { RepresentationConverter } from '../conversion/RepresentationConverter';
-import type { ResourceStore } from '../ResourceStore';
+import { ConvertingPatchHandler } from './ConvertingPatchHandler';
 import type { PatchHandlerArgs } from './PatchHandler';
-import { PatchHandler } from './PatchHandler';
 
 /**
- * PatchHandler that supports specific types of SPARQL updates.
- * Currently all DELETE/INSERT types are supported that have empty where bodies and no variables.
+ * Supports application/sparql-update PATCH requests on RDF resources.
  *
- * Will try to keep the content-type and metadata of the original resource intact.
- * In case this PATCH would create a new resource, it will have content-type `defaultType`.
+ * Only DELETE/INSERT updates without variables are supported.
  */
-export class SparqlUpdatePatchHandler extends PatchHandler {
+export class SparqlUpdatePatchHandler extends ConvertingPatchHandler {
   protected readonly logger = getLoggerFor(this);
 
-  private readonly converter: RepresentationConverter;
-  private readonly defaultType: string;
-
   public constructor(converter: RepresentationConverter, defaultType = 'text/turtle') {
-    super();
-    this.converter = converter;
-    this.defaultType = defaultType;
+    super(converter, INTERNAL_QUADS, defaultType);
   }
 
   public async canHandle({ patch }: PatchHandlerArgs): Promise<void> {
@@ -45,7 +37,7 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
 
   public async handle(input: PatchHandlerArgs): Promise<ResourceIdentifier[]> {
     // Verify the patch
-    const { source, identifier, patch } = input;
+    const { patch } = input;
     const op = (patch as SparqlUpdatePatch).algebra;
 
     // In case of a NOP we can skip everything
@@ -55,7 +47,8 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
 
     this.validateUpdate(op);
 
-    return this.applyPatch(source, identifier, op);
+    // Only start conversion if we know the operation is valid
+    return super.handle(input);
   }
 
   private isSparqlUpdate(patch: Patch): patch is SparqlUpdatePatch {
@@ -126,51 +119,27 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
   /**
    * Apply the given algebra operation to the given identifier.
    */
-  private async applyPatch(source: ResourceStore, identifier: ResourceIdentifier, op: Algebra.Operation):
-  Promise<ResourceIdentifier[]> {
-    // These are used to make sure we keep the original content-type and metadata
-    let contentType: string;
+  protected async patch(input: PatchHandlerArgs, representation?: Representation): Promise<Representation> {
+    const { identifier, patch } = input;
+    const result = new Store<BaseQuad>();
     let metadata: RepresentationMetadata;
 
-    const result = new Store<BaseQuad>();
-    try {
-      // Read the quads of the current representation
-      const representation = await source.getRepresentation(identifier, {});
-      contentType = representation.metadata.contentType ?? this.defaultType;
-      const preferences = { type: { [INTERNAL_QUADS]: 1 }};
-      const quads = await this.converter.handleSafe({ representation, identifier, preferences });
-      // eslint-disable-next-line prefer-destructuring
-      metadata = quads.metadata;
-
-      const importEmitter = result.import(quads.data);
+    if (representation) {
+      ({ metadata } = representation);
+      const importEmitter = result.import(representation.data);
       await new Promise((resolve, reject): void => {
         importEmitter.on('end', resolve);
         importEmitter.on('error', reject);
       });
       this.logger.debug(`${result.size} quads in ${identifier.path}.`);
-    } catch (error: unknown) {
-      // Solid, §5.1: "When a successful PUT or PATCH request creates a resource,
-      // the server MUST use the effective request URI to assign the URI to that resource."
-      // https://solid.github.io/specification/protocol#resource-type-heuristics
-      if (!NotFoundHttpError.isInstance(error)) {
-        throw error;
-      }
-      contentType = this.defaultType;
+    } else {
       metadata = new RepresentationMetadata(identifier, INTERNAL_QUADS);
-      this.logger.debug(`Patching new resource ${identifier.path}.`);
     }
 
-    this.applyOperation(result, op);
+    this.applyOperation(result, (patch as SparqlUpdatePatch).algebra);
     this.logger.debug(`${result.size} quads will be stored to ${identifier.path}.`);
 
-    // Convert back to the original type and write the result
-    const patched = new BasicRepresentation(result.match() as Readable, metadata);
-    const converted = await this.converter.handleSafe({
-      representation: patched,
-      identifier,
-      preferences: { type: { [contentType]: 1 }},
-    });
-    return source.setRepresentation(identifier, converted);
+    return new BasicRepresentation(result.match() as Readable, metadata);
   }
 
   /**


Test patch:
diff --git a/test/integration/Identity.test.ts b/test/integration/Identity.test.ts
index 8f805b344..247857d49 100644
--- a/test/integration/Identity.test.ts
+++ b/test/integration/Identity.test.ts
@@ -335,13 +335,20 @@ describe('A Solid server with IDP', (): void => {
     });
   });
 
-  describe('openid-configuration', (): void => {
-    it('should contain solid_oidc_supported key.', async(): Promise<void> => {
+  describe('setup', (): void => {
+    it('should contain the required configuration keys.', async(): Promise<void> => {
       const res = await fetch(`${baseUrl}.well-known/openid-configuration`);
       const jsonBody = await res.json();
 
       expect(res.status).toBe(200);
+      // https://solid.github.io/authentication-panel/solid-oidc/#discovery
       expect(jsonBody.solid_oidc_supported).toEqual('https://solidproject.org/TR/solid-oidc');
     });
+
+    it('should return correct error output.', async(): Promise<void> => {
+      const res = await fetch(`${baseUrl}idp/auth`);
+      expect(res.status).toBe(400);
+      await expect(res.text()).resolves.toContain('InvalidRequest: invalid_request');
+    });
   });
 });
diff --git a/test/unit/storage/patch/ConvertingPatchHandlers.test.ts b/test/unit/storage/patch/ConvertingPatchHandlers.test.ts
new file mode 100644
index 000000000..7e7271827
--- /dev/null
+++ b/test/unit/storage/patch/ConvertingPatchHandlers.test.ts
@@ -0,0 +1,118 @@
+import type { Patch } from '../../../../src/ldp/http/Patch';
+import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
+import type { Representation } from '../../../../src/ldp/representation/Representation';
+import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
+import type {
+  RepresentationConverter,
+  RepresentationConverterArgs,
+} from '../../../../src/storage/conversion/RepresentationConverter';
+import { ConvertingPatchHandler } from '../../../../src/storage/patch/ConvertingPatchHandler';
+import type { PatchHandlerArgs } from '../../../../src/storage/patch/PatchHandler';
+import type { ResourceStore } from '../../../../src/storage/ResourceStore';
+import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
+
+class SimpleConvertingPatchHandler extends ConvertingPatchHandler {
+  private readonly type: string;
+
+  public constructor(converter: RepresentationConverter, intermediateType: string, defaultType: string) {
+    super(converter, intermediateType, defaultType);
+    this.type = intermediateType;
+  }
+
+  public async patch(input: PatchHandlerArgs, representation?: Representation): Promise<Representation> {
+    return representation ?
+      new BasicRepresentation('patched', representation.metadata) :
+      new BasicRepresentation('patched', input.identifier, this.type);
+  }
+}
+
+describe('A ConvertingPatchHandler', (): void => {
+  const intermediateType = 'internal/quads';
+  const defaultType = 'text/turtle';
+  const identifier: ResourceIdentifier = { path: 'http://test.com/foo' };
+  const patch: Patch = new BasicRepresentation([], 'type/patch');
+  const representation: Representation = new BasicRepresentation([], 'application/trig');
+  let source: jest.Mocked<ResourceStore>;
+  let args: PatchHandlerArgs;
+  let converter: jest.Mocked<RepresentationConverter>;
+  let handler: jest.Mocked<SimpleConvertingPatchHandler>;
+
+  beforeEach(async(): Promise<void> => {
+    converter = {
+      handleSafe: jest.fn(async({ preferences }: RepresentationConverterArgs): Promise<any> =>
+        new BasicRepresentation('converted', Object.keys(preferences.type!)[0])),
+    } as any;
+
+    source = {
+      getRepresentation: jest.fn().mockResolvedValue(representation),
+      setRepresentation: jest.fn(async(id: ResourceIdentifier): Promise<ResourceIdentifier[]> => [ id ]),
+    } as any;
+
+    args = { patch, identifier, source };
+
+    handler = new SimpleConvertingPatchHandler(converter, intermediateType, defaultType) as any;
+    jest.spyOn(handler, 'patch');
+  });
+
+  it('converts the representation before calling the patch function.', async(): Promise<void> => {
+    await expect(handler.handle(args)).resolves.toEqual([ identifier ]);
+
+    // Convert input
+    expect(source.getRepresentation).toHaveBeenCalledTimes(1);
+    expect(source.getRepresentation).toHaveBeenLastCalledWith(identifier, { });
+    expect(converter.handleSafe).toHaveBeenCalledTimes(2);
+    expect(converter.handleSafe).toHaveBeenCalledWith({
+      representation: await source.getRepresentation.mock.results[0].value,
+      identifier,
+      preferences: { type: { [intermediateType]: 1 }},
+    });
+
+    // Patch
+    expect(handler.patch).toHaveBeenCalledTimes(1);
+    expect(handler.patch).toHaveBeenLastCalledWith(args, await converter.handleSafe.mock.results[0].value);
+
+    // Convert back
+    expect(converter.handleSafe).toHaveBeenLastCalledWith({
+      representation: await handler.patch.mock.results[0].value,
+      identifier,
+      preferences: { type: { 'application/trig': 1 }},
+    });
+    expect(source.setRepresentation).toHaveBeenCalledTimes(1);
+    expect(source.setRepresentation)
+      .toHaveBeenLastCalledWith(identifier, await converter.handleSafe.mock.results[1].value);
+    expect(source.setRepresentation.mock.calls[0][1].metadata.contentType).toBe(representation.metadata.contentType);
+  });
+
+  it('expects the patch function to create a new representation if there is none.', async(): Promise<void> => {
+    source.getRepresentation.mockRejectedValueOnce(new NotFoundHttpError());
+
+    await expect(handler.handle(args)).resolves.toEqual([ identifier ]);
+
+    // Try to get input
+    expect(source.getRepresentation).toHaveBeenCalledTimes(1);
+    expect(source.getRepresentation).toHaveBeenLastCalledWith(identifier, { });
+
+    // Patch
+    expect(handler.patch).toHaveBeenCalledTimes(1);
+    expect(handler.patch).toHaveBeenLastCalledWith(args, undefined);
+
+    // Convert new Representation to default type
+    expect(converter.handleSafe).toHaveBeenCalledTimes(1);
+    expect(converter.handleSafe).toHaveBeenLastCalledWith({
+      representation: await handler.patch.mock.results[0].value,
+      identifier,
+      preferences: { type: { [defaultType]: 1 }},
+    });
+    expect(source.setRepresentation).toHaveBeenCalledTimes(1);
+    expect(source.setRepresentation)
+      .toHaveBeenLastCalledWith(identifier, await converter.handleSafe.mock.results[0].value);
+    expect(source.setRepresentation.mock.calls[0][1].metadata.contentType).toBe(defaultType);
+  });
+
+  it('rethrows the error if something goes wrong getting the representation.', async(): Promise<void> => {
+    const error = new Error('bad data');
+    source.getRepresentation.mockRejectedValueOnce(error);
+
+    await expect(handler.handle(args)).rejects.toThrow(error);
+  });
+});
diff --git a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
index 03a9eb5cc..b44982db0 100644
--- a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
+++ b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
@@ -15,13 +15,13 @@ import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError
 import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
 
 describe('A SparqlUpdatePatchHandler', (): void => {
-  let converter: RepresentationConverter;
+  let converter: jest.Mocked<RepresentationConverter>;
   let handler: SparqlUpdatePatchHandler;
-  let source: ResourceStore;
+  let source: jest.Mocked<ResourceStore>;
   let startQuads: Quad[];
-  const dummyType = 'internal/not-quads';
+  const defaultType = 'internal/not-quads';
   const identifier = { path: 'http://test.com/foo' };
-  const fullfilledDataInsert = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 . }';
+  const fulfilledDataInsert = 'INSERT DATA { :s1 :p1 :o1 . :s2 :p2 :o2 . }';
 
   beforeEach(async(): Promise<void> => {
     startQuads = [ quad(
@@ -37,17 +37,14 @@ describe('A SparqlUpdatePatchHandler', (): void => {
     converter = {
       handleSafe: jest.fn(async({ representation, preferences }: RepresentationConverterArgs): Promise<any> =>
         new BasicRepresentation(representation.data, Object.keys(preferences.type!)[0])),
-    } as unknown as RepresentationConverter;
+    } as any;
 
     source = {
-      getRepresentation: jest.fn(async(): Promise<any> => new BasicRepresentation(startQuads, dummyType)),
+      getRepresentation: jest.fn(async(): Promise<any> => new BasicRepresentation(startQuads, defaultType)),
       setRepresentation: jest.fn(),
-      modifyResource: jest.fn(async(): Promise<any> => {
-        throw new Error('noModify');
-      }),
-    } as unknown as ResourceStore;
+    } as any;
 
-    handler = new SparqlUpdatePatchHandler(converter, dummyType);
+    handler = new SparqlUpdatePatchHandler(converter, defaultType);
   });
 
   async function basicChecks(quads: Quad[]): Promise<boolean> {
@@ -55,24 +52,24 @@ describe('A SparqlUpdatePatchHandler', (): void => {
     expect(source.getRepresentation).toHaveBeenLastCalledWith(identifier, { });
     expect(converter.handleSafe).toHaveBeenCalledTimes(2);
     expect(converter.handleSafe).toHaveBeenCalledWith({
-      representation: await (source.getRepresentation as jest.Mock).mock.results[0].value,
+      representation: await source.getRepresentation.mock.results[0].value,
       identifier,
       preferences: { type: { [INTERNAL_QUADS]: 1 }},
     });
     expect(converter.handleSafe).toHaveBeenLastCalledWith({
       representation: expect.objectContaining({ binary: false, metadata: expect.any(RepresentationMetadata) }),
       identifier,
-      preferences: { type: { [dummyType]: 1 }},
+      preferences: { type: { [defaultType]: 1 }},
     });
 
     expect(source.setRepresentation).toHaveBeenCalledTimes(1);
-    const setParams = (source.setRepresentation as jest.Mock).mock.calls[0];
+    const setParams = source.setRepresentation.mock.calls[0];
     expect(setParams[0]).toEqual(identifier);
     expect(setParams[1]).toEqual(expect.objectContaining({
       binary: true,
       metadata: expect.any(RepresentationMetadata),
     }));
-    expect(setParams[1].metadata.contentType).toEqual(dummyType);
+    expect(setParams[1].metadata.contentType).toEqual(defaultType);
     await expect(arrayifyStream(setParams[1].data)).resolves.toBeRdfIsomorphic(quads);
     return true;
   }
@@ -101,7 +98,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
   });
 
   it('handles INSERT DATA updates.', async(): Promise<void> => {
-    await handle(fullfilledDataInsert);
+    await handle(fulfilledDataInsert);
     expect(await basicChecks(startQuads.concat(
       [ quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')),
         quad(namedNode('http://test.com/s2'), namedNode('http://test.com/p2'), namedNode('http://test.com/o2')) ],
@@ -202,15 +199,13 @@ describe('A SparqlUpdatePatchHandler', (): void => {
   });
 
   it('throws the error returned by the store if there is one.', async(): Promise<void> => {
-    source.getRepresentation = jest.fn(async(): Promise<any> => {
-      throw new Error('error');
-    });
-    await expect(handle(fullfilledDataInsert)).rejects.toThrow('error');
+    source.getRepresentation.mockRejectedValueOnce(new Error('error'));
+    await expect(handle(fulfilledDataInsert)).rejects.toThrow('error');
   });
 
   it('creates a new resource if it does not exist yet.', async(): Promise<void> => {
     startQuads = [];
-    (source.getRepresentation as jest.Mock).mockRejectedValueOnce(new NotFoundHttpError());
+    source.getRepresentation.mockRejectedValueOnce(new NotFoundHttpError());
     const query = 'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }';
     await handle(query);
 
@@ -218,37 +213,26 @@ describe('A SparqlUpdatePatchHandler', (): void => {
     expect(converter.handleSafe).toHaveBeenLastCalledWith({
       representation: expect.objectContaining({ binary: false, metadata: expect.any(RepresentationMetadata) }),
       identifier,
-      preferences: { type: { [dummyType]: 1 }},
+      preferences: { type: { [defaultType]: 1 }},
     });
 
     const quads =
       [ quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')) ];
     expect(source.setRepresentation).toHaveBeenCalledTimes(1);
-    const setParams = (source.setRepresentation as jest.Mock).mock.calls[0];
-    expect(setParams[1].metadata.contentType).toEqual(dummyType);
+    const setParams = source.setRepresentation.mock.calls[0];
+    expect(setParams[1].metadata.contentType).toEqual(defaultType);
     await expect(arrayifyStream(setParams[1].data)).resolves.toBeRdfIsomorphic(quads);
   });
 
-  it('can handle representations without content-type.', async(): Promise<void> => {
-    (source.getRepresentation as jest.Mock).mockResolvedValueOnce(
-      new BasicRepresentation(startQuads, new RepresentationMetadata()),
-    );
-    await handle(fullfilledDataInsert);
-    expect(await basicChecks(startQuads.concat(
-      [ quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')),
-        quad(namedNode('http://test.com/s2'), namedNode('http://test.com/p2'), namedNode('http://test.com/o2')) ],
-    ))).toBe(true);
-  });
-
   it('defaults to text/turtle if no default type was set.', async(): Promise<void> => {
     handler = new SparqlUpdatePatchHandler(converter);
     startQuads = [];
-    (source.getRepresentation as jest.Mock).mockRejectedValueOnce(new NotFoundHttpError());
+    source.getRepresentation.mockRejectedValueOnce(new NotFoundHttpError());
     const query = 'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }';
     await handle(query);
 
     expect(source.setRepresentation).toHaveBeenCalledTimes(1);
-    const setParams = (source.setRepresentation as jest.Mock).mock.calls[0];
+    const setParams = source.setRepresentation.mock.calls[0];
     expect(setParams[1].metadata.contentType).toEqual('text/turtle');
   });
 });