@safeaccess/inline 0.1.1 → 0.1.2
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/.gitattributes +1 -1
- package/CHANGELOG.md +10 -5
- package/LICENSE +1 -1
- package/README.md +56 -14
- package/dist/accessors/abstract-accessor.d.ts +22 -10
- package/dist/accessors/abstract-accessor.js +21 -8
- package/dist/accessors/abstract-integration-accessor.d.ts +22 -0
- package/dist/accessors/abstract-integration-accessor.js +23 -0
- package/dist/accessors/formats/any-accessor.d.ts +10 -8
- package/dist/accessors/formats/any-accessor.js +9 -8
- package/dist/accessors/formats/array-accessor.d.ts +2 -0
- package/dist/accessors/formats/array-accessor.js +2 -0
- package/dist/accessors/formats/env-accessor.d.ts +2 -0
- package/dist/accessors/formats/env-accessor.js +2 -0
- package/dist/accessors/formats/ini-accessor.d.ts +2 -0
- package/dist/accessors/formats/ini-accessor.js +2 -0
- package/dist/accessors/formats/json-accessor.d.ts +2 -0
- package/dist/accessors/formats/json-accessor.js +2 -0
- package/dist/accessors/formats/ndjson-accessor.d.ts +2 -0
- package/dist/accessors/formats/ndjson-accessor.js +2 -0
- package/dist/accessors/formats/object-accessor.d.ts +2 -0
- package/dist/accessors/formats/object-accessor.js +2 -0
- package/dist/accessors/formats/xml-accessor.d.ts +2 -0
- package/dist/accessors/formats/xml-accessor.js +2 -0
- package/dist/accessors/formats/yaml-accessor.d.ts +3 -1
- package/dist/accessors/formats/yaml-accessor.js +4 -2
- package/dist/cache/simple-path-cache.d.ts +51 -0
- package/dist/cache/simple-path-cache.js +72 -0
- package/dist/contracts/accessors-interface.d.ts +2 -0
- package/dist/contracts/factory-accessors-interface.d.ts +2 -0
- package/dist/contracts/filter-evaluator-interface.d.ts +28 -0
- package/dist/contracts/filter-evaluator-interface.js +1 -0
- package/dist/contracts/parse-integration-interface.d.ts +2 -0
- package/dist/contracts/parser-interface.d.ts +92 -0
- package/dist/contracts/parser-interface.js +1 -0
- package/dist/contracts/path-cache-interface.d.ts +7 -6
- package/dist/contracts/readable-accessors-interface.d.ts +11 -6
- package/dist/contracts/security-guard-interface.d.ts +2 -0
- package/dist/contracts/security-parser-interface.d.ts +2 -0
- package/dist/contracts/validatable-parser-interface.d.ts +59 -0
- package/dist/contracts/validatable-parser-interface.js +1 -0
- package/dist/contracts/writable-accessors-interface.d.ts +5 -0
- package/dist/core/accessor-factory.d.ts +124 -0
- package/dist/core/accessor-factory.js +157 -0
- package/dist/core/dot-notation-parser.d.ts +34 -5
- package/dist/core/dot-notation-parser.js +51 -10
- package/dist/core/inline-builder-accessor.d.ts +82 -0
- package/dist/core/inline-builder-accessor.js +107 -0
- package/dist/exceptions/accessor-exception.d.ts +9 -0
- package/dist/exceptions/accessor-exception.js +9 -0
- package/dist/exceptions/invalid-format-exception.d.ts +5 -0
- package/dist/exceptions/invalid-format-exception.js +5 -0
- package/dist/exceptions/parser-exception.d.ts +4 -0
- package/dist/exceptions/parser-exception.js +4 -0
- package/dist/exceptions/path-not-found-exception.d.ts +4 -0
- package/dist/exceptions/path-not-found-exception.js +4 -0
- package/dist/exceptions/readonly-violation-exception.d.ts +4 -0
- package/dist/exceptions/readonly-violation-exception.js +4 -0
- package/dist/exceptions/security-exception.d.ts +6 -0
- package/dist/exceptions/security-exception.js +6 -0
- package/dist/exceptions/unsupported-type-exception.d.ts +4 -0
- package/dist/exceptions/unsupported-type-exception.js +4 -0
- package/dist/exceptions/yaml-parse-exception.d.ts +4 -0
- package/dist/exceptions/yaml-parse-exception.js +4 -0
- package/dist/index.js +2 -1
- package/dist/inline.d.ts +22 -56
- package/dist/inline.js +39 -111
- package/dist/parser/xml-parser.js +23 -10
- package/dist/parser/yaml-parser.d.ts +54 -7
- package/dist/parser/yaml-parser.js +268 -51
- package/dist/path-query/segment-filter-parser.d.ts +142 -0
- package/dist/path-query/segment-filter-parser.js +384 -0
- package/dist/path-query/segment-parser.d.ts +98 -0
- package/dist/path-query/segment-parser.js +283 -0
- package/dist/path-query/segment-path-resolver.d.ts +149 -0
- package/dist/path-query/segment-path-resolver.js +351 -0
- package/dist/path-query/segment-type.d.ts +85 -0
- package/dist/path-query/segment-type.js +35 -0
- package/dist/security/forbidden-keys.d.ts +2 -2
- package/dist/security/forbidden-keys.js +5 -5
- package/dist/security/security-guard.d.ts +3 -1
- package/dist/security/security-guard.js +5 -2
- package/dist/security/security-parser.d.ts +10 -1
- package/dist/security/security-parser.js +10 -1
- package/dist/type-format.d.ts +2 -0
- package/dist/type-format.js +2 -0
- package/package.json +11 -3
- package/src/accessors/abstract-accessor.ts +23 -19
- package/src/accessors/abstract-integration-accessor.ts +27 -0
- package/src/accessors/formats/any-accessor.ts +11 -11
- package/src/accessors/formats/array-accessor.ts +2 -0
- package/src/accessors/formats/env-accessor.ts +2 -0
- package/src/accessors/formats/ini-accessor.ts +2 -0
- package/src/accessors/formats/json-accessor.ts +2 -0
- package/src/accessors/formats/ndjson-accessor.ts +2 -0
- package/src/accessors/formats/object-accessor.ts +2 -0
- package/src/accessors/formats/xml-accessor.ts +2 -0
- package/src/accessors/formats/yaml-accessor.ts +4 -2
- package/src/cache/simple-path-cache.ts +77 -0
- package/src/contracts/accessors-interface.ts +2 -0
- package/src/contracts/factory-accessors-interface.ts +2 -0
- package/src/contracts/filter-evaluator-interface.ts +30 -0
- package/src/contracts/parse-integration-interface.ts +2 -0
- package/src/contracts/parser-interface.ts +114 -0
- package/src/contracts/path-cache-interface.ts +8 -6
- package/src/contracts/readable-accessors-interface.ts +11 -6
- package/src/contracts/security-guard-interface.ts +2 -0
- package/src/contracts/security-parser-interface.ts +2 -0
- package/src/contracts/validatable-parser-interface.ts +64 -0
- package/src/contracts/writable-accessors-interface.ts +5 -0
- package/src/core/accessor-factory.ts +173 -0
- package/src/core/dot-notation-parser.ts +74 -11
- package/src/core/inline-builder-accessor.ts +163 -0
- package/src/exceptions/accessor-exception.ts +9 -0
- package/src/exceptions/invalid-format-exception.ts +5 -0
- package/src/exceptions/parser-exception.ts +4 -0
- package/src/exceptions/path-not-found-exception.ts +4 -0
- package/src/exceptions/readonly-violation-exception.ts +4 -0
- package/src/exceptions/security-exception.ts +6 -0
- package/src/exceptions/unsupported-type-exception.ts +4 -0
- package/src/exceptions/yaml-parse-exception.ts +4 -0
- package/src/index.ts +3 -1
- package/src/inline.ts +42 -120
- package/src/parser/xml-parser.ts +31 -10
- package/src/parser/yaml-parser.ts +310 -45
- package/src/path-query/segment-filter-parser.ts +444 -0
- package/src/path-query/segment-parser.ts +321 -0
- package/src/path-query/segment-path-resolver.ts +521 -0
- package/src/path-query/segment-type.ts +82 -0
- package/src/security/forbidden-keys.ts +5 -5
- package/src/security/security-guard.ts +7 -2
- package/src/security/security-parser.ts +18 -3
- package/src/type-format.ts +2 -0
- package/stryker.config.json +8 -10
- package/tests/accessors/abstract-accessor.test.ts +217 -0
- package/tests/accessors/abstract-integration-accessor.test.ts +37 -0
- package/tests/accessors/formats/any-accessor.test.ts +57 -0
- package/tests/accessors/formats/array-accessor.test.ts +42 -0
- package/tests/accessors/formats/env-accessor.test.ts +103 -0
- package/tests/accessors/formats/ini-accessor.test.ts +186 -0
- package/tests/accessors/{json-accessor.test.ts → formats/json-accessor.test.ts} +6 -6
- package/tests/accessors/formats/ndjson-accessor.test.ts +49 -0
- package/tests/accessors/formats/object-accessor.test.ts +172 -0
- package/tests/accessors/formats/xml-accessor.test.ts +162 -0
- package/tests/accessors/formats/yaml-accessor.test.ts +36 -0
- package/tests/cache/simple-path-cache.test.ts +168 -0
- package/tests/core/accessor-factory.test.ts +157 -0
- package/tests/core/dot-notation-parser-edge-cases.test.ts +415 -0
- package/tests/core/dot-notation-parser.test.ts +0 -288
- package/tests/core/inline-builder-accessor.test.ts +114 -0
- package/tests/exceptions/accessor-exception.test.ts +28 -0
- package/tests/exceptions/invalid-format-exception.test.ts +31 -0
- package/tests/exceptions/path-not-found-exception.test.ts +33 -0
- package/tests/exceptions/readonly-violation-exception.test.ts +35 -0
- package/tests/exceptions/security-exception.test.ts +33 -0
- package/tests/exceptions/unsupported-type-exception.test.ts +33 -0
- package/tests/exceptions/yaml-parse-exception.test.ts +38 -0
- package/tests/mocks/fake-path-cache.ts +4 -3
- package/tests/parity-from.test.ts +118 -0
- package/tests/parity.test.ts +227 -10
- package/tests/parser/xml-parser-mutations.test.ts +579 -0
- package/tests/parser/xml-parser-scanner.test.ts +332 -0
- package/tests/parser/xml-parser.test.ts +10 -334
- package/tests/parser/yaml-parser-mutations.test.ts +750 -0
- package/tests/parser/yaml-parser.test.ts +844 -18
- package/tests/path-query/segment-filter-parser-mutations.test.ts +735 -0
- package/tests/path-query/segment-filter-parser.test.ts +1091 -0
- package/tests/path-query/segment-parser-mutations.test.ts +539 -0
- package/tests/path-query/segment-parser.test.ts +606 -0
- package/tests/path-query/segment-path-resolver-mutations.test.ts +626 -0
- package/tests/path-query/segment-path-resolver.test.ts +1009 -0
- package/tests/security/security-guard-advanced.test.ts +413 -0
- package/tests/security/security-guard-forbidden-keys.test.ts +87 -0
- package/tests/security/security-guard.test.ts +3 -484
- package/tests/security/security-parser.test.ts +18 -14
- package/vitest.config.ts +3 -3
- package/benchmarks/get.bench.ts +0 -26
- package/benchmarks/parse.bench.ts +0 -41
- package/tests/accessors/accessors.test.ts +0 -1017
|
@@ -7,6 +7,8 @@ import { SecurityException } from '../exceptions/security-exception.js';
|
|
|
7
7
|
* Validates payload size, maximum key count, recursion depth, and
|
|
8
8
|
* structural depth limits.
|
|
9
9
|
*
|
|
10
|
+
* @api
|
|
11
|
+
*
|
|
10
12
|
* @example
|
|
11
13
|
* const parser = new SecurityParser({ maxDepth: 10, maxKeys: 100 });
|
|
12
14
|
* parser.assertPayloadSize('{"key":"value"}');
|
|
@@ -27,7 +29,7 @@ export class SecurityParser implements SecurityParserInterface {
|
|
|
27
29
|
* @param options.maxKeys - Maximum total number of keys across the entire structure. Default: 10000.
|
|
28
30
|
* This value is also passed to `XmlParser` as the element-count cap for the Node.js manual XML
|
|
29
31
|
* parser path. Setting it below a document's element count will cause `fromXml()` to throw
|
|
30
|
-
* `SecurityException`. Non-positive or non-finite values disable that guard
|
|
32
|
+
* `SecurityException`. Non-positive or non-finite values disable that guard - prefer the default.
|
|
31
33
|
* @param options.maxCountRecursiveDepth - Maximum recursion depth when counting keys. Default: 100.
|
|
32
34
|
* @param options.maxResolveDepth - Maximum recursion depth for path resolution. Default: 100.
|
|
33
35
|
*/
|
|
@@ -41,9 +43,15 @@ export class SecurityParser implements SecurityParserInterface {
|
|
|
41
43
|
} = {},
|
|
42
44
|
) {
|
|
43
45
|
this.maxDepth = SecurityParser.clampOption(options.maxDepth, 512);
|
|
44
|
-
this.maxPayloadBytes = SecurityParser.clampOption(
|
|
46
|
+
this.maxPayloadBytes = SecurityParser.clampOption(
|
|
47
|
+
options.maxPayloadBytes,
|
|
48
|
+
10 * 1024 * 1024,
|
|
49
|
+
);
|
|
45
50
|
this.maxKeys = SecurityParser.clampOption(options.maxKeys, 10_000);
|
|
46
|
-
this.maxCountRecursiveDepth = SecurityParser.clampOption(
|
|
51
|
+
this.maxCountRecursiveDepth = SecurityParser.clampOption(
|
|
52
|
+
options.maxCountRecursiveDepth,
|
|
53
|
+
100,
|
|
54
|
+
);
|
|
47
55
|
this.maxResolveDepth = SecurityParser.clampOption(options.maxResolveDepth, 100);
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -226,6 +234,13 @@ export class SecurityParser implements SecurityParserInterface {
|
|
|
226
234
|
return max;
|
|
227
235
|
}
|
|
228
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Clamp an optional numeric option to its default when not finite.
|
|
239
|
+
*
|
|
240
|
+
* @param value - User-provided option value.
|
|
241
|
+
* @param defaultValue - Fallback when value is undefined or non-finite.
|
|
242
|
+
* @returns Clamped numeric value.
|
|
243
|
+
*/
|
|
229
244
|
private static clampOption(value: number | undefined, defaultValue: number): number {
|
|
230
245
|
/* Stryker disable next-line ConditionalExpression -- equivalent: Number.isFinite covers undefined (isFinite(undefined)===false); simplest safe form */
|
|
231
246
|
return Number.isFinite(value) ? (value as number) : defaultValue;
|
package/src/type-format.ts
CHANGED
package/stryker.config.json
CHANGED
|
@@ -6,19 +6,17 @@
|
|
|
6
6
|
"!src/index.ts",
|
|
7
7
|
"!src/contracts/**/*.ts",
|
|
8
8
|
"!src/exceptions/**/*.ts",
|
|
9
|
-
"!src/security/forbidden-keys.ts"
|
|
10
|
-
"!src/parser/yaml-parser.ts",
|
|
11
|
-
"!src/parser/xml-parser.ts"
|
|
9
|
+
"!src/security/forbidden-keys.ts"
|
|
12
10
|
],
|
|
13
11
|
"thresholds": {
|
|
14
12
|
"high": 100,
|
|
15
|
-
"low":
|
|
16
|
-
"break":
|
|
13
|
+
"low": 90,
|
|
14
|
+
"break": 93
|
|
17
15
|
},
|
|
18
|
-
"timeoutMS":
|
|
19
|
-
"
|
|
20
|
-
"reporters": ["
|
|
21
|
-
"
|
|
22
|
-
"fileName": "reports/
|
|
16
|
+
"timeoutMS": 30000,
|
|
17
|
+
"coverageAnalysis": "perTest",
|
|
18
|
+
"reporters": ["json", "clear-text", "progress"],
|
|
19
|
+
"jsonReporter": {
|
|
20
|
+
"fileName": "reports/mutation/mutation.json"
|
|
23
21
|
}
|
|
24
22
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { AbstractAccessor } from '../../src/accessors/abstract-accessor.js';
|
|
3
|
+
import { JsonAccessor } from '../../src/accessors/formats/json-accessor.js';
|
|
4
|
+
import { DotNotationParser } from '../../src/core/dot-notation-parser.js';
|
|
5
|
+
import { SecurityGuard } from '../../src/security/security-guard.js';
|
|
6
|
+
import { SecurityParser } from '../../src/security/security-parser.js';
|
|
7
|
+
import { SecurityException } from '../../src/exceptions/security-exception.js';
|
|
8
|
+
import { ReadonlyViolationException } from '../../src/exceptions/readonly-violation-exception.js';
|
|
9
|
+
import { PathNotFoundException } from '../../src/exceptions/path-not-found-exception.js';
|
|
10
|
+
|
|
11
|
+
function makeParser(secParser?: SecurityParser): DotNotationParser {
|
|
12
|
+
return new DotNotationParser(new SecurityGuard(), secParser ?? new SecurityParser());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe(AbstractAccessor.name, () => {
|
|
16
|
+
it('strict mode default is enabled - validates on ingest', () => {
|
|
17
|
+
expect(() => new JsonAccessor(makeParser()).from('{"__proto__": "bad"}')).toThrow(
|
|
18
|
+
SecurityException,
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('strict(false) disables validation', () => {
|
|
23
|
+
const a = new JsonAccessor(makeParser()).strict(false).from('{"__proto__": "ok"}');
|
|
24
|
+
expect(a.get('__proto__')).toBe('ok');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('strict(true) re-enables validation', () => {
|
|
28
|
+
const accessor = new JsonAccessor(makeParser()).strict(false);
|
|
29
|
+
const strictAgain = accessor.strict(true);
|
|
30
|
+
expect(() => strictAgain.from('{"__proto__": "bad"}')).toThrow(SecurityException);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('readonly(true) blocks set()', () => {
|
|
34
|
+
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
|
|
35
|
+
expect(() => a.set('x', 2)).toThrow(ReadonlyViolationException);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('readonly(true) blocks remove()', () => {
|
|
39
|
+
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
|
|
40
|
+
expect(() => a.remove('x')).toThrow(ReadonlyViolationException);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('readonly(false) allows mutation after readonly(true)', () => {
|
|
44
|
+
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true).readonly(false);
|
|
45
|
+
expect(() => a.set('x', 2)).not.toThrow();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('merge() combines two objects at root level', () => {
|
|
49
|
+
const a = new JsonAccessor(makeParser()).from('{"a":1}');
|
|
50
|
+
const merged = a.merge('', { b: 2 });
|
|
51
|
+
expect(merged.get('a')).toBe(1);
|
|
52
|
+
expect(merged.get('b')).toBe(2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('merge() at a nested path', () => {
|
|
56
|
+
const a = new JsonAccessor(makeParser()).from('{"user":{"name":"Alice"}}');
|
|
57
|
+
const merged = a.merge('user', { role: 'admin' });
|
|
58
|
+
expect(merged.get('user.name')).toBe('Alice');
|
|
59
|
+
expect(merged.get('user.role')).toBe('admin');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('all() returns all parsed data', () => {
|
|
63
|
+
const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2}');
|
|
64
|
+
expect(a.all()).toEqual({ a: 1, b: 2 });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('keys() returns root-level keys', () => {
|
|
68
|
+
const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2}');
|
|
69
|
+
expect(a.keys()).toEqual(['a', 'b']);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('count() returns number of root keys', () => {
|
|
73
|
+
const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2,"c":3}');
|
|
74
|
+
expect(a.count()).toBe(3);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('getRaw() returns original input', () => {
|
|
78
|
+
const json = '{"name":"Alice"}';
|
|
79
|
+
expect(new JsonAccessor(makeParser()).from(json).getRaw()).toBe(json);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('getOrFail() throws PathNotFoundException for missing path', () => {
|
|
83
|
+
const a = new JsonAccessor(makeParser()).from('{}');
|
|
84
|
+
expect(() => a.getOrFail('missing')).toThrow(PathNotFoundException);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('readonly() with no argument defaults to true (blocks mutations)', () => {
|
|
88
|
+
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly();
|
|
89
|
+
expect(() => a.set('x', 2)).toThrow(ReadonlyViolationException);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('strict() with no argument defaults to true (enables validation)', () => {
|
|
93
|
+
const accessor = new JsonAccessor(makeParser()).strict(false);
|
|
94
|
+
const strictAgain = accessor.strict();
|
|
95
|
+
expect(() => strictAgain.from('{"__proto__": "bad"}')).toThrow(SecurityException);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('getAt() returns null when path does not exist (default is null)', () => {
|
|
99
|
+
const a = new JsonAccessor(makeParser()).from('{"a":1}');
|
|
100
|
+
expect(a.getAt(['missing'])).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('getAt() resolves a value using pre-parsed segments', () => {
|
|
104
|
+
const a = new JsonAccessor(makeParser()).from('{"user":{"name":"Alice"}}');
|
|
105
|
+
expect(a.getAt(['user', 'name'])).toBe('Alice');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('hasAt() returns true when segments resolve to a value', () => {
|
|
109
|
+
const a = new JsonAccessor(makeParser()).from('{"a":{"b":1}}');
|
|
110
|
+
expect(a.hasAt(['a', 'b'])).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('hasAt() returns false when segments do not resolve', () => {
|
|
114
|
+
const a = new JsonAccessor(makeParser()).from('{"a":1}');
|
|
115
|
+
expect(a.hasAt(['missing'])).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('setAt() sets a value using pre-parsed segments', () => {
|
|
119
|
+
const a = new JsonAccessor(makeParser()).from('{}');
|
|
120
|
+
const updated = a.setAt(['user', 'name'], 'Alice');
|
|
121
|
+
expect(updated.get('user.name')).toBe('Alice');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('setAt() throws ReadonlyViolationException when readonly', () => {
|
|
125
|
+
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
|
|
126
|
+
expect(() => a.setAt(['x'], 2)).toThrow(ReadonlyViolationException);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('removeAt() removes a value using pre-parsed segments', () => {
|
|
130
|
+
const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2}');
|
|
131
|
+
const updated = a.removeAt(['a']);
|
|
132
|
+
expect(updated.has('a')).toBe(false);
|
|
133
|
+
expect(updated.has('b')).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('removeAt() throws ReadonlyViolationException when readonly', () => {
|
|
137
|
+
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
|
|
138
|
+
expect(() => a.removeAt(['x'])).toThrow(ReadonlyViolationException);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('getMany() returns map of paths to their values', () => {
|
|
142
|
+
const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2}');
|
|
143
|
+
expect(a.getMany({ a: 0, b: 0, missing: 'fallback' })).toEqual({
|
|
144
|
+
a: 1,
|
|
145
|
+
b: 2,
|
|
146
|
+
missing: 'fallback',
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('count(path) returns number of keys at a nested path', () => {
|
|
151
|
+
const a = new JsonAccessor(makeParser()).from('{"user":{"name":"Alice","age":30}}');
|
|
152
|
+
expect(a.count('user')).toBe(2);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('count(path) returns 0 when path resolves to a non-object', () => {
|
|
156
|
+
const a = new JsonAccessor(makeParser()).from('{"a":"string"}');
|
|
157
|
+
expect(a.count('a')).toBe(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('keys(path) returns keys at a nested path', () => {
|
|
161
|
+
const a = new JsonAccessor(makeParser()).from('{"user":{"name":"Alice","role":"admin"}}');
|
|
162
|
+
expect(a.keys('user')).toEqual(['name', 'role']);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('keys(path) returns empty array when path resolves to a non-object', () => {
|
|
166
|
+
const a = new JsonAccessor(makeParser()).from('{"a":42}');
|
|
167
|
+
expect(a.keys('a')).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('mergeAll() deep-merges into root', () => {
|
|
171
|
+
const a = new JsonAccessor(makeParser()).from('{"a":1}');
|
|
172
|
+
const merged = a.mergeAll({ b: 2, c: 3 });
|
|
173
|
+
expect(merged.get('a')).toBe(1);
|
|
174
|
+
expect(merged.get('b')).toBe(2);
|
|
175
|
+
expect(merged.get('c')).toBe(3);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('mergeAll() throws ReadonlyViolationException when readonly', () => {
|
|
179
|
+
const a = new JsonAccessor(makeParser()).from('{"a":1}').readonly(true);
|
|
180
|
+
expect(() => a.mergeAll({ b: 2 })).toThrow(ReadonlyViolationException);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('set() clone preserves readonly state', () => {
|
|
184
|
+
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
|
|
185
|
+
const b = a.readonly(false).set('x', 2);
|
|
186
|
+
expect(b.get('x')).toBe(2);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('set() clone inherits strict mode - security validation still enforced', () => {
|
|
190
|
+
const a = new JsonAccessor(makeParser()).from('{"x":1}');
|
|
191
|
+
const b = a.set('x', 2);
|
|
192
|
+
expect(() => b.from('{"__proto__":"bad"}')).toThrow(SecurityException);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('keys() returns [] when path resolves to null (typeof null is object in JS)', () => {
|
|
196
|
+
const a = new JsonAccessor(makeParser()).from('{"a":null}');
|
|
197
|
+
expect(a.keys('a')).toEqual([]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('count() returns 0 when path resolves to null (typeof null is object in JS)', () => {
|
|
201
|
+
const a = new JsonAccessor(makeParser()).from('{"a":null}');
|
|
202
|
+
expect(a.count('a')).toBe(0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('strict(false) bypasses payload size validation', () => {
|
|
206
|
+
const tinyParser = new SecurityParser({ maxPayloadBytes: 5 });
|
|
207
|
+
const parser = new DotNotationParser(new SecurityGuard(), tinyParser);
|
|
208
|
+
const a = new JsonAccessor(parser).strict(false).from('{"name":"Alice"}');
|
|
209
|
+
expect(a.get('name')).toBe('Alice');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('strict(true) enforces payload size validation', () => {
|
|
213
|
+
const tinyParser = new SecurityParser({ maxPayloadBytes: 5 });
|
|
214
|
+
const parser = new DotNotationParser(new SecurityGuard(), tinyParser);
|
|
215
|
+
expect(() => new JsonAccessor(parser).from('{"name":"Alice"}')).toThrow(SecurityException);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { AbstractIntegrationAccessor } from '../../src/accessors/abstract-integration-accessor.js';
|
|
3
|
+
import type { ParseIntegrationInterface } from '../../src/contracts/parse-integration-interface.js';
|
|
4
|
+
import type { ValidatableParserInterface } from '../../src/contracts/validatable-parser-interface.js';
|
|
5
|
+
import { DotNotationParser } from '../../src/core/dot-notation-parser.js';
|
|
6
|
+
import { SecurityGuard } from '../../src/security/security-guard.js';
|
|
7
|
+
import { SecurityParser } from '../../src/security/security-parser.js';
|
|
8
|
+
import { FakeParseIntegration } from '../mocks/fake-parse-integration.js';
|
|
9
|
+
|
|
10
|
+
class ConcreteIntegrationAccessor extends AbstractIntegrationAccessor {
|
|
11
|
+
protected parse(raw: unknown): Record<string, unknown> {
|
|
12
|
+
return raw as Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeParser(): ValidatableParserInterface {
|
|
17
|
+
return new DotNotationParser(new SecurityGuard(), new SecurityParser());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe(AbstractIntegrationAccessor.name, () => {
|
|
21
|
+
it('accepts a ValidatableParserInterface and ParseIntegrationInterface', () => {
|
|
22
|
+
const parser = makeParser();
|
|
23
|
+
const integration = new FakeParseIntegration();
|
|
24
|
+
const accessor = new ConcreteIntegrationAccessor(parser, integration);
|
|
25
|
+
|
|
26
|
+
expect(accessor).toBeInstanceOf(AbstractIntegrationAccessor);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('exposes the integration property to subclasses', () => {
|
|
30
|
+
const integration = new FakeParseIntegration(true, { key: 'value' });
|
|
31
|
+
const accessor = new ConcreteIntegrationAccessor(makeParser(), integration);
|
|
32
|
+
|
|
33
|
+
expect(
|
|
34
|
+
(accessor as unknown as { integration: ParseIntegrationInterface }).integration,
|
|
35
|
+
).toBe(integration);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { AnyAccessor } from '../../../src/accessors/formats/any-accessor.js';
|
|
3
|
+
import { DotNotationParser } from '../../../src/core/dot-notation-parser.js';
|
|
4
|
+
import { SecurityGuard } from '../../../src/security/security-guard.js';
|
|
5
|
+
import { SecurityParser } from '../../../src/security/security-parser.js';
|
|
6
|
+
import { InvalidFormatException } from '../../../src/exceptions/invalid-format-exception.js';
|
|
7
|
+
import { SecurityException } from '../../../src/exceptions/security-exception.js';
|
|
8
|
+
import { FakeParseIntegration } from '../../mocks/fake-parse-integration.js';
|
|
9
|
+
|
|
10
|
+
function makeParser(secParser?: SecurityParser): DotNotationParser {
|
|
11
|
+
return new DotNotationParser(new SecurityGuard(), secParser ?? new SecurityParser());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe(AnyAccessor.name, () => {
|
|
15
|
+
it('accepts data when integration assertFormat returns true', () => {
|
|
16
|
+
const integration = new FakeParseIntegration(true, { key: 'value' });
|
|
17
|
+
const a = new AnyAccessor(makeParser(), integration).from('some data');
|
|
18
|
+
expect(a.get('key')).toBe('value');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('throws InvalidFormatException when integration rejects format', () => {
|
|
22
|
+
const integration = new FakeParseIntegration(false, {});
|
|
23
|
+
expect(() => new AnyAccessor(makeParser(), integration).from('data')).toThrow(
|
|
24
|
+
InvalidFormatException,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('validates string payloads through assertPayload', () => {
|
|
29
|
+
const secParser = new SecurityParser({ maxPayloadBytes: 3 });
|
|
30
|
+
const parser = makeParser(secParser);
|
|
31
|
+
const integration = new FakeParseIntegration(true, {});
|
|
32
|
+
expect(() => new AnyAccessor(parser, integration).from('1234')).toThrow(SecurityException);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('does not call assertPayload for non-string data', () => {
|
|
36
|
+
const secParser = new SecurityParser({ maxPayloadBytes: 1 });
|
|
37
|
+
const parser = makeParser(secParser);
|
|
38
|
+
const integration = new FakeParseIntegration(true, { a: 1 });
|
|
39
|
+
expect(() => new AnyAccessor(parser, integration).from({ x: 1 })).not.toThrow();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('resolves nested path from parsed data', () => {
|
|
43
|
+
const integration = new FakeParseIntegration(true, { user: { name: 'Alice' } });
|
|
44
|
+
const a = new AnyAccessor(makeParser(), integration).from('anything');
|
|
45
|
+
expect(a.get('user.name')).toBe('Alice');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('error message mentions typeof when format is rejected', () => {
|
|
49
|
+
const integration = new FakeParseIntegration(false, {});
|
|
50
|
+
expect(() => new AnyAccessor(makeParser(), integration).from(42)).toThrow(/number/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('FakeParseIntegration default constructor accepts any input', () => {
|
|
54
|
+
const integration = new FakeParseIntegration();
|
|
55
|
+
expect(integration.assertFormat('test')).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ArrayAccessor } from '../../../src/accessors/formats/array-accessor.js';
|
|
3
|
+
import { DotNotationParser } from '../../../src/core/dot-notation-parser.js';
|
|
4
|
+
import { SecurityGuard } from '../../../src/security/security-guard.js';
|
|
5
|
+
import { SecurityParser } from '../../../src/security/security-parser.js';
|
|
6
|
+
import { InvalidFormatException } from '../../../src/exceptions/invalid-format-exception.js';
|
|
7
|
+
|
|
8
|
+
function makeParser(secParser?: SecurityParser): DotNotationParser {
|
|
9
|
+
return new DotNotationParser(new SecurityGuard(), secParser ?? new SecurityParser());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe(ArrayAccessor.name, () => {
|
|
13
|
+
it('accepts a plain object', () => {
|
|
14
|
+
const a = new ArrayAccessor(makeParser()).from({ key: 'value' });
|
|
15
|
+
expect(a.get('key')).toBe('value');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('accepts an array, indexing by position', () => {
|
|
19
|
+
const a = new ArrayAccessor(makeParser()).from(['a', 'b', 'c']);
|
|
20
|
+
expect(a.get('0')).toBe('a');
|
|
21
|
+
expect(a.get('2')).toBe('c');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('throws InvalidFormatException for string input', () => {
|
|
25
|
+
expect(() => new ArrayAccessor(makeParser()).from('string')).toThrow(
|
|
26
|
+
InvalidFormatException,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('throws InvalidFormatException for null input', () => {
|
|
31
|
+
expect(() => new ArrayAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('throws InvalidFormatException for number input', () => {
|
|
35
|
+
expect(() => new ArrayAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('resolves a nested path in an array-ingested object', () => {
|
|
39
|
+
const a = new ArrayAccessor(makeParser()).from({ user: { name: 'Alice' } });
|
|
40
|
+
expect(a.get('user.name')).toBe('Alice');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { EnvAccessor } from '../../../src/accessors/formats/env-accessor.js';
|
|
3
|
+
import { DotNotationParser } from '../../../src/core/dot-notation-parser.js';
|
|
4
|
+
import { SecurityGuard } from '../../../src/security/security-guard.js';
|
|
5
|
+
import { SecurityParser } from '../../../src/security/security-parser.js';
|
|
6
|
+
import { InvalidFormatException } from '../../../src/exceptions/invalid-format-exception.js';
|
|
7
|
+
|
|
8
|
+
function makeParser(secParser?: SecurityParser): DotNotationParser {
|
|
9
|
+
return new DotNotationParser(new SecurityGuard(), secParser ?? new SecurityParser());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe(EnvAccessor.name, () => {
|
|
13
|
+
it('parses KEY=VALUE pairs', () => {
|
|
14
|
+
const a = new EnvAccessor(makeParser()).from('DB_HOST=localhost\nPORT=5432');
|
|
15
|
+
expect(a.get('DB_HOST')).toBe('localhost');
|
|
16
|
+
expect(a.get('PORT')).toBe('5432');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('skips comment lines', () => {
|
|
20
|
+
const a = new EnvAccessor(makeParser()).from('# comment\nKEY=value');
|
|
21
|
+
expect(a.has('# comment')).toBe(false);
|
|
22
|
+
expect(a.get('KEY')).toBe('value');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('skips blank lines', () => {
|
|
26
|
+
const a = new EnvAccessor(makeParser()).from('\nKEY=value\n');
|
|
27
|
+
expect(a.get('KEY')).toBe('value');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('strips double quotes from values', () => {
|
|
31
|
+
const a = new EnvAccessor(makeParser()).from('MSG="hello world"');
|
|
32
|
+
expect(a.get('MSG')).toBe('hello world');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('strips single quotes from values', () => {
|
|
36
|
+
const a = new EnvAccessor(makeParser()).from("MSG='hello world'");
|
|
37
|
+
expect(a.get('MSG')).toBe('hello world');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('skips lines without = sign', () => {
|
|
41
|
+
const a = new EnvAccessor(makeParser()).from('INVALID_LINE\nKEY=value');
|
|
42
|
+
expect(a.has('INVALID_LINE')).toBe(false);
|
|
43
|
+
expect(a.get('KEY')).toBe('value');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('throws InvalidFormatException for non-string input', () => {
|
|
47
|
+
expect(() => new EnvAccessor(makeParser()).from(123)).toThrow(InvalidFormatException);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('throws InvalidFormatException for null input', () => {
|
|
51
|
+
expect(() => new EnvAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('handles KEY= with no value (empty string)', () => {
|
|
55
|
+
const a = new EnvAccessor(makeParser()).from('EMPTY=');
|
|
56
|
+
expect(a.get('EMPTY')).toBe('');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('handles value with multiple = signs', () => {
|
|
60
|
+
const a = new EnvAccessor(makeParser()).from('JWT=a=b=c');
|
|
61
|
+
expect(a.get('JWT')).toBe('a=b=c');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('trims whitespace from key names', () => {
|
|
65
|
+
const a = new EnvAccessor(makeParser()).from(' KEY =value');
|
|
66
|
+
expect(a.get('KEY')).toBe('value');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('trims whitespace from values', () => {
|
|
70
|
+
const a = new EnvAccessor(makeParser()).from('KEY= trimmed ');
|
|
71
|
+
expect(a.get('KEY')).toBe('trimmed');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('does not strip double quotes when only one side is present', () => {
|
|
75
|
+
const a = new EnvAccessor(makeParser()).from('KEY=hello"');
|
|
76
|
+
expect(a.get('KEY')).toBe('hello"');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('does not strip double quotes when value starts without quote', () => {
|
|
80
|
+
const a = new EnvAccessor(makeParser()).from('KEY="hello');
|
|
81
|
+
expect(a.get('KEY')).toBe('"hello');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('does not strip single quotes when only one side is present', () => {
|
|
85
|
+
const a = new EnvAccessor(makeParser()).from("KEY=hello'");
|
|
86
|
+
expect(a.get('KEY')).toBe("hello'");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('does not strip single quotes when value starts without quote', () => {
|
|
90
|
+
const a = new EnvAccessor(makeParser()).from("KEY='hello");
|
|
91
|
+
expect(a.get('KEY')).toBe("'hello");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('skips lines starting with # even if they contain =', () => {
|
|
95
|
+
const a = new EnvAccessor(makeParser()).from('# KEY=value\nREAL=ok');
|
|
96
|
+
expect(a.has('# KEY')).toBe(false);
|
|
97
|
+
expect(a.get('REAL')).toBe('ok');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('error message from from() includes the actual typeof data', () => {
|
|
101
|
+
expect(() => new EnvAccessor(makeParser()).from(42)).toThrow(/number/);
|
|
102
|
+
});
|
|
103
|
+
});
|