blob: 4a78140c3d7c59f45b5a638e501d1c77954a78aa [file] [log] [blame]
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup';
import './gr-router';
import {Page, PageContext} from './gr-page';
import {
stubBaseUrl,
stubRestApi,
addListenerForTest,
waitUntilCalled,
mockPromise,
MockPromise,
} from '../../../test/test-utils';
import {GrRouter, routerToken} from './gr-router';
import {GerritView} from '../../../services/router/router-model';
import {
BasePatchSetNum,
NumericChangeId,
PARENT,
RepoName,
RevisionPatchSetNum,
UrlEncodedCommentId,
} from '../../../types/common';
import {AppElementJustRegisteredParams} from '../../gr-app-types';
import {assert} from '@open-wc/testing';
import {AdminChildView, AdminViewState} from '../../../models/views/admin';
import {RepoDetailView} from '../../../models/views/repo';
import {GroupDetailView} from '../../../models/views/group';
import {ChangeChildView} from '../../../models/views/change';
import {PatchRangeParams} from '../../../utils/url-util';
import {testResolver} from '../../../test/common-test-setup';
import {
createAdminPluginsViewState,
createAdminReposViewState,
createChangeViewState,
createComment,
createDashboardViewState,
createDiff,
createDiffViewState,
createEditViewState,
createGroupViewState,
createParsedChange,
createRepoBranchesViewState,
createRepoTagsViewState,
createRepoViewState,
createRevision,
createSearchViewState,
} from '../../../test/test-data-generators';
import {ParsedChangeInfo} from '../../../types/types';
import {ViewState} from '../../../models/views/base';
import {DashboardType} from '../../../models/views/dashboard';
suite('gr-router tests', () => {
let router: GrRouter;
let page: Page;
setup(() => {
router = testResolver(routerToken);
page = router.page;
});
teardown(async () => {
router.finalize();
});
test('getHashFromCanonicalPath', () => {
let url = '/foo/bar';
let hash = router.getHashFromCanonicalPath(url);
assert.equal(hash, '');
url = '';
hash = router.getHashFromCanonicalPath(url);
assert.equal(hash, '');
url = '/foo#bar';
hash = router.getHashFromCanonicalPath(url);
assert.equal(hash, 'bar');
url = '/foo#bar#baz';
hash = router.getHashFromCanonicalPath(url);
assert.equal(hash, 'bar#baz');
url = '#foo#bar#baz';
hash = router.getHashFromCanonicalPath(url);
assert.equal(hash, 'foo#bar#baz');
});
suite('parseLineAddress', () => {
test('returns null for empty and invalid hashes', () => {
let actual = router.parseLineAddress('');
assert.isNull(actual);
actual = router.parseLineAddress('foobar');
assert.isNull(actual);
actual = router.parseLineAddress('foo123');
assert.isNull(actual);
actual = router.parseLineAddress('123bar');
assert.isNull(actual);
});
test('parses correctly', () => {
let actual = router.parseLineAddress('1234');
assert.isOk(actual);
assert.equal(actual!.lineNum, 1234);
assert.isFalse(actual!.leftSide);
actual = router.parseLineAddress('a4');
assert.isOk(actual);
assert.equal(actual!.lineNum, 4);
assert.isTrue(actual!.leftSide);
actual = router.parseLineAddress('b77');
assert.isOk(actual);
assert.equal(actual!.lineNum, 77);
assert.isTrue(actual!.leftSide);
});
});
test('startRouterForTesting requires auth for the right handlers', () => {
// This test encodes the lists of route handler methods that gr-router
// automatically checks for authentication before triggering.
const requiresAuth: any = {};
const doesNotRequireAuth: any = {};
sinon.stub(page, 'start');
sinon
.stub(router, 'mapRoute')
.callsFake((_pattern, methodName, _method, usesAuth) => {
if (usesAuth) {
requiresAuth[methodName] = true;
} else {
doesNotRequireAuth[methodName] = true;
}
});
router._testOnly_startRouter();
const actualRequiresAuth = Object.keys(requiresAuth);
actualRequiresAuth.sort();
const actualDoesNotRequireAuth = Object.keys(doesNotRequireAuth);
actualDoesNotRequireAuth.sort();
const shouldRequireAutoAuth = [
'handleAgreementsRoute',
'handleChangeEditRoute',
'handleCreateGroupRoute',
'handleCreateProjectRoute',
'handleDiffEditRoute',
'handleGroupAuditLogRoute',
'handleGroupInfoRoute',
'handleGroupListRoute',
'handleGroupMembersRoute',
'handleGroupRoute',
'handleGroupSelfRedirectRoute',
'handleNewAgreementsRoute',
'handlePluginListFilterRoute',
'handlePluginListRoute',
'handleRepoCommandsRoute',
'handleRepoEditFileRoute',
'handleServerInfoRoute',
'handleSettingsLegacyRoute',
'handleSettingsRoute',
];
assert.deepEqual(actualRequiresAuth, shouldRequireAutoAuth);
const unauthenticatedHandlers = [
'handleBranchListRoute',
'handleChangeIdQueryRoute',
'handleChangeNumberLegacyRoute',
'handleChangeRoute',
'handleCommentRoute',
'handleCommentsRoute',
'handleDiffRoute',
'handleDefaultRoute',
'handleChangeLegacyRoute',
'handleDocumentationRedirectRoute',
'handleDocumentationSearchRoute',
'handleDocumentationSearchRedirectRoute',
'handleLegacyLinenum',
'handleImproperlyEncodedPlusRoute',
'handleProjectDashboardRoute',
'handleLegacyProjectDashboardRoute',
'handleProjectsOldRoute',
'handleRepoAccessRoute',
'handleRepoDashboardsRoute',
'handleRepoGeneralRoute',
'handleRepoListRoute',
'handleRepoRoute',
'handleQueryLegacySuffixRoute',
'handleQueryRoute',
'handleRegisterRoute',
'handleTagListRoute',
'handlePluginScreen',
];
// Handler names that check authentication themselves, and thus don't need
// it performed for them.
const selfAuthenticatingHandlers = [
'handleDashboardRoute',
'handleCustomDashboardRoute',
'handleRootRoute',
];
const shouldNotRequireAuth = unauthenticatedHandlers.concat(
selfAuthenticatingHandlers
);
shouldNotRequireAuth.sort();
assert.deepEqual(actualDoesNotRequireAuth, shouldNotRequireAuth);
});
test('redirectIfNotLoggedIn while logged in', () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(true));
const redirectStub = sinon.stub(router, 'redirectToLogin');
return router.redirectIfNotLoggedIn('somepath').then(() => {
assert.isFalse(redirectStub.called);
});
});
test('redirectIfNotLoggedIn while logged out', () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
const redirectStub = sinon.stub(router, 'redirectToLogin');
return new Promise(resolve => {
router
.redirectIfNotLoggedIn('somepath')
.then(() => {
assert.isTrue(false, 'Should never execute');
})
.catch(() => {
assert.isTrue(redirectStub.calledOnce);
resolve(Promise.resolve());
});
});
});
suite('param normalization', () => {
suite('normalizePatchRangeParams', () => {
test('range n..n normalizes to n', () => {
const params: PatchRangeParams = {
basePatchNum: 4 as BasePatchSetNum,
patchNum: 4 as RevisionPatchSetNum,
};
router.normalizePatchRangeParams(params);
assert.equal(params.basePatchNum, PARENT);
assert.equal(params.patchNum, 4 as RevisionPatchSetNum);
});
test('range n.. normalizes to n', () => {
const params: PatchRangeParams = {basePatchNum: 4 as BasePatchSetNum};
router.normalizePatchRangeParams(params);
assert.equal(params.basePatchNum, PARENT);
assert.equal(params.patchNum, 4 as RevisionPatchSetNum);
});
});
});
suite('navigation blockers', () => {
let clock: sinon.SinonFakeTimers;
let redirectStub: sinon.SinonStub;
let urlPromise: MockPromise<string>;
setup(() => {
stubRestApi('addRepoNameToCache');
urlPromise = mockPromise<string>();
redirectStub = sinon
.stub(router, 'redirect')
.callsFake(urlPromise.resolve);
router._testOnly_startRouter();
clock = sinon.useFakeTimers();
});
test('no blockers: normal redirect', async () => {
router.page.show('/settings/agreements');
const url = await urlPromise;
assert.isTrue(redirectStub.calledOnce);
assert.equal(url, '/settings/#Agreements');
});
test('redirect blocked', async () => {
const firstAlertPromise = mockPromise<Event>();
addListenerForTest(document, 'show-alert', firstAlertPromise.resolve);
router.blockNavigation('a good reason');
router.page.show('/settings/agreements');
const firstAlert = (await firstAlertPromise) as CustomEvent;
assert.equal(
firstAlert.detail.message,
'Waiting 1 second for navigation blockers to resolve ...'
);
const secondAlertPromise = mockPromise<Event>();
addListenerForTest(document, 'show-alert', secondAlertPromise.resolve);
clock.tick(2000);
const secondAlert = (await secondAlertPromise) as CustomEvent;
assert.equal(
secondAlert.detail.message,
'Navigation is blocked by: a good reason'
);
assert.isFalse(redirectStub.called);
});
test('redirect blocked, but resolved within one second', async () => {
const firstAlertPromise = mockPromise<Event>();
addListenerForTest(document, 'show-alert', firstAlertPromise.resolve);
router.blockNavigation('a good reason');
router.page.show('/settings/agreements');
const firstAlert = (await firstAlertPromise) as CustomEvent;
assert.equal(
firstAlert.detail.message,
'Waiting 1 second for navigation blockers to resolve ...'
);
const secondAlertPromise = mockPromise<Event>();
addListenerForTest(document, 'show-alert', secondAlertPromise.resolve);
clock.tick(500);
router.releaseNavigation('a good reason');
clock.tick(2000);
await urlPromise;
assert.isTrue(redirectStub.calledOnce);
});
});
suite('route handlers', () => {
let redirectStub: sinon.SinonStub;
let setStateStub: sinon.SinonStub;
let windowReloadStub: sinon.SinonStub;
let redirectToLoginStub: sinon.SinonStub;
async function checkUrlToState<T extends ViewState>(
url: string,
state: T | AppElementJustRegisteredParams
) {
setStateStub.reset();
router.page.show(url);
await waitUntilCalled(setStateStub, 'setState');
assert.isTrue(setStateStub.calledOnce);
assert.deepEqual(setStateStub.lastCall.firstArg, state);
}
async function checkRedirect(fromUrl: string, toUrl: string) {
redirectStub.reset();
router.page.show(fromUrl);
await waitUntilCalled(redirectStub, 'redirect');
assert.isTrue(redirectStub.calledOnce);
assert.isFalse(setStateStub.called);
assert.equal(redirectStub.lastCall.firstArg, toUrl);
}
async function checkRedirectToLogin(fromUrl: string, toUrl: string) {
redirectToLoginStub.reset();
router.page.show(fromUrl);
await waitUntilCalled(redirectToLoginStub, 'redirectToLogin');
assert.isTrue(redirectToLoginStub.calledOnce);
assert.isFalse(redirectStub.called);
assert.isFalse(setStateStub.called);
assert.equal(redirectToLoginStub.lastCall.firstArg, toUrl);
}
setup(() => {
stubRestApi('addRepoNameToCache');
redirectStub = sinon.stub(router, 'redirect');
redirectToLoginStub = sinon.stub(router, 'redirectToLogin');
setStateStub = sinon.stub(router, 'setState');
windowReloadStub = sinon.stub(router, 'windowReload');
router._testOnly_startRouter();
});
test('LEGACY_PROJECT_DASHBOARD', async () => {
// LEGACY_PROJECT_DASHBOARD: /^\/projects\/(.+),dashboards\/(.+)/,
await checkRedirect(
'/projects/gerrit/project,dashboards/dashboard:main',
'/p/gerrit/project/+/dashboard/dashboard:main'
);
});
test('AGREEMENTS', async () => {
// AGREEMENTS: /^\/settings\/agreements\/?/,
await checkRedirect('/settings/agreements', '/settings/#Agreements');
});
test('NEW_AGREEMENTS', async () => {
// NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/,
await checkUrlToState('/settings/new-agreement', {
view: GerritView.AGREEMENTS,
});
await checkUrlToState('/settings/new-agreement/', {
view: GerritView.AGREEMENTS,
});
});
test('SETTINGS', async () => {
// SETTINGS: /^\/settings\/?/,
// SETTINGS_LEGACY: /^\/settings\/VE\/(\S+)/,
await checkUrlToState('/settings', {view: GerritView.SETTINGS});
await checkUrlToState('/settings/', {view: GerritView.SETTINGS});
await checkUrlToState('/settings/VE/asdf', {
view: GerritView.SETTINGS,
emailToken: 'asdf',
});
await checkUrlToState('/settings/VE/asdf%40qwer', {
view: GerritView.SETTINGS,
emailToken: 'asdf@qwer',
});
});
test('handleDefaultRoute on first load', () => {
const spy = sinon.spy();
addListenerForTest(document, 'page-error', spy);
router.handleDefaultRoute();
assert.isTrue(spy.calledOnce);
assert.equal(spy.lastCall.args[0].detail.response.status, 404);
});
test('handleDefaultRoute after internal navigation', () => {
let onExit: Function | null = null;
const onRegisteringExit = (
_match: string | RegExp,
_onExit: Function
) => {
onExit = _onExit;
};
sinon.stub(page, 'registerExitRoute').callsFake(onRegisteringExit);
sinon.stub(page, 'start');
router._testOnly_startRouter();
router.handleDefaultRoute();
onExit!('', () => {}); // we left page;
router.handleDefaultRoute();
assert.isTrue(windowReloadStub.calledOnce);
});
test('IMPROPERLY_ENCODED_PLUS', async () => {
// IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/ \/(.+)$/,
await checkRedirect('/c/repo/ /42', '/c/repo/+/42');
await checkRedirect('/c/repo/%20/42', '/c/repo/+/42');
await checkRedirect('/c/repo/ /42#foo', '/c/repo/+/42#foo');
});
test('QUERY', async () => {
// QUERY: /^\/q\/(.+?)(,(\d+))?$/,
await checkUrlToState('/q/asdf', {
...createSearchViewState(),
query: 'asdf',
});
await checkUrlToState('/q/project:foo/bar/baz', {
...createSearchViewState(),
query: 'project:foo/bar/baz',
});
await checkUrlToState('/q/asdf,123', {
...createSearchViewState(),
query: 'asdf',
offset: '123',
});
await checkUrlToState('/q/asdf,qwer', {
...createSearchViewState(),
query: 'asdf,qwer',
});
await checkUrlToState('/q/asdf,qwer,123', {
...createSearchViewState(),
query: 'asdf,qwer',
offset: '123',
});
});
test('QUERY_LEGACY_SUFFIX', async () => {
// QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/,
await checkRedirect('/q/foo+bar,n,z', '/q/foo+bar');
});
test('CHANGE_ID_QUERY', async () => {
// CHANGE_ID_QUERY: /^\/id\/(I[0-9a-f]{40})$/,
await checkUrlToState('/id/I0123456789abcdef0123456789abcdef01234567', {
...createSearchViewState(),
query: 'I0123456789abcdef0123456789abcdef01234567',
});
});
test('REGISTER', async () => {
// REGISTER: /^\/register(\/.*)?$/,
await checkUrlToState('/register/foo/bar', {
justRegistered: true,
});
assert.isTrue(redirectStub.calledWithExactly('/foo/bar'));
await checkUrlToState('/register', {
justRegistered: true,
});
assert.isTrue(redirectStub.calledWithExactly('/'));
await checkUrlToState('/register/register', {
justRegistered: true,
});
assert.isTrue(redirectStub.calledWithExactly('/'));
});
suite('ROOT', () => {
test('closes for closeAfterLogin', () => {
const ctx = {
querystring: 'closeAfterLogin',
canonicalPath: '',
} as PageContext;
const closeStub = sinon.stub(window, 'close');
const result = router.handleRootRoute(ctx);
assert.isNotOk(result);
assert.isTrue(closeStub.called);
assert.isFalse(redirectStub.called);
});
test('ROOT logged in', async () => {
stubRestApi('getLoggedIn').resolves(true);
await checkRedirect('/', '/dashboard/self');
});
test('ROOT not logged in', async () => {
stubRestApi('getLoggedIn').resolves(false);
await checkRedirect('/', '/q/status:open+-is:wip');
});
suite('ROOT GWT hash-path URLs', () => {
test('ROOT hash-path URLs', async () => {
await checkRedirect('/#/foo/bar/baz', '/foo/bar/baz');
});
test('ROOT hash-path URLs w/o leading slash', async () => {
await checkRedirect('/#foo/bar/baz', '/foo/bar/baz');
});
test('ROOT normalizes "/ /" in hash to "/+/"', async () => {
await checkRedirect('/#/foo/bar/+/123/4', '/foo/bar/+/123/4');
});
test('ROOT prepends baseurl to hash-path', async () => {
stubBaseUrl('/baz');
await checkRedirect('/#/foo/bar', '/baz/foo/bar');
});
test('ROOT normalizes /VE/ settings hash-paths', async () => {
await checkRedirect('/#/VE/foo/bar', '/settings/VE/foo/bar');
});
test('ROOT does not drop "inner hashes"', async () => {
await checkRedirect('/#/foo/bar#baz', '/foo/bar#baz');
});
});
});
suite('DASHBOARD', () => {
test('DASHBOARD own dashboard but signed out redirects to login', async () => {
stubRestApi('getLoggedIn').resolves(false);
await checkRedirectToLogin('/dashboard/seLF', '/dashboard/seLF');
});
test('DASHBOARD non-self dashboard but signed out redirects', async () => {
stubRestApi('getLoggedIn').resolves(false);
await checkRedirect('/dashboard/foo', '/q/owner:foo');
});
test('DASHBOARD', async () => {
// DASHBOARD: /^\/dashboard\/(.+)$/,
await checkUrlToState('/dashboard/foo', {
...createDashboardViewState(),
user: 'foo',
});
});
});
suite('CUSTOM_DASHBOARD', () => {
test('CUSTOM_DASHBOARD no user specified', async () => {
await checkRedirect('/dashboard/', '/dashboard/self');
});
test('CUSTOM_DASHBOARD', async () => {
// CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
await checkUrlToState('/dashboard?title=Custom Dashboard&a=b&d=e', {
...createDashboardViewState(),
type: DashboardType.CUSTOM,
sections: [
{name: 'a', query: 'b'},
{name: 'd', query: 'e'},
],
title: 'Custom Dashboard',
});
await checkUrlToState('/dashboard?a=b&c&d=&=e&foreach=is:open', {
...createDashboardViewState(),
type: DashboardType.CUSTOM,
sections: [{name: 'a', query: 'is:open b'}],
title: 'Custom Dashboard',
});
});
});
suite('group routes', () => {
test('GROUP_INFO', async () => {
// GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/,
await checkRedirect('/admin/groups/1234,info', '/admin/groups/1234');
});
test('GROUP_AUDIT_LOG', async () => {
// GROUP_AUDIT_LOG: /^\/admin\/groups\/(?:uuid-)?(.+),audit-log$/,
await checkUrlToState('/admin/groups/1234,audit-log', {
...createGroupViewState(),
detail: GroupDetailView.LOG,
groupId: '1234',
});
});
test('GROUP_MEMBERS', async () => {
// GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/,
await checkUrlToState('/admin/groups/1234,members', {
...createGroupViewState(),
detail: GroupDetailView.MEMBERS,
groupId: '1234',
});
});
test('GROUP_LIST', async () => {
// GROUP_LIST: /^\/admin\/groups(\/q\/filter:(.*?))?(,(\d+))?(\/)?$/,
const defaultState: AdminViewState = {
view: GerritView.ADMIN,
adminView: AdminChildView.GROUPS,
offset: '0',
openCreateModal: false,
filter: '',
};
await checkUrlToState('/admin/groups', defaultState);
await checkUrlToState('/admin/groups/', defaultState);
await checkUrlToState('/admin/groups#create', {
...defaultState,
openCreateModal: true,
});
await checkUrlToState('/admin/groups,42', {
...defaultState,
offset: '42',
});
// #create is ignored when there is an offset
await checkUrlToState('/admin/groups,42#create', {
...defaultState,
offset: '42',
});
await checkUrlToState('/admin/groups/q/filter:foo', {
...defaultState,
filter: 'foo',
});
await checkUrlToState('/admin/groups/q/filter:foo/%2F%20%2525%252F', {
...defaultState,
filter: 'foo// %/',
});
await checkUrlToState('/admin/groups/q/filter:foo,42', {
...defaultState,
filter: 'foo',
offset: '42',
});
// #create is ignored when filtering
await checkUrlToState('/admin/groups/q/filter:foo,42#create', {
...defaultState,
filter: 'foo',
offset: '42',
});
});
test('GROUP', async () => {
// GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/,
await checkUrlToState('/admin/groups/4321', {
...createGroupViewState(),
groupId: '4321',
});
});
});
suite('REPO*', () => {
test('PROJECT_OLD', async () => {
// PROJECT_OLD: /^\/admin\/(projects)\/?(.+)?$/,
await checkRedirect('/admin/projects/', '/admin/repos/');
await checkRedirect('/admin/projects/test', '/admin/repos/test');
await checkRedirect(
'/admin/projects/test,branches',
'/admin/repos/test,branches'
);
});
test('REPO', async () => {
// REPO: /^\/admin\/repos\/([^,]+)$/,
await checkRedirect('/admin/repos/test', '/admin/repos/test,general');
});
test('REPO_GENERAL', async () => {
// REPO_GENERAL: /^\/admin\/repos\/(.+),general$/,
await checkUrlToState('/admin/repos/4321,general', {
...createRepoViewState(),
detail: RepoDetailView.GENERAL,
repo: '4321' as RepoName,
});
});
test('REPO_COMMANDS', async () => {
// REPO_COMMANDS: /^\/admin\/repos\/(.+),commands$/,
await checkUrlToState('/admin/repos/4321,commands', {
...createRepoViewState(),
detail: RepoDetailView.COMMANDS,
repo: '4321' as RepoName,
});
});
test('REPO_ACCESS', async () => {
// REPO_ACCESS: /^\/admin\/repos\/(.+),access$/,
await checkUrlToState('/admin/repos/4321,access', {
...createRepoViewState(),
detail: RepoDetailView.ACCESS,
repo: '4321' as RepoName,
});
});
test('BRANCH_LIST', async () => {
await checkUrlToState('/admin/repos/4321,branches', {
...createRepoBranchesViewState(),
repo: '4321' as RepoName,
});
await checkUrlToState('/admin/repos/4321,branches,42', {
...createRepoBranchesViewState(),
repo: '4321' as RepoName,
offset: '42',
});
await checkUrlToState('/admin/repos/4321,branches/q/filter:foo,42', {
...createRepoBranchesViewState(),
repo: '4321' as RepoName,
offset: '42',
filter: 'foo',
});
await checkUrlToState('/admin/repos/4321,branches/q/filter:foo', {
...createRepoBranchesViewState(),
repo: '4321' as RepoName,
filter: 'foo',
});
await checkUrlToState(
'/admin/repos/asdf/%2F%20%2525%252Fqwer,branches/q/filter:foo/%2F%20%2525%252F',
{
...createRepoBranchesViewState(),
repo: 'asdf// %/qwer' as RepoName,
filter: 'foo// %/',
}
);
});
test('TAG_LIST', async () => {
await checkUrlToState('/admin/repos/4321,tags', {
...createRepoTagsViewState(),
repo: '4321' as RepoName,
});
await checkUrlToState('/admin/repos/4321,tags,42', {
...createRepoTagsViewState(),
repo: '4321' as RepoName,
offset: '42',
});
await checkUrlToState('/admin/repos/4321,tags/q/filter:foo,42', {
...createRepoTagsViewState(),
repo: '4321' as RepoName,
offset: '42',
filter: 'foo',
});
await checkUrlToState('/admin/repos/4321,tags/q/filter:foo', {
...createRepoTagsViewState(),
repo: '4321' as RepoName,
filter: 'foo',
});
await checkUrlToState(
'/admin/repos/asdf/%2F%20%2525%252Fqwer,tags/q/filter:foo/%2F%20%2525%252F',
{
...createRepoTagsViewState(),
repo: 'asdf// %/qwer' as RepoName,
filter: 'foo// %/',
}
);
});
test('REPO_LIST', async () => {
await checkUrlToState('/admin/repos', {
...createAdminReposViewState(),
});
await checkUrlToState('/admin/repos/', {
...createAdminReposViewState(),
});
await checkUrlToState('/admin/repos,42', {
...createAdminReposViewState(),
offset: '42',
});
await checkUrlToState('/admin/repos#create', {
...createAdminReposViewState(),
openCreateModal: true,
});
await checkUrlToState('/admin/repos/q/filter:foo', {
...createAdminReposViewState(),
filter: 'foo',
});
await checkUrlToState('/admin/repos/q/filter:foo/%2F%20%2525%252F', {
...createAdminReposViewState(),
filter: 'foo// %/',
});
await checkUrlToState('/admin/repos/q/filter:foo,42', {
...createAdminReposViewState(),
filter: 'foo',
offset: '42',
});
});
});
test('PLUGIN_LIST', async () => {
await checkUrlToState('/admin/plugins', {
...createAdminPluginsViewState(),
});
await checkUrlToState('/admin/plugins/', {
...createAdminPluginsViewState(),
});
await checkUrlToState('/admin/plugins,42', {
...createAdminPluginsViewState(),
offset: '42',
});
await checkUrlToState('/admin/plugins/q/filter:foo', {
...createAdminPluginsViewState(),
filter: 'foo',
});
await checkUrlToState('/admin/plugins/q/filter:foo%2F%20%2525%252F', {
...createAdminPluginsViewState(),
filter: 'foo/ %/',
});
await checkUrlToState('/admin/plugins/q/filter:foo,42', {
...createAdminPluginsViewState(),
offset: '42',
filter: 'foo',
});
await checkUrlToState('/admin/plugins/q/filter:foo,asdf', {
...createAdminPluginsViewState(),
filter: 'foo,asdf',
});
});
suite('CHANGE* / DIFF*', () => {
test('CHANGE_NUMBER_LEGACY', async () => {
// CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
await checkRedirect('/12345', '/c/12345');
});
test('CHANGE_LEGACY', async () => {
// CHANGE_LEGACY: /^\/c\/(\d+)\/?(.*)$/,
stubRestApi('getRepoName').resolves('project' as RepoName);
await checkRedirect('/c/1234', '/c/project/+/1234/');
await checkRedirect(
'/c/1234/comment/6789',
'/c/project/+/1234/comment/6789'
);
});
test('DIFF_LEGACY_LINENUM', async () => {
await checkRedirect(
'/c/1234/3..8/foo/bar@321',
'/c/1234/3..8/foo/bar#321'
);
await checkRedirect(
'/c/1234/3..8/foo/bar@b321',
'/c/1234/3..8/foo/bar#b321'
);
});
test('CHANGE', async () => {
// CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
await checkUrlToState('/c/test-project/+/42', {
...createChangeViewState(),
basePatchNum: undefined,
patchNum: undefined,
});
await checkUrlToState('/c/test-project/+/42/7', {
...createChangeViewState(),
basePatchNum: PARENT,
patchNum: 7,
});
await checkUrlToState('/c/test-project/+/42/4..7', {
...createChangeViewState(),
basePatchNum: 4,
patchNum: 7,
});
await checkUrlToState(
'/c/test-project/+/42/4..7?tab=checks&filter=fff&attempt=1&checksRunsSelected=asdf,qwer&checksResultsFilter=asdf.*qwer',
{
...createChangeViewState(),
basePatchNum: 4,
patchNum: 7,
attempt: 1,
filter: 'fff',
tab: 'checks',
checksRunsSelected: new Set(['asdf', 'qwer']),
checksResultsFilter: 'asdf.*qwer',
}
);
});
test('COMMENTS_TAB', async () => {
// COMMENTS_TAB: /^\/c\/(.+)\/\+\/(\d+)\/comments(?:\/)?(\w+)?\/?$/,
await checkUrlToState(
'/c/gerrit/+/264833/comments/00049681_f34fd6a9/',
{
...createChangeViewState(),
repo: 'gerrit' as RepoName,
changeNum: 264833 as NumericChangeId,
commentId: '00049681_f34fd6a9' as UrlEncodedCommentId,
view: GerritView.CHANGE,
childView: ChangeChildView.OVERVIEW,
}
);
});
suite('handleDiffRoute', () => {
test('DIFF', async () => {
// DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/,
await checkUrlToState('/c/test-project/+/42/4..7/foo/bar/baz#b44', {
...createDiffViewState(),
basePatchNum: 4 as BasePatchSetNum,
patchNum: 7 as RevisionPatchSetNum,
diffView: {
path: 'foo/bar/baz',
lineNum: 44,
leftSide: true,
},
});
});
test('COMMENT base..1', async () => {
const change: ParsedChangeInfo = createParsedChange();
const repo = change.project;
const changeNum = change._number;
const ps = 1 as RevisionPatchSetNum;
const line = 23;
const id = '00049681_f34fd6a9' as UrlEncodedCommentId;
stubRestApi('getChangeDetail').resolves(change);
stubRestApi('getDiffComments').resolves({
filepath: [{...createComment(), id, patch_set: ps, line}],
});
await checkRedirect(
`/c/${repo}/+/${changeNum}/comment/${id}/`,
`/c/${repo}/+/${changeNum}/${ps}/filepath#${line}`
);
});
test('COMMENT 1..2', async () => {
const change: ParsedChangeInfo = {
...createParsedChange(),
revisions: {
abc: createRevision(1),
def: createRevision(2),
},
};
const repo = change.project;
const changeNum = change._number;
const ps = 1 as RevisionPatchSetNum;
const line = 23;
const id = '00049681_f34fd6a9' as UrlEncodedCommentId;
stubRestApi('getChangeDetail').resolves(change);
stubRestApi('getDiffComments').resolves({
filepath: [{...createComment(), id, patch_set: ps, line}],
});
const diffStub = stubRestApi('getDiff');
// If getDiff() returns a diff with changes, then we will compare
// the patchset of the comment (1) against latest (2).
diffStub.onFirstCall().resolves(createDiff());
await checkRedirect(
`/c/${repo}/+/${changeNum}/comment/${id}/`,
`/c/${repo}/+/${changeNum}/${ps}..2/filepath#b${line}`
);
// If getDiff() returns an unchanged diff, then we will compare
// the patchset of the comment (1) against base.
diffStub.onSecondCall().resolves({
...createDiff(),
content: [],
});
await checkRedirect(
`/c/${repo}/+/${changeNum}/comment/${id}/`,
`/c/${repo}/+/${changeNum}/${ps}/filepath#${line}`
);
});
});
test('DIFF_EDIT', async () => {
// DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(#\d+)?$/,
await checkUrlToState('/c/foo/bar/+/1234/3/foo/bar/baz,edit', {
...createEditViewState(),
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
view: GerritView.CHANGE,
childView: ChangeChildView.EDIT,
patchNum: 3 as RevisionPatchSetNum,
editView: {path: 'foo/bar/baz', lineNum: 0},
});
await checkUrlToState('/c/foo/bar/+/1234/3/foo/bar/baz,edit#4', {
...createEditViewState(),
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
view: GerritView.CHANGE,
childView: ChangeChildView.EDIT,
patchNum: 3 as RevisionPatchSetNum,
editView: {path: 'foo/bar/baz', lineNum: 4},
});
});
test('CHANGE_EDIT', async () => {
// CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/,
await checkUrlToState('/c/foo/bar/+/1234/3,edit', {
...createChangeViewState(),
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
view: GerritView.CHANGE,
childView: ChangeChildView.OVERVIEW,
patchNum: 3 as RevisionPatchSetNum,
edit: true,
});
});
});
test('PLUGIN_SCREEN', async () => {
// PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
await checkUrlToState('/x/foo/bar', {
view: GerritView.PLUGIN_SCREEN,
plugin: 'foo',
screen: 'bar',
});
});
test('DOCUMENTATION_SEARCH*', async () => {
// DOCUMENTATION_SEARCH_FILTER: '/Documentation/q/filter::filter',
// DOCUMENTATION_SEARCH: /^\/Documentation\/q\/(.*)$/,
await checkRedirect(
'/Documentation/q/asdf',
'/Documentation/q/filter:asdf'
);
await checkRedirect(
'/Documentation/q/as%3Fdf',
'/Documentation/q/filter:as%3Fdf'
);
await checkUrlToState('/Documentation/q/filter:', {
view: GerritView.DOCUMENTATION_SEARCH,
filter: '',
});
await checkUrlToState('/Documentation/q/filter:asdf', {
view: GerritView.DOCUMENTATION_SEARCH,
filter: 'asdf',
});
// Percent decoding works fine. gr-page decodes twice, so the only problem
// is having `%25` in the URL, because the first decoding pass will yield
// `%`, and then the second decoding pass will throw `URI malformed`.
await checkUrlToState('/Documentation/q/filter:as%20%2fdf', {
view: GerritView.DOCUMENTATION_SEARCH,
filter: 'as /df',
});
// We accept and process double-encoded values, but only *require* it for
// the percent symbol `%`.
await checkUrlToState('/Documentation/q/filter:as%252f%2525df', {
view: GerritView.DOCUMENTATION_SEARCH,
filter: 'as/%df',
});
});
});
});