[MM-39885] Migrate unit tests to Jest, fleshed out tests for common/util, a bunch of cleanup (#1852)

* [MM-39885] Migrate unit tests to Jest, fleshed out tests for common/util/url

* Typo fix

* Oops

* I found more tests!

* Fixed a bug

* Oops again

* Tests for common/utils/util

* A bunch of cleanup

* Oops
This commit is contained in:
Devin Binnie 2021-11-08 10:04:47 -05:00 committed by GitHub
parent fbb3c9c705
commit 38270fcfe8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 7224 additions and 348 deletions

View file

@ -11,6 +11,3 @@ import './window_test.js';
import './browser/index_test.js';
import './browser/modal_test.js';
import './browser/settings_test.js';
import './main/user_activity_monitor_test.js';
import './utils/util_test.js';

View file

@ -1,106 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
import assert from 'assert';
import urlUtils from '../../../src/common/utils/url';
describe('Utils', () => {
describe('isValidURL', () => {
it('should be true for a valid web url', () => {
const testURL = 'https://developers.mattermost.com/';
assert.equal(urlUtils.isValidURL(testURL), true);
});
it('should be true for a valid, non-https web url', () => {
const testURL = 'http://developers.mattermost.com/';
assert.equal(urlUtils.isValidURL(testURL), true);
});
it('should be true for an invalid, self-defined, top-level domain', () => {
const testURL = 'https://www.example.x';
assert.equal(urlUtils.isValidURL(testURL), true);
});
it('should be true for a file download url', () => {
const testURL = 'https://community.mattermost.com/api/v4/files/ka3xbfmb3ffnmgdmww8otkidfw?download=1';
assert.equal(urlUtils.isValidURL(testURL), true);
});
it('should be true for a permalink url', () => {
const testURL = 'https://community.mattermost.com/test-channel/pl/pdqowkij47rmbyk78m5hwc7r6r';
assert.equal(urlUtils.isValidURL(testURL), true);
});
it('should be true for a valid, internal domain', () => {
const testURL = 'https://mattermost.company-internal';
assert.equal(urlUtils.isValidURL(testURL), true);
});
it('should be true for a second, valid internal domain', () => {
const testURL = 'https://serverXY/mattermost';
assert.equal(urlUtils.isValidURL(testURL), true);
});
it('should be true for a valid, non-https internal domain', () => {
const testURL = 'http://mattermost.local';
assert.equal(urlUtils.isValidURL(testURL), true);
});
it('should be true for a valid, non-https, ip address with port number', () => {
const testURL = 'http://localhost:8065';
assert.equal(urlUtils.isValidURL(testURL), true);
});
});
describe('isValidURI', () => {
it('should be true for a deeplink url', () => {
const testURL = 'mattermost://community-release.mattermost.com/core/channels/developers';
assert.equal(urlUtils.isValidURI(testURL), true);
});
it('should be false for a malicious url', () => {
const testURL = String.raw`mattermost:///" --data-dir "\\deans-mbp\mattermost`;
assert.equal(urlUtils.isValidURI(testURL), false);
});
});
describe('isInternalURL', () => {
it('should be false for different hosts', () => {
const currentURL = new URL('http://localhost/team/channel1');
const targetURL = new URL('http://example.com/team/channel2');
const basename = '/';
assert.equal(urlUtils.isInternalURL(targetURL, currentURL, basename), false);
});
it('should be false for same hosts, non-matching basename', () => {
const currentURL = new URL('http://localhost/subpath/team/channel1');
const targetURL = new URL('http://localhost/team/channel2');
const basename = '/subpath';
assert.equal(urlUtils.isInternalURL(targetURL, currentURL, basename), false);
});
it('should be true for same hosts, matching basename', () => {
const currentURL = new URL('http://localhost/subpath/team/channel1');
const targetURL = new URL('http://localhost/subpath/team/channel2');
const basename = '/subpath';
assert.equal(urlUtils.isInternalURL(targetURL, currentURL, basename), true);
});
it('should be true for same hosts, default basename', () => {
const currentURL = new URL('http://localhost/team/channel1');
const targetURL = new URL('http://localhost/team/channel2');
const basename = '/';
assert.equal(urlUtils.isInternalURL(targetURL, currentURL, basename), true);
});
it('should be true for same hosts, default basename, empty target path', () => {
const currentURL = new URL('http://localhost/team/channel1');
const targetURL = new URL('http://localhost/');
const basename = '/';
assert.equal(urlUtils.isInternalURL(targetURL, currentURL, basename), true);
});
});
describe('getHost', () => {
it('should return the origin of a well formed url', () => {
const myurl = 'https://mattermost.com/download';
assert.equal(urlUtils.getHost(myurl), 'https://mattermost.com');
});
it('shoud raise an error on malformed urls', () => {
const myurl = 'http://example.com:-80/';
assert.throws(() => urlUtils.getHost(myurl), Error);
});
});
});

6664
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -43,7 +43,7 @@
"test:e2e:nobuild": "cross-env NODE_ENV=test npm-run-all test:e2e:build test:e2e:run",
"test:e2e:build": "webpack-cli --bail --config webpack.config.test.js",
"test:e2e:run": "electron-mocha -r @babel/register --reporter mocha-circleci-reporter dist/tests/e2e_bundle.js",
"test:unit": "npm-run-all test:unit:build test:unit:run",
"test:unit": "jest",
"test:unit:build": "cross-env NODE_ENV=test webpack-cli --bail --config webpack.config.test.js",
"test:unit:run": "cross-env NODE_ENV=test mocha --reporter mocha-circleci-reporter dist/tests/test_bundle.js",
"package:all": "cross-env NODE_ENV=production npm-run-all check-build-config package:windows package:mac package:mac-universal package:linux",
@ -59,6 +59,20 @@
"check-build-config:run": "node -r @babel/register scripts/check_build_config.js",
"check-types": "tsc"
},
"jest": {
"moduleDirectories": [
"",
"node_modules"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
],
"testMatch": ["**/src/**/*.test.js"]
},
"devDependencies": {
"@babel/cli": "^7.14.5",
"@babel/core": "^7.2.0",
@ -108,6 +122,7 @@
"eslint-plugin-react": "7.22.0",
"file-loader": "^2.0.0",
"image-webpack-loader": "5.0.0",
"jest": "^27.3.1",
"mdi-react": "^6.2.0",
"mini-css-extract-plugin": "1.6.0",
"mocha": "^5.2.0",

View file

@ -1,8 +0,0 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import deepmerge from 'deepmerge';
export default function deepMergeProxy<T>(x: Partial<T>, y: Partial<T>, options: deepmerge.Options) {
return deepmerge(x, y, options); // due to webpack conversion
}

View file

@ -1,20 +0,0 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
import os from 'os';
const releaseSplit = os.release().split('.');
export default {
major: parseInt(releaseSplit[0], 10),
minor: parseInt(releaseSplit[1], 10),
isLowerThanOrEqualWindows8_1(): boolean {
if (process.platform !== 'win32') {
return false;
}
// consider Windows 7 and later.
return (this.major <= 6 && this.minor <= 3);
},
};

View file

@ -13,20 +13,4 @@ export class MattermostServer {
throw new Error('Invalid url for creating a server');
}
}
getServerInfo = () => {
// does the server have a subpath?
const normalizedPath = this.url.pathname.toLowerCase();
const subpath = normalizedPath.endsWith('/') ? normalizedPath : `${normalizedPath}/`;
return {origin: this.url.origin, subpath, url: this.url.toString()};
}
sameOrigin = (otherURL: string) => {
const parsedUrl = urlUtils.parseURL(otherURL);
return parsedUrl && this.url.origin === parsedUrl.origin;
}
equals = (otherServer: MattermostServer) => {
return (this.name === otherServer.name) && (this.url.toString() === otherServer.url.toString());
}
}

View file

@ -23,6 +23,31 @@ export const DEFAULT_WINDOW_HEIGHT = 700;
export const MINIMUM_WINDOW_WIDTH = 700;
export const MINIMUM_WINDOW_HEIGHT = 240;
// supported custom login paths (oath, saml)
export const customLoginRegexPaths = [
/^\/oauth\/authorize$/i,
/^\/oauth\/deauthorize$/i,
/^\/oauth\/access_token$/i,
/^\/oauth\/[A-Za-z0-9]+\/complete$/i,
/^\/oauth\/[A-Za-z0-9]+\/login$/i,
/^\/oauth\/[A-Za-z0-9]+\/signup$/i,
/^\/api\/v3\/oauth\/[A-Za-z0-9]+\/complete$/i,
/^\/signup\/[A-Za-z0-9]+\/complete$/i,
/^\/login\/[A-Za-z0-9]+\/complete$/i,
/^\/login\/sso\/saml$/i,
];
export const nonTeamUrlPaths = [
'plugins',
'signup',
'login',
'admin',
'channel',
'post',
'oauth',
'admin_console',
];
export const localeTranslations: Record<string, string> = {
'af': 'Afrikaans',
'af-ZA': 'Afrikaans (South Africa)',

View file

@ -2,17 +2,415 @@
// See LICENSE.txt for license information.
'use strict';
import assert from 'assert';
import urlUtils, {getFormattedPathName, isUrlType, equalUrlsIgnoringSubpath, equalUrlsWithSubpath} from 'common/utils/url';
import urlUtils from 'common/utils/url';
jest.mock('common/tabs/TabView', () => ({
getServerView: (srv, tab) => {
return {
name: `${srv.name}_${tab.name}`,
url: `${srv.url}${srv.url.toString().endsWith('/') ? '' : '/'}${tab.name.split('-')[1] || ''}`,
};
},
}));
describe('common/utils/url', () => {
describe('isValidURL', () => {
it('should be true for a valid web url', () => {
const testURL = 'https://developers.mattermost.com/';
expect(urlUtils.isValidURL(testURL)).toBe(true);
});
it('should be true for a valid, non-https web url', () => {
const testURL = 'http://developers.mattermost.com/';
expect(urlUtils.isValidURL(testURL)).toBe(true);
});
it('should be true for an invalid, self-defined, top-level domain', () => {
const testURL = 'https://www.example.x';
expect(urlUtils.isValidURL(testURL)).toBe(true);
});
it('should be true for a file download url', () => {
const testURL = 'https://community.mattermost.com/api/v4/files/ka3xbfmb3ffnmgdmww8otkidfw?download=1';
expect(urlUtils.isValidURL(testURL)).toBe(true);
});
it('should be true for a permalink url', () => {
const testURL = 'https://community.mattermost.com/test-channel/pl/pdqowkij47rmbyk78m5hwc7r6r';
expect(urlUtils.isValidURL(testURL)).toBe(true);
});
it('should be true for a valid, internal domain', () => {
const testURL = 'https://mattermost.company-internal';
expect(urlUtils.isValidURL(testURL)).toBe(true);
});
it('should be true for a second, valid internal domain', () => {
const testURL = 'https://serverXY/mattermost';
expect(urlUtils.isValidURL(testURL)).toBe(true);
});
it('should be true for a valid, non-https internal domain', () => {
const testURL = 'http://mattermost.local';
expect(urlUtils.isValidURL(testURL)).toBe(true);
});
it('should be true for a valid, non-https, ip address with port number', () => {
const testURL = 'http://localhost:8065';
expect(urlUtils.isValidURL(testURL)).toBe(true);
});
});
describe('isValidURI', () => {
it('should be true for a deeplink url', () => {
const testURL = 'mattermost://community-release.mattermost.com/core/channels/developers';
expect(urlUtils.isValidURI(testURL)).toBe(true);
});
it('should be false for a malicious url', () => {
const testURL = String.raw`mattermost:///" --data-dir "\\deans-mbp\mattermost`;
expect(urlUtils.isValidURI(testURL)).toBe(false);
});
});
describe('getHost', () => {
it('should return the origin of a well formed url', () => {
const myurl = 'https://mattermost.com/download';
expect(urlUtils.getHost(myurl)).toBe('https://mattermost.com');
});
it('shoud raise an error on malformed urls', () => {
const myurl = 'http://example.com:-80/';
expect(() => {
urlUtils.getHost(myurl);
}).toThrow(SyntaxError);
});
});
describe('URL', () => {
describe('parseURL', () => {
it('should return the URL if it is already a URL', () => {
const url = new URL('http://mattermost.com');
expect(urlUtils.parseURL(url)).toBe(url);
});
it('should return undefined when a bad url is passed', () => {
const badURL = 'not-a-real-url-at-all';
expect(urlUtils.parseURL(badURL)).toBe(undefined);
});
it('should remove duplicate slashes in a URL when parsing', () => {
const urlWithExtraSlashes = 'https://mattermost.com//sub//path//example';
const parsedURL = urlUtils.parseURL(urlWithExtraSlashes);
assert.strictEqual(parsedURL.toString(), 'https://mattermost.com/sub/path/example');
expect(parsedURL.toString()).toBe('https://mattermost.com/sub/path/example');
});
});
describe('isInternalURL', () => {
it('should return false on different hosts', () => {
const baseURL = new URL('http://mattermost.com');
const externalURL = new URL('http://google.com');
expect(urlUtils.isInternalURL(externalURL, baseURL)).toBe(false);
});
it('should return false on different ports', () => {
const baseURL = new URL('http://mattermost.com:8080');
const externalURL = new URL('http://mattermost.com:9001');
expect(urlUtils.isInternalURL(externalURL, baseURL)).toBe(false);
});
it('should return false on different subpaths', () => {
const baseURL = new URL('http://mattermost.com/sub/path/');
const externalURL = new URL('http://mattermost.com/different/sub/path');
expect(urlUtils.isInternalURL(externalURL, baseURL)).toBe(false);
});
it('should return true if matching', () => {
const baseURL = new URL('http://mattermost.com/');
const externalURL = new URL('http://mattermost.com');
expect(urlUtils.isInternalURL(externalURL, baseURL)).toBe(true);
});
it('should return true if matching with subpath', () => {
const baseURL = new URL('http://mattermost.com/sub/path/');
const externalURL = new URL('http://mattermost.com/sub/path');
expect(urlUtils.isInternalURL(externalURL, baseURL)).toBe(true);
});
it('should return true if subpath of', () => {
const baseURL = new URL('http://mattermost.com/');
const externalURL = new URL('http://mattermost.com/sub/path');
expect(urlUtils.isInternalURL(externalURL, baseURL)).toBe(true);
});
});
describe('getFormattedPathName', () => {
it('should format all to lower case', () => {
const unformattedPathName = '/aAbBbB/cC/DdeR/';
expect(getFormattedPathName(unformattedPathName)).toBe('/aabbbb/cc/dder/');
});
it('should add trailing slash', () => {
const unformattedPathName = '/aAbBbB/cC/DdeR';
expect(getFormattedPathName(unformattedPathName)).toBe('/aabbbb/cc/dder/');
});
});
describe('isUrlType', () => {
const serverURL = new URL('http://mattermost.com');
const urlType = 'url-type';
it('should identify base url', () => {
const adminURL = new URL(`http://mattermost.com/${urlType}`);
expect(isUrlType('url-type', serverURL, adminURL)).toBe(true);
});
it('should identify url of correct type', () => {
const adminURL = new URL(`http://mattermost.com/${urlType}/some/path`);
expect(isUrlType('url-type', serverURL, adminURL)).toBe(true);
});
it('should not identify other url', () => {
const adminURL = new URL('http://mattermost.com/some/other/path');
expect(isUrlType('url-type', serverURL, adminURL)).toBe(false);
});
});
describe('getView', () => {
const servers = [
{
name: 'server-1',
url: 'http://server-1.com',
tabs: [
{
name: 'tab',
},
{
name: 'tab-type1',
},
{
name: 'tab-type2',
},
],
},
{
name: 'server-2',
url: 'http://server-2.com/subpath',
tabs: [
{
name: 'tab-type1',
},
{
name: 'tab-type2',
},
{
name: 'tab',
},
],
},
];
it('should match the correct server - base URL', () => {
const inputURL = new URL('http://server-1.com');
expect(urlUtils.getView(inputURL, servers)).toStrictEqual({name: 'server-1_tab', url: 'http://server-1.com/'});
});
it('should match the correct server - base tab', () => {
const inputURL = new URL('http://server-1.com/team');
expect(urlUtils.getView(inputURL, servers)).toStrictEqual({name: 'server-1_tab', url: 'http://server-1.com/'});
});
it('should match the correct server - different tab', () => {
const inputURL = new URL('http://server-1.com/type1/app');
expect(urlUtils.getView(inputURL, servers)).toStrictEqual({name: 'server-1_tab-type1', url: 'http://server-1.com/type1'});
});
it('should return undefined for server with subpath and URL without', () => {
const inputURL = new URL('http://server-2.com');
expect(urlUtils.getView(inputURL, servers)).toBe(undefined);
});
it('should return undefined for server with subpath and URL with wrong subpath', () => {
const inputURL = new URL('http://server-2.com/different/subpath');
expect(urlUtils.getView(inputURL, servers)).toBe(undefined);
});
it('should match the correct server with a subpath - base URL', () => {
const inputURL = new URL('http://server-2.com/subpath');
expect(urlUtils.getView(inputURL, servers)).toStrictEqual({name: 'server-2_tab', url: 'http://server-2.com/subpath/'});
});
it('should match the correct server with a subpath - base tab', () => {
const inputURL = new URL('http://server-2.com/subpath/team');
expect(urlUtils.getView(inputURL, servers)).toStrictEqual({name: 'server-2_tab', url: 'http://server-2.com/subpath/'});
});
it('should match the correct server with a subpath - different tab', () => {
const inputURL = new URL('http://server-2.com/subpath/type2/team');
expect(urlUtils.getView(inputURL, servers)).toStrictEqual({name: 'server-2_tab-type2', url: 'http://server-2.com/subpath/type2'});
});
it('should return undefined for wrong server', () => {
const inputURL = new URL('http://server-3.com');
expect(urlUtils.getView(inputURL, servers)).toBe(undefined);
});
});
describe('equalUrls', () => {
it('base urls', () => {
const url1 = new URL('http://server-1.com');
const url2 = new URL('http://server-1.com');
expect(equalUrlsIgnoringSubpath(url1, url2)).toBe(true);
expect(equalUrlsWithSubpath(url1, url2)).toBe(true);
});
it('different urls', () => {
const url1 = new URL('http://server-1.com');
const url2 = new URL('http://server-2.com');
expect(equalUrlsIgnoringSubpath(url1, url2)).toBe(false);
expect(equalUrlsWithSubpath(url1, url2)).toBe(false);
});
it('same host, different subpath', () => {
const url1 = new URL('http://server-1.com/subpath');
const url2 = new URL('http://server-1.com');
expect(equalUrlsIgnoringSubpath(url1, url2)).toBe(true);
expect(equalUrlsWithSubpath(url1, url2)).toBe(false);
});
it('same host and subpath', () => {
const url1 = new URL('http://server-1.com/subpath');
const url2 = new URL('http://server-1.com/subpath');
expect(equalUrlsIgnoringSubpath(url1, url2)).toBe(true);
expect(equalUrlsWithSubpath(url1, url2)).toBe(true);
});
it('same host, different URL scheme', () => {
const url1 = new URL('http://server-1.com');
const url2 = new URL('mattermost://server-1.com');
expect(equalUrlsIgnoringSubpath(url1, url2)).toBe(false);
expect(equalUrlsWithSubpath(url1, url2)).toBe(false);
});
it('same host, different URL scheme, with ignore scheme', () => {
const url1 = new URL('http://server-1.com');
const url2 = new URL('mattermost://server-1.com');
expect(equalUrlsIgnoringSubpath(url1, url2, true)).toBe(true);
expect(equalUrlsWithSubpath(url1, url2, true)).toBe(true);
});
it('same host, different ports', () => {
const url1 = new URL('http://server-1.com:8080');
const url2 = new URL('http://server-1.com');
expect(equalUrlsIgnoringSubpath(url1, url2, true)).toBe(false);
expect(equalUrlsWithSubpath(url1, url2, true)).toBe(false);
});
});
describe('isCustomLoginURL', () => {
it('should match correct URL', () => {
expect(urlUtils.isCustomLoginURL(
'http://server.com/oauth/authorize',
{
url: 'http://server.com',
},
[
{
name: 'a',
url: 'http://server.com',
tabs: [
{
name: 'tab',
},
],
},
])).toBe(true);
});
it('should not match incorrect URL', () => {
expect(urlUtils.isCustomLoginURL(
'http://server.com/oauth/notauthorize',
{
url: 'http://server.com',
},
[
{
name: 'a',
url: 'http://server.com',
tabs: [
{
name: 'tab',
},
],
},
])).toBe(false);
});
it('should not match base URL', () => {
expect(urlUtils.isCustomLoginURL(
'http://server.com/',
{
url: 'http://server.com',
},
[
{
name: 'a',
url: 'http://server.com',
tabs: [
{
name: 'tab',
},
],
},
])).toBe(false);
});
it('should match with subpath', () => {
expect(urlUtils.isCustomLoginURL(
'http://server.com/subpath/oauth/authorize',
{
url: 'http://server.com/subpath',
},
[
{
name: 'a',
url: 'http://server.com/subpath',
tabs: [
{
name: 'tab',
},
],
},
])).toBe(true);
});
it('should not match with different subpath', () => {
expect(urlUtils.isCustomLoginURL(
'http://server.com/subpath/oauth/authorize',
{
url: 'http://server.com/different/subpath',
},
[
{
name: 'a',
url: 'http://server.com/different/subpath',
tabs: [
{
name: 'tab',
},
],
},
])).toBe(false);
});
it('should not match with oauth subpath', () => {
expect(urlUtils.isCustomLoginURL(
'http://server.com/oauth/authorize',
{
url: 'http://server.com/oauth/authorize',
},
[
{
name: 'a',
url: 'http://server.com/oauth/authorize',
tabs: [
{
name: 'tab',
},
],
},
])).toBe(false);
});
});
});

View file

@ -6,28 +6,10 @@ import {isHttpsUri, isHttpUri, isUri} from 'valid-url';
import {TeamWithTabs} from 'types/config';
import {ServerFromURL} from 'types/utils';
import buildConfig from '../config/buildConfig';
import {MattermostServer} from '../servers/MattermostServer';
import {getServerView} from '../tabs/TabView';
// supported custom login paths (oath, saml)
const customLoginRegexPaths = [
/^\/oauth\/authorize$/i,
/^\/oauth\/deauthorize$/i,
/^\/oauth\/access_token$/i,
/^\/oauth\/[A-Za-z0-9]+\/complete$/i,
/^\/oauth\/[A-Za-z0-9]+\/login$/i,
/^\/oauth\/[A-Za-z0-9]+\/signup$/i,
/^\/api\/v3\/oauth\/[A-Za-z0-9]+\/complete$/i,
/^\/signup\/[A-Za-z0-9]+\/complete$/i,
/^\/login\/[A-Za-z0-9]+\/complete$/i,
/^\/login\/sso\/saml$/i,
];
function getDomain(inputURL: URL | string) {
const parsedURL = parseURL(inputURL);
return parsedURL?.origin;
}
import buildConfig from 'common/config/buildConfig';
import {MattermostServer} from 'common/servers/MattermostServer';
import {getServerView} from 'common/tabs/TabView';
import {customLoginRegexPaths, nonTeamUrlPaths} from 'common/utils/constants';
function isValidURL(testURL: string) {
return Boolean(isHttpUri(testURL) || isHttpsUri(testURL)) && Boolean(parseURL(testURL));
@ -53,18 +35,17 @@ function getHost(inputURL: URL | string) {
if (parsedURL) {
return parsedURL.origin;
}
throw new Error(`Couldn't parse url: ${inputURL}`);
throw new SyntaxError(`Couldn't parse url: ${inputURL}`);
}
// isInternalURL determines if the target url is internal to the application.
// - currentURL is the current url inside the webview
// - basename is the global export from the Mattermost application defining the subpath, if any
function isInternalURL(targetURL: URL, currentURL: URL, basename = '/') {
function isInternalURL(targetURL: URL, currentURL: URL) {
if (targetURL.host !== currentURL.host) {
return false;
}
if (!(targetURL.pathname || '/').startsWith(basename)) {
if (!equalUrlsWithSubpath(targetURL, currentURL) && !(targetURL.pathname || '/').startsWith(currentURL.pathname)) {
return false;
}
@ -84,7 +65,7 @@ function getServerInfo(serverUrl: URL | string) {
}
export function getFormattedPathName(pn: string) {
return pn.endsWith('/') ? pn.toLowerCase() : `${pn}/`;
return pn.endsWith('/') ? pn.toLowerCase() : `${pn.toLowerCase()}/`;
}
function getManagedResources() {
@ -95,72 +76,46 @@ function getManagedResources() {
return buildConfig.managedResources || [];
}
function isAdminUrl(serverUrl: URL | string, inputUrl: URL | string) {
const parsedURL = parseURL(inputUrl);
const server = getServerInfo(serverUrl);
if (!parsedURL || !server || (!equalUrlsIgnoringSubpath(server.url, parsedURL))) {
return null;
}
return (parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}/admin_console/`) ||
parsedURL.pathname.toLowerCase().startsWith('/admin_console/'));
}
function isTeamUrl(serverUrl: URL | string, inputUrl: URL | string, withApi?: boolean) {
if (!serverUrl || !inputUrl) {
export function isUrlType(urlType: string, serverUrl: URL | string, inputURL: URL | string) {
if (!serverUrl || !inputURL) {
return false;
}
const parsedURL = parseURL(inputUrl);
const parsedURL = parseURL(inputURL);
const server = getServerInfo(serverUrl);
if (!parsedURL || !server || (!equalUrlsIgnoringSubpath(server.url, parsedURL))) {
return null;
return false;
}
return (getFormattedPathName(parsedURL.pathname).startsWith(`${server.subpath}${urlType}/`) ||
getFormattedPathName(parsedURL.pathname).startsWith(`/${urlType}/`));
}
function isAdminUrl(serverUrl: URL | string, inputURL: URL | string) {
return isUrlType('admin_console', serverUrl, inputURL);
}
function isTeamUrl(serverUrl: URL | string, inputURL: URL | string, withApi?: boolean) {
const parsedURL = parseURL(inputURL);
const server = getServerInfo(serverUrl);
if (!parsedURL || !server || (!equalUrlsIgnoringSubpath(server.url, parsedURL))) {
return false;
}
// pre process nonTeamUrlPaths
let nonTeamUrlPaths = [
'plugins',
'signup',
'login',
'admin',
'channel',
'post',
'oauth',
'admin_console',
];
const managedResources = getManagedResources();
nonTeamUrlPaths = nonTeamUrlPaths.concat(managedResources);
const paths = [...getManagedResources(), ...nonTeamUrlPaths];
if (withApi) {
nonTeamUrlPaths.push('api');
paths.push('api');
}
return !(nonTeamUrlPaths.some((testPath) => (
parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}${testPath}/`) ||
parsedURL.pathname.toLowerCase().startsWith(`/${testPath}/`))));
return !(paths.some((testPath) => isUrlType(testPath, serverUrl, inputURL)));
}
function isPluginUrl(serverUrl: URL | string, inputURL: URL | string) {
const server = getServerInfo(serverUrl);
const parsedURL = parseURL(inputURL);
if (!parsedURL || !server) {
return false;
}
return (
equalUrlsIgnoringSubpath(server.url, parsedURL) &&
(parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}plugins/`) ||
parsedURL.pathname.toLowerCase().startsWith('/plugins/')));
return isUrlType('plugins', serverUrl, inputURL);
}
function isManagedResource(serverUrl: URL | string, inputURL: URL | string) {
const server = getServerInfo(serverUrl);
const parsedURL = parseURL(inputURL);
if (!parsedURL || !server) {
return false;
}
const managedResources = getManagedResources();
return (
equalUrlsIgnoringSubpath(server.url, parsedURL) && managedResources && managedResources.length &&
managedResources.some((managedResource) => (parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}${managedResource}/`) || parsedURL.pathname.toLowerCase().startsWith(`/${managedResource}/`))));
const paths = [...getManagedResources()];
return paths.some((testPath) => isUrlType(testPath, serverUrl, inputURL));
}
function getView(inputURL: URL | string, teams: TeamWithTabs[], ignoreScheme = false): ServerFromURL | undefined {
@ -172,20 +127,27 @@ function getView(inputURL: URL | string, teams: TeamWithTabs[], ignoreScheme = f
let secondOption;
teams.forEach((team) => {
const srv = new MattermostServer(team.name, team.url);
team.tabs.forEach((tab) => {
// sort by length so that we match the highest specificity last
const filteredTabs = team.tabs.map((tab) => {
const tabView = getServerView(srv, tab);
const parsedServerUrl = parseURL(tabView.url);
if (parsedServerUrl) {
return {tabView, parsedServerUrl};
});
filteredTabs.sort((a, b) => a.tabView.url.toString().length - b.tabView.url.toString().length);
filteredTabs.forEach((tab) => {
if (tab.parsedServerUrl) {
// check server and subpath matches (without subpath pathname is \ so it always matches)
if (equalUrlsWithSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
firstOption = {name: tabView.name, url: parsedServerUrl.toString()};
if (getFormattedPathName(tab.parsedServerUrl.pathname) !== '/' && equalUrlsWithSubpath(tab.parsedServerUrl, parsedURL, ignoreScheme)) {
firstOption = {name: tab.tabView.name, url: tab.parsedServerUrl.toString()};
}
if (equalUrlsIgnoringSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
if (getFormattedPathName(tab.parsedServerUrl.pathname) === '/' && equalUrlsIgnoringSubpath(tab.parsedServerUrl, parsedURL, ignoreScheme)) {
// in case the user added something on the path that doesn't really belong to the server
// there might be more than one that matches, but we can't differentiate, so last one
// is as good as any other in case there is no better match (e.g.: two subpath servers with the same origin)
// e.g.: https://community.mattermost.com/core
secondOption = {name: tabView.name, url: parsedServerUrl.toString()};
secondOption = {name: tab.tabView.name, url: tab.parsedServerUrl.toString()};
}
}
});
@ -194,14 +156,14 @@ function getView(inputURL: URL | string, teams: TeamWithTabs[], ignoreScheme = f
}
// next two functions are defined to clarify intent
function equalUrlsWithSubpath(url1: URL, url2: URL, ignoreScheme?: boolean) {
export function equalUrlsWithSubpath(url1: URL, url2: URL, ignoreScheme?: boolean) {
if (ignoreScheme) {
return url1.host === url2.host && url2.pathname.toLowerCase().startsWith(url1.pathname.toLowerCase());
return url1.host === url2.host && getFormattedPathName(url2.pathname).startsWith(getFormattedPathName(url1.pathname));
}
return url1.origin === url2.origin && url2.pathname.toLowerCase().startsWith(url1.pathname.toLowerCase());
return url1.origin === url2.origin && getFormattedPathName(url2.pathname).startsWith(getFormattedPathName(url1.pathname));
}
function equalUrlsIgnoringSubpath(url1: URL, url2: URL, ignoreScheme?: boolean) {
export function equalUrlsIgnoringSubpath(url1: URL, url2: URL, ignoreScheme?: boolean) {
if (ignoreScheme) {
return url1.host.toLowerCase() === url2.host.toLowerCase();
}
@ -227,27 +189,18 @@ function isCustomLoginURL(url: URL | string, server: ServerFromURL, teams: TeamW
return false;
}
const urlPath = parsedURL.pathname;
if (subpath !== '' && subpath !== '/' && urlPath.startsWith(subpath)) {
const replacement = subpath.endsWith('/') ? '/' : '';
const replacedPath = urlPath.replace(subpath, replacement);
for (const regexPath of customLoginRegexPaths) {
if (replacedPath.match(regexPath)) {
return true;
}
}
}
// if there is no subpath, or we are adding the team and got redirected to the real server it'll be caught here
const replacement = subpath.endsWith('/') ? '/' : '';
const replacedPath = urlPath.replace(subpath, replacement);
for (const regexPath of customLoginRegexPaths) {
if (urlPath.match(regexPath)) {
if (replacedPath.match(regexPath)) {
return true;
}
}
return false;
}
export default {
getDomain,
isValidURL,
isValidURI,
isInternalURL,

View file

@ -0,0 +1,91 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Utils from 'common/utils/util';
describe('common/utils/util', () => {
describe('shorten', () => {
it('should shorten based on max', () => {
const string = '123456789012345678901234567890';
expect(Utils.shorten(string, 10)).toBe('1234567...');
});
it('should use DEFAULT_MAX for max < 4', () => {
const string = '123456789012345678901234567890';
expect(Utils.shorten(string, 3)).toBe('12345678901234567...');
});
});
describe('isVersionGreaterThanOrEqualTo', () => {
test('should consider two empty versions as equal', () => {
const a = '';
const b = '';
expect(Utils.isVersionGreaterThanOrEqualTo(a, b)).toEqual(true);
});
test('should consider different strings without components as equal', () => {
const a = 'not a server version';
const b = 'also not a server version';
expect(Utils.isVersionGreaterThanOrEqualTo(a, b)).toEqual(true);
});
test('should consider different malformed versions normally (not greater than case)', () => {
const a = '1.2.3';
const b = '1.2.4';
expect(Utils.isVersionGreaterThanOrEqualTo(a, b)).toEqual(false);
});
test('should consider different malformed versions normally (greater than case)', () => {
const a = '1.2.4';
const b = '1.2.3';
expect(Utils.isVersionGreaterThanOrEqualTo(a, b)).toEqual(true);
});
test('should work correctly for different numbers of digits', () => {
const a = '10.0.1';
const b = '4.8.0';
expect(Utils.isVersionGreaterThanOrEqualTo(a, b)).toEqual(true);
});
test('should consider an empty version as not greater than or equal', () => {
const a = '';
const b = '4.7.1.dev.c51676437bc02ada78f3a0a0a2203c60.true';
expect(Utils.isVersionGreaterThanOrEqualTo(a, b)).toEqual(false);
});
test('should consider the same versions equal', () => {
const a = '4.7.1.dev.c51676437bc02ada78f3a0a0a2203c60.true';
const b = '4.7.1.dev.c51676437bc02ada78f3a0a0a2203c60.true';
expect(Utils.isVersionGreaterThanOrEqualTo(a, b)).toEqual(true);
});
test('should consider different release versions (not greater than case)', () => {
const a = '4.7.0.12.c51676437bc02ada78f3a0a0a2203c60.true';
const b = '4.7.1.12.c51676437bc02ada78f3a0a0a2203c60.true';
expect(Utils.isVersionGreaterThanOrEqualTo(a, b)).toEqual(false);
});
test('should consider different release versions (greater than case)', () => {
const a = '4.7.1.12.c51676437bc02ada78f3a0a0a2203c60.true';
const b = '4.7.0.12.c51676437bc02ada78f3a0a0a2203c60.true';
expect(Utils.isVersionGreaterThanOrEqualTo(a, b)).toEqual(true);
});
test('should consider different build numbers unequal', () => {
const a = '4.7.1.12.c51676437bc02ada78f3a0a0a2203c60.true';
const b = '4.7.1.13.c51676437bc02ada78f3a0a0a2203c60.true';
expect(Utils.isVersionGreaterThanOrEqualTo(a, b)).toEqual(false);
});
test('should ignore different config hashes', () => {
const a = '4.7.1.12.c51676437bc02ada78f3a0a0a2203c60.true';
const b = '4.7.1.12.c51676437bc02ada78f3a0a0a2203c61.true';
expect(Utils.isVersionGreaterThanOrEqualTo(a, b)).toEqual(true);
});
test('should ignore different licensed statuses', () => {
const a = '4.7.1.13.c51676437bc02ada78f3a0a0a2203c60.false';
const b = '4.7.1.12.c51676437bc02ada78f3a0a0a2203c60.true';
expect(Utils.isVersionGreaterThanOrEqualTo(a, b)).toEqual(true);
});
});
});

View file

@ -1,8 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import assert from 'assert';
import UserActivityMonitor from '../../../src/main/UserActivityMonitor';
import UserActivityMonitor from './UserActivityMonitor';
describe('UserActivityMonitor', () => {
describe('updateIdleTime', () => {
@ -10,7 +9,7 @@ describe('UserActivityMonitor', () => {
const userActivityMonitor = new UserActivityMonitor();
const idleTime = Math.round(Date.now() / 1000);
userActivityMonitor.updateIdleTime(idleTime);
assert.equal(userActivityMonitor.userIdleTime, idleTime);
expect(userActivityMonitor.userIdleTime).toBe(idleTime);
});
});
@ -23,11 +22,11 @@ describe('UserActivityMonitor', () => {
it('should set user status to active', () => {
userActivityMonitor.setActivityState(true);
assert.equal(userActivityMonitor.userIsActive, true);
expect(userActivityMonitor.userIsActive).toBe(true);
});
it('should set user status to inactive', () => {
userActivityMonitor.setActivityState(false);
assert.equal(userActivityMonitor.userIsActive, false);
expect(userActivityMonitor.userIsActive).toBe(false);
});
});
@ -40,28 +39,28 @@ describe('UserActivityMonitor', () => {
it('should emit a non-system triggered status event indicating a user is active', () => {
userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => {
assert.equal(userIsActive && !isSystemEvent, true);
expect(userIsActive && !isSystemEvent).toBe(true);
});
userActivityMonitor.setActivityState(true, false);
});
it('should emit a non-system triggered status event indicating a user is inactive', () => {
userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => {
assert.equal(!userIsActive && !isSystemEvent, true);
expect(!userIsActive && !isSystemEvent).toBe(true);
});
userActivityMonitor.setActivityState(false, false);
});
it('should emit a system triggered status event indicating a user is active', () => {
userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => {
assert.equal(userIsActive && isSystemEvent, true);
expect(userIsActive && isSystemEvent).toBe(true);
});
userActivityMonitor.setActivityState(true, true);
});
it('should emit a system triggered status event indicating a user is inactive', () => {
userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => {
assert.equal(!userIsActive && isSystemEvent, true);
expect(!userIsActive && isSystemEvent).toBe(true);
});
userActivityMonitor.setActivityState(false, true);
});

View file

@ -1,12 +1,14 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import os from 'os';
import path from 'path';
import {app, Notification} from 'electron';
import {MentionOptions} from 'types/notification';
import osVersion from 'common/osVersion';
import Utils from 'common/utils/util';
const assetsDir = path.resolve(app.getAppPath(), 'assets');
const appIconURL = path.resolve(assetsDir, 'appicon_48.png');
@ -30,7 +32,7 @@ export class Mention extends Notification {
// Notification Center shows app's icon, so there were two icons on the notification.
Reflect.deleteProperty(options, 'icon');
}
const isWin7 = (process.platform === 'win32' && osVersion.isLowerThanOrEqualWindows8_1() && DEFAULT_WIN7);
const isWin7 = (process.platform === 'win32' && !Utils.isVersionGreaterThanOrEqualTo(os.version(), '6.3') && DEFAULT_WIN7);
const customSound = String(!options.silent && ((options.data && options.data.soundName !== 'None' && options.data.soundName) || isWin7));
if (customSound) {
options.silent = true;

View file

@ -2,10 +2,12 @@
// See LICENSE.txt for license information.
'use strict';
import assert from 'assert';
import TrustedOriginsStore from 'main/trustedOrigins';
import {BASIC_AUTH_PERMISSION} from 'common/permissions';
import TrustedOriginsStore from 'main/trustedOrigins.ts';
import {BASIC_AUTH_PERMISSION} from 'common/permissions.ts';
jest.mock('electron-log', () => ({
error: jest.fn(),
}));
function mockTOS(fileName, returnvalue) {
const tos = new TrustedOriginsStore(fileName);
@ -20,24 +22,28 @@ describe('Trusted Origins', () => {
it('should be empty if there is no file', () => {
const tos = mockTOS('emptyfile', null);
tos.load();
assert.deepEqual(tos.data.size, 0);
expect(tos.data.size).toStrictEqual(0);
});
it('should throw an error if data isn\'t an object', () => {
const tos = mockTOS('notobject', 'this is not my object!');
assert.throws(tos.load, SyntaxError);
expect(() => {
tos.load();
}).toThrow(SyntaxError);
});
it('should throw an error if data isn\'t in the expected format', () => {
const tos = mockTOS('badobject', '{"https://mattermost.com": "this is not my object!"}');
assert.throws(tos.load, /^Error: Provided TrustedOrigins file does not validate, using defaults instead\.$/);
expect(() => {
tos.load();
}).toThrow(/^Provided TrustedOrigins file does not validate, using defaults instead\.$/);
});
it('should drop keys that aren\'t urls', () => {
const tos = mockTOS('badobject2', `{"this is not an uri": {"${BASIC_AUTH_PERMISSION}": true}}`);
tos.load();
assert.equal(typeof tos.data['this is not an uri'], 'undefined');
expect(typeof tos.data['this is not an uri']).toBe('undefined');
});
it('should contain valid data if everything goes right', () => {
@ -47,7 +53,7 @@ describe('Trusted Origins', () => {
}};
const tos = mockTOS('okfile', JSON.stringify(value));
tos.load();
assert.deepEqual(Object.fromEntries(tos.data.entries()), value);
expect(Object.fromEntries(tos.data.entries())).toStrictEqual(value);
});
});
describe('validate testing permissions', () => {
@ -62,19 +68,19 @@ describe('Trusted Origins', () => {
const tos = mockTOS('permission_test', JSON.stringify(value));
tos.load();
it('tos should contain 2 elements', () => {
assert.equal(tos.data.size, 2);
expect(tos.data.size).toBe(2);
});
it('should say ok if the permission is set', () => {
assert.equal(tos.checkPermission('https://mattermost.com', BASIC_AUTH_PERMISSION), true);
expect(tos.checkPermission('https://mattermost.com', BASIC_AUTH_PERMISSION)).toBe(true);
});
it('should say ko if the permission is set to false', () => {
assert.equal(tos.checkPermission('https://notmattermost.com', BASIC_AUTH_PERMISSION), false);
expect(tos.checkPermission('https://notmattermost.com', BASIC_AUTH_PERMISSION)).toBe(false);
});
it('should say ko if the uri is not set', () => {
assert.equal(tos.checkPermission('https://undefined.com', BASIC_AUTH_PERMISSION), null);
expect(tos.checkPermission('https://undefined.com', BASIC_AUTH_PERMISSION)).toBe(undefined);
});
it('should say null if the permission is unknown', () => {
assert.equal(tos.checkPermission('https://mattermost.com'), null);
expect(tos.checkPermission('https://mattermost.com')).toBe(null);
});
});
@ -90,9 +96,9 @@ describe('Trusted Origins', () => {
const tos = mockTOS('permission_test', JSON.stringify(value));
tos.load();
it('deleting revokes access', () => {
assert.equal(tos.checkPermission('https://mattermost.com', BASIC_AUTH_PERMISSION), true);
expect(tos.checkPermission('https://mattermost.com', BASIC_AUTH_PERMISSION)).toBe(true);
tos.delete('https://mattermost.com');
assert.equal(tos.checkPermission('https://mattermost.com', BASIC_AUTH_PERMISSION), null);
expect(tos.checkPermission('https://mattermost.com', BASIC_AUTH_PERMISSION)).toBe(undefined);
});
});
});

View file

@ -304,7 +304,7 @@ export class MattermostView extends EventEmitter {
}
handleUpdateTarget = (e: Event, url: string) => {
if (!url || !this.tab.server.sameOrigin(url)) {
if (!url || !urlUtils.isInternalURL(new URL(url), this.tab.server.url)) {
this.emit(UPDATE_TARGET_URL, url);
}
}