Repository: solid/community-server
Pull request #374 (2020-12-01 14:55:55)

Implement --baseUrl flag


Patch:
diff --git a/bin/server.ts b/bin/server.ts
index d06aab527..9a850be23 100644
--- a/bin/server.ts
+++ b/bin/server.ts
@@ -1,4 +1,3 @@
 #!/usr/bin/env node
-import * as Path from 'path';
 import { runCli } from '../src/init/CliRunner';
-runCli(Path.join(__dirname, '..'));
+runCli();
diff --git a/config/presets/cli-params.json b/config/presets/cli-params.json
index af95638df..260b76994 100644
--- a/config/presets/cli-params.json
+++ b/config/presets/cli-params.json
@@ -6,7 +6,7 @@
       "@type": "Variable"
     },
     {
-      "@id": "urn:solid-server:default:variable:base",
+      "@id": "urn:solid-server:default:variable:baseUrl",
       "@type": "Variable"
     },
     {
diff --git a/config/presets/middleware.json b/config/presets/middleware.json
index 32a989809..564319ad6 100644
--- a/config/presets/middleware.json
+++ b/config/presets/middleware.json
@@ -31,8 +31,8 @@
         },
         {
           "@type": "WebSocketAdvertiser",
-          "WebSocketAdvertiser:_settings_port": {
-            "@id": "urn:solid-server:default:variable:port"
+          "WebSocketAdvertiser:_baseUrl": {
+            "@id": "urn:solid-server:default:variable:baseUrl"
           }
         }
       ]
diff --git a/config/presets/setup.json b/config/presets/setup.json
index 5b849c784..a1d01bb12 100644
--- a/config/presets/setup.json
+++ b/config/presets/setup.json
@@ -17,7 +17,7 @@
         "@id": "urn:solid-server:default:LoggerFactory"
       },
       "Setup:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       },
       "Setup:_port": {
         "@id": "urn:solid-server:default:variable:port"
diff --git a/config/presets/storage/backend/storage-filesystem.json b/config/presets/storage/backend/storage-filesystem.json
index 971d404c3..9e8c7bbb7 100644
--- a/config/presets/storage/backend/storage-filesystem.json
+++ b/config/presets/storage/backend/storage-filesystem.json
@@ -5,7 +5,7 @@
       "@id": "urn:solid-server:default:FileIdentifierMapper",
       "@type": "ExtensionBasedMapper",
       "ExtensionBasedMapper:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       },
       "ExtensionBasedMapper:_rootFilepath": {
         "@id": "urn:solid-server:default:variable:rootFilePath"
@@ -27,7 +27,7 @@
         "@id": "urn:solid-server:default:FileDataAccessor"
       },
       "DataAccessorBasedStore:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       }
     }
   ]
diff --git a/config/presets/storage/backend/storage-memory.json b/config/presets/storage/backend/storage-memory.json
index d5d44d200..2ae772557 100644
--- a/config/presets/storage/backend/storage-memory.json
+++ b/config/presets/storage/backend/storage-memory.json
@@ -5,7 +5,7 @@
       "@id": "urn:solid-server:default:MemoryDataAccessor",
       "@type": "InMemoryDataAccessor",
       "InMemoryDataAccessor:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       }
     },
     {
@@ -15,7 +15,7 @@
         "@id": "urn:solid-server:default:MemoryDataAccessor"
       },
       "DataAccessorBasedStore:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       }
     }
   ]
diff --git a/config/presets/storage/backend/storage-sparql-endpoint.json b/config/presets/storage/backend/storage-sparql-endpoint.json
index a2c9c0e68..0aa123bce 100644
--- a/config/presets/storage/backend/storage-sparql-endpoint.json
+++ b/config/presets/storage/backend/storage-sparql-endpoint.json
@@ -8,7 +8,7 @@
         "@id": "urn:solid-server:default:variable:sparqlEndpoint"
       },
       "SparqlDataAccessor:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       }
     },
 
@@ -19,7 +19,7 @@
         "@id": "urn:solid-server:default:SparqlDataAccessor"
       },
       "DataAccessorBasedStore:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       }
     },
 
diff --git a/config/presets/storage/routing/regex-routing.json b/config/presets/storage/routing/regex-routing.json
index e5f5b9728..32be5c179 100644
--- a/config/presets/storage/routing/regex-routing.json
+++ b/config/presets/storage/routing/regex-routing.json
@@ -5,7 +5,7 @@
       "@id": "urn:solid-server:default:RegexRouterRule",
       "@type": "RegexRouterRule",
       "RegexRouterRule:_base": {
-        "@id": "urn:solid-server:default:variable:base"
+        "@id": "urn:solid-server:default:variable:baseUrl"
       }
     },
     {
diff --git a/src/init/CliRunner.ts b/src/init/CliRunner.ts
index d7222ba34..bd6fa4023 100644
--- a/src/init/CliRunner.ts
+++ b/src/init/CliRunner.ts
@@ -1,9 +1,10 @@
-import * as Path from 'path';
+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 type { Setup } from './Setup';
 
 const logger = getLoggerFor('CliRunner');
@@ -11,34 +12,39 @@ const logger = getLoggerFor('CliRunner');
 /**
  * Generic run function for starting the server from a given config
  * @param args - Command line arguments.
- * @param stdin - Standard input stream.
- * @param stdout - Standard output stream.
  * @param stderr - Standard error stream.
  * @param properties - Components loader properties.
  */
-export const runCustom = function(
-  args: string[],
-  stdin: ReadStream,
-  stdout: WriteStream,
-  stderr: WriteStream,
-  properties: LoaderProperties,
-): void {
-  const { argv } = yargs
+export const runCli = function({
+  argv = process.argv,
+  stderr = process.stderr,
+  properties = {
+    mainModulePath: path.join(__dirname, '../../'),
+  },
+}: {
+  argv?: string[];
+  stdin?: ReadStream;
+  stdout?: WriteStream;
+  stderr?: WriteStream;
+  properties?: LoaderProperties;
+} = {}): void {
+  const { argv: params } = yargs(argv.slice(2))
     .usage('node ./bin/server.js [args]')
     .options({
-      port: { type: 'number', alias: 'p', default: 3000 },
+      baseUrl: { type: 'string', alias: 'b' },
       config: { type: 'string', alias: 'c' },
+      loggingLevel: { type: 'string', alias: 'l', default: 'info' },
+      port: { type: 'number', alias: 'p', default: 3000 },
       rootFilePath: { type: 'string', alias: 'f' },
       sparqlEndpoint: { type: 'string', alias: 's' },
-      level: { type: 'string', alias: 'l', default: 'info' },
     })
     .help();
 
   (async(): Promise<string> => {
     // Load provided or default config file
-    const configPath = argv.config ?
-      Path.join(process.cwd(), argv.config) :
-      `${__dirname}/../../config/config-default.json`;
+    const configPath = params.config ?
+      path.join(process.cwd(), params.config) :
+      path.join(__dirname, '/../../config/config-default.json');
 
     // Setup from config file
     const loader = new Loader(properties);
@@ -46,27 +52,19 @@ export const runCustom = function(
     const setup: Setup = await loader
       .instantiateFromUrl('urn:solid-server:default', configPath, undefined, {
         variables: {
-          'urn:solid-server:default:variable:port': argv.port,
-          'urn:solid-server:default:variable:base': `http://localhost:${argv.port}/`,
-          'urn:solid-server:default:variable:rootFilePath': argv.rootFilePath ?? process.cwd(),
-          'urn:solid-server:default:variable:sparqlEndpoint': argv.sparqlEndpoint,
-          'urn:solid-server:default:variable:loggingLevel': argv.level,
+          'urn:solid-server:default:variable:baseUrl':
+            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': params.rootFilePath ?? process.cwd(),
+          'urn:solid-server:default:variable:sparqlEndpoint': params.sparqlEndpoint,
         },
       }) as Setup;
     return await setup.setup();
-  })().then((base: string): void => {
-    logger.info(`Running at ${base}`);
+  })().then((baseUrl: string): void => {
+    logger.info(`Running at ${baseUrl}`);
   }).catch((error): void => {
     // This is the only time we can *not* use the logger to print error messages, as dependency injection has failed.
     stderr.write(`${error}\n`);
   });
 };
-
-/**
- * Run function for starting the server from the command line
- * @param moduleRootPath - Path to the module's root.
- */
-export const runCli = function(moduleRootPath: string): void {
-  const argv = process.argv.slice(2);
-  runCustom(argv, process.stdin, process.stdout, process.stderr, { mainModulePath: moduleRootPath });
-};
diff --git a/src/server/middleware/WebSocketAdvertiser.ts b/src/server/middleware/WebSocketAdvertiser.ts
index bb121175c..c1fa4e1fb 100644
--- a/src/server/middleware/WebSocketAdvertiser.ts
+++ b/src/server/middleware/WebSocketAdvertiser.ts
@@ -2,26 +2,16 @@ import { addHeader } from '../../util/HeaderUtil';
 import { HttpHandler } from '../HttpHandler';
 import type { HttpResponse } from '../HttpResponse';
 
-interface WebSocketSettings {
-  hostname?: string;
-  port?: number;
-  protocol?: string;
-}
-
 /**
  * Handler that advertises a WebSocket through the Updates-Via header.
  */
 export class WebSocketAdvertiser extends HttpHandler {
   private readonly socketUrl: string;
 
-  public constructor(settings: WebSocketSettings = {}) {
+  public constructor(baseUrl: string) {
     super();
-    const { hostname = 'localhost', port = 80, protocol = 'ws:' } = settings;
-    const secure = /^(?:https|wss)/u.test(protocol);
-    const socketUrl = new URL(`${secure ? 'wss' : 'ws'}://${hostname}:${port}/`);
-    if (socketUrl.hostname !== hostname) {
-      throw new Error(`Invalid hostname: ${hostname}`);
-    }
+    const socketUrl = new URL(baseUrl);
+    socketUrl.protocol = /^(?:http|ws):/u.test(baseUrl) ? 'ws:' : 'wss:';
     this.socketUrl = socketUrl.href;
   }
 


Test patch:
diff --git a/test/integration/Middleware.test.ts b/test/integration/Middleware.test.ts
index 8d0710bb0..7e2b1888d 100644
--- a/test/integration/Middleware.test.ts
+++ b/test/integration/Middleware.test.ts
@@ -23,6 +23,7 @@ describe('An Express server with middleware', (): void => {
       'urn:solid-server:default:ExpressHttpServerFactory', 'middleware.json', {
         'urn:solid-server:default:LdpHandler': new SimpleHttpHandler(),
         'urn:solid-server:default:variable:port': port,
+        'urn:solid-server:default:variable:baseUrl': 'https://example.pod/',
       },
     ) as ExpressHttpServerFactory;
     server = factory.startServer(port);
diff --git a/test/integration/WebSocketsProtocol.test.ts b/test/integration/WebSocketsProtocol.test.ts
index d3d64ef1b..c679df58c 100644
--- a/test/integration/WebSocketsProtocol.test.ts
+++ b/test/integration/WebSocketsProtocol.test.ts
@@ -5,7 +5,7 @@ import type { HttpServerFactory } from '../../src/server/HttpServerFactory';
 import { instantiateFromConfig } from '../configs/Util';
 
 const port = 6001;
-const baseUrl = `http://localhost:${port}/`;
+const serverUrl = `http://localhost:${port}/`;
 
 describe('A server with the Solid WebSockets API', (): void => {
   let server: Server;
@@ -14,7 +14,7 @@ describe('A server with the Solid WebSockets API', (): void => {
     const factory = await instantiateFromConfig(
       'urn:solid-server:default:ServerFactory', 'websockets.json', {
         'urn:solid-server:default:variable:port': port,
-        'urn:solid-server:default:variable:base': baseUrl,
+        'urn:solid-server:default:variable:baseUrl': 'http://example.pod/',
       },
     ) as HttpServerFactory;
     server = factory.startServer(port);
@@ -27,17 +27,17 @@ describe('A server with the Solid WebSockets API', (): void => {
   });
 
   it('returns a 200.', async(): Promise<void> => {
-    const response = await fetch(baseUrl);
+    const response = await fetch(serverUrl, { headers: { host: 'example.pod' }});
     expect(response.status).toBe(200);
   });
 
   it('sets the Updates-Via header.', async(): Promise<void> => {
-    const response = await fetch(baseUrl);
-    expect(response.headers.get('Updates-Via')).toBe(`ws://localhost:${port}/`);
+    const response = await fetch(serverUrl, { headers: { host: 'example.pod' }});
+    expect(response.headers.get('Updates-Via')).toBe('ws://example.pod/');
   });
 
   it('exposes the Updates-Via header via CORS.', async(): Promise<void> => {
-    const response = await fetch(baseUrl);
+    const response = await fetch(serverUrl, { headers: { host: 'example.pod' }});
     expect(response.headers.get('Access-Control-Expose-Headers')!.split(','))
       .toContain('Updates-Via');
   });
@@ -47,7 +47,7 @@ describe('A server with the Solid WebSockets API', (): void => {
     const messages = new Array<string>();
 
     beforeAll(async(): Promise<void> => {
-      client = new WebSocket(`ws://localhost:${port}`, [ 'solid/0.1.0-alpha' ]);
+      client = new WebSocket(`ws://localhost:${port}`, [ 'solid/0.1.0-alpha' ], { headers: { host: 'example.pod' }});
       client.on('message', (message: string): any => messages.push(message));
       await new Promise((resolve): any => client.on('open', resolve));
     });
@@ -69,21 +69,24 @@ describe('A server with the Solid WebSockets API', (): void => {
 
     describe('when the client subscribes to a resource', (): void => {
       beforeAll(async(): Promise<void> => {
-        client.send(`sub ${baseUrl}my-resource`);
+        client.send(`sub http://example.pod/my-resource`);
         await new Promise((resolve): any => client.once('message', resolve));
       });
 
       it('acknowledges the subscription.', async(): Promise<void> => {
-        expect(messages).toEqual([ `ack ${baseUrl}my-resource` ]);
+        expect(messages).toEqual([ `ack http://example.pod/my-resource` ]);
       });
 
       it('notifies the client of resource updates.', async(): Promise<void> => {
-        await fetch(`${baseUrl}my-resource`, {
+        await fetch(`${serverUrl}my-resource`, {
           method: 'PUT',
-          headers: { 'content-type': 'application/json' },
+          headers: {
+            host: 'example.pod',
+            'content-type': 'application/json',
+          },
           body: '{}',
         });
-        expect(messages).toEqual([ `pub ${baseUrl}my-resource` ]);
+        expect(messages).toEqual([ `pub http://example.pod/my-resource` ]);
       });
     });
   });
diff --git a/test/unit/init/CliRunner.test.ts b/test/unit/init/CliRunner.test.ts
index a4cbb118e..9a8376d08 100644
--- a/test/unit/init/CliRunner.test.ts
+++ b/test/unit/init/CliRunner.test.ts
@@ -1,98 +1,126 @@
-import * as Path from 'path';
-import type { Loader } from 'componentsjs';
+import * as path from 'path';
+import { Loader } from 'componentsjs';
 import { runCli } from '../../../src/init/CliRunner';
 import type { Setup } from '../../../src/init/Setup';
 
-let calledInstantiateFromUrl: boolean;
-let calledRegisterAvailableModuleResources: boolean;
-let throwError: boolean;
-let outsideResolve: () => void;
-let functionToResolve: Promise<unknown>;
+const mainModulePath = path.join(__dirname, '../../../');
 
-const mockSetup = {
-  setup: jest.fn(),
-} as unknown as jest.Mocked<Setup>;
+const mockSetup: jest.Mocked<Setup> = {
+  setup: jest.fn(async(): Promise<any> => null),
+} as any;
+const loader: jest.Mocked<Loader> = {
+  instantiateFromUrl: jest.fn(async(): Promise<any> => mockSetup),
+  registerAvailableModuleResources: jest.fn(async(): Promise<any> => mockSetup),
+} as any;
 
 // Mock the Loader class.
-jest.mock('componentsjs', (): any => (
-  // eslint-disable-next-line @typescript-eslint/naming-convention, object-shorthand
-  { Loader: function(): Loader {
-    return {
-      instantiateFromUrl(): any {
-        calledInstantiateFromUrl = true;
-        if (throwError) {
-          throw new Error('Error! :o');
-        }
-        return mockSetup;
-      },
-      registerAvailableModuleResources(): any {
-        calledRegisterAvailableModuleResources = true;
-      },
-    } as unknown as Loader;
-  } }
-));
-
-jest.mock('yargs', (): any => ({
-  usage(): any {
-    return this;
-  },
-  options(): any {
-    return this;
-  },
-  help(): any {
-    // Return once with and once without values so that both branches are tested.
-    if (throwError) {
-      return {
-        argv: { config: 'value', rootFilePath: 'root' },
-      };
-    }
-    return {
-      argv: { },
-    };
-  },
+jest.mock('componentsjs', (): any => ({
+  // eslint-disable-next-line @typescript-eslint/naming-convention
+  Loader: jest.fn((): Loader => loader),
 }));
 
 describe('CliRunner', (): void => {
-  beforeAll(async(): Promise<void> => {
-    mockSetup.setup.mockImplementation(async(): Promise<any> => {
-      // The info method will be called when all other code has been executed, so end the waiting function.
-      outsideResolve();
-    });
+  afterEach((): void => {
+    jest.clearAllMocks();
   });
 
-  beforeEach(async(): Promise<void> => {
-    calledInstantiateFromUrl = false;
-    calledRegisterAvailableModuleResources = false;
-    throwError = false;
-
-    // Initialize a function that will be resolved as soon as all necessary but asynchronous calls are completed.
-    functionToResolve = new Promise((resolve): any => {
-      outsideResolve = resolve;
+  it('starts the server with default settings.', async(): Promise<void> => {
+    runCli({
+      argv: [ 'node', 'script' ],
     });
-  });
+    await mockSetup.setup();
 
-  it('Runs function for starting the server from the command line.', async(): Promise<void> => {
-    runCli(Path.join(__dirname, '..'));
+    expect(Loader).toHaveBeenCalledTimes(1);
+    expect(Loader).toHaveBeenCalledWith({ mainModulePath });
+    expect(loader.instantiateFromUrl).toHaveBeenCalledTimes(1);
+    expect(loader.instantiateFromUrl).toHaveBeenCalledWith(
+      'urn:solid-server:default',
+      path.join(__dirname, '/../../../config/config-default.json'),
+      undefined,
+      {
+        variables: {
+          'urn:solid-server:default:variable:port': 3000,
+          'urn:solid-server:default:variable:baseUrl': 'http://localhost:3000/',
+          'urn:solid-server:default:variable:rootFilePath': process.cwd(),
+          'urn:solid-server:default:variable:sparqlEndpoint': undefined,
+          'urn:solid-server:default:variable:loggingLevel': 'info',
+        },
+      },
+    );
+    expect(loader.registerAvailableModuleResources).toHaveBeenCalledTimes(1);
+    expect(loader.registerAvailableModuleResources).toHaveBeenCalledWith();
+    expect(mockSetup.setup).toHaveBeenCalledTimes(1);
+    expect(mockSetup.setup).toHaveBeenCalledWith();
+  });
 
-    await functionToResolve;
+  it('accepts abbreviated flags.', async(): Promise<void> => {
+    runCli({
+      argv: [
+        'node', 'script',
+        '-p', '4000',
+        '-b', 'http://pod.example/',
+        '-c', 'myconfig.json',
+        '-f', '/root',
+        '-s', 'http://localhost:5000/sparql',
+        '-l', 'debug',
+      ],
+    });
+    await mockSetup.setup();
 
-    expect(calledInstantiateFromUrl).toBeTruthy();
-    expect(calledRegisterAvailableModuleResources).toBeTruthy();
-    expect(mockSetup.setup).toHaveBeenCalledTimes(1);
+    expect(loader.instantiateFromUrl).toHaveBeenCalledWith(
+      'urn:solid-server:default',
+      path.join(process.cwd(), 'myconfig.json'),
+      undefined,
+      {
+        variables: {
+          'urn:solid-server:default:variable:port': 4000,
+          'urn:solid-server:default:variable:baseUrl': 'http://pod.example/',
+          'urn:solid-server:default:variable:rootFilePath': '/root',
+          'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql',
+          'urn:solid-server:default:variable:loggingLevel': 'debug',
+        },
+      },
+    );
   });
 
-  it('Writes to stderr when an exception occurs.', async(): Promise<void> => {
-    const mockStderr = jest.spyOn(process.stderr, 'write').mockImplementation((): any => {
-      // This method will be called when an error has occurred, so end the waiting function.
-      outsideResolve();
+  it('accepts full flags.', async(): Promise<void> => {
+    runCli({
+      argv: [
+        'node', 'script',
+        '--port', '4000',
+        '--baseUrl', 'http://pod.example/',
+        '--config', 'myconfig.json',
+        '--rootFilePath', '/root',
+        '--sparqlEndpoint', 'http://localhost:5000/sparql',
+        '--loggingLevel', 'debug',
+      ],
     });
-    throwError = true;
+    await mockSetup.setup();
+
+    expect(loader.instantiateFromUrl).toHaveBeenCalledWith(
+      'urn:solid-server:default',
+      path.join(process.cwd(), 'myconfig.json'),
+      undefined,
+      {
+        variables: {
+          'urn:solid-server:default:variable:port': 4000,
+          'urn:solid-server:default:variable:baseUrl': 'http://pod.example/',
+          'urn:solid-server:default:variable:rootFilePath': '/root',
+          'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql',
+          'urn:solid-server:default:variable:loggingLevel': 'debug',
+        },
+      },
+    );
+  });
 
-    runCli(Path.join(__dirname, '..'));
+  it('writes to stderr when an error occurs.', async(): Promise<void> => {
+    jest.spyOn(process.stderr, 'write');
+    loader.instantiateFromUrl.mockRejectedValueOnce(new Error('Fatal'));
 
-    await functionToResolve;
+    runCli();
+    await new Promise((resolve): any => setImmediate(resolve));
 
-    expect(mockStderr).toHaveBeenCalledTimes(1);
-    mockStderr.mockRestore();
+    expect(process.stderr.write).toHaveBeenCalledTimes(1);
+    expect(process.stderr.write).toHaveBeenCalledWith('Error: Fatal\n');
   });
 });
diff --git a/test/unit/server/middleware/WebSocketAdvertiser.test.ts b/test/unit/server/middleware/WebSocketAdvertiser.test.ts
index 5421c5346..37695f968 100644
--- a/test/unit/server/middleware/WebSocketAdvertiser.test.ts
+++ b/test/unit/server/middleware/WebSocketAdvertiser.test.ts
@@ -2,36 +2,31 @@ import { createResponse } from 'node-mocks-http';
 import { WebSocketAdvertiser } from '../../../../src/server/middleware/WebSocketAdvertiser';
 
 describe('A WebSocketAdvertiser', (): void => {
-  it('writes a default HTTP WebSocket.', async(): Promise<void> => {
-    const writer = new WebSocketAdvertiser();
+  it('writes a ws: socket when given an http: URL.', async(): Promise<void> => {
+    const writer = new WebSocketAdvertiser('http://test.example/');
     const response = createResponse();
     await writer.handle({ response } as any);
-    expect(response.getHeaders()).toEqual({ 'updates-via': 'ws://localhost/' });
+    expect(response.getHeaders()).toEqual({ 'updates-via': 'ws://test.example/' });
   });
 
-  it('writes an HTTP WebSocket with port 80.', async(): Promise<void> => {
-    const writer = new WebSocketAdvertiser({ hostname: 'test.example', port: 80, protocol: 'http' });
+  it('writes a ws: socket when given a ws: URL.', async(): Promise<void> => {
+    const writer = new WebSocketAdvertiser('ws://test.example/');
     const response = createResponse();
     await writer.handle({ response } as any);
     expect(response.getHeaders()).toEqual({ 'updates-via': 'ws://test.example/' });
   });
 
-  it('writes an HTTP WebSocket with port 3000.', async(): Promise<void> => {
-    const writer = new WebSocketAdvertiser({ hostname: 'test.example', port: 3000, protocol: 'http' });
+  it('writes a wss: socket when given an https: URL.', async(): Promise<void> => {
+    const writer = new WebSocketAdvertiser('https://test.example/');
     const response = createResponse();
     await writer.handle({ response } as any);
-    expect(response.getHeaders()).toEqual({ 'updates-via': 'ws://test.example:3000/' });
+    expect(response.getHeaders()).toEqual({ 'updates-via': 'wss://test.example/' });
   });
 
-  it('writes an HTTPS WebSocket with port 443.', async(): Promise<void> => {
-    const writer = new WebSocketAdvertiser({ hostname: 'test.example', port: 443, protocol: 'https' });
+  it('writes a wss: socket when given a wss: URL.', async(): Promise<void> => {
+    const writer = new WebSocketAdvertiser('wss://test.example/');
     const response = createResponse();
     await writer.handle({ response } as any);
     expect(response.getHeaders()).toEqual({ 'updates-via': 'wss://test.example/' });
   });
-
-  it('rejects an invalid hostname.', (): void => {
-    expect((): any => new WebSocketAdvertiser({ hostname: 'test.example/invalid' }))
-      .toThrow('Invalid hostname: test.example/invalid');
-  });
 });