/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {AssetGroupConfig, Manifest} from '../src/manifest'; import {sha1} from '../src/sha1'; import {MockResponse} from './fetch'; export type HeaderMap = { [key: string]: string }; export class MockFile { constructor( readonly path: string, readonly contents: string, readonly headers = {}, readonly hashThisFile: boolean) {} get hash(): string { return sha1(this.contents); } } export class MockFileSystemBuilder { private resources = new Map(); addFile(path: string, contents: string, headers?: HeaderMap): MockFileSystemBuilder { this.resources.set(path, new MockFile(path, contents, headers, true)); return this; } addUnhashedFile(path: string, contents: string, headers?: HeaderMap): MockFileSystemBuilder { this.resources.set(path, new MockFile(path, contents, headers, false)); return this; } build(): MockFileSystem { return new MockFileSystem(this.resources); } } export class MockFileSystem { constructor(private resources: Map) {} lookup(path: string): MockFile|undefined { return this.resources.get(path); } extend(): MockFileSystemBuilder { const builder = new MockFileSystemBuilder(); Array.from(this.resources.keys()).forEach(path => { const res = this.resources.get(path)!; if (res.hashThisFile) { builder.addFile(path, res.contents, res.headers); } else { builder.addUnhashedFile(path, res.contents, res.headers); } }); return builder; } list(): string[] { return Array.from(this.resources.keys()); } } export class MockServerStateBuilder { private resources = new Map(); private errors = new Set(); withStaticFiles(fs: MockFileSystem): MockServerStateBuilder { fs.list().forEach(path => { const file = fs.lookup(path)!; this.resources.set(path, new MockResponse(file.contents, {headers: file.headers})); }); return this; } withManifest(manifest: Manifest): MockServerStateBuilder { this.resources.set('ngsw.json', new MockResponse(JSON.stringify(manifest))); return this; } withRedirect(from: string, to: string, toContents: string): MockServerStateBuilder { this.resources.set(from, new MockResponse(toContents, {redirected: true, url: to})); this.resources.set(to, new MockResponse(toContents)); return this; } withError(url: string): MockServerStateBuilder { this.errors.add(url); return this; } build(): MockServerState { // Take a "snapshot" of the current `resources` and `errors`. const resources = new Map(this.resources.entries()); const errors = new Set(this.errors.values()); return new MockServerState(resources, errors); } } export class MockServerState { private requests: Request[] = []; private gate: Promise = Promise.resolve(); private resolve: Function|null = null; // TODO(issue/24571): remove '!'. private resolveNextRequest!: Function; online = true; nextRequest: Promise; constructor(private resources: Map, private errors: Set) { this.nextRequest = new Promise(resolve => { this.resolveNextRequest = resolve; }); } async fetch(req: Request): Promise { this.resolveNextRequest(req); this.nextRequest = new Promise(resolve => { this.resolveNextRequest = resolve; }); await this.gate; if (!this.online) { throw new Error('Offline.'); } this.requests.push(req); if ((req.credentials === 'include') || (req.mode === 'no-cors')) { return new MockResponse(null, {status: 0, statusText: '', type: 'opaque'}); } const url = req.url.split('?')[0]; if (this.resources.has(url)) { return this.resources.get(url)!.clone(); } if (this.errors.has(url)) { throw new Error('Intentional failure!'); } return new MockResponse(null, {status: 404, statusText: 'Not Found'}); } pause(): void { this.gate = new Promise(resolve => { this.resolve = resolve; }); } unpause(): void { if (this.resolve === null) { return; } this.resolve(); this.resolve = null; } assertSawRequestFor(url: string): void { if (!this.sawRequestFor(url)) { throw new Error(`Expected request for ${url}, got none.`); } } assertNoRequestFor(url: string): void { if (this.sawRequestFor(url)) { throw new Error(`Expected no request for ${url} but saw one.`); } } sawRequestFor(url: string): boolean { const matching = this.requests.filter(req => req.url.split('?')[0] === url); if (matching.length > 0) { this.requests = this.requests.filter(req => req !== matching[0]); return true; } return false; } assertNoOtherRequests(): void { if (!this.noOtherRequests()) { throw new Error(`Expected no other requests, got requests for ${ this.requests.map(req => req.url.split('?')[0]).join(', ')}`); } } noOtherRequests(): boolean { return this.requests.length === 0; } clearRequests(): void { this.requests = []; } reset(): void { this.clearRequests(); this.nextRequest = new Promise(resolve => { this.resolveNextRequest = resolve; }); this.gate = Promise.resolve(); this.resolve = null; this.online = true; } } export function tmpManifestSingleAssetGroup(fs: MockFileSystem): Manifest { const files = fs.list(); const hashTable: {[url: string]: string} = {}; files.forEach(path => { hashTable[path] = fs.lookup(path)!.hash; }); return { configVersion: 1, timestamp: 1234567890123, index: '/index.html', assetGroups: [ { name: 'group', installMode: 'prefetch', updateMode: 'prefetch', urls: files, patterns: [], cacheQueryOptions: {ignoreVary: true} }, ], navigationUrls: [], hashTable, }; } export function tmpHashTableForFs( fs: MockFileSystem, breakHashes: {[url: string]: boolean} = {}): {[url: string]: string} { const table: {[url: string]: string} = {}; fs.list().forEach(path => { const file = fs.lookup(path)!; if (file.hashThisFile) { table[path] = file.hash; if (breakHashes[path]) { table[path] = table[path].split('').reverse().join(''); } } }); return table; } export function tmpHashTable(manifest: Manifest): Map { const map = new Map(); Object.keys(manifest.hashTable).forEach(url => { const hash = manifest.hashTable[url]; map.set(url, hash); }); return map; }