Repository: solid/community-server
Pull request #513 (2021-01-14 22:43:27)

Use baseIRI in QuadToRdfConverter
Closes https://github.com/solid/community-server/issues/512


Patch:
diff --git a/config/presets/init.json b/config/presets/init.json
index cc0b898de..afd1f0b11 100644
--- a/config/presets/init.json
+++ b/config/presets/init.json
@@ -15,24 +15,24 @@
         {
           "@id": "urn:solid-server:default:RootContainerInitializer",
           "@type": "RootContainerInitializer",
-          "RootContainerInitializer:_baseUrl": {
-            "@id": "urn:solid-server:default:variable:baseUrl"
-          },
-          "RootContainerInitializer:_store": {
+          "RootContainerInitializer:_settings_store": {
             "@id": "urn:solid-server:default:ResourceStore"
+          },
+          "RootContainerInitializer:_settings_baseUrl": {
+            "@id": "urn:solid-server:default:variable:baseUrl"
           }
         },
         {
           "@id": "urn:solid-server:default:AclInitializer",
           "@type": "AclInitializer",
-          "AclInitializer:_baseUrl": {
-            "@id": "urn:solid-server:default:variable:baseUrl"
-          },
-          "AclInitializer:_store": {
+          "AclInitializer:_settings_store": {
             "@id": "urn:solid-server:default:ResourceStore"
           },
-          "AclInitializer:_aclManager": {
+          "AclInitializer:_settings_aclManager": {
             "@id": "urn:solid-server:default:AclManager"
+          },
+          "AclInitializer:_settings_baseUrl": {
+            "@id": "urn:solid-server:default:variable:baseUrl"
           }
         },
         {
diff --git a/package-lock.json b/package-lock.json
index a76f1a98b..d653ee490 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1102,9 +1102,9 @@
       }
     },
     "@solid/identity-token-verifier": {
-      "version": "0.4.3",
-      "resolved": "https://registry.npmjs.org/@solid/identity-token-verifier/-/identity-token-verifier-0.4.3.tgz",
-      "integrity": "sha512-LwE3QOImK4prBMR2Q79Jx70Aghk5K+rH+BrsSlgqYVv+zl0u2Ir47LtptBvxTq6CjpYeoZ7Hxgao5g8B/AMCQw==",
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/@solid/identity-token-verifier/-/identity-token-verifier-0.5.0.tgz",
+      "integrity": "sha512-PJoGEVDh9rpuZe7FUwcqpsCD4Fz2+YrIEStnosLwBahyc5EKICtnP2qaIWx2p1AOxYGNR02Ub8BYOJgDlzC3BQ==",
       "requires": {
         "cross-fetch": "^3.0.6",
         "jose": "^3.5.0",
@@ -6438,9 +6438,9 @@
       "dev": true
     },
     "jose": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/jose/-/jose-3.5.0.tgz",
-      "integrity": "sha512-pW9z+ny33gxX2wXLQl3SkPQWGaUvOMYLijuiMHIHUYIDsrZjdMqYYS5UTkusuMzZkqe5T8YImA4FOqqB1IWmRg=="
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-3.5.1.tgz",
+      "integrity": "sha512-BQQJafCDvsmtc/LaK57cjfhU/1AKBhJSbJhKPq+uhrjMoV/sh3/dbk9cm08nAeSGS6j+sjhWoY+LZPUXiKzYzw=="
     },
     "js-tokens": {
       "version": "4.0.0",
@@ -7136,9 +7136,9 @@
       "dev": true
     },
     "n3": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/n3/-/n3-1.7.0.tgz",
-      "integrity": "sha512-8R0Qj545WnVLQxOfxxyFKzOpO13hF3jhSMJfO0FNqvbsPZDiR9ZDmGGjXAlcoZDf/88OsCYd7rHML284vm1h6A==",
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/n3/-/n3-1.8.0.tgz",
+      "integrity": "sha512-/PEmoB3UJrG6aXGZenDHFBJtmPp2rtfB2YLzAm2dU9stInD+ztvy4fKv5fv2ggsrSlpu7BYDTsz/c6S391uuEg==",
       "requires": {
         "queue-microtask": "^1.1.2",
         "readable-stream": "^3.6.0"
diff --git a/package.json b/package.json
index 6c8361307..f040c8904 100644
--- a/package.json
+++ b/package.json
@@ -70,11 +70,12 @@
   "files": [
     "bin",
     "dist",
-    "config"
+    "config",
+    "templates"
   ],
   "dependencies": {
     "@rdfjs/data-model": "^1.2.0",
-    "@solid/identity-token-verifier": "^0.4.3",
+    "@solid/identity-token-verifier": "^0.5.0",
     "@types/arrayify-stream": "^1.0.0",
     "@types/async-lock": "^1.1.2",
     "@types/cors": "^2.8.6",
@@ -96,7 +97,7 @@
     "fetch-sparql-endpoint": "^1.8.0",
     "handlebars": "^4.7.6",
     "mime-types": "^2.1.27",
-    "n3": "^1.7.0",
+    "n3": "^1.8.0",
     "rdf-parse": "^1.5.0",
     "rdf-serialize": "^1.0.0",
     "rdf-terms": "^1.5.1",
diff --git a/src/init/AclInitializer.ts b/src/init/AclInitializer.ts
index fe8632f5f..28fb4a3f3 100644
--- a/src/init/AclInitializer.ts
+++ b/src/init/AclInitializer.ts
@@ -1,3 +1,4 @@
+import { promises as fsPromises } from 'fs';
 import type { AclManager } from '../authorization/AclManager';
 import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
 import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
@@ -5,9 +6,10 @@ import { getLoggerFor } from '../logging/LogUtil';
 import type { ResourceStore } from '../storage/ResourceStore';
 import { containsResource } from '../storage/StoreUtil';
 import { TEXT_TURTLE } from '../util/ContentTypes';
-import { ensureTrailingSlash } from '../util/PathUtil';
+import { ensureTrailingSlash, joinFilePath } from '../util/PathUtil';
 import { Initializer } from './Initializer';
 
+const DEFAULT_ACL_PATH = joinFilePath(__dirname, '../../templates/root/.acl');
 /**
  * Ensures that a root ACL is present.
  */
@@ -15,21 +17,24 @@ export class AclInitializer extends Initializer {
   protected readonly logger = getLoggerFor(this);
   private readonly store: ResourceStore;
   private readonly aclManager: AclManager;
-  private readonly baseUrl: string;
+  private readonly root: ResourceIdentifier;
+  private readonly aclPath: string;
 
-  public constructor(
-    baseUrl: string,
-    store: ResourceStore,
-    aclManager: AclManager,
-  ) {
+  public constructor(settings: {
+    store: ResourceStore;
+    aclManager: AclManager;
+    baseUrl: string;
+    aclPath?: string;
+  }) {
     super();
-    this.baseUrl = ensureTrailingSlash(baseUrl);
-    this.store = store;
-    this.aclManager = aclManager;
+    this.store = settings.store;
+    this.aclManager = settings.aclManager;
+    this.root = { path: ensureTrailingSlash(settings.baseUrl) };
+    this.aclPath = settings.aclPath ?? DEFAULT_ACL_PATH;
   }
 
   public async handle(): Promise<void> {
-    const rootAcl = await this.aclManager.getAclDocument({ path: this.baseUrl });
+    const rootAcl = await this.aclManager.getAclDocument(this.root);
     if (!await containsResource(this.store, rootAcl)) {
       await this.setRootAclDocument(rootAcl);
     } else {
@@ -37,26 +42,11 @@ export class AclInitializer extends Initializer {
     }
   }
 
-  // Set up ACL so everything can still be done by default
-  // Note that this will need to be adapted to go through all the correct channels later on
-  //
   // Solid, §4.1: "The root container (pim:Storage) MUST have an ACL auxiliary resource directly associated to it.
   // The associated ACL document MUST include an authorization policy with acl:Control access privilege."
   // https://solid.github.io/specification/protocol#storage
   protected async setRootAclDocument(rootAcl: ResourceIdentifier): Promise<void> {
-    const acl = `@prefix   acl:  <http://www.w3.org/ns/auth/acl#>.
-@prefix  foaf:  <http://xmlns.com/foaf/0.1/>.
-
-<#authorization>
-    a               acl:Authorization;
-    acl:agentClass  foaf:Agent;
-    acl:mode        acl:Read;
-    acl:mode        acl:Write;
-    acl:mode        acl:Append;
-    acl:mode        acl:Delete;
-    acl:mode        acl:Control;
-    acl:accessTo    <${this.baseUrl}>;
-    acl:default     <${this.baseUrl}>.`;
+    const acl = await fsPromises.readFile(this.aclPath, 'utf8');
     this.logger.debug(`Installing root ACL document at ${rootAcl.path}`);
     await this.store.setRepresentation(rootAcl, new BasicRepresentation(acl, rootAcl, TEXT_TURTLE));
   }
diff --git a/src/init/CliRunner.ts b/src/init/CliRunner.ts
index 5a6ef6c1f..4c522027e 100644
--- a/src/init/CliRunner.ts
+++ b/src/init/CliRunner.ts
@@ -88,7 +88,7 @@ export class CliRunner {
         this.resolveFilePath(params.rootFilePath),
       'urn:solid-server:default:variable:sparqlEndpoint': params.sparqlEndpoint,
       'urn:solid-server:default:variable:podTemplateFolder':
-         this.resolveFilePath(params.podTemplateFolder, 'templates'),
+         this.resolveFilePath(params.podTemplateFolder, 'templates/pod'),
     };
   }
 
diff --git a/src/init/RootContainerInitializer.ts b/src/init/RootContainerInitializer.ts
index c24bd048f..f614e6a26 100644
--- a/src/init/RootContainerInitializer.ts
+++ b/src/init/RootContainerInitializer.ts
@@ -21,13 +21,13 @@ import namedNode = DataFactory.namedNode;
  */
 export class RootContainerInitializer extends Initializer {
   protected readonly logger = getLoggerFor(this);
-  private readonly baseId: ResourceIdentifier;
   private readonly store: ResourceStore;
+  private readonly baseId: ResourceIdentifier;
 
-  public constructor(baseUrl: string, store: ResourceStore) {
+  public constructor(settings: { store: ResourceStore; baseUrl: string }) {
     super();
-    this.baseId = { path: ensureTrailingSlash(baseUrl) };
-    this.store = store;
+    this.store = settings.store;
+    this.baseId = { path: ensureTrailingSlash(settings.baseUrl) };
   }
 
   public async handle(): Promise<void> {
diff --git a/src/storage/conversion/QuadToRdfConverter.ts b/src/storage/conversion/QuadToRdfConverter.ts
index a1ec3bd1e..757b28c2d 100644
--- a/src/storage/conversion/QuadToRdfConverter.ts
+++ b/src/storage/conversion/QuadToRdfConverter.ts
@@ -24,7 +24,8 @@ export class QuadToRdfConverter extends TypedRepresentationConverter {
     );
   }
 
-  public async handle({ representation: quads, preferences }: RepresentationConverterArgs): Promise<Representation> {
+  public async handle({ identifier, representation: quads, preferences }: RepresentationConverterArgs):
+  Promise<Representation> {
     const contentType = matchingMediaTypes(preferences.type, await this.getOutputTypes())[0];
     let data: Readable;
 
@@ -32,7 +33,8 @@ export class QuadToRdfConverter extends TypedRepresentationConverter {
     if (/(?:turtle|trig)$/u.test(contentType)) {
       const prefixes = Object.fromEntries(quads.metadata.quads(null, PREFERRED_PREFIX_TERM, null)
         .map(({ subject, object }): [string, string] => [ object.value, subject.value ]));
-      data = pipeSafely(quads.data, new StreamWriter({ format: contentType, prefixes }));
+      const options = { format: contentType, baseIRI: identifier.path, prefixes };
+      data = pipeSafely(quads.data, new StreamWriter(options));
     // Otherwise, write without prefixes
     } else {
       data = rdfSerializer.serialize(quads.data, { contentType }) as Readable;
diff --git a/templates/.acl b/templates/pod/.acl
similarity index 100%
rename from templates/.acl
rename to templates/pod/.acl
diff --git a/templates/.meta b/templates/pod/.meta
similarity index 100%
rename from templates/.meta
rename to templates/pod/.meta
diff --git a/templates/profile/card$.ttl b/templates/pod/profile/card$.ttl
similarity index 100%
rename from templates/profile/card$.ttl
rename to templates/pod/profile/card$.ttl
diff --git a/templates/root/.acl b/templates/root/.acl
new file mode 100644
index 000000000..107adcc82
--- /dev/null
+++ b/templates/root/.acl
@@ -0,0 +1,13 @@
+@prefix acl: <http://www.w3.org/ns/auth/acl#>.
+@prefix foaf: <http://xmlns.com/foaf/0.1/>.
+
+<#authorization>
+    a               acl:Authorization;
+    acl:agentClass  foaf:Agent;
+    acl:mode        acl:Read;
+    acl:mode        acl:Write;
+    acl:mode        acl:Append;
+    acl:mode        acl:Delete;
+    acl:mode        acl:Control;
+    acl:accessTo    <./>;
+    acl:default     <./>.


Test patch:
diff --git a/test/integration/LdpHandlerWithoutAuth.test.ts b/test/integration/LdpHandlerWithoutAuth.test.ts
index b46564894..84754289d 100644
--- a/test/integration/LdpHandlerWithoutAuth.test.ts
+++ b/test/integration/LdpHandlerWithoutAuth.test.ts
@@ -64,7 +64,7 @@ describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn,
     expect(response.getHeaders()).toHaveProperty('content-type', 'text/turtle');
 
     const data = response._getData().toString();
-    expect(data).toContain(`<${BASE}/> a ldp:Container`);
+    expect(data).toContain(`<> a ldp:Container`);
     expect(response.getHeaders().link).toContain(`<${LDP.Container}>; rel="type"`);
   });
 
@@ -266,7 +266,7 @@ describe.each(stores)('An LDP handler without auth using %s', (name, { storeUrn,
     response = await resourceHelper.performRequest(new URL(`${BASE}/${slug}/`), 'GET', { accept: 'text/turtle' });
     expect(response.statusCode).toBe(200);
 
-    const parser = new Parser();
+    const parser = new Parser({ baseIRI: `${BASE}/${slug}/` });
     const quads = parser.parse(response._getData());
     expect(quads.some((entry): boolean => entry.equals(quad(
       namedNode(`${BASE}/${slug}/`),
diff --git a/test/integration/LockingResourceStore.test.ts b/test/integration/LockingResourceStore.test.ts
index e87cb0265..ba99cc63e 100644
--- a/test/integration/LockingResourceStore.test.ts
+++ b/test/integration/LockingResourceStore.test.ts
@@ -29,7 +29,7 @@ describe('A LockingResourceStore', (): void => {
     source = new DataAccessorBasedStore(new InMemoryDataAccessor(base), new SingleRootIdentifierStrategy(base));
 
     // Initialize store
-    const initializer = new RootContainerInitializer(BASE, source);
+    const initializer = new RootContainerInitializer({ store: source, baseUrl: BASE });
     await initializer.handleSafe();
 
     locker = new SingleThreadedResourceLocker();
diff --git a/test/integration/WebSocketsProtocol.test.ts b/test/integration/WebSocketsProtocol.test.ts
index 210af516a..afb97c7fb 100644
--- a/test/integration/WebSocketsProtocol.test.ts
+++ b/test/integration/WebSocketsProtocol.test.ts
@@ -16,7 +16,7 @@ describe('A server with the Solid WebSockets API behind a proxy', (): void => {
       'urn:solid-server:default:ServerFactory', 'server-without-auth.json', {
         'urn:solid-server:default:variable:port': port,
         'urn:solid-server:default:variable:baseUrl': 'https://example.pod/',
-        'urn:solid-server:default:variable:podTemplateFolder': 'templates',
+        'urn:solid-server:default:variable:podTemplateFolder': 'templates/pod',
       },
     ) as HttpServerFactory;
     server = factory.startServer(port);
diff --git a/test/unit/init/AclInitializer.test.ts b/test/unit/init/AclInitializer.test.ts
index b5f02393b..5b9d4d670 100644
--- a/test/unit/init/AclInitializer.test.ts
+++ b/test/unit/init/AclInitializer.test.ts
@@ -1,9 +1,15 @@
 import type { AclManager } from '../../../src/authorization/AclManager';
 import { AclInitializer } from '../../../src/init/AclInitializer';
+import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation';
 import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
 import type { ResourceStore } from '../../../src/storage/ResourceStore';
 import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
 
+jest.mock('../../../src/ldp/representation/BasicRepresentation');
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+const RepresentationMock: jest.Mock<BasicRepresentation> = BasicRepresentation as any;
+
 describe('AclInitializer', (): void => {
   const store: jest.Mocked<ResourceStore> = {
     getRepresentation: jest.fn().mockRejectedValue(new NotFoundHttpError()),
@@ -14,22 +20,40 @@ describe('AclInitializer', (): void => {
   } as any;
   const baseUrl = 'http://localhost:3000/';
 
-  let initializer: AclInitializer;
-  beforeEach(async(): Promise<void> => {
-    initializer = new AclInitializer(baseUrl, store, aclManager);
-  });
-
   afterEach((): void => {
     jest.clearAllMocks();
   });
 
-  it('invokes ACL initialization.', async(): Promise<void> => {
+  it('sets the default ACL when none exists already.', async(): Promise<void> => {
+    const initializer = new AclInitializer({ baseUrl, store, aclManager });
     await initializer.handle();
 
     expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: 'http://localhost:3000/' });
     expect(store.getRepresentation).toHaveBeenCalledTimes(1);
     expect(store.getRepresentation).toHaveBeenCalledWith({ path: 'http://test.com/.acl' }, {});
     expect(store.setRepresentation).toHaveBeenCalledTimes(1);
+    expect(store.setRepresentation).toHaveBeenCalledWith(
+      { path: 'http://test.com/.acl' }, RepresentationMock.mock.instances[0],
+    );
+    expect(RepresentationMock).toHaveBeenCalledWith(
+      expect.stringMatching('<#authorization>'), { path: 'http://test.com/.acl' }, 'text/turtle',
+    );
+  });
+
+  it('sets the specific ACL when one was specified.', async(): Promise<void> => {
+    const initializer = new AclInitializer({ baseUrl, store, aclManager, aclPath: __filename });
+    await initializer.handle();
+
+    expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: 'http://localhost:3000/' });
+    expect(store.getRepresentation).toHaveBeenCalledTimes(1);
+    expect(store.getRepresentation).toHaveBeenCalledWith({ path: 'http://test.com/.acl' }, {});
+    expect(store.setRepresentation).toHaveBeenCalledTimes(1);
+    expect(store.setRepresentation).toHaveBeenCalledWith(
+      { path: 'http://test.com/.acl' }, RepresentationMock.mock.instances[0],
+    );
+    expect(RepresentationMock).toHaveBeenCalledWith(
+      expect.stringMatching('Joachim'), { path: 'http://test.com/.acl' }, 'text/turtle',
+    );
   });
 
   it('does not invoke ACL initialization when a root ACL already exists.', async(): Promise<void> => {
@@ -37,6 +61,7 @@ describe('AclInitializer', (): void => {
       data: { destroy: jest.fn() },
     } as any));
 
+    const initializer = new AclInitializer({ baseUrl, store, aclManager });
     await initializer.handle();
 
     expect(aclManager.getAclDocument).toHaveBeenCalledWith({ path: 'http://localhost:3000/' });
@@ -47,6 +72,8 @@ describe('AclInitializer', (): void => {
 
   it('errors when the root ACL check errors.', async(): Promise<void> => {
     store.getRepresentation.mockRejectedValueOnce(new Error('Fatal'));
+
+    const initializer = new AclInitializer({ baseUrl, store, aclManager });
     await expect(initializer.handle()).rejects.toThrow('Fatal');
   });
 });
diff --git a/test/unit/init/CliRunner.test.ts b/test/unit/init/CliRunner.test.ts
index 19cc805e0..ab0afc46e 100644
--- a/test/unit/init/CliRunner.test.ts
+++ b/test/unit/init/CliRunner.test.ts
@@ -59,7 +59,7 @@ describe('CliRunner', (): void => {
           'urn:solid-server:default:variable:rootFilePath': '/var/cwd/',
           'urn:solid-server:default:variable:sparqlEndpoint': undefined,
           'urn:solid-server:default:variable:loggingLevel': 'info',
-          'urn:solid-server:default:variable:podTemplateFolder': joinFilePath(__dirname, '../../../templates'),
+          'urn:solid-server:default:variable:podTemplateFolder': joinFilePath(__dirname, '../../../templates/pod'),
         },
       },
     );
diff --git a/test/unit/init/RootContainerInitializer.test.ts b/test/unit/init/RootContainerInitializer.test.ts
index ba7942f82..7c3c978c5 100644
--- a/test/unit/init/RootContainerInitializer.test.ts
+++ b/test/unit/init/RootContainerInitializer.test.ts
@@ -8,7 +8,7 @@ describe('A RootContainerInitializer', (): void => {
     getRepresentation: jest.fn().mockRejectedValue(new NotFoundHttpError()),
     setRepresentation: jest.fn(),
   } as any;
-  const initializer = new RootContainerInitializer(baseUrl, store);
+  const initializer = new RootContainerInitializer({ store, baseUrl });
 
   afterEach((): void => {
     jest.clearAllMocks();
diff --git a/test/unit/pods/generate/TemplatedResourcesGenerator.test.ts b/test/unit/pods/generate/TemplatedResourcesGenerator.test.ts
index 5f387c580..7b4c92bab 100644
--- a/test/unit/pods/generate/TemplatedResourcesGenerator.test.ts
+++ b/test/unit/pods/generate/TemplatedResourcesGenerator.test.ts
@@ -38,7 +38,7 @@ async function genToArray<T>(iterable: AsyncIterable<T>): Promise<T[]> {
 }
 
 describe('A TemplatedResourcesGenerator', (): void => {
-  const rootFilePath = 'templates';
+  const rootFilePath = 'templates/pod';
   // Using handlebars engine since it's smaller than any possible dummy
   const generator = new TemplatedResourcesGenerator(rootFilePath, new DummyFactory(), new HandlebarsTemplateEngine());
   let cache: { data: any };
diff --git a/test/unit/storage/conversion/QuadToRdfConverter.test.ts b/test/unit/storage/conversion/QuadToRdfConverter.test.ts
index 5a7bfe8c2..35a3131b7 100644
--- a/test/unit/storage/conversion/QuadToRdfConverter.test.ts
+++ b/test/unit/storage/conversion/QuadToRdfConverter.test.ts
@@ -12,11 +12,11 @@ import { DC, PREFERRED_PREFIX_TERM } from '../../../../src/util/Vocabularies';
 
 describe('A QuadToRdfConverter', (): void => {
   const converter = new QuadToRdfConverter();
-  const identifier: ResourceIdentifier = { path: 'path' };
+  const identifier: ResourceIdentifier = { path: 'http://example.org/foo/bar/' };
   let metadata: RepresentationMetadata;
 
   beforeEach((): void => {
-    metadata = new RepresentationMetadata(INTERNAL_QUADS);
+    metadata = new RepresentationMetadata(identifier, INTERNAL_QUADS);
   });
 
   it('supports parsing quads.', async(): Promise<void> => {
@@ -35,7 +35,7 @@ describe('A QuadToRdfConverter', (): void => {
       .resolves.toEqual(outputPreferences);
   });
 
-  it('can handle quad to turtle conversions.', async(): Promise<void> => {
+  it('can handle quad to Turtle conversions.', async(): Promise<void> => {
     const representation = { metadata } as Representation;
     const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
     await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined();
@@ -47,7 +47,7 @@ describe('A QuadToRdfConverter', (): void => {
     await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined();
   });
 
-  it('converts quads to turtle.', async(): Promise<void> => {
+  it('converts quads to Turtle.', async(): Promise<void> => {
     const representation = {
       data: streamifyArray([ triple(
         namedNode('http://test.com/s'),
@@ -69,7 +69,7 @@ describe('A QuadToRdfConverter', (): void => {
     );
   });
 
-  it('converts quads with prefixes to turtle.', async(): Promise<void> => {
+  it('converts quads with prefixes to Turtle.', async(): Promise<void> => {
     metadata.addQuad(DC.terms.namespace, PREFERRED_PREFIX_TERM, 'dc');
     metadata.addQuad('http://test.com/', PREFERRED_PREFIX_TERM, 'test');
     const representation = {
@@ -82,10 +82,6 @@ describe('A QuadToRdfConverter', (): void => {
     } as Representation;
     const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
     const result = await converter.handle({ identifier, representation, preferences });
-    expect(result).toMatchObject({
-      binary: true,
-      metadata: expect.any(RepresentationMetadata),
-    });
     expect(result.metadata.contentType).toEqual('text/turtle');
     await expect(stringifyStream(result.data)).resolves.toEqual(
       `@prefix dc: <http://purl.org/dc/terms/>.
@@ -96,6 +92,24 @@ test:s dc:modified test:o.
     );
   });
 
+  it('uses the base IRI when converting quads to Turtle.', async(): Promise<void> => {
+    const representation = {
+      data: streamifyArray([ triple(
+        namedNode('http://example.org/foo/bar/'),
+        namedNode('http://example.org/foo/bar/#abc'),
+        namedNode('http://example.org/foo/bar/def/ghi'),
+      ) ]),
+      metadata,
+    } as Representation;
+    const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
+    const result = await converter.handle({ identifier, representation, preferences });
+    expect(result.metadata.contentType).toEqual('text/turtle');
+    await expect(stringifyStream(result.data)).resolves.toEqual(
+      `<> <#abc> <def/ghi>.
+`,
+    );
+  });
+
   it('converts quads to JSON-LD.', async(): Promise<void> => {
     metadata.contentType = INTERNAL_QUADS;
     const representation = {