test(docs-infra): update firebase-test-utils to support regex-based redirects (#41842)

This commit updates the utilities in `firebase-test-utils/` to also
support testing Firebase redirects that are configured using regular
expressions (via the `regex` property).
See the [Firebase docs][1] for more details.

[1]: https://firebase.google.com/docs/hosting/full-config#redirects

PR Close #41842
This commit is contained in:
George Kalpakas
2021-04-27 14:05:23 -07:00
committed by Jessica Janiuk
parent ae1ce5f9c7
commit d1f5a9e44b
6 changed files with 345 additions and 179 deletions
@@ -3,18 +3,28 @@ import { FirebaseRedirect } from './FirebaseRedirect';
describe('FirebaseRedirect', () => {
describe('replace', () => {
it('should return undefined if the redirect does not match the url', () => {
const redirect = new FirebaseRedirect({source: '/a/b/c', destination: '/x/y/z'});
expect(redirect.replace('/1/2/3')).toBe(undefined);
const globRedirect = new FirebaseRedirect({source: '/a/b/c', destination: '/x/y/z'});
expect(globRedirect.replace('/1/2/3')).toBe(undefined);
const regexRedirect = new FirebaseRedirect({regex: '^/a/b/c$', destination: '/x/y/z'});
expect(regexRedirect.replace('/1/2/3')).toBe(undefined);
});
it('should return the destination if there is a match', () => {
const redirect = new FirebaseRedirect({source: '/a/b/c', destination: '/x/y/z'});
expect(redirect.replace('/a/b/c')).toBe('/x/y/z');
const globRedirect = new FirebaseRedirect({source: '/a/b/c', destination: '/x/y/z'});
expect(globRedirect.replace('/a/b/c')).toBe('/x/y/z');
const regexRedirect = new FirebaseRedirect({regex: '^/a/b/c$', destination: '/x/y/z'});
expect(regexRedirect.replace('/a/b/c')).toBe('/x/y/z');
});
it('should inject name params into the destination', () => {
const redirect = new FirebaseRedirect({source: '/api/:package/:api-*', destination: '<:package><:api>'});
expect(redirect.replace('/api/common/NgClass-directive')).toEqual('<common><NgClass>');
const globRedirect = new FirebaseRedirect({source: '/api/:package/:api-*', destination: '<:package><:api>'});
expect(globRedirect.replace('/api/common/NgClass-directive')).toBe('<common><NgClass>');
const regexRedirect = new FirebaseRedirect(
{regex: '^/api/(?P<package>[^/]+)/(?P<api>[^/]+)-.*$', destination: '<:package><:api>'});
expect(regexRedirect.replace('/api/common/NgClass-directive')).toBe('<common><NgClass>');
});
it('should inject rest params into the destination', () => {
@@ -6,8 +6,10 @@ export class FirebaseRedirect {
source: FirebaseRedirectSource;
destination: string;
constructor({source, destination}: FirebaseRedirectConfig) {
this.source = FirebaseRedirectSource.fromGlobPattern(source);
constructor({source, regex, destination}: FirebaseRedirectConfig) {
this.source = (typeof source === 'string') ?
FirebaseRedirectSource.fromGlobPattern(source) :
FirebaseRedirectSource.fromRegexPattern(regex!);
this.destination = destination;
}
File diff suppressed because it is too large Load Diff
@@ -53,6 +53,29 @@ export class FirebaseRedirectSource {
}
}
static fromRegexPattern(regex: string): FirebaseRedirectSource {
try {
// NOTE:
// Firebase redirect regexes use the [RE2 library](https://github.com/google/re2/wiki/Syntax),
// which requires named capture groups to begin with `?P`. See
// https://firebase.google.com/docs/hosting/full-config#capture-url-segments-for-redirects.
if (/\(\?<[^>]+>/.test(regex)) {
// Throw if the regex contains a non-RE2 named capture group.
throw new Error(
'The regular expression pattern contains a named capture group of the format ' +
'`(?<name>...)`, which is not compatible with the RE2 library. Use `(?P<name>...)` ' +
'instead.');
}
// Replace `(?P<...>` with just `(?<...>` to convert to `XRegExp`-compatible syntax for named
// capture groups.
return new FirebaseRedirectSource(regex.replace(/(\(\?)P(<[^>]+>)/g, '$1$2'));
} catch (err) {
throw new Error(`Error in FirebaseRedirectSource: "${regex}" - ${err.message}`);
}
}
test(url: string): boolean {
return XRegExp.test(url, this.regex);
}
@@ -4,7 +4,7 @@ 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' },
{ regex: '^/a/(?P<foo>[^/]+)/c$', destination: '/X/:foo/Z' },
{ source: '/**/:foo/c', destination: '/A/:foo/zzz' },
]);
expect(redirector.redirect('/a/b/c')).toEqual('/X/Y/Z');
@@ -15,9 +15,9 @@ describe('FirebaseRedirector', () => {
it('should return the original url if no redirect matches', () => {
const redirector = new FirebaseRedirector([
{ source: 'x', destination: 'X' },
{ regex: '^x$', destination: 'X' },
{ source: 'y', destination: 'Y' },
{ source: 'z', destination: 'Z' },
{ regex: '^z$', destination: 'Z' },
]);
expect(redirector.redirect('a')).toEqual('a');
});
@@ -25,7 +25,7 @@ describe('FirebaseRedirector', () => {
it('should recursively redirect', () => {
const redirector = new FirebaseRedirector([
{ source: 'a', destination: 'b' },
{ source: 'b', destination: 'c' },
{ regex: '^b$', destination: 'c' },
{ source: 'c', destination: 'd' },
]);
expect(redirector.redirect('a')).toEqual('d');
@@ -33,9 +33,9 @@ describe('FirebaseRedirector', () => {
it('should throw if stuck in an infinite loop', () => {
const redirector = new FirebaseRedirector([
{ source: 'a', destination: 'b' },
{ regex: '^a$', destination: 'b' },
{ source: 'b', destination: 'c' },
{ source: 'c', destination: 'a' },
{ regex: '^c$', destination: 'a' },
]);
expect(() => redirector.redirect('a')).toThrowError('infinite redirect loop');
});
@@ -1,9 +1,8 @@
import { FirebaseRedirect } from './FirebaseRedirect';
export interface FirebaseRedirectConfig {
source: string;
destination: string;
}
export type FirebaseRedirectConfig =
{ source: string, regex?: undefined, destination: string } |
{ source?: undefined, regex: string, destination: string };
export class FirebaseRedirector {
private redirects: FirebaseRedirect[];