New: Add rule to validate JSLL usage
This commit is contained in:
Родитель
fbeaf8dce2
Коммит
9a19620035
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"lockfileVersion": 1
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"connector": {
|
||||
"name": "jsdom",
|
||||
"options": {
|
||||
"waitFor": 1000
|
||||
}
|
||||
},
|
||||
"formatters": "stylish",
|
||||
"parsers": ["javascript"],
|
||||
"rulesTimeout": 120000,
|
||||
"rules": {
|
||||
"jsll-script-included": "error",
|
||||
"jsll-awa-init": "error",
|
||||
"jsll-optional-config": "warning",
|
||||
"jsll-required-config": "error"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
# jsll (`jsll`)
|
||||
|
||||
Validate the inclusion and initialization of the JSLL script via
|
||||
multiple related rules.
|
||||
|
||||
## What does the rule check?
|
||||
|
||||
JSLL is the analytics library used by Microsoft. This package
|
||||
contains the following rules:
|
||||
|
||||
* `jsll-script-included`
|
||||
* `jsll-awa-init`
|
||||
* `jsll-required-config`
|
||||
* `jsll-optional-config`
|
||||
|
||||
These rules test the following:
|
||||
|
||||
* Whether or not the JSLL script has been included.
|
||||
* If the script is inlcuded in `<head>`.
|
||||
* If the JSLL script is included before any other scripts.
|
||||
* If the script version is valid.
|
||||
* Whether or not the JSLL init script has been included.
|
||||
* If the init script is placed immediately after the JSLL script.
|
||||
* If `awa.init` was used to initialize JSLL.
|
||||
* If `config` was defined.
|
||||
* Validate required properties.
|
||||
* Validate Optional properties.
|
||||
|
||||
### Examples that **trigger** the rule
|
||||
|
||||
The JSLL script was not included
|
||||
|
||||
```html
|
||||
<head>
|
||||
...
|
||||
</head>
|
||||
```
|
||||
|
||||
The JSLL init script was not included.
|
||||
|
||||
```html
|
||||
<head>
|
||||
<script src="https://az725175.vo.msecnd.net/scripts/jsll-4.js" type="text/javascript"></script>
|
||||
</head>
|
||||
```
|
||||
|
||||
The JSLL init script doesn't follow the JSLL script immediately.
|
||||
|
||||
```html
|
||||
<head>
|
||||
<script src="https://az725175.vo.msecnd.net/scripts/jsll-4.js" type="text/javascript"></script>
|
||||
<script>var a = 1;</script>
|
||||
<script>
|
||||
config = {...};
|
||||
awa.init(config);
|
||||
</script>
|
||||
</head>
|
||||
```
|
||||
|
||||
Required/Optional Properties are missing in `config`.
|
||||
|
||||
```html
|
||||
<head>
|
||||
<script src="https://az725175.vo.msecnd.net/scripts/jsll-4.js" type="text/javascript"></script>
|
||||
<script>
|
||||
awa.init({});
|
||||
</script>
|
||||
</head>
|
||||
```
|
||||
|
||||
Invalid required/optional properties.
|
||||
|
||||
```html
|
||||
<head>
|
||||
<script src="https://az725175.vo.msecnd.net/scripts/jsll-4.js" type="text/javascript"></script>
|
||||
<script>
|
||||
var config = {
|
||||
autoCapture: true // invalid type
|
||||
coreData: {
|
||||
appId: 'YourAppId',
|
||||
env: 'env',
|
||||
market: 'oo-oo', // invalid code
|
||||
pageName: 'name',
|
||||
pageType: 'type'
|
||||
},
|
||||
useShortNameForContentBlob: true
|
||||
};
|
||||
awa.init(config);
|
||||
</script>
|
||||
</head>
|
||||
```
|
||||
|
||||
### Examples that **pass** the rule
|
||||
|
||||
```html
|
||||
<head>
|
||||
<script src="https://az725175.vo.msecnd.net/scripts/jsll-4.js" type="text/javascript"></script>
|
||||
<script>
|
||||
var config = {
|
||||
autoCapture: {
|
||||
lineage: true,
|
||||
scroll: true
|
||||
},
|
||||
coreData: {
|
||||
appId: 'YourAppId',
|
||||
env: 'env',
|
||||
market: 'en-us',
|
||||
pageName: 'name',
|
||||
pageType: 'type'
|
||||
},
|
||||
useShortNameForContentBlob: true
|
||||
};
|
||||
awa.init(config);
|
||||
</script>
|
||||
</head>
|
||||
```
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"author": "",
|
||||
"ava": {
|
||||
"babel": {
|
||||
"presets": []
|
||||
},
|
||||
"concurrency": 5,
|
||||
"failFast": false,
|
||||
"files": [
|
||||
"dist/tests/**/*.js"
|
||||
],
|
||||
"timeout": "1m"
|
||||
},
|
||||
"description": "Validate the initialization of the JSLL script.",
|
||||
"devDependencies": {
|
||||
"@types/debug": "0.0.30",
|
||||
"@types/node": "8.0.14",
|
||||
"async-retry": "^1.1.4",
|
||||
"ava": "^0.24.0",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-plugin-istanbul": "^4.1.5",
|
||||
"babel-preset-es2017": "^6.22.0",
|
||||
"babel-register": "^6.26.0",
|
||||
"cpx": "^1.5.0",
|
||||
"eslint": "^4.15.0",
|
||||
"eslint-plugin-markdown": "^1.0.0-beta.6",
|
||||
"eslint-plugin-typescript": "^0.8.1",
|
||||
"express": "^4.16.2",
|
||||
"markdownlint-cli": "^0.6.0",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"on-headers": "^1.0.1",
|
||||
"rimraf": "^2.6.2",
|
||||
"sonarwhal": "^0.22.1",
|
||||
"typescript": "^2.6.2",
|
||||
"typescript-eslint-parser": "^12.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"rule",
|
||||
"sonarwhal"
|
||||
],
|
||||
"main": "dist/src/index.js",
|
||||
"name": "sonarwhal-rule-jsll",
|
||||
"scripts": {
|
||||
"build": "npm run clean && npm-run-all build:*",
|
||||
"build:assets": "cpx \"./{src,tests}/**/{!(*.ts),.!(ts)}\" dist",
|
||||
"build:ts": "tsc --outDir \"./dist\" --rootDir .",
|
||||
"clean": "rimraf dist",
|
||||
"test": "npm run lint && npm run build && ava",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:js": "eslint --ext md --ext ts --ignore-pattern dist .",
|
||||
"lint:md": "markdownlint README.md",
|
||||
"watch:ts": "npm run build:ts -- --watch",
|
||||
"init": "npm install && npm run build",
|
||||
"sonarwhal": "node node_modules/sonarwhal/dist/src/bin/sonarwhal.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"sonarwhal": "^0.22.1"
|
||||
},
|
||||
"repository": "https://github.com/Microsoft/sonarwhal-microsoft-rules",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"locale-code": "^1.1.1",
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* @fileoverview jsll the initialization of the JSLL script.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
'jsll-awa-init': require('./rules/jsll-awa-init/jsll-awa-init'),
|
||||
'jsll-optional-config': require('./rules/jsll-optional-config/jsll-optional-config'),
|
||||
'jsll-required-config': require('./rules/jsll-required-config/jsll-required-config'),
|
||||
'jsll-script-included': require('./rules/jsll-script-included/jsll-script-included')
|
||||
};
|
|
@ -0,0 +1,122 @@
|
|||
/**
|
||||
* @fileoverview Validate the use of `awa.init` to initialize the JSLL script.
|
||||
*/
|
||||
|
||||
import { Category } from 'sonarwhal/dist/src/lib/enums/category';
|
||||
import { RuleContext } from 'sonarwhal/dist/src/lib/rule-context';
|
||||
import { IRule, IAsyncHTMLElement, IRuleBuilder, IElementFound, IScriptParse, ITraverseUp } from 'sonarwhal/dist/src/lib/types';
|
||||
import { debug as d } from 'sonarwhal/dist/src/lib/utils/debug';
|
||||
|
||||
import { validateAwaInit } from '../validator';
|
||||
import { isJsllDir, isHeadElement } from '../utils';
|
||||
|
||||
import { Linter } from 'eslint';
|
||||
|
||||
const debug: debug.IDebugger = d(__filename);
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------------------
|
||||
* Public
|
||||
* ------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const rule: IRuleBuilder = {
|
||||
create(context: RuleContext): IRule {
|
||||
const linter = new Linter();
|
||||
let validated: boolean = false; // If `validateAwaInit` has run.
|
||||
let hasJSLLScript: boolean = false; // If link to JSLL scripts has been included.
|
||||
|
||||
linter.defineRule('jsll-awa-init', {
|
||||
create(eslintContext) {
|
||||
let isFirstExpressionStatement: boolean = true;
|
||||
|
||||
return {
|
||||
ExpressionStatement(node) {
|
||||
if (!isFirstExpressionStatement) { // Only the first expression statement should be checked.
|
||||
return;
|
||||
}
|
||||
|
||||
isFirstExpressionStatement = false;
|
||||
|
||||
validateAwaInit(node, eslintContext, true);
|
||||
|
||||
validated = true;
|
||||
},
|
||||
'Program:exit'(programNode) {
|
||||
if (hasJSLLScript && (!validated)) {
|
||||
// Should verify init but no ExpressionStatement was encountered.
|
||||
// e.g.:
|
||||
// ...
|
||||
// <script src="../jsll-4.js"></script>
|
||||
// <script>var a = 2;</script>
|
||||
eslintContext.report(programNode, `JSLL is not initialized with "awa.init(config)" function. Initialization script should be placed immediately after JSLL script.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const validateScript = async (scriptParse: IScriptParse) => {
|
||||
if (!hasJSLLScript) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = linter.verify(scriptParse.sourceCode, { rules: { 'jsll-awa-init': 'error' } });
|
||||
|
||||
hasJSLLScript = false;
|
||||
// Only validates the script included immediately after the JSLL link.
|
||||
// So flip this flag to avoid duplicates.
|
||||
|
||||
for (const result of results) {
|
||||
await context.report(scriptParse.resource, null, result.message);
|
||||
}
|
||||
};
|
||||
|
||||
const enableInitValidate = (data: IElementFound) => {
|
||||
const { element }: { element: IAsyncHTMLElement, resource: string } = data;
|
||||
|
||||
if (hasJSLLScript) {
|
||||
return;
|
||||
}
|
||||
|
||||
// JSLL script has been included at this point.
|
||||
// Now JSLL init needs to be verified.
|
||||
hasJSLLScript = isJsllDir(element);
|
||||
};
|
||||
|
||||
const validateInit = async (event: ITraverseUp) => {
|
||||
const { resource }: { resource: string } = event;
|
||||
|
||||
if (!isHeadElement(event.element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasJSLLScript && (!validated)) {
|
||||
// Should verify init but no script tag was encountered after the JSLL link.
|
||||
// e.g.:
|
||||
// <head>
|
||||
// <script src="../jsll-4.js"></script>
|
||||
// </head>
|
||||
await context.report(resource, null, `JSLL is not initialized with "awa.init(config)" function. Initialization script should be placed immediately after JSLL script.`);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return {
|
||||
'element::script': enableInitValidate,
|
||||
'parse::javascript': validateScript,
|
||||
'traverse::up': validateInit
|
||||
};
|
||||
},
|
||||
meta: {
|
||||
docs: {
|
||||
category: Category.other,
|
||||
description: `Validate the use of 'awa.init' to initialize the JSLL script.`
|
||||
},
|
||||
recommended: false,
|
||||
schema: [],
|
||||
worksWithLocalFiles: true
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = rule;
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* @fileoverview Validate the required optional properties.
|
||||
*/
|
||||
|
||||
import { Category } from 'sonarwhal/dist/src/lib/enums/category';
|
||||
import { RuleContext } from 'sonarwhal/dist/src/lib/rule-context';
|
||||
import { IRule, IAsyncHTMLElement, IRuleBuilder, IElementFound, IScriptParse } from 'sonarwhal/dist/src/lib/types';
|
||||
import { debug as d } from 'sonarwhal/dist/src/lib/utils/debug';
|
||||
|
||||
import { validateNodeProps, validateAwaInit, configProps } from '../validator';
|
||||
import { isJsllDir } from '../utils';
|
||||
|
||||
import { Linter } from 'eslint';
|
||||
|
||||
const debug: debug.IDebugger = d(__filename);
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------------------
|
||||
* Public
|
||||
* ------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const rule: IRuleBuilder = {
|
||||
create(context: RuleContext): IRule {
|
||||
const linter = new Linter();
|
||||
let hasJSLLScript: boolean = false; // If link to JSLL scripts has been included.
|
||||
|
||||
linter.defineRule('jsll-required-config', {
|
||||
create(eslintContext) {
|
||||
let isFirstExpressionStatement = true;
|
||||
|
||||
return {
|
||||
ExpressionStatement(node) {
|
||||
if (!isFirstExpressionStatement) { // Only the first expression statement should be checked.
|
||||
return;
|
||||
}
|
||||
|
||||
isFirstExpressionStatement = false;
|
||||
const configNode = validateAwaInit(node, eslintContext, false);
|
||||
|
||||
if (!configNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const category = `optional`;
|
||||
|
||||
validateNodeProps(configProps.config[category], configNode, category, eslintContext);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
const validateScript = async (scriptParse: IScriptParse) => {
|
||||
if (!hasJSLLScript) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = linter.verify(scriptParse.sourceCode, { rules: { 'jsll-required-config': 'warn' } });
|
||||
|
||||
hasJSLLScript = false;
|
||||
// Only the script included immediately after the JSLL link needs to be validated.
|
||||
// So flip the flag.
|
||||
|
||||
|
||||
for (const result of results) {
|
||||
await context.report(scriptParse.resource, null, result.message);
|
||||
}
|
||||
};
|
||||
|
||||
const enableInitValidate = (data: IElementFound) => {
|
||||
const { element }: { element: IAsyncHTMLElement, resource: string } = data;
|
||||
|
||||
if (hasJSLLScript) {
|
||||
return;
|
||||
}
|
||||
|
||||
// JSLL init needs to be verified if JSLL script was included.
|
||||
hasJSLLScript = isJsllDir(element);
|
||||
};
|
||||
|
||||
return {
|
||||
'element::script': enableInitValidate,
|
||||
'parse::javascript': validateScript
|
||||
};
|
||||
},
|
||||
meta: {
|
||||
docs: {
|
||||
category: Category.other,
|
||||
description: `Validate the optional config properties.`
|
||||
},
|
||||
recommended: false,
|
||||
schema: [],
|
||||
worksWithLocalFiles: true
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = rule;
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* @fileoverview Validate the required config properties.
|
||||
*/
|
||||
|
||||
import { Category } from 'sonarwhal/dist/src/lib/enums/category';
|
||||
import { RuleContext } from 'sonarwhal/dist/src/lib/rule-context';
|
||||
import { IRule, IAsyncHTMLElement, IRuleBuilder, IElementFound, IScriptParse } from 'sonarwhal/dist/src/lib/types';
|
||||
import { debug as d } from 'sonarwhal/dist/src/lib/utils/debug';
|
||||
|
||||
import { validateNodeProps, validateAwaInit, configProps } from '../validator';
|
||||
import { isJsllDir } from '../utils';
|
||||
|
||||
import { Linter } from 'eslint';
|
||||
|
||||
const debug: debug.IDebugger = d(__filename);
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------------------
|
||||
* Public
|
||||
* ------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const rule: IRuleBuilder = {
|
||||
create(context: RuleContext): IRule {
|
||||
const linter = new Linter();
|
||||
let initValidate = false; // If JSLL initialization should be verified, only the script immediately after the JSLL should be verified.
|
||||
|
||||
linter.defineRule('jsll-required-config', {
|
||||
create(eslintContext) {
|
||||
let isFirstExpressionStatement = true;
|
||||
|
||||
return {
|
||||
ExpressionStatement(node) {
|
||||
if (!isFirstExpressionStatement) {
|
||||
// Only the first expression statement should be checked.
|
||||
return;
|
||||
}
|
||||
|
||||
isFirstExpressionStatement = false;
|
||||
const configNode = validateAwaInit(node, eslintContext, false);
|
||||
|
||||
if (!configNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const category = `required`;
|
||||
|
||||
validateNodeProps(configProps.config[category], configNode, category, eslintContext);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
const validateScript = async (scriptParse: IScriptParse) => {
|
||||
if (!initValidate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = linter.verify(scriptParse.sourceCode, { rules: { 'jsll-required-config': 'error' } });
|
||||
|
||||
initValidate = false;
|
||||
// Only the script included immediately after the JSLL link needs to be validated.
|
||||
// So flip the flag.
|
||||
|
||||
for (const result of results) {
|
||||
await context.report(scriptParse.resource, null, result.message);
|
||||
}
|
||||
};
|
||||
|
||||
const enableInitValidate = (data: IElementFound) => {
|
||||
const { element }: { element: IAsyncHTMLElement, resource: string } = data;
|
||||
|
||||
if (initValidate) {
|
||||
return;
|
||||
}
|
||||
|
||||
// JSLL init needs to be verified if JSLL script was included.
|
||||
initValidate = isJsllDir(element);
|
||||
};
|
||||
|
||||
return {
|
||||
'element::script': enableInitValidate,
|
||||
'parse::javascript': validateScript
|
||||
};
|
||||
},
|
||||
meta: {
|
||||
docs: {
|
||||
category: Category.other,
|
||||
description: `Validate the required config properties.`
|
||||
},
|
||||
recommended: false,
|
||||
schema: [],
|
||||
worksWithLocalFiles: true
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = rule;
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* @fileoverview This rule confirms that JSLL script is included in the page
|
||||
*/
|
||||
import { Category } from 'sonarwhal/dist/src/lib/enums/category';
|
||||
import { RuleContext } from 'sonarwhal/dist/src/lib/rule-context';
|
||||
import { IAsyncHTMLElement, IRule, IRuleBuilder, IElementFound, ITraverseUp, Severity } from 'sonarwhal/dist/src/lib/types';
|
||||
import { normalizeString } from 'sonarwhal/dist/src/lib/utils/misc';
|
||||
|
||||
import { isJsllDir } from '../utils';
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------------------
|
||||
* Public
|
||||
* ------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const rule: IRuleBuilder = {
|
||||
create(context: RuleContext): IRule {
|
||||
// Messages.
|
||||
const noScriptInHeadMsg: string = `No JSLL script was included in the <head> tag.`;
|
||||
const redundantScriptInHeadMsg: string = `More than one JSLL scripts were included in the <head> tag.`;
|
||||
const warningScriptVersionMsg: string = `Use the latest release of JSLL with 'jsll-4.js'. It is not recommended to specify the version number unless you wish to lock to a specific release.`;
|
||||
const invalidScriptVersionMsg: string = `The jsll script versioning is not valid.`;
|
||||
const wrongScriptOrderMsg: string = `The JSLL script isn't placed prior to other scripts.`;
|
||||
|
||||
const jsllDir: string = `https://az725175.vo.msecnd.net/scripts/jsll-`;
|
||||
let isHead: boolean = true; // Flag to indicate if script is in head.
|
||||
let totalHeadScriptCount: number = 0; // Total number of script tags in head.
|
||||
let jsllScriptCount: number = 0; // Total number of JSLL script tag in head.
|
||||
|
||||
const validateScript = async (data: IElementFound) => {
|
||||
const { element, resource }: { element: IAsyncHTMLElement, resource: string } = data;
|
||||
const passRegex = new RegExp(`^(\\d+\\.)js`); // 4.js
|
||||
const warningRegex = new RegExp(`^(\\d+\\.){2,}js`); // 4.2.1.js
|
||||
|
||||
if (!isHead) {
|
||||
return;
|
||||
}
|
||||
|
||||
totalHeadScriptCount += 1;
|
||||
|
||||
if (!isJsllDir(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
jsllScriptCount += 1;
|
||||
|
||||
if (jsllScriptCount > 1) {
|
||||
await context.report(resource, element, redundantScriptInHeadMsg);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalHeadScriptCount > 1) {
|
||||
// There are other scripts in <head> prior to this JSLL script.
|
||||
await context.report(resource, element, wrongScriptOrderMsg);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName: string = normalizeString(element.getAttribute('src').replace(jsllDir, ''));
|
||||
|
||||
if (passRegex.test(fileName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (warningRegex.test(fileName)) {
|
||||
await context.report(resource, element, warningScriptVersionMsg, null, null, Severity.warning);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await context.report(resource, element, invalidScriptVersionMsg);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
const enterBody = async (event: ITraverseUp) => {
|
||||
const { resource }: { resource: string } = event;
|
||||
|
||||
if (jsllScriptCount === 0) {
|
||||
await context.report(resource, null, noScriptInHeadMsg);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
isHead = false;
|
||||
};
|
||||
|
||||
return {
|
||||
'element::body': enterBody,
|
||||
'element::script': validateScript
|
||||
};
|
||||
},
|
||||
meta: {
|
||||
docs: {
|
||||
category: Category.other,
|
||||
description: `This rule confirms that JSLL script is included in the head of the page`
|
||||
},
|
||||
recommended: false,
|
||||
schema: [],
|
||||
worksWithLocalFiles: false
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = rule;
|
|
@ -0,0 +1,46 @@
|
|||
import { IAsyncHTMLElement } from 'sonarwhal/dist/src/lib/types';
|
||||
import { normalizeString } from 'sonarwhal/dist/src/lib/utils/misc';
|
||||
|
||||
const jsllDir = `https://az725175.vo.msecnd.net/scripts/jsll-`;
|
||||
|
||||
const severityMatch = (parentObj: object, prop: string, severity: string) => {
|
||||
return parentObj[severity].includes(prop);
|
||||
};
|
||||
|
||||
const pluralizedVerb = (props: Array<string>) => {
|
||||
return props.length > 1 ? 'are' : 'is';
|
||||
};
|
||||
|
||||
const reportMissingProps = (props: Array<string>, existingProps: Array<string>, severity: string, target, eslintContext) => {
|
||||
if (!props || !props.length) {
|
||||
return;
|
||||
}
|
||||
const missingProps = props && props.filter((prop) => {
|
||||
return !existingProps.includes(prop);
|
||||
});
|
||||
|
||||
if (missingProps && missingProps.length) {
|
||||
eslintContext.report(target, `${missingProps.join(', ')} ${pluralizedVerb(missingProps)} ${severity} but missing.`);
|
||||
}
|
||||
};
|
||||
|
||||
const isJsllDir = (element: IAsyncHTMLElement) => {
|
||||
const src = element.getAttribute('src');
|
||||
|
||||
if (!src) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return normalizeString(src).startsWith(jsllDir);
|
||||
};
|
||||
|
||||
const isHeadElement = (element: IAsyncHTMLElement): boolean => {
|
||||
return normalizeString(element.nodeName) === 'head';
|
||||
};
|
||||
|
||||
export {
|
||||
isHeadElement,
|
||||
isJsllDir,
|
||||
severityMatch,
|
||||
reportMissingProps
|
||||
};
|
|
@ -0,0 +1,234 @@
|
|||
import * as validateCode from 'locale-code';
|
||||
import { severityMatch, reportMissingProps } from './utils';
|
||||
|
||||
/** List of types in the parsed Javascript. */
|
||||
const types = {
|
||||
identifier: 'Identifier',
|
||||
object: 'ObjectExpression',
|
||||
string: 'Literal'
|
||||
};
|
||||
|
||||
/** Optional and required properties of 'config'. */
|
||||
const config = {
|
||||
optional: ['useShortNameForContentBlob'],
|
||||
required: ['autoCapture', 'coreData']
|
||||
};
|
||||
|
||||
/** Optional and required properties of 'coreData'. */
|
||||
const coreData = {
|
||||
optional: ['pageName', 'pageType', 'env', 'market'],
|
||||
required: ['appId']
|
||||
};
|
||||
|
||||
/** Optional properties of 'autoCapture'. */
|
||||
const autoCapture = { optional: ['scroll', 'lineage'] };
|
||||
|
||||
/** Validate the 'coreData' property. */
|
||||
const validateCoreData = (property, eslintContext, severity: string) => {
|
||||
const coreDataValue = property.value;
|
||||
const report: boolean = config[severity].includes('coreData');
|
||||
|
||||
if (report && coreDataValue.type !== types.object) {
|
||||
return eslintContext.report(coreDataValue, `The "coreData" property must be a valid object.`);
|
||||
}
|
||||
|
||||
return validateNodeProps(coreData[severity], coreDataValue, severity, eslintContext); // eslint-disable-line typescript/no-use-before-define,no-use-before-define
|
||||
};
|
||||
|
||||
/** Validate the 'autoCapture' property. */
|
||||
const validateAutoCapture = (property, eslintContext, severity: string) => {
|
||||
const report: boolean = severityMatch(config, 'autoCapture', severity);
|
||||
const autoCaptureValue = property.value;
|
||||
|
||||
if (report && autoCaptureValue.type !== types.object) {
|
||||
return eslintContext.report(autoCaptureValue, `The "autoCapture" property is not a valid object.`);
|
||||
}
|
||||
|
||||
return validateNodeProps(autoCapture[severity], autoCaptureValue, severity, eslintContext); // eslint-disable-line typescript/no-use-before-define,no-use-before-define
|
||||
};
|
||||
|
||||
/** Validate the 'useShortNameForContentBlob' property. */
|
||||
const validateUseShortName = (property, eslintContext, severity: string) => {
|
||||
if (!severityMatch(config, 'useShortNameForContentBlob', severity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const useShortNameValue = property.value;
|
||||
|
||||
if (!useShortNameValue || useShortNameValue.value !== true) {
|
||||
eslintContext.report(property.value, `"useShortNameForContentBlob" parameter is not set to true.`);
|
||||
}
|
||||
};
|
||||
|
||||
/** Validate the 'appId' property. */
|
||||
const validateAppId = (property, eslintContext, severity: string) => {
|
||||
if (!severityMatch(coreData, 'appId', severity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = property.value;
|
||||
|
||||
if (id.type !== types.string || !id.value || !id.value.length) {
|
||||
eslintContext.report(property.value, `The "appId" must be a non-empty string.`);
|
||||
}
|
||||
};
|
||||
|
||||
/** Validate the 'lineage' property. */
|
||||
const validateLineage = (property, eslintContext, severity: string) => {
|
||||
if (!severityMatch(autoCapture, 'lineage', severity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lineageValue = property.value;
|
||||
|
||||
if (!lineageValue || lineageValue.value !== true) {
|
||||
eslintContext.report(property.value, `"lineage" parameter is not set to true.`);
|
||||
}
|
||||
};
|
||||
|
||||
/** Validate the 'market' property. */
|
||||
const validateMarket = (property, eslintContext, severity: string) => {
|
||||
if (!severityMatch(coreData, 'market', severity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const marketValue = property.value;
|
||||
|
||||
if (!marketValue) {
|
||||
eslintContext.report(marketValue, `"market" parameter needs to be defined.`);
|
||||
|
||||
return;
|
||||
}
|
||||
const regex: RegExp = /[a-z]*-[a-z]*/;
|
||||
|
||||
if (!regex.test(marketValue.value)) {
|
||||
eslintContext.report(marketValue, `The format of "market" parameter is not valid.`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const [languageCode, countryCode] = marketValue.value.split('-');
|
||||
const denormalizedCode: string = `${languageCode}-${countryCode.toUpperCase()}`;
|
||||
// The validator doesn't recognize lowercase country codes.
|
||||
|
||||
if (!validateCode.validateLanguageCode(denormalizedCode)) {
|
||||
eslintContext.report(property.value, `The "market" parameter contains invalid language code "${languageCode}".`);
|
||||
}
|
||||
|
||||
if (!validateCode.validateLanguageCode(denormalizedCode)) {
|
||||
eslintContext.report(property.value, `The "market" parameter contains invalid country code "${countryCode}".`);
|
||||
}
|
||||
};
|
||||
|
||||
/** Validate the initialization of JSLL using `awa.init(config)`. */
|
||||
export const validateAwaInit = (node, eslintContext, report: boolean) => {
|
||||
const expression = node.expression;
|
||||
const scope = eslintContext.getScope();
|
||||
const variables = scope.variables;
|
||||
const { callee } = expression;
|
||||
let configNode;
|
||||
|
||||
if (!callee || !callee.object || callee.object.name !== 'awa' || callee.property.name !== 'init') {
|
||||
if (report) {
|
||||
eslintContext.report(node, 'JSLL is not initialized with "awa.init(config)" function. Initialization script should be placed immediately after JSLL script.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const initArgs = expression.arguments;
|
||||
|
||||
if (initArgs.length < 1) {
|
||||
if (report) {
|
||||
eslintContext.report(node, `JSLL initialization function "awa.init(config)" missing required parameter "config".`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (initArgs.length > 1) {
|
||||
if (report) {
|
||||
eslintContext.report(node, `"init" arguments can't take more than one arguments.`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const configVal = initArgs[0];
|
||||
|
||||
if (!['Identifier', 'ObjectExpression'].includes(configVal.type)) {
|
||||
if (report) {
|
||||
eslintContext.report(configVal, `The argument of "awa.init" is not of type Object.`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (configVal.type === types.identifier) { // e.g., awa.init(config);
|
||||
const configDefinition = variables.find((variable) => {
|
||||
return variable.name === configVal.name;
|
||||
});
|
||||
|
||||
if (!configDefinition) {
|
||||
if (report) {
|
||||
eslintContext.report(node, `${configVal.name} is not defined.`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
configNode = configDefinition.defs[0].node.init;
|
||||
|
||||
if (configNode.type !== types.object) {
|
||||
if (report) {
|
||||
eslintContext.report(configNode, `${configDefinition.name} is not of type Object.`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
configNode = configVal; // awa.init({...});
|
||||
}
|
||||
|
||||
return configNode;
|
||||
};
|
||||
|
||||
/** List of validators. */
|
||||
const validators = {
|
||||
appId: validateAppId,
|
||||
autoCapture: validateAutoCapture,
|
||||
coreData: validateCoreData,
|
||||
lineage: validateLineage,
|
||||
market: validateMarket,
|
||||
useShortNameForContentBlob: validateUseShortName
|
||||
};
|
||||
|
||||
/** Validate properties of the current node based on severity. */
|
||||
export const validateNodeProps = (expectedProps: Array<string>, target, severity: string, eslintContext) => { // eslint-disable-line consistent-return
|
||||
if (!expectedProps || !expectedProps.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const properties = target.properties;
|
||||
const existingProps = [];
|
||||
|
||||
properties.forEach((property) => {
|
||||
const key = property.key.name || property.key.value;
|
||||
|
||||
const validator = validators[key];
|
||||
|
||||
if (validator) {
|
||||
validator(property, eslintContext, severity);
|
||||
}
|
||||
existingProps.push(key);
|
||||
|
||||
});
|
||||
reportMissingProps(expectedProps, existingProps, severity, target, eslintContext);
|
||||
};
|
||||
|
||||
export const configProps = {
|
||||
autoCapture,
|
||||
config,
|
||||
coreData,
|
||||
types
|
||||
};
|
|
@ -0,0 +1,79 @@
|
|||
import * as cloneDeep from 'lodash.cloneDeep';
|
||||
|
||||
/** Look for the target property recursively and modify/delete the value */
|
||||
export const modifyValue = (obj, targetProp: string, targetValue) => {
|
||||
if (!(obj instanceof Object) || Array.isArray(obj)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDelete: boolean = (typeof targetValue === 'undefined') || (targetValue === null);
|
||||
|
||||
if (obj.hasOwnProperty(targetProp)) {
|
||||
if (isDelete) {
|
||||
delete obj[targetProp];
|
||||
} else {
|
||||
obj[targetProp] = targetValue;
|
||||
}
|
||||
} else {
|
||||
const props: Array<string> = Object.keys(obj);
|
||||
|
||||
props.forEach((childProp) => {
|
||||
modifyValue(obj[childProp], targetProp, targetValue);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const perfectConfig = {
|
||||
autoCapture: {
|
||||
lineage: true,
|
||||
scroll: true
|
||||
},
|
||||
coreData: {
|
||||
appId: 'YourAppId',
|
||||
env: 'env',
|
||||
market: 'en-us',
|
||||
pageName: 'name',
|
||||
pageType: 'type'
|
||||
},
|
||||
useShortNameForContentBlob: true
|
||||
};
|
||||
|
||||
export const code = {
|
||||
emptyObjconfig: `awa.init({});`,
|
||||
initConfig: `awa.init(config);`,
|
||||
jsllScript: `<script src="https://az725175.vo.msecnd.net/scripts/jsll-4.js" type="text/javascript"></script>`,
|
||||
noConfigArgs: `awa.init();`,
|
||||
notImmediateInitNoFn: `var a = 1;</script><script>awa.init({})`,
|
||||
notImmediateInithasFn: `console.log('a');</script><script>awa.init({})`
|
||||
};
|
||||
|
||||
/** Delete one or more properties. */
|
||||
export const deleteProp = (prop: string | Array<string>): string => {
|
||||
const missiongPropConfig = cloneDeep(perfectConfig);
|
||||
const props: Array<string> = Array.isArray(prop) ? prop : [prop];
|
||||
|
||||
props.forEach((property) => {
|
||||
modifyValue(missiongPropConfig, property, null);
|
||||
});
|
||||
|
||||
return `var config=${JSON.stringify(missiongPropConfig)};`;
|
||||
};
|
||||
|
||||
/** Modify the value of a (nested property). */
|
||||
export const modifyConfigVal = (targetProp: string, targetValue: any): string => {
|
||||
const modifiedConfig = cloneDeep(perfectConfig);
|
||||
|
||||
modifyValue(modifiedConfig, targetProp, targetValue);
|
||||
|
||||
return `var config=${JSON.stringify(modifiedConfig)};`;
|
||||
};
|
||||
|
||||
export const scriptWrapper = (config: string, initCode: string, includeJSLLScript: boolean = true): string => {
|
||||
let res = `${includeJSLLScript ? code.jsllScript : ''}`;
|
||||
|
||||
if (config || initCode) {
|
||||
res += `<script>${config || ''}${initCode || ''}</script>`;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
import { generateHTMLPage } from 'sonarwhal/dist/tests/helpers/misc';
|
||||
import { getRuleName } from 'sonarwhal/dist/src/lib/utils/rule-helpers';
|
||||
import { IRuleTest } from 'sonarwhal/dist/tests/helpers/rule-test-type';
|
||||
import * as ruleRunner from 'sonarwhal/dist/tests/helpers/rule-runner';
|
||||
|
||||
import { code, scriptWrapper } from '../helpers/common';
|
||||
|
||||
const ruleName = getRuleName(__dirname);
|
||||
const messages = {
|
||||
noConfigArgs: `JSLL initialization function "awa.init(config)" missing required parameter "config".`,
|
||||
noInit: `JSLL is not initialized with "awa.init(config)" function. Initialization script should be placed immediately after JSLL script.`
|
||||
};
|
||||
|
||||
const tests: Array<IRuleTest> = [
|
||||
{
|
||||
// Validate init shouldn't run if the JSLL script link is not included.
|
||||
name: `The JSLL script itself was not included`,
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInithasFn, false)}`)
|
||||
},
|
||||
{
|
||||
name: `The script following JSLL script doesn't include code that initializes JSLL, and the spacer script has no function calls`,
|
||||
reports: [{ message: messages.noInit }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInitNoFn)}`)
|
||||
},
|
||||
{
|
||||
name: `The script following JSLL script doesn't include code that initializes JSLL, and the spacer script has function calls`,
|
||||
reports: [{ message: messages.noInit }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInithasFn)}`)
|
||||
},
|
||||
{
|
||||
name: `"awa.init" doesn't have the required parameter "config"`,
|
||||
reports: [{ message: messages.noConfigArgs }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.noConfigArgs)}`)
|
||||
},
|
||||
{
|
||||
name: `"awa.init" has an empty object as config parameter`,
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.emptyObjconfig)}`)
|
||||
},
|
||||
{
|
||||
// <head>
|
||||
// <script src="../jsll-4.js"></script>
|
||||
// </head>
|
||||
name: `No script tages are encountered after the JSLL script link`,
|
||||
reports: [{ message: messages.noInit }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, null, true)}`)
|
||||
}
|
||||
];
|
||||
|
||||
ruleRunner.testRule(ruleName, tests, { parsers: ['javascript'] });
|
|
@ -0,0 +1,84 @@
|
|||
import { generateHTMLPage } from 'sonarwhal/dist/tests/helpers/misc';
|
||||
import { getRuleName } from 'sonarwhal/dist/src/lib/utils/rule-helpers';
|
||||
import { IRuleTest } from 'sonarwhal/dist/tests/helpers/rule-test-type';
|
||||
import * as ruleRunner from 'sonarwhal/dist/tests/helpers/rule-runner';
|
||||
|
||||
import { code, deleteProp, modifyConfigVal, scriptWrapper } from '../helpers/common';
|
||||
|
||||
const ruleName = getRuleName(__dirname);
|
||||
|
||||
const messages = {
|
||||
invalidCountryCode: `The "market" parameter contains invalid country code "oo".`,
|
||||
invalidLangCode: `The "market" parameter contains invalid language code "oo".`,
|
||||
invalidLineage: `"lineage" parameter is not set to true.`,
|
||||
invalidMarketValue: `The format of "market" parameter is not valid.`,
|
||||
invalidUseShortNameForContentBlob: `"useShortNameForContentBlob" parameter is not set to true.`,
|
||||
missingLineage: `lineage" parameter is not set to true.`,
|
||||
missingOptionalConfigProp: `useShortNameForContentBlob is optional but missing.`,
|
||||
missingPageName: `pageName is optional but missing.`,
|
||||
missingPageNameAndEnv: `pageName, env are optional but missing.`,
|
||||
missingUseShortNameForContentBlob: `useShortNameForContentBlob is optional but missing.`
|
||||
};
|
||||
|
||||
const tests: Array<IRuleTest> = [
|
||||
{
|
||||
// Validate init shouldn't run if the JSLL script link is not included.
|
||||
name: `The JSLL script itself was not included`,
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInithasFn, false)}`)
|
||||
},
|
||||
{
|
||||
name: `The script following JSLL script doesn't include code that initializes JSLL, and the spacer script has no function calls`,
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInitNoFn)}`)
|
||||
},
|
||||
{
|
||||
name: `The script following JSLL script doesn't include code that initializes JSLL, and the spacer script has function calls`,
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInithasFn)}`)
|
||||
},
|
||||
{
|
||||
name: `"awa.init" doesn't have any arguments`,
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.noConfigArgs)}`)
|
||||
},
|
||||
// All the tests beyond this point should pass, because they will be reported in rule `jsll-awa-init`.
|
||||
{
|
||||
name: `"config" misses optional properties`,
|
||||
reports: [{ message: messages.missingOptionalConfigProp }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(deleteProp('useShortNameForContentBlob'), code.initConfig)}`)
|
||||
},
|
||||
{
|
||||
name: `"market" has an invalid value`,
|
||||
reports: [{ message: messages.invalidMarketValue }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(modifyConfigVal('market', 'market'), code.initConfig)}`)
|
||||
},
|
||||
{
|
||||
name: `"market" has an invalid lauguage/country code`,
|
||||
reports: [{ message: messages.invalidLangCode }, { message: messages.invalidCountryCode }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(modifyConfigVal('market', 'oo-oo'), code.initConfig)}`)
|
||||
},
|
||||
{
|
||||
name: `"pageName" is missing`, // optional child property (pageName) of a required parent property (coreData)
|
||||
reports: [{ message: messages.missingPageName }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(deleteProp('pageName'), code.initConfig)}`)
|
||||
},
|
||||
{
|
||||
name: `"lineage" is not set as true`,
|
||||
reports: [{ message: messages.invalidLineage }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(modifyConfigVal('lineage', 1), code.initConfig)}`)
|
||||
},
|
||||
{
|
||||
name: `"useShortNameForContentBlob" is not set as true`,
|
||||
reports: [{ message: messages.invalidUseShortNameForContentBlob }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(modifyConfigVal('useShortNameForContentBlob', false), code.initConfig)}`)
|
||||
},
|
||||
{
|
||||
name: `"config" is an empty object`,
|
||||
reports: [{ message: messages.missingUseShortNameForContentBlob }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.emptyObjconfig)}`)
|
||||
},
|
||||
{
|
||||
name: `Both required and optional properties are missing in "config"`,
|
||||
reports: [{ message: messages.missingPageNameAndEnv }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(deleteProp(['appId', 'pageName', 'env']), code.initConfig)}`)
|
||||
}
|
||||
];
|
||||
|
||||
ruleRunner.testRule(ruleName, tests, { parsers: ['javascript'] });
|
|
@ -0,0 +1,86 @@
|
|||
import { generateHTMLPage } from 'sonarwhal/dist/tests/helpers/misc';
|
||||
import { getRuleName } from 'sonarwhal/dist/src/lib/utils/rule-helpers';
|
||||
import { IRuleTest } from 'sonarwhal/dist/tests/helpers/rule-test-type';
|
||||
import * as ruleRunner from 'sonarwhal/dist/tests/helpers/rule-runner';
|
||||
|
||||
import { code, deleteProp, modifyConfigVal, scriptWrapper } from '../helpers/common';
|
||||
|
||||
const ruleName = getRuleName(__dirname);
|
||||
const messages = {
|
||||
invalidAppId: `The "appId" must be a non-empty string.`,
|
||||
invalidAutoCapture: `The "autoCapture" property is not a valid object.`,
|
||||
missingAppId: `appId is required but missing.`,
|
||||
missingAutoCapture: `autoCapture is required but missing.`,
|
||||
missingRequiredConfigProps: `autoCapture, coreData are required but missing.`
|
||||
};
|
||||
const stringPropertyConfig = {
|
||||
'autoCapture': { // eslint-disable-line quote-props
|
||||
lineage: true,
|
||||
scroll: true
|
||||
},
|
||||
coreData: {
|
||||
'appId': '1', // eslint-disable-line quote-props
|
||||
env: 'env',
|
||||
market: 'en-us',
|
||||
pageName: 'name',
|
||||
pageType: 'type'
|
||||
},
|
||||
useShortNameForContentBlob: true
|
||||
};
|
||||
|
||||
const tests: Array<IRuleTest> = [
|
||||
{
|
||||
// Validate init shouldn't run if the JSLL script link is not included.
|
||||
name: `The JSLL script itself was not included`,
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInithasFn, false)}`)
|
||||
},
|
||||
{
|
||||
name: `The script following JSLL script doesn't include code that initializes JSLL, and the spacer script has no function calls`,
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInitNoFn)}`)
|
||||
},
|
||||
{
|
||||
name: `The script following JSLL script doesn't include code that initializes JSLL, and the spacer script has function calls`,
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.notImmediateInithasFn)}`)
|
||||
},
|
||||
{
|
||||
name: `"awa.init" doesn't have the required parameter "config"`,
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.noConfigArgs)}`)
|
||||
},
|
||||
// All the tests beyond this point should pass, because they will be reported in rule `jsll-awa-init`.
|
||||
{
|
||||
name: `"config" misses required properties "autoCapture"`,
|
||||
reports: [{ message: messages.missingAutoCapture }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(deleteProp('autoCapture'), code.initConfig)}`)
|
||||
},
|
||||
{
|
||||
name: `"appId" is missing`,
|
||||
reports: [{ message: messages.missingAppId }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(deleteProp('appId'), code.initConfig)}`)
|
||||
},
|
||||
{
|
||||
name: `"appId" is not a string`,
|
||||
reports: [{ message: messages.invalidAppId }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(modifyConfigVal('appId', 1), code.initConfig)}`)
|
||||
},
|
||||
{
|
||||
name: `"autoCapture" is not a valid object`,
|
||||
reports: [{ message: messages.invalidAutoCapture }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(modifyConfigVal('autoCapture', true), code.initConfig)}`)
|
||||
},
|
||||
{
|
||||
name: `"config" is an empty object`,
|
||||
reports: [{ message: messages.missingRequiredConfigProps }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(null, code.emptyObjconfig)}`)
|
||||
},
|
||||
{
|
||||
name: `"config" properties are strings instead of identifiers`,
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(`var config=${JSON.stringify(stringPropertyConfig)};`, code.initConfig)}`)
|
||||
},
|
||||
{
|
||||
name: `Both required and optional properties are missing in "config"`,
|
||||
reports: [{ message: messages.missingAppId }],
|
||||
serverConfig: generateHTMLPage(`${scriptWrapper(deleteProp(['appId', 'pageName', 'env']), code.initConfig)}`)
|
||||
}
|
||||
];
|
||||
|
||||
ruleRunner.testRule(ruleName, tests, { parsers: ['javascript'] });
|
|
@ -0,0 +1,68 @@
|
|||
import { generateHTMLPage } from 'sonarwhal/dist/tests/helpers/misc';
|
||||
import { IRuleTest } from 'sonarwhal/dist/tests/helpers/rule-test-type';
|
||||
import { getRuleName } from 'sonarwhal/dist/src/lib/utils/rule-helpers';
|
||||
import * as ruleRunner from 'sonarwhal/dist/tests/helpers/rule-runner';
|
||||
|
||||
const ruleName: string = getRuleName(__dirname);
|
||||
const baseUrl: string = `https://az725175.vo.msecnd.net/scripts/jsll`;
|
||||
const simpleVersionLink: string = `${baseUrl}-4.js`;
|
||||
const specifiedVersionLink: string = `${baseUrl}-4.2.1.js`;
|
||||
const invalidVersionLink: string = `${baseUrl}-4-2-1.js`;
|
||||
const redundantScriptLinks: Array<string> = [`${baseUrl}-4.js`, `${baseUrl}-4.2.1.js`];
|
||||
const wrongScriptOrderLinks: Array<string> = [`https://uhf.microsoft.com/mscc/statics/mscc-0.3.6.min.js`, `${baseUrl}-4.js`];
|
||||
|
||||
const generateScript = (links: string | Array<string>) => {
|
||||
let generatedScript = '';
|
||||
const scriptLinks = Array.isArray(links) ? links : [links];
|
||||
|
||||
scriptLinks.forEach((link) => {
|
||||
generatedScript += `<script src="${link}"></script>`;
|
||||
});
|
||||
|
||||
return generatedScript;
|
||||
};
|
||||
|
||||
// Messages.
|
||||
const noScriptInHeadMsg = `No JSLL script was included in the <head> tag.`;
|
||||
const redundantScriptInHeadMsg = `More than one JSLL scripts were included in the <head> tag.`;
|
||||
const warningScriptVersionMsg = `Use the latest release of JSLL with 'jsll-4.js'. It is not recommended to specify the version number unless you wish to lock to a specific release.`;
|
||||
const invalidScriptVersionMsg = `The jsll script versioning is not valid.`;
|
||||
const wrongScriptOrderMsg = `The JSLL script isn't placed prior to other scripts.`;
|
||||
|
||||
const tests: Array<IRuleTest> = [
|
||||
{
|
||||
name: 'JSLL script locates in the <head> tag and has the recommended version format',
|
||||
serverConfig: generateHTMLPage(generateScript(simpleVersionLink))
|
||||
},
|
||||
{
|
||||
name: 'Multiple JSLL scripts are included in the page',
|
||||
reports: [{ message: redundantScriptInHeadMsg }],
|
||||
serverConfig: generateHTMLPage(generateScript(redundantScriptLinks))
|
||||
},
|
||||
{
|
||||
name: 'JSLL script locates in the <body> tag instead of the <head> tag',
|
||||
reports: [{ message: noScriptInHeadMsg }],
|
||||
serverConfig: generateHTMLPage(null, generateScript(simpleVersionLink))
|
||||
},
|
||||
{
|
||||
name: 'JSLL script name has the specified version format',
|
||||
reports: [{ message: warningScriptVersionMsg }],
|
||||
serverConfig: generateHTMLPage(generateScript(specifiedVersionLink))
|
||||
},
|
||||
{
|
||||
name: 'JSLL script name has an invalid version in name',
|
||||
reports: [{ message: invalidScriptVersionMsg }],
|
||||
serverConfig: generateHTMLPage(generateScript(invalidVersionLink))
|
||||
},
|
||||
{
|
||||
name: 'JSLL script is not placed prior to other scripts',
|
||||
reports: [{ message: wrongScriptOrderMsg }],
|
||||
serverConfig: generateHTMLPage(generateScript(wrongScriptOrderLinks))
|
||||
},
|
||||
{
|
||||
name: 'JSLL script placed prior to other scripts',
|
||||
serverConfig: generateHTMLPage(generateScript(wrongScriptOrderLinks.reverse()))
|
||||
}
|
||||
];
|
||||
|
||||
ruleRunner.testRule(ruleName, tests);
|
|
@ -17,7 +17,6 @@
|
|||
"node_modules"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"tests/**/*.ts"
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче