Repository: solid/community-server
Pull request #465 (2021-01-04 11:07:07)

Also run CI on Windows

RubenVerborgh: > Is it possible that just running an OS matrix would work?

I wish https://github.com/solid/community-server/actions/runs/460881540
matthieubosquet: Ah shame, I didn't know that.

On Mon, 4 Jan 2021 at 11:41, Ruben Verborgh <notifications@github.com>
wrote:

> Is it possible that just running an OS matrix would work?
>
> I wish https://github.com/solid/community-server/actions/runs/460881540
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/solid/community-server/pull/465#issuecomment-753927820>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAEVZ466LB274J5ZYPREWQ3SYGSOBANCNFSM4VS3PTDA>
> .
>

RubenVerborgh: @joachimvh I have no idea why `__dirname` suddenly appears to be relative on Windows: https://github.com/solid/community-server/pull/465/checks?check_run_id=1643522394#step:6:308
RubenVerborgh: @rubensworks Components.js does not seem to recognize Windows paths; should we prefix with `file://`?
rubensworks: @RubenVerborgh Not sure if things will work with `file://`.

Config resolution is also one of the things that has been rewritten in the new version, so this _may_ also be already fixed there.
joachimvh: The main issue is [this](https://github.com/solid/community-server/pull/465/files#diff-33e091ff0999f44395e80f6c9fce28725a2f6e650b36ce0390a475ab9788da0bR19) line. The problem seems to be that if a path starts with `C:/`. I'm guessing there's a check in components.js that assumes the input is an url if it starts with `:/`. If the original `path.join` is used on that line it works fine.

I assumed that a temporary fix could have been to remove the call to `slash` in PathUtil but that seems to throw other errors now so might look into that. Sort of want this fixed so all tests succeed locally again for me 😅 
RubenVerborgh: OK, all that is remaining is a CR/LF issue 😅

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dc1f1f9fa..16ad4ec7d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -63,6 +63,33 @@ jobs:
       - name: Run deployment tests
         run: npm run test:deploy
 
+  test-windows:
+    runs-on: windows-latest
+    strategy:
+      matrix:
+        node-version:
+          - 12.x
+          - 14.x
+    steps:
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v2
+        with:
+          node-version: ${{ matrix.node-version }}
+      - name: Ensure line endings are consistent
+        run: git config --global core.autocrlf input
+      - name: Check out repository
+        uses: actions/checkout@v2
+      - name: Install dependencies
+        run: npm ci --ignore-scripts
+      - name: Run build scripts
+        run: npm run build
+      - name: Run unit tests
+        run: npm run test:unit
+      - name: Run integration tests
+        run: npm run test:integration
+      - name: Run deployment tests
+        run: npm run test:deploy
+
   coveralls:
     needs: test
     runs-on: ubuntu-latest
diff --git a/src/init/CliRunner.ts b/src/init/CliRunner.ts
index ebb50b19b..fdbc48c08 100644
--- a/src/init/CliRunner.ts
+++ b/src/init/CliRunner.ts
@@ -1,12 +1,11 @@
 /* eslint-disable unicorn/no-process-exit */
 
-import * as path from 'path';
 import type { ReadStream, WriteStream } from 'tty';
 import type { LoaderProperties } from 'componentsjs';
 import { Loader } from 'componentsjs';
 import yargs from 'yargs';
 import { getLoggerFor } from '../logging/LogUtil';
-import { ensureTrailingSlash } from '../util/PathUtil';
+import { joinFilePath, toSystemFilePath, ensureTrailingSlash } from '../util/PathUtil';
 import type { Initializer } from './Initializer';
 
 export class CliRunner {
@@ -44,14 +43,14 @@ export class CliRunner {
 
     // Gather settings for instantiating the server
     const loaderProperties: LoaderProperties = {
-      mainModulePath: this.resolvePath(params.mainModulePath),
+      mainModulePath: toSystemFilePath(this.resolveFilePath(params.mainModulePath)),
       scanGlobal: params.globalModules,
     };
-    const configFile = this.resolvePath(params.config, 'config/config-default.json');
+    const configFile = this.resolveFilePath(params.config, 'config/config-default.json');
     const variables = this.createVariables(params);
 
     // Create and execute the server initializer
-    this.createInitializer(loaderProperties, configFile, variables)
+    this.createInitializer(loaderProperties, toSystemFilePath(configFile), variables)
       .then(
         async(initializer): Promise<void> => initializer.handleSafe(),
         (error: Error): void => {
@@ -70,10 +69,10 @@ export class CliRunner {
    * Resolves a path relative to the current working directory,
    * falling back to a path relative to this module.
    */
-  protected resolvePath(cwdPath?: string | null, modulePath = ''): string {
+  protected resolveFilePath(cwdPath?: string | null, modulePath = ''): string {
     return typeof cwdPath === 'string' ?
-      path.join(process.cwd(), cwdPath) :
-      path.join(__dirname, '../../', modulePath);
+      joinFilePath(process.cwd(), cwdPath) :
+      joinFilePath(__dirname, '../../', modulePath);
   }
 
   /**
@@ -85,10 +84,11 @@ export class CliRunner {
         params.baseUrl ? ensureTrailingSlash(params.baseUrl) : `http://localhost:${params.port}/`,
       'urn:solid-server:default:variable:loggingLevel': params.loggingLevel,
       'urn:solid-server:default:variable:port': params.port,
-      'urn:solid-server:default:variable:rootFilePath': this.resolvePath(params.rootFilePath),
+      'urn:solid-server:default:variable:rootFilePath':
+        this.resolveFilePath(params.rootFilePath),
       'urn:solid-server:default:variable:sparqlEndpoint': params.sparqlEndpoint,
       'urn:solid-server:default:variable:podTemplateFolder':
-        params.podTemplateFolder ?? this.resolvePath(null, 'templates'),
+         this.resolveFilePath(params.podTemplateFolder, 'templates'),
     };
   }
 
diff --git a/src/pods/generate/TemplatedResourcesGenerator.ts b/src/pods/generate/TemplatedResourcesGenerator.ts
index a35cfefc6..f7982cf5c 100644
--- a/src/pods/generate/TemplatedResourcesGenerator.ts
+++ b/src/pods/generate/TemplatedResourcesGenerator.ts
@@ -1,5 +1,4 @@
 import { promises as fsPromises } from 'fs';
-import { posix } from 'path';
 import { Parser } from 'n3';
 import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
 import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
@@ -8,14 +7,12 @@ import type {
   FileIdentifierMapperFactory,
   ResourceLink,
 } from '../../storage/mapping/FileIdentifierMapper';
-import { isContainerIdentifier } from '../../util/PathUtil';
+import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil';
 import { guardedStreamFrom } from '../../util/StreamUtil';
 import type { Resource, ResourcesGenerator } from './ResourcesGenerator';
 import type { TemplateEngine } from './TemplateEngine';
 import Dict = NodeJS.Dict;
 
-const { join: joinPath } = posix;
-
 /**
  * Generates resources by making use of a template engine.
  * The template folder structure will be kept.
@@ -77,7 +74,7 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
   private async* generateLinks(folderPath: string, mapper: FileIdentifierMapper): AsyncIterable<ResourceLink> {
     const files = await fsPromises.readdir(folderPath);
     for (const name of files) {
-      const filePath = joinPath(folderPath, name);
+      const filePath = joinFilePath(folderPath, name);
       const stats = await fsPromises.lstat(filePath);
       yield mapper.mapFilePathToUrl(filePath, stats.isDirectory());
     }
diff --git a/src/storage/accessors/FileDataAccessor.ts b/src/storage/accessors/FileDataAccessor.ts
index ca832f986..73fee2657 100644
--- a/src/storage/accessors/FileDataAccessor.ts
+++ b/src/storage/accessors/FileDataAccessor.ts
@@ -1,6 +1,5 @@
 import type { Stats } from 'fs';
 import { createWriteStream, createReadStream, promises as fsPromises } from 'fs';
-import { posix } from 'path';
 import type { Readable } from 'stream';
 import { DataFactory } from 'n3';
 import type { NamedNode, Quad } from 'rdf-js';
@@ -13,7 +12,7 @@ import { isSystemError } from '../../util/errors/SystemError';
 import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
 import { guardStream } from '../../util/GuardedStream';
 import type { Guarded } from '../../util/GuardedStream';
-import { isContainerIdentifier } from '../../util/PathUtil';
+import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil';
 import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil';
 import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
 import { toLiteral } from '../../util/TermUtil';
@@ -21,8 +20,6 @@ import { CONTENT_TYPE, DC, LDP, POSIX, RDF, XSD } from '../../util/Vocabularies'
 import type { FileIdentifierMapper, ResourceLink } from '../mapping/FileIdentifierMapper';
 import type { DataAccessor } from './DataAccessor';
 
-const { join: joinPath } = posix;
-
 /**
  * DataAccessor that uses the file system to store documents as files and containers as folders.
  */
@@ -298,14 +295,14 @@ export class FileDataAccessor implements DataAccessor {
       }
 
       // Ignore non-file/directory entries in the folder
-      const childStats = await fsPromises.lstat(joinPath(link.filePath, childName));
+      const childStats = await fsPromises.lstat(joinFilePath(link.filePath, childName));
       if (!childStats.isFile() && !childStats.isDirectory()) {
         continue;
       }
 
       // Generate the URI corresponding to the child resource
       const childLink = await this.resourceMapper
-        .mapFilePathToUrl(joinPath(link.filePath, childName), childStats.isDirectory());
+        .mapFilePathToUrl(joinFilePath(link.filePath, childName), childStats.isDirectory());
 
       // Generate metadata of this specific child
       const subject = DataFactory.namedNode(childLink.identifier.path);
diff --git a/src/storage/mapping/ExtensionBasedMapper.ts b/src/storage/mapping/ExtensionBasedMapper.ts
index f2a9982d5..948587666 100644
--- a/src/storage/mapping/ExtensionBasedMapper.ts
+++ b/src/storage/mapping/ExtensionBasedMapper.ts
@@ -1,5 +1,4 @@
 import { promises as fsPromises } from 'fs';
-import { posix } from 'path';
 import * as mime from 'mime-types';
 import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
 import { getLoggerFor } from '../../logging/LogUtil';
@@ -9,13 +8,13 @@ import {
   encodeUriPathComponents,
   ensureTrailingSlash,
   isContainerIdentifier,
+  joinFilePath,
+  normalizeFilePath,
   trimTrailingSlashes,
 } from '../../util/PathUtil';
 import type { FileIdentifierMapper, FileIdentifierMapperFactory, ResourceLink } from './FileIdentifierMapper';
 import { getAbsolutePath, getRelativePath, validateRelativePath } from './MapperUtil';
 
-const { join: joinPath, normalize: normalizePath } = posix;
-
 export interface ResourcePath {
 
   /**
@@ -50,7 +49,7 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
 
   public constructor(base: string, rootFilepath: string, overrideTypes = { acl: TEXT_TURTLE, meta: TEXT_TURTLE }) {
     this.baseRequestURI = trimTrailingSlashes(base);
-    this.rootFilepath = trimTrailingSlashes(normalizePath(rootFilepath));
+    this.rootFilepath = trimTrailingSlashes(normalizeFilePath(rootFilepath));
     this.types = { ...mime.types, ...overrideTypes };
   }
 
@@ -101,7 +100,7 @@ export class ExtensionBasedMapper implements FileIdentifierMapper {
 
       // Matching file found
       if (fileName) {
-        filePath = joinPath(folder, fileName);
+        filePath = joinFilePath(folder, fileName);
       }
 
       this.logger.info(`The path for ${identifier.path} is ${filePath}`);
diff --git a/src/storage/mapping/FixedContentTypeMapper.ts b/src/storage/mapping/FixedContentTypeMapper.ts
index 700c6e799..855c9cdd0 100644
--- a/src/storage/mapping/FixedContentTypeMapper.ts
+++ b/src/storage/mapping/FixedContentTypeMapper.ts
@@ -1,17 +1,16 @@
-import { posix } from 'path';
 import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
 import { getLoggerFor } from '../../logging/LogUtil';
 import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
 import {
   encodeUriPathComponents,
-  ensureTrailingSlash, isContainerIdentifier,
+  ensureTrailingSlash,
+  isContainerIdentifier,
+  normalizeFilePath,
   trimTrailingSlashes,
 } from '../../util/PathUtil';
 import type { FileIdentifierMapper, ResourceLink } from './FileIdentifierMapper';
 import { getAbsolutePath, getRelativePath, validateRelativePath } from './MapperUtil';
 
-const { normalize: normalizePath } = posix;
-
 /**
  * A mapper that always returns a fixed content type for files.
  */
@@ -24,7 +23,7 @@ export class FixedContentTypeMapper implements FileIdentifierMapper {
 
   public constructor(base: string, rootFilepath: string, contentType: string) {
     this.baseRequestURI = trimTrailingSlashes(base);
-    this.rootFilepath = trimTrailingSlashes(normalizePath(rootFilepath));
+    this.rootFilepath = trimTrailingSlashes(normalizeFilePath(rootFilepath));
     this.contentType = contentType;
   }
 
diff --git a/src/storage/mapping/MapperUtil.ts b/src/storage/mapping/MapperUtil.ts
index 22f6b3a03..f68f00520 100644
--- a/src/storage/mapping/MapperUtil.ts
+++ b/src/storage/mapping/MapperUtil.ts
@@ -1,11 +1,8 @@
-import { posix } from 'path';
 import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
 import { getLoggerFor } from '../../logging/LogUtil';
 import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
 import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
-import { decodeUriPathComponents } from '../../util/PathUtil';
-
-const { join: joinPath } = posix;
+import { joinFilePath, decodeUriPathComponents } from '../../util/PathUtil';
 
 const logger = getLoggerFor('MapperUtil');
 
@@ -18,7 +15,7 @@ const logger = getLoggerFor('MapperUtil');
  * @returns Absolute path of the file.
  */
 export const getAbsolutePath = (rootFilepath: string, path: string, identifier = ''): string =>
-  joinPath(rootFilepath, path, identifier);
+  joinFilePath(rootFilepath, path, identifier);
 
 /**
  * Strips the baseRequestURI from the identifier and checks if the stripped base URI matches the store's one.
diff --git a/src/util/PathUtil.ts b/src/util/PathUtil.ts
index 02992a32e..a25966f16 100644
--- a/src/util/PathUtil.ts
+++ b/src/util/PathUtil.ts
@@ -1,5 +1,46 @@
+import platform, { posix } from 'path';
 import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
 
+/**
+ * Changes a potential Windows path into a POSIX path.
+ *
+ * @param path - Path to check (POSIX or Windows).
+ *
+ * @returns The potentially changed path (POSIX).
+ */
+const windowsToPosixPath = (path: string): string => path.replace(/\\+/gu, '/');
+
+/**
+ * Resolves relative segments in the path/
+ *
+ * @param path - Path to check (POSIX or Windows).
+ *
+ * @returns The potentially changed path (POSIX).
+ */
+export const normalizeFilePath = (path: string): string =>
+  posix.normalize(windowsToPosixPath(path));
+
+/**
+ * Adds the paths to the base path.
+ *
+ * @param basePath - The base path (POSIX or Windows).
+ * @param paths - Subpaths to attach (POSIX).
+ *
+ * @returns The potentially changed path (POSIX).
+ */
+export const joinFilePath = (basePath: string, ...paths: string[]): string =>
+  posix.join(windowsToPosixPath(basePath), ...paths);
+
+/**
+ * Converts the path into an OS-dependent path.
+ *
+ * @param path - Path to check (POSIX).
+ *
+ * @returns The potentially changed path (OS-dependent).
+ */
+export const toSystemFilePath = (path: string): string =>
+  platform.normalize(path);
+
 /**
  * Makes sure the input path has exactly 1 slash at the end.
  * Multiple slashes will get merged into one.


Test patch:
diff --git a/test/integration/Config.ts b/test/integration/Config.ts
index 14e036169..c486d1f28 100644
--- a/test/integration/Config.ts
+++ b/test/integration/Config.ts
@@ -1,8 +1,7 @@
 import { mkdirSync } from 'fs';
-import { join } from 'path';
-import * as Path from 'path';
 import { Loader } from 'componentsjs';
 import * as rimraf from 'rimraf';
+import { joinFilePath, toSystemFilePath } from '../../src/util/PathUtil';
 
 export const BASE = 'http://test.com';
 
@@ -12,17 +11,17 @@ export const BASE = 'http://test.com';
 export const instantiateFromConfig = async(componentUrl: string, configFile: string,
   variables?: Record<string, any>): Promise<any> => {
   // Initialize the Components.js loader
-  const mainModulePath = Path.join(__dirname, '../../');
+  const mainModulePath = joinFilePath(__dirname, '../../');
   const loader = new Loader({ mainModulePath });
   await loader.registerAvailableModuleResources();
 
   // Instantiate the component from the config
-  const configPath = Path.join(__dirname, 'config', configFile);
+  const configPath = toSystemFilePath(joinFilePath(__dirname, 'config', configFile));
   return loader.instantiateFromUrl(componentUrl, configPath, undefined, { variables });
 };
 
 export const getTestFolder = (name: string): string =>
-  join(__dirname, '../tmp', name);
+  joinFilePath(__dirname, '../tmp', name);
 
 export const createFolder = (folder: string): void => {
   mkdirSync(folder, { recursive: true });
diff --git a/test/integration/LdpHandlerWithAuth.test.ts b/test/integration/LdpHandlerWithAuth.test.ts
index 7dc57bcc2..2c716753b 100644
--- a/test/integration/LdpHandlerWithAuth.test.ts
+++ b/test/integration/LdpHandlerWithAuth.test.ts
@@ -1,9 +1,9 @@
 import { createReadStream } from 'fs';
-import { join } from 'path';
 import type { HttpHandler, Initializer, ResourceStore } from '../../src/';
-import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
-import { guardStream } from '../../src/util/GuardedStream';
-import { CONTENT_TYPE, LDP } from '../../src/util/Vocabularies';
+import {
+  CONTENT_TYPE, LDP,
+  RepresentationMetadata, guardStream, joinFilePath,
+} from '../../src/';
 import { AclHelper, ResourceHelper } from '../util/TestHelpers';
 import { BASE, getTestFolder, createFolder, removeFolder, instantiateFromConfig } from './Config';
 
@@ -58,7 +58,7 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeUrn, se
     // Write test resource
     await store.setRepresentation({ path: `${BASE}/permanent.txt` }, {
       binary: true,
-      data: guardStream(createReadStream(join(__dirname, '../assets/permanent.txt'))),
+      data: guardStream(createReadStream(joinFilePath(__dirname, '../assets/permanent.txt'))),
       metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/plain' }),
     });
   });
diff --git a/test/integration/PodCreation.test.ts b/test/integration/PodCreation.test.ts
index 68951044f..69461dd92 100644
--- a/test/integration/PodCreation.test.ts
+++ b/test/integration/PodCreation.test.ts
@@ -1,7 +1,7 @@
 import type { Server } from 'http';
-import { join } from 'path';
 import fetch from 'cross-fetch';
 import type { HttpServerFactory } from '../../src/server/HttpServerFactory';
+import { joinFilePath } from '../../src/util/PathUtil';
 import { readableToString } from '../../src/util/StreamUtil';
 import { instantiateFromConfig } from './Config';
 
@@ -17,7 +17,7 @@ describe('A server with a pod handler', (): void => {
       'urn:solid-server:default:ServerFactory', 'server-without-auth.json', {
         'urn:solid-server:default:variable:port': port,
         'urn:solid-server:default:variable:baseUrl': baseUrl,
-        'urn:solid-server:default:variable:podTemplateFolder': join(__dirname, '../assets/templates'),
+        'urn:solid-server:default:variable:podTemplateFolder': joinFilePath(__dirname, '../assets/templates'),
       },
     ) as HttpServerFactory;
     server = factory.startServer(port);
diff --git a/test/unit/init/CliRunner.test.ts b/test/unit/init/CliRunner.test.ts
index c414d94e1..e024d6d54 100644
--- a/test/unit/init/CliRunner.test.ts
+++ b/test/unit/init/CliRunner.test.ts
@@ -1,7 +1,7 @@
-import * as path from 'path';
 import { Loader } from 'componentsjs';
 import { CliRunner } from '../../../src/init/CliRunner';
 import type { Initializer } from '../../../src/init/Initializer';
+import { joinFilePath, toSystemFilePath } from '../../../src/util/PathUtil';
 
 const initializer: jest.Mocked<Initializer> = {
   handleSafe: jest.fn(),
@@ -34,12 +34,12 @@ describe('CliRunner', (): void => {
 
     expect(Loader).toHaveBeenCalledTimes(1);
     expect(Loader).toHaveBeenCalledWith({
-      mainModulePath: path.join(__dirname, '../../../'),
+      mainModulePath: toSystemFilePath(joinFilePath(__dirname, '../../../')),
     });
     expect(loader.instantiateFromUrl).toHaveBeenCalledTimes(1);
     expect(loader.instantiateFromUrl).toHaveBeenCalledWith(
       'urn:solid-server:default:Initializer',
-      path.join(__dirname, '/../../../config/config-default.json'),
+      toSystemFilePath(joinFilePath(__dirname, '/../../../config/config-default.json')),
       undefined,
       {
         variables: {
@@ -48,7 +48,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': path.join(__dirname, '../../../templates'),
+          'urn:solid-server:default:variable:podTemplateFolder': joinFilePath(__dirname, '../../../templates'),
         },
       },
     );
@@ -77,18 +77,18 @@ describe('CliRunner', (): void => {
 
     expect(Loader).toHaveBeenCalledTimes(1);
     expect(Loader).toHaveBeenCalledWith({
-      mainModulePath: '/var/cwd/module/path',
+      mainModulePath: toSystemFilePath('/var/cwd/module/path'),
       scanGlobal: true,
     });
     expect(loader.instantiateFromUrl).toHaveBeenCalledWith(
       'urn:solid-server:default:Initializer',
-      '/var/cwd/myconfig.json',
+      toSystemFilePath('/var/cwd/myconfig.json'),
       undefined,
       {
         variables: {
           'urn:solid-server:default:variable:baseUrl': 'http://pod.example/',
           'urn:solid-server:default:variable:loggingLevel': 'debug',
-          'urn:solid-server:default:variable:podTemplateFolder': 'templates',
+          'urn:solid-server:default:variable:podTemplateFolder': '/var/cwd/templates',
           'urn:solid-server:default:variable:port': 4000,
           'urn:solid-server:default:variable:rootFilePath': '/var/cwd/root',
           'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql',
@@ -116,18 +116,18 @@ describe('CliRunner', (): void => {
 
     expect(Loader).toHaveBeenCalledTimes(1);
     expect(Loader).toHaveBeenCalledWith({
-      mainModulePath: '/var/cwd/module/path',
+      mainModulePath: toSystemFilePath('/var/cwd/module/path'),
       scanGlobal: true,
     });
     expect(loader.instantiateFromUrl).toHaveBeenCalledWith(
       'urn:solid-server:default:Initializer',
-      '/var/cwd/myconfig.json',
+      toSystemFilePath('/var/cwd/myconfig.json'),
       undefined,
       {
         variables: {
           'urn:solid-server:default:variable:baseUrl': 'http://pod.example/',
           'urn:solid-server:default:variable:loggingLevel': 'debug',
-          'urn:solid-server:default:variable:podTemplateFolder': 'templates',
+          'urn:solid-server:default:variable:podTemplateFolder': '/var/cwd/templates',
           'urn:solid-server:default:variable:port': 4000,
           'urn:solid-server:default:variable:rootFilePath': '/var/cwd/root',
           'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql',
diff --git a/test/unit/util/PathUtil.test.ts b/test/unit/util/PathUtil.test.ts
index 27d545ef1..abd2f04fb 100644
--- a/test/unit/util/PathUtil.test.ts
+++ b/test/unit/util/PathUtil.test.ts
@@ -1,11 +1,41 @@
+import { sep } from 'path';
 import {
   decodeUriPathComponents,
   encodeUriPathComponents,
   ensureTrailingSlash,
+  joinFilePath,
+  normalizeFilePath,
   toCanonicalUriPath,
+  toSystemFilePath,
 } from '../../../src/util/PathUtil';
 
 describe('PathUtil', (): void => {
+  describe('normalizeFilePath', (): void => {
+    it('normalizes POSIX paths.', async(): Promise<void> => {
+      expect(normalizeFilePath('/foo/bar/../baz')).toEqual('/foo/baz');
+    });
+
+    it('normalizes Windows paths.', async(): Promise<void> => {
+      expect(normalizeFilePath('c:\\foo\\bar\\..\\baz')).toEqual('c:/foo/baz');
+    });
+  });
+
+  describe('joinFilePath', (): void => {
+    it('joins POSIX paths.', async(): Promise<void> => {
+      expect(joinFilePath('/foo/bar/', '..', '/baz')).toEqual('/foo/baz');
+    });
+
+    it('joins Windows paths.', async(): Promise<void> => {
+      expect(joinFilePath('c:\\foo\\bar\\', '..', '/baz')).toEqual(`c:/foo/baz`);
+    });
+  });
+
+  describe('toSystemFilePath', (): void => {
+    it('converts a POSIX path to an OS-specific path.', async(): Promise<void> => {
+      expect(toSystemFilePath('c:/foo/bar/')).toEqual(`c:${sep}foo${sep}bar${sep}`);
+    });
+  });
+
   describe('#ensureTrailingSlash', (): void => {
     it('makes sure there is always exactly 1 slash.', async(): Promise<void> => {
       expect(ensureTrailingSlash('http://test.com')).toEqual('http://test.com/');
diff --git a/test/util/TestHelpers.ts b/test/util/TestHelpers.ts
index aa34e4812..7f1c5847c 100644
--- a/test/util/TestHelpers.ts
+++ b/test/util/TestHelpers.ts
@@ -1,13 +1,12 @@
 import { EventEmitter } from 'events';
 import { promises as fs } from 'fs';
 import type { IncomingHttpHeaders } from 'http';
-import { join } from 'path';
 import { Readable } from 'stream';
 import * as url from 'url';
 import type { MockResponse } from 'node-mocks-http';
 import { createResponse } from 'node-mocks-http';
 import type { ResourceStore, PermissionSet, HttpHandler, HttpRequest } from '../../src/';
-import { guardedStreamFrom, RepresentationMetadata, ensureTrailingSlash } from '../../src/';
+import { guardedStreamFrom, RepresentationMetadata, joinFilePath, ensureTrailingSlash } from '../../src/';
 import { CONTENT_TYPE } from '../../src/util/Vocabularies';
 import { performRequest } from './Util';
 
@@ -109,7 +108,7 @@ export class ResourceHelper {
   public async createResource(fileLocation: string, slug: string, contentType: string, mayFail = false):
   Promise<MockResponse<any>> {
     const fileData = await fs.readFile(
-      join(__dirname, fileLocation),
+      joinFilePath(__dirname, fileLocation),
     );
 
     const response: MockResponse<any> = await this.performRequestWithBody(
@@ -131,7 +130,7 @@ export class ResourceHelper {
   public async replaceResource(fileLocation: string, requestUrl: string, contentType: string):
   Promise<MockResponse<any>> {
     const fileData = await fs.readFile(
-      join(__dirname, fileLocation),
+      joinFilePath(__dirname, fileLocation),
     );
 
     const putUrl = new URL(requestUrl);