fix(webviewShared.js): Support all valid URI schemes and add testing (#293)
* Change origin whitelist to allow for all valid URIs - Now supports +, -, and . - Prevent whitelist from matching when preceded by unwanted characters - URI must begin with letter - URI Scheme syntax: https://tools.ietf.org/html/rfc3986#section-3.1 * Add jest testing framework and run it on CI * Add tests for WebViewShared's createOnShouldStartLoadWithRequest
This commit is contained in:
parent
20d5cf06a8
commit
fb78d13120
|
@ -0,0 +1,12 @@
|
||||||
|
module.exports = function (api) {
|
||||||
|
api && api.cache(false);
|
||||||
|
return {
|
||||||
|
env: {
|
||||||
|
test: {
|
||||||
|
presets: [
|
||||||
|
"module:metro-react-native-babel-preset"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -49,6 +49,7 @@ $ yarn add ../react-native-webview && react-native link react-native-webview
|
||||||
- After pulling this repo and installing all dependencies, you can run flow on iOS and Android-specific files using the commands:
|
- After pulling this repo and installing all dependencies, you can run flow on iOS and Android-specific files using the commands:
|
||||||
- `yarn test:ios:flow` for iOS
|
- `yarn test:ios:flow` for iOS
|
||||||
- `yarn test:android:flow` for Android
|
- `yarn test:android:flow` for Android
|
||||||
|
- You can run Jest tests using the command: `yarn test:js`
|
||||||
- If you want to add another React Native platform to this repository, you will need to create another `.flowconfig` for it. If your platform is `example`, copy the main flowconfig and rename it to `.flowconfig.example`. Then edit the config to ignore other platforms, and add `.*/*[.]example.js` to the ignore lists of the other platforms. Then add an entry to `package.json` like this:
|
- If you want to add another React Native platform to this repository, you will need to create another `.flowconfig` for it. If your platform is `example`, copy the main flowconfig and rename it to `.flowconfig.example`. Then edit the config to ignore other platforms, and add `.*/*[.]example.js` to the ignore lists of the other platforms. Then add an entry to `package.json` like this:
|
||||||
- `"test:example:flow": "flow check --flowconfig-name .flowconfig.example"`
|
- `"test:example:flow": "flow check --flowconfig-name .flowconfig.example"`
|
||||||
- Currently you need to install React Native 0.57 to be able to test these types - `flow check` will not pass against 0.56.
|
- Currently you need to install React Native 0.57 to be able to test these types - `flow check` will not pass against 0.56.
|
||||||
|
|
|
@ -0,0 +1,185 @@
|
||||||
|
// For a detailed explanation regarding each configuration property, visit:
|
||||||
|
// https://jestjs.io/docs/en/configuration.html
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// All imported modules in your tests should be mocked automatically
|
||||||
|
// automock: false,
|
||||||
|
|
||||||
|
// Stop running tests after `n` failures
|
||||||
|
// bail: 0,
|
||||||
|
|
||||||
|
// Respect "browser" field in package.json when resolving modules
|
||||||
|
// browser: false,
|
||||||
|
|
||||||
|
// The directory where Jest should store its cached dependency information
|
||||||
|
// cacheDirectory: "/private/var/folders/8f/kgcy219d1dvfvbcqky441_d00000gp/T/jest_dy",
|
||||||
|
|
||||||
|
// Automatically clear mock calls and instances between every test
|
||||||
|
clearMocks: true,
|
||||||
|
|
||||||
|
// Indicates whether the coverage information should be collected while executing the test
|
||||||
|
// collectCoverage: false,
|
||||||
|
|
||||||
|
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||||
|
// collectCoverageFrom: null,
|
||||||
|
|
||||||
|
// The directory where Jest should output its coverage files
|
||||||
|
// coverageDirectory: "coverage",
|
||||||
|
|
||||||
|
// An array of regexp pattern strings used to skip coverage collection
|
||||||
|
// coveragePathIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// A list of reporter names that Jest uses when writing coverage reports
|
||||||
|
// coverageReporters: [
|
||||||
|
// "json",
|
||||||
|
// "text",
|
||||||
|
// "lcov",
|
||||||
|
// "clover"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An object that configures minimum threshold enforcement for coverage results
|
||||||
|
// coverageThreshold: null,
|
||||||
|
|
||||||
|
// A path to a custom dependency extractor
|
||||||
|
// dependencyExtractor: null,
|
||||||
|
|
||||||
|
// Make calling deprecated APIs throw helpful error messages
|
||||||
|
// errorOnDeprecated: false,
|
||||||
|
|
||||||
|
// Force coverage collection from ignored files usin a array of glob patterns
|
||||||
|
// forceCoverageMatch: [],
|
||||||
|
|
||||||
|
// A path to a module which exports an async function that is triggered once before all test suites
|
||||||
|
// globalSetup: null,
|
||||||
|
|
||||||
|
// A path to a module which exports an async function that is triggered once after all test suites
|
||||||
|
// globalTeardown: null,
|
||||||
|
|
||||||
|
// A set of global variables that need to be available in all test environments
|
||||||
|
// globals: {},
|
||||||
|
|
||||||
|
// An array of directory names to be searched recursively up from the requiring module's location
|
||||||
|
// moduleDirectories: [
|
||||||
|
// "node_modules"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of file extensions your modules use
|
||||||
|
// moduleFileExtensions: [
|
||||||
|
// "js",
|
||||||
|
// "json",
|
||||||
|
// "jsx",
|
||||||
|
// "ts",
|
||||||
|
// "tsx",
|
||||||
|
// "node"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// A map from regular expressions to module names that allow to stub out resources with a single module
|
||||||
|
// moduleNameMapper: {},
|
||||||
|
|
||||||
|
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||||
|
// modulePathIgnorePatterns: [],
|
||||||
|
|
||||||
|
// Activates notifications for test results
|
||||||
|
// notify: false,
|
||||||
|
|
||||||
|
// An enum that specifies notification mode. Requires { notify: true }
|
||||||
|
// notifyMode: "failure-change",
|
||||||
|
|
||||||
|
// A preset that is used as a base for Jest's configuration
|
||||||
|
preset: "react-native",
|
||||||
|
|
||||||
|
// Run tests from one or more projects
|
||||||
|
// projects: null,
|
||||||
|
|
||||||
|
// Use this configuration option to add custom reporters to Jest
|
||||||
|
// reporters: undefined,
|
||||||
|
|
||||||
|
// Automatically reset mock state between every test
|
||||||
|
// resetMocks: false,
|
||||||
|
|
||||||
|
// Reset the module registry before running each individual test
|
||||||
|
// resetModules: false,
|
||||||
|
|
||||||
|
// A path to a custom resolver
|
||||||
|
// resolver: null,
|
||||||
|
|
||||||
|
// Automatically restore mock state between every test
|
||||||
|
// restoreMocks: false,
|
||||||
|
|
||||||
|
// The root directory that Jest should scan for tests and modules within
|
||||||
|
// rootDir: null,
|
||||||
|
|
||||||
|
// A list of paths to directories that Jest should use to search for files in
|
||||||
|
// roots: [
|
||||||
|
// "<rootDir>"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// Allows you to use a custom runner instead of Jest's default test runner
|
||||||
|
// runner: "jest-runner",
|
||||||
|
|
||||||
|
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||||
|
// setupFiles: [],
|
||||||
|
|
||||||
|
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||||
|
// setupFilesAfterEnv: [],
|
||||||
|
|
||||||
|
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||||
|
// snapshotSerializers: [],
|
||||||
|
|
||||||
|
// The test environment that will be used for testing
|
||||||
|
testEnvironment: "node",
|
||||||
|
|
||||||
|
// Options that will be passed to the testEnvironment
|
||||||
|
// testEnvironmentOptions: {},
|
||||||
|
|
||||||
|
// Adds a location field to test results
|
||||||
|
// testLocationInResults: false,
|
||||||
|
|
||||||
|
// The glob patterns Jest uses to detect test files
|
||||||
|
// testMatch: [
|
||||||
|
// "**/__tests__/**/*.[jt]s?(x)",
|
||||||
|
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||||
|
// testPathIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||||
|
// testRegex: [],
|
||||||
|
|
||||||
|
// This option allows the use of a custom results processor
|
||||||
|
// testResultsProcessor: null,
|
||||||
|
|
||||||
|
// This option allows use of a custom test runner
|
||||||
|
// testRunner: "jasmine2",
|
||||||
|
|
||||||
|
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||||
|
// testURL: "http://localhost",
|
||||||
|
|
||||||
|
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||||
|
// timers: "real",
|
||||||
|
|
||||||
|
// A map from regular expressions to paths to transformers
|
||||||
|
// transform: null,
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||||
|
// transformIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||||
|
// unmockedModulePathPatterns: undefined,
|
||||||
|
|
||||||
|
// Indicates whether each individual test should be reported during the run
|
||||||
|
// verbose: null,
|
||||||
|
|
||||||
|
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||||
|
// watchPathIgnorePatterns: [],
|
||||||
|
|
||||||
|
// Whether to use watchman for file crawling
|
||||||
|
// watchman: true,
|
||||||
|
};
|
|
@ -19,12 +19,12 @@ import type {
|
||||||
const defaultOriginWhitelist = ['http://*', 'https://*'];
|
const defaultOriginWhitelist = ['http://*', 'https://*'];
|
||||||
|
|
||||||
const extractOrigin = (url: string): string => {
|
const extractOrigin = (url: string): string => {
|
||||||
const result = /^[A-Za-z0-9]+:(\/\/)?[^/]*/.exec(url);
|
const result = /^[A-Za-z][A-Za-z0-9\+\-\.]+:(\/\/)?[^/]*/.exec(url);
|
||||||
return result === null ? '' : result[0];
|
return result === null ? '' : result[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
const originWhitelistToRegex = (originWhitelist: string): string =>
|
const originWhitelistToRegex = (originWhitelist: string): string =>
|
||||||
escapeStringRegexp(originWhitelist).replace(/\\\*/g, '.*');
|
`^${escapeStringRegexp(originWhitelist).replace(/\\\*/g, '.*')}`;
|
||||||
|
|
||||||
const passesWhitelist = (compiledWhitelist: Array<string>, url: string) => {
|
const passesWhitelist = (compiledWhitelist: Array<string>, url: string) => {
|
||||||
const origin = extractOrigin(url);
|
const origin = extractOrigin(url);
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
import { Linking } from 'react-native';
|
||||||
|
|
||||||
|
import {
|
||||||
|
defaultOriginWhitelist,
|
||||||
|
createOnShouldStartLoadWithRequest,
|
||||||
|
} from '../WebViewShared';
|
||||||
|
|
||||||
|
describe('WebViewShared', () => {
|
||||||
|
test('exports defaultOriginWhitelist', () => {
|
||||||
|
expect(defaultOriginWhitelist).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createOnShouldStartLoadWithRequest', () => {
|
||||||
|
const alwaysTrueOnShouldStartLoadWithRequest = (nativeEvent) => {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const alwaysFalseOnShouldStartLoadWithRequest = (nativeEvent) => {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadRequest = jest.fn();
|
||||||
|
|
||||||
|
test('loadRequest is called without onShouldStartLoadWithRequest override', () => {
|
||||||
|
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
|
||||||
|
loadRequest,
|
||||||
|
defaultOriginWhitelist,
|
||||||
|
);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: 'https://www.example.com/', lockIdentifier: 1 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenCalledTimes(0);
|
||||||
|
expect(loadRequest).toHaveBeenCalledWith(true, 'https://www.example.com/', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Linking.openURL is called without onShouldStartLoadWithRequest override', () => {
|
||||||
|
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
|
||||||
|
loadRequest,
|
||||||
|
defaultOriginWhitelist,
|
||||||
|
);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: 'invalid://example.com/', lockIdentifier: 2 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenCalledWith('invalid://example.com/');
|
||||||
|
expect(loadRequest).toHaveBeenCalledWith(false, 'invalid://example.com/', 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadRequest with true onShouldStartLoadWithRequest override is called', () => {
|
||||||
|
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
|
||||||
|
loadRequest,
|
||||||
|
defaultOriginWhitelist,
|
||||||
|
alwaysTrueOnShouldStartLoadWithRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: 'https://www.example.com/', lockIdentifier: 1 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenCalledTimes(0);
|
||||||
|
expect(loadRequest).toHaveBeenLastCalledWith(true, 'https://www.example.com/', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Linking.openURL with true onShouldStartLoadWithRequest override is called for links not passing the whitelist', () => {
|
||||||
|
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
|
||||||
|
loadRequest,
|
||||||
|
defaultOriginWhitelist,
|
||||||
|
alwaysTrueOnShouldStartLoadWithRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: 'invalid://example.com/', lockIdentifier: 1 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenLastCalledWith('invalid://example.com/');
|
||||||
|
expect(loadRequest).toHaveBeenLastCalledWith(true, 'invalid://example.com/', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadRequest with false onShouldStartLoadWithRequest override is called', () => {
|
||||||
|
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
|
||||||
|
loadRequest,
|
||||||
|
defaultOriginWhitelist,
|
||||||
|
alwaysFalseOnShouldStartLoadWithRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: 'https://www.example.com/', lockIdentifier: 1 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenCalledTimes(0);
|
||||||
|
expect(loadRequest).toHaveBeenLastCalledWith(false, 'https://www.example.com/', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadRequest with limited whitelist', () => {
|
||||||
|
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
|
||||||
|
loadRequest,
|
||||||
|
['https://*'],
|
||||||
|
);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: 'https://www.example.com/', lockIdentifier: 1 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenCalledTimes(0);
|
||||||
|
expect(loadRequest).toHaveBeenLastCalledWith(true, 'https://www.example.com/', 1);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: 'http://insecure.com/', lockIdentifier: 2 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenLastCalledWith('http://insecure.com/');
|
||||||
|
expect(loadRequest).toHaveBeenLastCalledWith(false, 'http://insecure.com/', 2);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: 'git+https://insecure.com/', lockIdentifier: 3 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenLastCalledWith('git+https://insecure.com/');
|
||||||
|
expect(loadRequest).toHaveBeenLastCalledWith(false, 'git+https://insecure.com/', 3);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: 'fakehttps://insecure.com/', lockIdentifier: 4 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenLastCalledWith('fakehttps://insecure.com/');
|
||||||
|
expect(loadRequest).toHaveBeenLastCalledWith(false, 'fakehttps://insecure.com/', 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadRequest allows for valid URIs', () => {
|
||||||
|
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
|
||||||
|
loadRequest,
|
||||||
|
['plus+https://*', 'DOT.https://*', 'dash-https://*', '0invalid://*', '+invalid://*'],
|
||||||
|
);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: 'plus+https://www.example.com/', lockIdentifier: 1 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenCalledTimes(0);
|
||||||
|
expect(loadRequest).toHaveBeenLastCalledWith(true, 'plus+https://www.example.com/', 1);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: 'DOT.https://www.example.com/', lockIdentifier: 2 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenCalledTimes(0);
|
||||||
|
expect(loadRequest).toHaveBeenLastCalledWith(true, 'DOT.https://www.example.com/', 2);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: 'dash-https://www.example.com/', lockIdentifier: 3 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenCalledTimes(0);
|
||||||
|
expect(loadRequest).toHaveBeenLastCalledWith(true, 'dash-https://www.example.com/', 3);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: '0invalid://www.example.com/', lockIdentifier: 4 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenLastCalledWith('0invalid://www.example.com/');
|
||||||
|
expect(loadRequest).toHaveBeenLastCalledWith(false, '0invalid://www.example.com/', 4);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: '+invalid://www.example.com/', lockIdentifier: 5 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenLastCalledWith('+invalid://www.example.com/');
|
||||||
|
expect(loadRequest).toHaveBeenLastCalledWith(false, '+invalid://www.example.com/', 5);
|
||||||
|
|
||||||
|
onShouldStartLoadWithRequest({ nativeEvent: { url: 'FAKE+plus+https://www.example.com/', lockIdentifier: 6 } });
|
||||||
|
expect(Linking.openURL).toHaveBeenLastCalledWith('FAKE+plus+https://www.example.com/');
|
||||||
|
expect(loadRequest).toHaveBeenLastCalledWith(false, 'FAKE+plus+https://www.example.com/', 6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,8 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`WebViewShared exports defaultOriginWhitelist 1`] = `
|
||||||
|
Array [
|
||||||
|
"http://*",
|
||||||
|
"https://*",
|
||||||
|
]
|
||||||
|
`;
|
|
@ -11,11 +11,13 @@
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"homepage": "https://github.com/react-native-community/react-native-webview#readme",
|
"homepage": "https://github.com/react-native-community/react-native-webview#readme",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"test:js": "jest",
|
||||||
"test:ios:flow": "flow check",
|
"test:ios:flow": "flow check",
|
||||||
"test:android:flow": "flow check --flowconfig-name .flowconfig.android",
|
"test:android:flow": "flow check --flowconfig-name .flowconfig.android",
|
||||||
"ci:publish": "yarn semantic-release",
|
"ci:publish": "yarn semantic-release",
|
||||||
"ci:test": "yarn ci:test:flow",
|
"ci:test": "yarn ci:test:flow && yarn ci:test:js",
|
||||||
"ci:test:flow": "yarn test:ios:flow && yarn test:android:flow",
|
"ci:test:flow": "yarn test:ios:flow && yarn test:android:flow",
|
||||||
|
"ci:test:js": "yarn test:js",
|
||||||
"semantic-release": "semantic-release"
|
"semantic-release": "semantic-release"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
@ -27,10 +29,14 @@
|
||||||
"fbjs": "^0.8.17"
|
"fbjs": "^0.8.17"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.2.2",
|
||||||
"@semantic-release/git": "7.0.5",
|
"@semantic-release/git": "7.0.5",
|
||||||
"@types/react": "^16.4.18",
|
"@types/react": "^16.4.18",
|
||||||
"@types/react-native": "^0.57.6",
|
"@types/react-native": "^0.57.6",
|
||||||
|
"babel-jest": "^24.0.0",
|
||||||
"flow-bin": "^0.80.0",
|
"flow-bin": "^0.80.0",
|
||||||
|
"jest": "^24.0.0",
|
||||||
|
"metro-react-native-babel-preset": "^0.51.1",
|
||||||
"react-native": "^0.57",
|
"react-native": "^0.57",
|
||||||
"semantic-release": "15.10.3"
|
"semantic-release": "15.10.3"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue