-
Notifications
You must be signed in to change notification settings - Fork 24.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(aio): update redirects to fix unwanted 404s #21712
Changes from all commits
a45e18d
ce528db
0851f12
8bce587
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,50 +12,93 @@ | |
// make sure the routing RegExp in `ngsw-manifest.json` is updated accordingly. | ||
////////////////////////////////////////////////////////////////////////////////////////////// | ||
|
||
// cli-quickstart.html, glossary.html, quickstart.html, server-communication.html, style-guide.html | ||
{"type": 301, "source": "/docs/ts/latest/cli-quickstart.html", "destination": "/guide/quickstart"}, | ||
{"type": 301, "source": "/docs/ts/latest/glossary.html", "destination": "/guide/glossary"}, | ||
{"type": 301, "source": "/docs/ts/latest/quickstart.html", "destination": "/guide/quickstart"}, | ||
{"type": 301, "source": "/docs/ts/latest/guide/server-communication.html", "destination": "/guide/http"}, | ||
{"type": 301, "source": "/docs/ts/latest/guide/style-guide.html", "destination": "/guide/styleguide"}, | ||
// A random bad indexed page that used `api/api` | ||
{"type": 301, "source": "/api/api/:rest*", "destination": "api/:rest"}, | ||
|
||
// guide/cli-quickstart, styleguide | ||
// Guide renames | ||
{"type": 301, "source": "/docs/*/latest/cli-quickstart.html", "destination": "/guide/quickstart"}, | ||
{"type": 301, "source": "/docs/*/latest/glossary.html", "destination": "/guide/glossary"}, | ||
{"type": 301, "source": "/docs/*/latest/quickstart.html", "destination": "/guide/quickstart"}, | ||
{"type": 301, "source": "/docs/*/latest/guide/server-communication.html", "destination": "/guide/http"}, | ||
{"type": 301, "source": "/docs/*/latest/guide/style-guide.html", "destination": "/guide/styleguide"}, | ||
{"type": 301, "source": "/guide/cli-quickstart", "destination": "/guide/quickstart"}, | ||
{"type": 301, "source": "/styleguide", "destination": "/guide/styleguide"}, | ||
{"type": 301, "source": "/guide/service-worker-getstart", "destination": "/guide/service-worker-getting-started"}, | ||
{"type": 301, "source": "/guide/service-worker-comm", "destination": "/guide/service-worker-communications"}, | ||
{"type": 301, "source": "/guide/service-worker-configref", "destination": "/guide/service-worker-config"}, | ||
|
||
// cookbook/a1-a2-quick-reference.html, cookbook/component-communication.html, cookbook/dependency-injection.html | ||
{"type": 301, "source": "/docs/ts/latest/cookbook/a1-a2-quick-reference.html", "destination": "/guide/ajs-quick-reference"}, | ||
{"type": 301, "source": "/docs/ts/latest/cookbook/component-communication.html", "destination": "/guide/component-interaction"}, | ||
{"type": 301, "source": "/docs/ts/latest/cookbook/dependency-injection.html", "destination": "/guide/dependency-injection-in-action"}, | ||
// some top level guide pages on old site were moved below the guide folder | ||
{"type": 301, "source": "/styleguide", "destination": "/guide/styleguide"}, | ||
{"type": 301, "source": "/docs/styleguide", "destination": "/guide/styleguide"}, | ||
|
||
// cookbook, cookbook/, cookbook/index.html | ||
{"type": 301, "source": "/docs/ts/latest/cookbook", "destination": "/docs"}, | ||
{"type": 301, "source": "/docs/ts/latest/cookbook/", "destination": "/docs"}, | ||
{"type": 301, "source": "/docs/ts/latest/cookbook/index.html", "destination": "/docs"}, | ||
// news is now blog | ||
{"type": 301, "source": "/news*", "destination": "https://blog.angular.io/"}, | ||
|
||
// cookbook/*.html | ||
{"type": 301, "source": "/docs/ts/latest/cookbook/:cookbook.html", "destination": "/guide/:cookbook"}, | ||
// cookbook guides were moved (and sometime renamed or removed) | ||
{"type": 301, "source": "/docs/*/latest/cookbook", "destination": "/docs"}, | ||
{"type": 301, "source": "/docs/*/latest/cookbook/", "destination": "/docs"}, | ||
{"type": 301, "source": "/docs/*/latest/cookbook/index.html", "destination": "/docs"}, | ||
{"type": 301, "source": "/**/cookbook/ts-to-js*", "destination": "https://github.com/angular/angular/blob/master/aio/content/guide/change-log.md#es6--described-in-typescript-to-javascript-2016-11-14"}, | ||
{"type": 301, "source": "/docs/*/latest/cookbook/a1-a2-quick-reference.html", "destination": "/guide/ajs-quick-reference"}, | ||
{"type": 301, "source": "/docs/*/latest/cookbook/component-communication.html", "destination": "/guide/component-interaction"}, | ||
{"type": 301, "source": "/docs/*/latest/cookbook/dependency-injection.html", "destination": "/guide/dependency-injection-in-action"}, | ||
{"type": 301, "source": "/docs/*/latest/cookbook/:cookbook.html", "destination": "/guide/:cookbook"}, | ||
|
||
// docs/ts/latest/api/<package>/index/*-<type>.html (+ special case for `NgFor` which has been renamed) | ||
{"type": 301, "source": "/docs/ts/latest/api/common/index/NgFor-directive.html", "destination": "/api/common/NgForOf"}, | ||
{"type": 301, "source": "/docs/ts/latest/api/:package/index/:api-*.html", "destination": "/api/:package/:api"}, | ||
// Forms related code was moved from the `common` to `forms` package (and NgFor was renamed to NgForOf) | ||
{"type": 301, "source": "/**/NgFor-*", "destination": "/api/common/NgForOf"}, | ||
{"type": 301, "source": "/**/api/common/index/MaxLengthValidator-*", "destination": "/api/forms/MaxLengthValidator"}, | ||
{"type": 301, "source": "/**/api/common/ControlGroup*", "destination": "/api/forms/FormGroup"}, | ||
{"type": 301, "source": "/**/api/common/Control*", "destination": "/api/forms/FormControl"}, | ||
{"type": 301, "source": "/**/api/common/SelectControlValueAccessor-*", "destination": "/api/forms/SelectControlValueAccessor"}, | ||
{"type": 301, "source": "/**/api/common/NgModel", "destination": "/api/forms/NgModel"}, | ||
|
||
// docs/ts/latest | ||
{"type": 301, "source": "/docs/ts/latest", "destination": "/docs"}, | ||
// AnimationStateDeclarationMetadata was removed | ||
{"type": 301, "source": "/**/AnimationStateDeclarationMetadata*", "destination": "/api/animation"}, | ||
// `AnimationDriver` was moved to the `animations/browser` package | ||
{"type": 301, "source": "/api/platform-browser/AnimationDriver", "destination": "/api/animations/browser/AnimationDriver"}, | ||
|
||
// guide/*, tutorial/*, **/* | ||
{"type": 301, "source": "/docs/ts/latest/:any*", "destination": "/:any*"}, | ||
// The `testing` package was renamed to `core/testing` | ||
{"type": 301, "source": "/api/testing/:api-*", "destination": "/api/core/testing/:api"}, | ||
|
||
// aot-compiler.md and metadata.md combined into aot-compiler.md - issue #19510 | ||
{"type": 301, "source": "/guide/metadata", "destination": "/guide/aot-compiler"}, | ||
// CORE_DIRECTIVES & PLATFORM_PIPES were removed and are now in the CommonModule | ||
{"type": 301, "source": "/**/CORE_DIRECTIVES*", "destination": "/api/common/CommonModule"}, | ||
{"type": 301, "source": "/**/PLATFORM_PIPES*", "destination": "/api/common"}, | ||
|
||
// ngmodule.md renamed to ngmodules.md | ||
{"type": 301, "source": "/guide/ngmodule", "destination": "/guide/ngmodules"}, | ||
// DirectiveMetadata is now covered by the Directive decorator | ||
{"type": 301, "source": "/**/DirectiveMetadata-*", "destination": "/api/core/Directive"}, | ||
|
||
// service-worker-getstart.md, service-worker-comm.md, service-worker-configref.md | ||
{"type": 301, "source": "/guide/service-worker-getstart", "destination": "/guide/service-worker-getting-started"}, | ||
{"type": 301, "source": "/guide/service-worker-comm", "destination": "/guide/service-worker-communications"}, | ||
{"type": 301, "source": "/guide/service-worker-configref", "destination": "/guide/service-worker-config"} | ||
// HTTP_PROVIDERS was removed and is now provided in HttpModule | ||
{"type": 301, "source": "/**/HTTP_PROVIDERS*", "destination": "/api/http/HttpModule"}, | ||
|
||
// URLs that use the old scheme of adding the type to the end (e.g. `SomeClass-class`) | ||
{"type": 301, "source": "/api/:package/:api-*", "destination": "/api/:package/:api"}, | ||
{"type": 301, "source": "/api/:package/testing/index/:api-*", "destination": "/api/:package/testing/:api"}, | ||
{"type": 301, "source": "/api/:package/testing/:api-*", "destination": "/api/:package/testing/:api"}, | ||
{"type": 301, "source": "/api/upgrade/:package/index/:api-*", "destination": "/api/upgrade/:package/:api"}, | ||
{"type": 301, "source": "/api/upgrade/:package/:api-*", "destination": "/api/upgrade/:package/:api"}, | ||
|
||
// URLs that use the old scheme before we moved the docs to the angular/angular repo | ||
{"type": 301, "source": "/docs/*/latest", "destination": "/docs"}, | ||
{"type": 301, "source": "/docs/*/latest/api/:package", "destination": "/api/:package"}, | ||
{"type": 301, "source": "/docs/*/latest/api/:package/:api-*", "destination": "/api/:package/:api"}, | ||
{"type": 301, "source": "/docs/*/latest/api/:package/index/:api-*", "destination": "/api/:package/:api"}, | ||
{"type": 301, "source": "/docs/*/latest/api/:package/testing", "destination": "/api/:package/testing"}, | ||
{"type": 301, "source": "/docs/*/latest/api/:package/testing/index/:api-*", "destination": "/api/:package/testing/:api"}, | ||
{"type": 301, "source": "/docs/*/latest/api/platform-browser/animations/index/:api-*", "destination": "/api/platform-browser/animations/:api"}, | ||
{"type": 301, "source": "/docs/*/latest/api/testing/:api-*", "destination": "/api/core/testing/:api"}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAICT, URL matching this pattern will also match There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think since it will then do another round of redirect it will end up at the correct place. E.g.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't know redirects are applied recursively 👍 |
||
{"type": 301, "source": "/docs/*/latest/api/upgrade/:package/:api-*", "destination": "/api/upgrade/:package/:api"}, | ||
{"type": 301, "source": "/docs/*/latest/api/upgrade/:package/index/:api-*", "destination": "/api/upgrade/:package/:api"}, | ||
{"type": 301, "source": "/docs/*/latest/api", "destination": "/api"}, | ||
{"type": 301, "source": "/docs/*/latest/glossary", "destination": "/guide/glossary"}, | ||
{"type": 301, "source": "/docs/*/latest/guide/", "destination": "/guide"}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where is this supposed to go. I don't think we have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will 404 at the moment - we should have a placeholder for that folder really. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not redirect to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought it would motivate us to fix it |
||
{"type": 301, "source": "/docs/*/latest/guide/lifecycle-hooks", "destination": "/guide/lifecycle-hooks"}, | ||
{"type": 301, "source": "/docs/*/latest/:any*", "destination": "/:any*"}, | ||
{"type": 301, "source": "/docs/latest/:any*", "destination": "/:any*"}, | ||
{"type": 301, "source": "/docs/styleguide*", "destination": "/guide/styleguide"}, | ||
{"type": 301, "source": "/guide/metadata", "destination": "/guide/aot-compiler"}, | ||
{"type": 301, "source": "/guide/ngmodule", "destination": "/guide/ngmodules"}, | ||
{"type": 301, "source": "/guide/learning-angular*", "destination": "/guide/quickstart"}, | ||
{"type": 301, "source": "/testing", "destination": "/guide/testing"}, | ||
{"type": 301, "source": "/testing/**", "destination": "/guide/testing"} | ||
], | ||
"rewrites": [ | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,7 @@ | |
"routing": { | ||
"index": "/index.html", | ||
"routes": { | ||
"^(?!/docs/ts/latest|/guide/(?:cli-quickstart|metadata|ngmodule|service-worker-(?:getstart|comm|configref))/?$|/styleguide).*/(?!e?plnkr)[^/.]*$": { | ||
"^(?!/docs/.|(?:/guide/(?:cli-quickstart|metadata|ngmodule|service-worker-(?:getstart|comm|configref)|learning-angular)|/news/?)$|/testing|/api/(?:common/NgModel|platform-browser/AnimationDriver|testing|api)).*/(?!e?plnkr|(?:NgFor|MaxLengthValidator)-|Control(?:Group)?|AnimationStateDeclarationMetadata|CORE_DIRECTIVES|PLATFORM_PIPES|DirectiveMetadata|HTTP_PROVIDERS)[^/.]*$": { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The -|/news/?)$
+|/news)/?$ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is because of this PR. Before that, the optional trailing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But this PR has not landed on 5.2 yet. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apparently, some of it did 😁 |
||
"match": "regex" | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -92,6 +92,9 @@ fi | |
# Include any mode-specific files | ||
cp -rf src/extra-files/$deployEnv/. dist/ | ||
|
||
# Check that the redirects are setup correctly | ||
yarn redirects-test | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be better to test this in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was just thinking about that on the way home There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This actually deploys to firebase, so it would not prevent PRs from getting previews, it would prevent deployments to master and stable. The real problem is that we need to run these tests in PRs that are not being deployed to Firebase. |
||
|
||
# Check payload size | ||
yarn payload-size | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { FirebaseGlob } from './FirebaseGlob'; | ||
|
||
describe('FirebaseGlob', () => { | ||
|
||
describe('test', () => { | ||
it('should match * parts', () => { | ||
testGlob('asdf/*.jpg', | ||
['asdf/asdf.jpg', 'asdf/asdf_asdf.jpg', 'asdf/.jpg'], | ||
['asdf/asdf/asdf.jpg', 'xxxasdf/asdf.jpgxxx']); | ||
}); | ||
|
||
it('should match ** parts', () => { | ||
testGlob('asdf/**.jpg', | ||
['asdf/asdf.jpg', 'asdf/asdf_asdf.jpg', 'asdf/asdf/asdf.jpg', 'asdf/asdf/asdf/asdf/asdf.jpg'], | ||
['/asdf/asdf.jpg', 'asdff/asdf.jpg', 'xxxasdf/asdf.jpgxxx']); | ||
|
||
testGlob('**/*.js', | ||
['asdf/asdf.js', 'asdf/asdf/asdfasdf_asdf.js', '/asdf/asdf.js', '/asdf/aasdf-asdf.2.1.4.js'], | ||
['/asdf/asdf.jpg', 'asdf.js']); | ||
}); | ||
|
||
it('should match groups', () => { | ||
testGlob('asdf/*.(jpg|jpeg)', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to this, we should use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is my bad. |
||
['asdf/asdf.jpg', 'asdf/asdf_asdf.jpeg'], | ||
['/asdf/asdf.jpg', 'asdff/asdf.jpg']); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe test with a different extension as well. |
||
}); | ||
|
||
it('should match named parts', () => { | ||
testGlob('/api/:package/:api-*', | ||
['/api/common/NgClass-directive', '/api/core/Renderer-class'], | ||
['/moo/common/NgClass-directive', '/api/common/', '/api/common/NgClass']); | ||
}); | ||
|
||
it('should match wildcard named parts', () => { | ||
testGlob('/api/:rest*', | ||
['/api/a', '/api/a/b'], | ||
['/apx/a', '/apx/a/b']); | ||
}); | ||
}); | ||
|
||
describe('match', () => { | ||
it('should extract the named parts', () => { | ||
const glob = new FirebaseGlob('/api/:package/:api-*'); | ||
const match: any = glob.match('/api/common/NgClass-directive'); | ||
expect(match).toEqual({package: 'common', api: 'NgClass'}); | ||
}); | ||
}); | ||
}); | ||
|
||
function testGlob(pattern: string, matches: string[], nonMatches: string[]) { | ||
const glob = new FirebaseGlob(pattern); | ||
matches.forEach(url => expect(glob.test(url)).toBe(true, url)); | ||
nonMatches.forEach(url => expect(glob.test(url)).toBe(false, url)); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import * as XRegExp from 'xregexp'; | ||
|
||
const questionExpr = /\?([^(])/g; | ||
const catchAllNamedMatchExpr = /\/:([A-Za-z]+)\*/g | ||
const namedMatchExpr = /\/:([A-Za-z]+)([^/]*)/g | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: The |
||
const wildcardSegmentExpr = /(^\/).🐷\//; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
||
export class FirebaseGlob { | ||
pattern: string; | ||
regex: XRegExp; | ||
constructor(glob: string) { | ||
const pattern = glob | ||
.replace('.', '\\.') | ||
.replace(questionExpr, '[^/]$1') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no test for that 😉 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to mock as close to Firebase as possible, otherwise our own redirect tests might not represent what happens in the real deployment. Perhaps we inadvertently add in a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 for that, as long as we test all the features we support 😛 |
||
.replace(catchAllNamedMatchExpr, '/(?<$1>.+)') | ||
.replace(namedMatchExpr, '/(?<$1>[^/]+)$2') | ||
.replace('**', '.🐷') | ||
.replace('*', '[^/]*') | ||
.replace(wildcardSegmentExpr, '(?:$1|/.*/)') | ||
.replace('.🐷', '.*'); | ||
this.pattern = `^${pattern}$`; | ||
this.regex = XRegExp(this.pattern); | ||
} | ||
|
||
test(url: string) { | ||
return XRegExp.test(url, this.regex); | ||
} | ||
|
||
match(url: string) { | ||
const match = XRegExp.exec(url, this.regex); | ||
if (match) { | ||
const result = {}; | ||
const names = (this.regex as any).xregexp.captureNames || []; | ||
names.forEach(name => result[name] = match[name]); | ||
return result; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { FirebaseRedirect } from './FirebaseRedirect'; | ||
|
||
describe('FirebaseRedirect', () => { | ||
describe('replace', () => { | ||
it('should inject the named captures into the destination', () => { | ||
const redirect = new FirebaseRedirect('/api/:package/:api-*', '<:package><:api>'); | ||
const newUrl = redirect.replace('/api/common/NgClass-directive'); | ||
expect(newUrl).toEqual('<common><NgClass>'); | ||
}); | ||
|
||
it('should handle empty ** sections', () => { | ||
const redirect2 = new FirebaseRedirect('/**/a/b', '/xxx'); | ||
expect(redirect2.replace('/a/b')).toEqual('/xxx'); | ||
}); | ||
|
||
it('should return `undefined` if the redirect failed to match', () => { | ||
const redirect = new FirebaseRedirect('/api/:package/:api-*', '<:package><:api>'); | ||
const newUrl = redirect.replace('/common/NgClass-directive'); | ||
expect(newUrl).toBe(undefined); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import * as XRegExp from 'xregexp'; | ||
import { FirebaseGlob } from './FirebaseGlob'; | ||
|
||
export class FirebaseRedirect { | ||
glob = new FirebaseGlob(this.source); | ||
constructor(public source: string, public destination: string) {} | ||
|
||
replace(url: string) { | ||
const match = this.glob.match(url); | ||
if (match) { | ||
const replacers = Object.keys(match).map(name => [ XRegExp(`:${name}`, 'g'), match[name] ]); | ||
return XRegExp.replaceEach(this.destination, replacers); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { FirebaseRedirector } from './FirebaseRedirector'; | ||
|
||
describe('FirebaseRedirector', () => { | ||
it('should replace with the first matching redirect', () => { | ||
const redirector = new FirebaseRedirector([ | ||
{ source: '/a/b/c', destination: '/X/Y/Z' }, | ||
{ source: '/a/:foo/c', destination: '/X/:foo/Z' }, | ||
{ source: '/**/:foo/c', destination: '/A/:foo/zzz' }, | ||
]); | ||
expect(redirector.redirect('/a/b/c')).toEqual('/X/Y/Z'); | ||
expect(redirector.redirect('/a/moo/c')).toEqual('/X/moo/Z'); | ||
expect(redirector.redirect('/x/y/a/b/c')).toEqual('/A/b/zzz'); | ||
expect(redirector.redirect('/x/y/c')).toEqual('/A/y/zzz'); | ||
}); | ||
|
||
it('should return the original url if no redirect matches', () => { | ||
const redirector = new FirebaseRedirector([ | ||
{ source: 'x', destination: 'X' }, | ||
{ source: 'y', destination: 'Y' }, | ||
{ source: 'z', destination: 'Z' }, | ||
]); | ||
expect(redirector.redirect('a')).toEqual('a'); | ||
}); | ||
|
||
it('should recursively redirect', () => { | ||
const redirector = new FirebaseRedirector([ | ||
{ source: 'a', destination: 'b' }, | ||
{ source: 'b', destination: 'c' }, | ||
{ source: 'c', destination: 'd' }, | ||
]); | ||
expect(redirector.redirect('a')).toEqual('d'); | ||
}); | ||
|
||
it('should throw if stuck in an infinite loop', () => { | ||
const redirector = new FirebaseRedirector([ | ||
{ source: 'a', destination: 'b' }, | ||
{ source: 'b', destination: 'c' }, | ||
{ source: 'c', destination: 'a' }, | ||
]); | ||
expect(() => redirector.redirect('a')).toThrowError('infinite redirect loop'); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️