feat: split rule and helpers, test rule
This commit is contained in:
parent
f8059452bc
commit
b3dc864f0d
|
@ -85,6 +85,7 @@ const bundle = {
|
||||||
'BaseError',
|
'BaseError',
|
||||||
'ConsoleLogger',
|
'ConsoleLogger',
|
||||||
'logWithLevel',
|
'logWithLevel',
|
||||||
|
'NullLogger',
|
||||||
],
|
],
|
||||||
'node_modules/js-yaml/index.js': [
|
'node_modules/js-yaml/index.js': [
|
||||||
'DEFAULT_SAFE_SCHEMA',
|
'DEFAULT_SAFE_SCHEMA',
|
||||||
|
|
|
@ -31,13 +31,16 @@ spec:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: 200m
|
cpu: 200m
|
||||||
|
# missing memory
|
||||||
requests:
|
requests:
|
||||||
cpu: 100m
|
cpu: 100m
|
||||||
|
# same rule
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
name: example
|
name: example
|
||||||
|
# missing labels
|
||||||
spec:
|
spec:
|
||||||
template:
|
template:
|
||||||
spec:
|
spec:
|
||||||
|
|
19
src/index.ts
19
src/index.ts
|
@ -3,7 +3,8 @@ import { createLogger } from 'bunyan';
|
||||||
import { loadConfig } from './config';
|
import { loadConfig } from './config';
|
||||||
import { CONFIG_ARGS_NAME, CONFIG_ARGS_PATH, parseArgs } from './config/args';
|
import { CONFIG_ARGS_NAME, CONFIG_ARGS_PATH, parseArgs } from './config/args';
|
||||||
import { YamlParser } from './parser/YamlParser';
|
import { YamlParser } from './parser/YamlParser';
|
||||||
import { loadRules, resolveRules, visitRules } from './rule';
|
import { loadRules, resolveRules } from './rule';
|
||||||
|
import { visitRules } from './rule/SchemaRule';
|
||||||
import { loadSource, writeSource } from './source';
|
import { loadSource, writeSource } from './source';
|
||||||
import { VERSION_INFO } from './version';
|
import { VERSION_INFO } from './version';
|
||||||
import { VisitorContext } from './visitor/VisitorContext';
|
import { VisitorContext } from './visitor/VisitorContext';
|
||||||
|
@ -59,19 +60,19 @@ export async function main(argv: Array<string>): Promise<number> {
|
||||||
await visitRules(ctx, activeRules, data);
|
await visitRules(ctx, activeRules, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.errors.length > 0) {
|
if (ctx.errors.length === 0) {
|
||||||
logger.error({ count: ctx.errors.length, errors: ctx.errors }, 'some rules failed');
|
|
||||||
if (args.count) {
|
|
||||||
return Math.min(ctx.errors.length, STATUS_MAX);
|
|
||||||
} else {
|
|
||||||
return STATUS_ERROR;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.info('all rules passed');
|
logger.info('all rules passed');
|
||||||
const output = parser.dump(...docs);
|
const output = parser.dump(...docs);
|
||||||
await writeSource(args.dest, output);
|
await writeSource(args.dest, output);
|
||||||
return STATUS_SUCCESS;
|
return STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.error({ count: ctx.errors.length, errors: ctx.errors }, 'some rules failed');
|
||||||
|
if (args.count) {
|
||||||
|
return Math.min(ctx.errors.length, STATUS_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
return STATUS_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
main(process.argv).then((status) => process.exit(status)).catch((err) => {
|
main(process.argv).then((status) => process.exit(status)).catch((err) => {
|
||||||
|
|
218
src/rule.ts
218
src/rule.ts
|
@ -1,218 +0,0 @@
|
||||||
import { ValidateFunction } from 'ajv';
|
|
||||||
import { applyDiff, diff } from 'deep-diff';
|
|
||||||
import { JSONPath } from 'jsonpath-plus';
|
|
||||||
import { cloneDeep, defaultTo, Dictionary, intersection, isNil } from 'lodash';
|
|
||||||
import { LogLevel } from 'noicejs';
|
|
||||||
|
|
||||||
import { YamlParser } from './parser/YamlParser';
|
|
||||||
import { readFileSync } from './source';
|
|
||||||
import { ensureArray, hasItems } from './utils';
|
|
||||||
import { friendlyError } from './utils/ajv';
|
|
||||||
import { Visitor } from './visitor';
|
|
||||||
import { VisitorContext } from './visitor/VisitorContext';
|
|
||||||
import { VisitorError } from './visitor/VisitorError';
|
|
||||||
import { VisitorResult } from './visitor/VisitorResult';
|
|
||||||
|
|
||||||
/* tslint:disable:no-any */
|
|
||||||
|
|
||||||
export interface RuleData {
|
|
||||||
// metadata
|
|
||||||
desc: string;
|
|
||||||
level: LogLevel;
|
|
||||||
name: string;
|
|
||||||
tags: Array<string>;
|
|
||||||
// data
|
|
||||||
check: any;
|
|
||||||
filter?: any;
|
|
||||||
select: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RuleSelector {
|
|
||||||
excludeLevel: Array<LogLevel>;
|
|
||||||
excludeName: Array<string>;
|
|
||||||
excludeTag: Array<string>;
|
|
||||||
includeLevel: Array<LogLevel>;
|
|
||||||
includeName: Array<string>;
|
|
||||||
includeTag: Array<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RuleSource {
|
|
||||||
definitions?: Dictionary<any>;
|
|
||||||
name: string;
|
|
||||||
rules: Array<RuleData>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RuleResult extends VisitorResult {
|
|
||||||
rule: Rule;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Rule implements RuleData, Visitor<RuleResult> {
|
|
||||||
public readonly check: ValidateFunction;
|
|
||||||
public readonly desc: string;
|
|
||||||
public readonly filter?: ValidateFunction;
|
|
||||||
public readonly level: LogLevel;
|
|
||||||
public readonly name: string;
|
|
||||||
public readonly select: string;
|
|
||||||
public readonly tags: Array<string>;
|
|
||||||
|
|
||||||
constructor(data: RuleData) {
|
|
||||||
this.desc = data.desc;
|
|
||||||
this.level = data.level;
|
|
||||||
this.name = data.name;
|
|
||||||
this.select = defaultTo(data.select, '$');
|
|
||||||
this.tags = Array.from(data.tags);
|
|
||||||
|
|
||||||
// copy schema objects
|
|
||||||
this.check = cloneDeep(data.check);
|
|
||||||
if (!isNil(data.filter)) {
|
|
||||||
this.filter = cloneDeep(data.filter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async pick(ctx: VisitorContext, root: any): Promise<Array<any>> {
|
|
||||||
const scopes = JSONPath({
|
|
||||||
json: root,
|
|
||||||
path: this.select,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isNil(scopes) || scopes.length === 0) {
|
|
||||||
ctx.logger.debug('no data selected');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return scopes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async visit(ctx: VisitorContext, node: any): Promise<RuleResult> {
|
|
||||||
ctx.logger.debug({ item: node, rule: this }, 'visiting node');
|
|
||||||
|
|
||||||
const check = ctx.compile(this.check);
|
|
||||||
const filter = this.compileFilter(ctx);
|
|
||||||
const errors: Array<VisitorError> = [];
|
|
||||||
const result: RuleResult = {
|
|
||||||
changes: [],
|
|
||||||
errors,
|
|
||||||
rule: this,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (filter(node)) {
|
|
||||||
ctx.logger.debug({ item: node }, 'checking item');
|
|
||||||
if (!check(node) && !isNil(check.errors) && check.errors.length > 0) {
|
|
||||||
ctx.error(...Array.from(check.errors).map(friendlyError));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.logger.debug({ errors: filter.errors, item: node }, 'skipping item');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected compileFilter(ctx: VisitorContext): ValidateFunction {
|
|
||||||
if (isNil(this.filter)) {
|
|
||||||
return () => true;
|
|
||||||
} else {
|
|
||||||
return ctx.compile(this.filter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeSelector(options: Partial<RuleSelector>) {
|
|
||||||
return {
|
|
||||||
excludeLevel: ensureArray(options.excludeLevel),
|
|
||||||
excludeName: ensureArray(options.excludeName),
|
|
||||||
excludeTag: ensureArray(options.excludeTag),
|
|
||||||
includeLevel: ensureArray(options.includeLevel),
|
|
||||||
includeName: ensureArray(options.includeName),
|
|
||||||
includeTag: ensureArray(options.includeTag),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadRules(paths: Array<string>, ctx: VisitorContext): Promise<Array<Rule>> {
|
|
||||||
const parser = new YamlParser();
|
|
||||||
const rules = [];
|
|
||||||
|
|
||||||
for (const path of paths) {
|
|
||||||
const contents = await readFileSync(path, {
|
|
||||||
encoding: 'utf-8',
|
|
||||||
});
|
|
||||||
|
|
||||||
const docs = parser.parse(contents) as Array<RuleSource>;
|
|
||||||
|
|
||||||
for (const data of docs) {
|
|
||||||
if (!isNil(data.definitions)) {
|
|
||||||
ctx.addSchema(data.name, data.definitions);
|
|
||||||
}
|
|
||||||
|
|
||||||
rules.push(...data.rules.map((it: RuleData) => new Rule(it)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rules;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resolveRules(rules: Array<Rule>, selector: RuleSelector): Promise<Array<Rule>> {
|
|
||||||
const activeRules = new Set<Rule>();
|
|
||||||
|
|
||||||
for (const r of rules) {
|
|
||||||
if (selector.excludeLevel.includes(r.level)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selector.excludeName.includes(r.name)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const excludedTags = intersection(selector.excludeTag, r.tags);
|
|
||||||
if (excludedTags.length > 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selector.includeLevel.includes(r.level)) {
|
|
||||||
activeRules.add(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selector.includeName.includes(r.name)) {
|
|
||||||
activeRules.add(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
const includedTags = intersection(selector.includeTag, r.tags);
|
|
||||||
if (includedTags.length > 0) {
|
|
||||||
activeRules.add(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(activeRules);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function visitRules(ctx: VisitorContext, rules: Array<Rule>, data: any): Promise<VisitorContext> {
|
|
||||||
for (const rule of rules) {
|
|
||||||
const items = await rule.pick(ctx, data);
|
|
||||||
for (const item of items) {
|
|
||||||
const itemCopy = cloneDeep(item);
|
|
||||||
const itemResult = await rule.visit(ctx, itemCopy);
|
|
||||||
|
|
||||||
if (itemResult.errors.length > 0) {
|
|
||||||
ctx.logger.warn({ count: itemResult.errors.length, rule }, 'rule failed');
|
|
||||||
ctx.mergeResult(itemResult);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemDiff = diff(item, itemCopy);
|
|
||||||
if (hasItems(itemDiff)) {
|
|
||||||
ctx.logger.info({
|
|
||||||
diff: itemDiff,
|
|
||||||
item,
|
|
||||||
rule: rule.name,
|
|
||||||
}, 'rule passed with modifications');
|
|
||||||
|
|
||||||
if (ctx.innerOptions.mutate) {
|
|
||||||
applyDiff(item, itemCopy);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.logger.info({ rule: rule.name }, 'rule passed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx;
|
|
||||||
}
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { ValidateFunction } from 'ajv';
|
||||||
|
import { applyDiff, diff } from 'deep-diff';
|
||||||
|
import { JSONPath } from 'jsonpath-plus';
|
||||||
|
import { cloneDeep, defaultTo, isNil } from 'lodash';
|
||||||
|
import { LogLevel } from 'noicejs';
|
||||||
|
|
||||||
|
import { RuleData } from '.';
|
||||||
|
import { hasItems } from '../utils';
|
||||||
|
import { friendlyError } from '../utils/ajv';
|
||||||
|
import { Visitor } from '../visitor';
|
||||||
|
import { VisitorContext } from '../visitor/VisitorContext';
|
||||||
|
import { VisitorError } from '../visitor/VisitorError';
|
||||||
|
import { VisitorResult } from '../visitor/VisitorResult';
|
||||||
|
|
||||||
|
export interface RuleResult extends VisitorResult {
|
||||||
|
rule: SchemaRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SchemaRule implements RuleData, Visitor<RuleResult> {
|
||||||
|
public readonly check: ValidateFunction;
|
||||||
|
public readonly desc: string;
|
||||||
|
public readonly filter?: ValidateFunction;
|
||||||
|
public readonly level: LogLevel;
|
||||||
|
public readonly name: string;
|
||||||
|
public readonly select: string;
|
||||||
|
public readonly tags: Array<string>;
|
||||||
|
|
||||||
|
constructor(data: RuleData) {
|
||||||
|
this.desc = data.desc;
|
||||||
|
this.level = data.level;
|
||||||
|
this.name = data.name;
|
||||||
|
this.select = defaultTo(data.select, '$');
|
||||||
|
this.tags = Array.from(data.tags);
|
||||||
|
|
||||||
|
// copy schema objects
|
||||||
|
this.check = cloneDeep(data.check);
|
||||||
|
if (!isNil(data.filter)) {
|
||||||
|
this.filter = cloneDeep(data.filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pick(ctx: VisitorContext, root: any): Promise<Array<any>> {
|
||||||
|
const scopes = JSONPath({
|
||||||
|
json: root,
|
||||||
|
path: this.select,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasItems(scopes)) {
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.logger.debug('no data selected');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async visit(ctx: VisitorContext, node: any): Promise<RuleResult> {
|
||||||
|
ctx.logger.debug({ item: node, rule: this }, 'visiting node');
|
||||||
|
|
||||||
|
const check = ctx.compile(this.check);
|
||||||
|
const filter = this.compileFilter(ctx);
|
||||||
|
const errors: Array<VisitorError> = [];
|
||||||
|
const result: RuleResult = {
|
||||||
|
changes: [],
|
||||||
|
errors,
|
||||||
|
rule: this,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filter(node)) {
|
||||||
|
ctx.logger.debug({ item: node }, 'checking item');
|
||||||
|
if (!check(node) && !isNil(check.errors) && check.errors.length > 0) {
|
||||||
|
ctx.error(...Array.from(check.errors).map(friendlyError));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.logger.debug({ errors: filter.errors, item: node }, 'skipping item');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected compileFilter(ctx: VisitorContext): ValidateFunction {
|
||||||
|
if (isNil(this.filter)) {
|
||||||
|
return () => true;
|
||||||
|
} else {
|
||||||
|
return ctx.compile(this.filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function visitRules(ctx: VisitorContext, rules: Array<SchemaRule>, data: any): Promise<VisitorContext> {
|
||||||
|
for (const rule of rules) {
|
||||||
|
const items = await rule.pick(ctx, data);
|
||||||
|
for (const item of items) {
|
||||||
|
const itemResult = cloneDeep(item);
|
||||||
|
const ruleResult = await rule.visit(ctx, itemResult);
|
||||||
|
|
||||||
|
if (ruleResult.errors.length > 0) {
|
||||||
|
ctx.logger.warn({ count: ruleResult.errors.length, rule }, 'rule failed');
|
||||||
|
ctx.mergeResult(ruleResult);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemDiff = diff(item, itemResult);
|
||||||
|
if (hasItems(itemDiff)) {
|
||||||
|
ctx.logger.info({
|
||||||
|
diff: itemDiff,
|
||||||
|
item,
|
||||||
|
rule: rule.name,
|
||||||
|
}, 'rule passed with modifications');
|
||||||
|
|
||||||
|
if (ctx.innerOptions.mutate) {
|
||||||
|
applyDiff(item, itemResult);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.logger.info({ rule: rule.name }, 'rule passed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { Dictionary, intersection, isNil } from 'lodash';
|
||||||
|
import { LogLevel } from 'noicejs';
|
||||||
|
|
||||||
|
import { YamlParser } from '../parser/YamlParser';
|
||||||
|
import { readFileSync } from '../source';
|
||||||
|
import { ensureArray } from '../utils';
|
||||||
|
import { VisitorContext } from '../visitor/VisitorContext';
|
||||||
|
import { SchemaRule } from './SchemaRule';
|
||||||
|
|
||||||
|
/* tslint:disable:no-any */
|
||||||
|
|
||||||
|
export interface RuleData {
|
||||||
|
// metadata
|
||||||
|
desc: string;
|
||||||
|
level: LogLevel;
|
||||||
|
name: string;
|
||||||
|
tags: Array<string>;
|
||||||
|
// data
|
||||||
|
check: any;
|
||||||
|
filter?: any;
|
||||||
|
select: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuleSelector {
|
||||||
|
excludeLevel: Array<LogLevel>;
|
||||||
|
excludeName: Array<string>;
|
||||||
|
excludeTag: Array<string>;
|
||||||
|
includeLevel: Array<LogLevel>;
|
||||||
|
includeName: Array<string>;
|
||||||
|
includeTag: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuleSource {
|
||||||
|
definitions?: Dictionary<any>;
|
||||||
|
name: string;
|
||||||
|
rules: Array<RuleData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeSelector(options: Partial<RuleSelector>) {
|
||||||
|
return {
|
||||||
|
excludeLevel: ensureArray(options.excludeLevel),
|
||||||
|
excludeName: ensureArray(options.excludeName),
|
||||||
|
excludeTag: ensureArray(options.excludeTag),
|
||||||
|
includeLevel: ensureArray(options.includeLevel),
|
||||||
|
includeName: ensureArray(options.includeName),
|
||||||
|
includeTag: ensureArray(options.includeTag),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadRules(paths: Array<string>, ctx: VisitorContext): Promise<Array<SchemaRule>> {
|
||||||
|
const parser = new YamlParser();
|
||||||
|
const rules = [];
|
||||||
|
|
||||||
|
for (const path of paths) {
|
||||||
|
const contents = await readFileSync(path, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
const docs = parser.parse(contents) as Array<RuleSource>;
|
||||||
|
|
||||||
|
for (const data of docs) {
|
||||||
|
if (!isNil(data.definitions)) {
|
||||||
|
ctx.addSchema(data.name, data.definitions);
|
||||||
|
}
|
||||||
|
|
||||||
|
rules.push(...data.rules.map((it: RuleData) => new SchemaRule(it)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveRules(rules: Array<SchemaRule>, selector: RuleSelector): Promise<Array<SchemaRule>> {
|
||||||
|
const activeRules = new Set<SchemaRule>();
|
||||||
|
|
||||||
|
for (const r of rules) {
|
||||||
|
if (selector.excludeLevel.includes(r.level)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selector.excludeName.includes(r.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const excludedTags = intersection(selector.excludeTag, r.tags);
|
||||||
|
if (excludedTags.length > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selector.includeLevel.includes(r.level)) {
|
||||||
|
activeRules.add(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selector.includeName.includes(r.name)) {
|
||||||
|
activeRules.add(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
const includedTags = intersection(selector.includeTag, r.tags);
|
||||||
|
if (includedTags.length > 0) {
|
||||||
|
activeRules.add(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(activeRules);
|
||||||
|
}
|
|
@ -2,25 +2,26 @@ import { expect } from 'chai';
|
||||||
import { ConsoleLogger } from 'noicejs';
|
import { ConsoleLogger } from 'noicejs';
|
||||||
import { mock, spy } from 'sinon';
|
import { mock, spy } from 'sinon';
|
||||||
|
|
||||||
import { makeSelector, resolveRules, Rule, visitRules } from '../src/rule';
|
import { makeSelector, resolveRules } from '../../src/rule';
|
||||||
import { VisitorContext } from '../src/visitor/VisitorContext';
|
import { SchemaRule, visitRules } from '../../src/rule/SchemaRule';
|
||||||
import { describeLeaks, itLeaks } from './helpers/async';
|
import { VisitorContext } from '../../src/visitor/VisitorContext';
|
||||||
|
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||||
|
|
||||||
const TEST_RULES = [new Rule({
|
const TEST_RULES = [new SchemaRule({
|
||||||
check: {},
|
check: {},
|
||||||
desc: '',
|
desc: '',
|
||||||
level: 'info',
|
level: 'info',
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
select: '$',
|
select: '$',
|
||||||
tags: ['all', 'foo'],
|
tags: ['all', 'foo'],
|
||||||
}), new Rule({
|
}), new SchemaRule({
|
||||||
check: {},
|
check: {},
|
||||||
desc: '',
|
desc: '',
|
||||||
level: 'warn',
|
level: 'warn',
|
||||||
name: 'bar',
|
name: 'bar',
|
||||||
select: '$',
|
select: '$',
|
||||||
tags: ['all', 'test'],
|
tags: ['all', 'test'],
|
||||||
}), new Rule({
|
}), new SchemaRule({
|
||||||
check: {},
|
check: {},
|
||||||
desc: '',
|
desc: '',
|
||||||
level: 'warn',
|
level: 'warn',
|
||||||
|
@ -111,7 +112,7 @@ describeLeaks('rule visitor', async () => {
|
||||||
logger: new ConsoleLogger(),
|
logger: new ConsoleLogger(),
|
||||||
});
|
});
|
||||||
const data = {};
|
const data = {};
|
||||||
const rule = new Rule({
|
const rule = new SchemaRule({
|
||||||
check: {},
|
check: {},
|
||||||
desc: '',
|
desc: '',
|
||||||
level: 'info',
|
level: 'info',
|
||||||
|
@ -143,7 +144,7 @@ describeLeaks('rule visitor', async () => {
|
||||||
logger: new ConsoleLogger(),
|
logger: new ConsoleLogger(),
|
||||||
});
|
});
|
||||||
const data = {};
|
const data = {};
|
||||||
const rule = new Rule({
|
const rule = new SchemaRule({
|
||||||
check: {},
|
check: {},
|
||||||
desc: '',
|
desc: '',
|
||||||
level: 'info',
|
level: 'info',
|
||||||
|
@ -180,7 +181,7 @@ describeLeaks('rule visitor', async () => {
|
||||||
const data = {
|
const data = {
|
||||||
foo: 3,
|
foo: 3,
|
||||||
};
|
};
|
||||||
const rule = new Rule({
|
const rule = new SchemaRule({
|
||||||
check: {},
|
check: {},
|
||||||
desc: '',
|
desc: '',
|
||||||
level: 'info',
|
level: 'info',
|
||||||
|
@ -207,7 +208,7 @@ describeLeaks('rule visitor', async () => {
|
||||||
const data = {
|
const data = {
|
||||||
foo: 3,
|
foo: 3,
|
||||||
};
|
};
|
||||||
const rule = new Rule({
|
const rule = new SchemaRule({
|
||||||
check: {},
|
check: {},
|
||||||
desc: '',
|
desc: '',
|
||||||
filter: {
|
filter: {
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { NullLogger } from 'noicejs';
|
||||||
|
import { stub } from 'sinon';
|
||||||
|
|
||||||
|
import { SchemaRule } from '../../src/rule/SchemaRule';
|
||||||
|
import { VisitorContext } from '../../src/visitor/VisitorContext';
|
||||||
|
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||||
|
|
||||||
|
const TEST_NAME = 'test-rule';
|
||||||
|
|
||||||
|
describeLeaks('schema rule', async () => {
|
||||||
|
itLeaks('should pick items from the root', async () => {
|
||||||
|
const ctx = new VisitorContext({
|
||||||
|
innerOptions: {
|
||||||
|
coerce: false,
|
||||||
|
defaults: false,
|
||||||
|
mutate: false,
|
||||||
|
},
|
||||||
|
logger: NullLogger.global,
|
||||||
|
});
|
||||||
|
const rule = new SchemaRule({
|
||||||
|
check: undefined,
|
||||||
|
desc: TEST_NAME,
|
||||||
|
level: 'info',
|
||||||
|
name: TEST_NAME,
|
||||||
|
select: '$.foo',
|
||||||
|
tags: [],
|
||||||
|
});
|
||||||
|
const results = await rule.pick(ctx, {
|
||||||
|
foo: [1, 2, 3],
|
||||||
|
});
|
||||||
|
expect(Array.isArray(results)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
itLeaks('should visit selected items', async () => {
|
||||||
|
const ctx = new VisitorContext({
|
||||||
|
innerOptions: {
|
||||||
|
coerce: false,
|
||||||
|
defaults: false,
|
||||||
|
mutate: false,
|
||||||
|
},
|
||||||
|
logger: NullLogger.global,
|
||||||
|
});
|
||||||
|
|
||||||
|
const check = {};
|
||||||
|
const checkSpy = stub().returns(true);
|
||||||
|
const filter = {};
|
||||||
|
const filterSpy = stub().returns(true);
|
||||||
|
ctx.compile = stub().onFirstCall().returns(checkSpy).onSecondCall().returns(filterSpy);
|
||||||
|
|
||||||
|
const rule = new SchemaRule({
|
||||||
|
check,
|
||||||
|
desc: TEST_NAME,
|
||||||
|
filter,
|
||||||
|
level: 'info',
|
||||||
|
name: TEST_NAME,
|
||||||
|
select: '$.foo',
|
||||||
|
tags: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = {};
|
||||||
|
await rule.visit(ctx, data);
|
||||||
|
|
||||||
|
expect(filterSpy, 'filter spy should have been called with data').to.have.callCount(1).and.been.calledWithExactly(data);
|
||||||
|
expect(checkSpy, 'check spy should have been called with data').to.have.callCount(1).and.been.calledWithExactly(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
itLeaks('should skip filtered items', async () => {
|
||||||
|
const ctx = new VisitorContext({
|
||||||
|
innerOptions: {
|
||||||
|
coerce: false,
|
||||||
|
defaults: false,
|
||||||
|
mutate: false,
|
||||||
|
},
|
||||||
|
logger: NullLogger.global,
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkSpy = stub().throws(new Error('check spy error'));
|
||||||
|
const filterSpy = stub().returns(false);
|
||||||
|
ctx.compile = stub().onFirstCall().returns(checkSpy).onSecondCall().returns(filterSpy);
|
||||||
|
|
||||||
|
const rule = new SchemaRule({
|
||||||
|
check: undefined,
|
||||||
|
desc: TEST_NAME,
|
||||||
|
filter: {},
|
||||||
|
level: 'info',
|
||||||
|
name: TEST_NAME,
|
||||||
|
select: '$.foo',
|
||||||
|
tags: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = {};
|
||||||
|
await rule.visit(ctx, data);
|
||||||
|
|
||||||
|
expect(filterSpy, 'filter spy should have been called with data').to.have.callCount(1).and.been.calledWithExactly(data);
|
||||||
|
expect(checkSpy, 'check spy should not have been called').to.have.callCount(0);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue