@daltonr/authwrite-testing 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +24 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
- package/src/index.ts +78 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AuthEngine, AuthObserver, DecisionEvent, Decision, Subject, Resource } from '@daltonr/authwrite-core';
|
|
2
|
+
export interface DecisionRecorder extends AuthObserver {
|
|
3
|
+
/** All decision events recorded so far, in order. */
|
|
4
|
+
all(): DecisionEvent[];
|
|
5
|
+
/** Shorthand — just the Decision objects, without the wrapping event. */
|
|
6
|
+
decisions(): Decision[];
|
|
7
|
+
/** Clear all recorded events. */
|
|
8
|
+
clear(): void;
|
|
9
|
+
}
|
|
10
|
+
export declare function decisionRecorder(): DecisionRecorder;
|
|
11
|
+
export interface CoverageReport {
|
|
12
|
+
totalRules: number;
|
|
13
|
+
coveredRules: string[];
|
|
14
|
+
untouchedRules: string[];
|
|
15
|
+
coveragePercent: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Analyse which rules in the engine's active policy were never the deciding
|
|
19
|
+
* reason in any recorded event. Pass `recorder.all()` as the second argument.
|
|
20
|
+
*
|
|
21
|
+
* An untouched rule is a silent security hole — if a deny rule has never fired
|
|
22
|
+
* in your test suite, you have no evidence it works.
|
|
23
|
+
*/
|
|
24
|
+
export declare function coverageReport<S extends Subject = Subject, R extends Resource = Resource>(engine: AuthEngine<S, R>, events: DecisionEvent[]): CoverageReport;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function decisionRecorder() {
|
|
2
|
+
const events = [];
|
|
3
|
+
return {
|
|
4
|
+
onDecision(event) {
|
|
5
|
+
events.push(event);
|
|
6
|
+
},
|
|
7
|
+
all() {
|
|
8
|
+
return [...events];
|
|
9
|
+
},
|
|
10
|
+
decisions() {
|
|
11
|
+
return events.map(e => e.decision);
|
|
12
|
+
},
|
|
13
|
+
clear() {
|
|
14
|
+
events.length = 0;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Analyse which rules in the engine's active policy were never the deciding
|
|
20
|
+
* reason in any recorded event. Pass `recorder.all()` as the second argument.
|
|
21
|
+
*
|
|
22
|
+
* An untouched rule is a silent security hole — if a deny rule has never fired
|
|
23
|
+
* in your test suite, you have no evidence it works.
|
|
24
|
+
*/
|
|
25
|
+
export function coverageReport(engine, events) {
|
|
26
|
+
const policy = engine.getPolicy();
|
|
27
|
+
if (!policy) {
|
|
28
|
+
throw new Error('coverageReport requires a static policy — engine has a dynamic or composite resolver. ' +
|
|
29
|
+
'Run at least one evaluation first, or pass a static PolicyDefinition to createAuthEngine.');
|
|
30
|
+
}
|
|
31
|
+
const allRuleIds = policy.rules.map(r => r.id);
|
|
32
|
+
const reasonsHit = new Set(events.map(e => e.decision.reason));
|
|
33
|
+
const coveredRules = allRuleIds.filter(id => reasonsHit.has(id));
|
|
34
|
+
const untouchedRules = allRuleIds.filter(id => !reasonsHit.has(id));
|
|
35
|
+
const coveragePercent = allRuleIds.length === 0 ? 100 : (coveredRules.length / allRuleIds.length) * 100;
|
|
36
|
+
return { totalRules: allRuleIds.length, coveredRules, untouchedRules, coveragePercent };
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,MAAM,UAAU,gBAAgB;IAC9B,MAAM,MAAM,GAAoB,EAAE,CAAA;IAElC,OAAO;QACL,UAAU,CAAC,KAAK;YACd,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACpB,CAAC;QACD,GAAG;YACD,OAAO,CAAC,GAAG,MAAM,CAAC,CAAA;QACpB,CAAC;QACD,SAAS;YACP,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;QACpC,CAAC;QACD,KAAK;YACH,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;QACnB,CAAC;KACF,CAAA;AACH,CAAC;AAWD;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAwB,EACxB,MAAuB;IAEvB,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,CAAA;IACjC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,wFAAwF;YACxF,2FAA2F,CAC5F,CAAA;IACH,CAAC;IACD,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAC9C,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;IAE9D,MAAM,YAAY,GAAG,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IAChE,MAAM,cAAc,GAAG,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IAEnE,MAAM,eAAe,GACnB,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,GAAG,GAAG,CAAA;IAEjF,OAAO,EAAE,UAAU,EAAE,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,CAAA;AACzF,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@daltonr/authwrite-testing",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Test helpers for AuthEngine: decision recorder and policy coverage report.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/richardadalton/authwrite.git",
|
|
10
|
+
"directory": "packages/testing"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["authorization", "authz", "testing", "coverage", "policy"],
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"files": ["dist", "src", "README.md", "LICENSE"],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
26
|
+
"prepublishOnly": "test -d dist && echo 'dist already built, skipping' || (npm run clean && npm run build)"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@daltonr/authwrite-core": "*"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthEngine,
|
|
3
|
+
AuthObserver,
|
|
4
|
+
DecisionEvent,
|
|
5
|
+
Decision,
|
|
6
|
+
PolicyDefinition,
|
|
7
|
+
Subject,
|
|
8
|
+
Resource,
|
|
9
|
+
} from '@daltonr/authwrite-core'
|
|
10
|
+
|
|
11
|
+
// ─── DecisionRecorder ────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export interface DecisionRecorder extends AuthObserver {
|
|
14
|
+
/** All decision events recorded so far, in order. */
|
|
15
|
+
all(): DecisionEvent[]
|
|
16
|
+
/** Shorthand — just the Decision objects, without the wrapping event. */
|
|
17
|
+
decisions(): Decision[]
|
|
18
|
+
/** Clear all recorded events. */
|
|
19
|
+
clear(): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function decisionRecorder(): DecisionRecorder {
|
|
23
|
+
const events: DecisionEvent[] = []
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
onDecision(event) {
|
|
27
|
+
events.push(event)
|
|
28
|
+
},
|
|
29
|
+
all() {
|
|
30
|
+
return [...events]
|
|
31
|
+
},
|
|
32
|
+
decisions() {
|
|
33
|
+
return events.map(e => e.decision)
|
|
34
|
+
},
|
|
35
|
+
clear() {
|
|
36
|
+
events.length = 0
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── CoverageReport ──────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export interface CoverageReport {
|
|
44
|
+
totalRules: number
|
|
45
|
+
coveredRules: string[]
|
|
46
|
+
untouchedRules: string[]
|
|
47
|
+
coveragePercent: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Analyse which rules in the engine's active policy were never the deciding
|
|
52
|
+
* reason in any recorded event. Pass `recorder.all()` as the second argument.
|
|
53
|
+
*
|
|
54
|
+
* An untouched rule is a silent security hole — if a deny rule has never fired
|
|
55
|
+
* in your test suite, you have no evidence it works.
|
|
56
|
+
*/
|
|
57
|
+
export function coverageReport<S extends Subject = Subject, R extends Resource = Resource>(
|
|
58
|
+
engine: AuthEngine<S, R>,
|
|
59
|
+
events: DecisionEvent[]
|
|
60
|
+
): CoverageReport {
|
|
61
|
+
const policy = engine.getPolicy()
|
|
62
|
+
if (!policy) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
'coverageReport requires a static policy — engine has a dynamic or composite resolver. ' +
|
|
65
|
+
'Run at least one evaluation first, or pass a static PolicyDefinition to createAuthEngine.'
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
const allRuleIds = policy.rules.map(r => r.id)
|
|
69
|
+
const reasonsHit = new Set(events.map(e => e.decision.reason))
|
|
70
|
+
|
|
71
|
+
const coveredRules = allRuleIds.filter(id => reasonsHit.has(id))
|
|
72
|
+
const untouchedRules = allRuleIds.filter(id => !reasonsHit.has(id))
|
|
73
|
+
|
|
74
|
+
const coveragePercent =
|
|
75
|
+
allRuleIds.length === 0 ? 100 : (coveredRules.length / allRuleIds.length) * 100
|
|
76
|
+
|
|
77
|
+
return { totalRules: allRuleIds.length, coveredRules, untouchedRules, coveragePercent }
|
|
78
|
+
}
|