@safeaccess/inline 0.1.1 → 0.1.3
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 +23 -5
- package/LICENSE +1 -1
- package/README.md +79 -21
- package/dist/accessors/abstract-accessor.d.ts +24 -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 +26 -56
- package/dist/inline.js +43 -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 +4 -1
- package/dist/security/security-guard.js +7 -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 +25 -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 +46 -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 +10 -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 +379 -0
- package/tests/parser/xml-parser.test.ts +17 -330
- 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 +8 -479
- 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
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DotNotationParser } from '../../src/core/dot-notation-parser.js';
|
|
3
|
+
import { SecurityGuard } from '../../src/security/security-guard.js';
|
|
4
|
+
import { SecurityParser } from '../../src/security/security-parser.js';
|
|
5
|
+
import { SecurityException } from '../../src/exceptions/security-exception.js';
|
|
6
|
+
import { PathNotFoundException } from '../../src/exceptions/path-not-found-exception.js';
|
|
7
|
+
import { FakePathCache } from '../mocks/fake-path-cache.js';
|
|
8
|
+
|
|
9
|
+
function makeParser(): DotNotationParser {
|
|
10
|
+
return new DotNotationParser();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe(`${DotNotationParser.name} > pathCache integration`, () => {
|
|
14
|
+
it('stores parsed segments in the cache on first access', () => {
|
|
15
|
+
const cache = new FakePathCache();
|
|
16
|
+
const parser = new DotNotationParser(new SecurityGuard(), new SecurityParser(), cache);
|
|
17
|
+
parser.get({ a: { b: 1 } }, 'a.b');
|
|
18
|
+
expect(cache.store.has('a.b')).toBe(true);
|
|
19
|
+
expect(cache.store.get('a.b')).toEqual([
|
|
20
|
+
{ type: 'key', value: 'a' },
|
|
21
|
+
{ type: 'key', value: 'b' },
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('reads from the cache on subsequent calls without re-parsing', () => {
|
|
26
|
+
const cache = new FakePathCache();
|
|
27
|
+
const parser = new DotNotationParser(new SecurityGuard(), new SecurityParser(), cache);
|
|
28
|
+
parser.get({ a: { b: 1 } }, 'a.b');
|
|
29
|
+
const getCountAfterFirst = cache.getCallCount;
|
|
30
|
+
parser.get({ a: { b: 1 } }, 'a.b');
|
|
31
|
+
// Second call should hit the cache (getCallCount increases, setCallCount stays the same)
|
|
32
|
+
expect(cache.setCallCount).toBe(1);
|
|
33
|
+
expect(cache.getCallCount).toBeGreaterThan(getCountAfterFirst);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns the correct value when cache is used', () => {
|
|
37
|
+
const cache = new FakePathCache();
|
|
38
|
+
const parser = new DotNotationParser(new SecurityGuard(), new SecurityParser(), cache);
|
|
39
|
+
expect(parser.get({ a: { b: 42 } }, 'a.b')).toBe(42);
|
|
40
|
+
expect(parser.get({ a: { b: 42 } }, 'a.b')).toBe(42);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('works without a cache (undefined)', () => {
|
|
44
|
+
const parser = new DotNotationParser(new SecurityGuard(), new SecurityParser());
|
|
45
|
+
expect(parser.get({ a: { b: 1 } }, 'a.b')).toBe(1);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Additional branch-coverage tests (targeting Stryker survivors)
|
|
50
|
+
|
|
51
|
+
describe(`${DotNotationParser.name} > get empty path branch`, () => {
|
|
52
|
+
// Kills lines 50:13/22/26 - `path === ''` condition in get()
|
|
53
|
+
it('returns defaultValue (not null) for empty path', () => {
|
|
54
|
+
const parser = makeParser();
|
|
55
|
+
expect(parser.get({ a: 1 }, '', 'custom_default')).toBe('custom_default');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns null when empty path and no default', () => {
|
|
59
|
+
const parser = makeParser();
|
|
60
|
+
expect(parser.get({ a: 1 }, '')).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe(`${DotNotationParser.name} > has empty path branch`, () => {
|
|
65
|
+
// Kills lines 86:13/22/26 - `path === ''` condition in has()
|
|
66
|
+
it('returns false for empty path (not "has everything")', () => {
|
|
67
|
+
const parser = makeParser();
|
|
68
|
+
// If the condition were removed, sentinel lookup would always find the data itself → true
|
|
69
|
+
expect(parser.has({ a: 1 }, '')).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe(`${DotNotationParser.name} > getAt branch conditions`, () => {
|
|
74
|
+
// Kills lines 128/129 - conditions inside getAt loop
|
|
75
|
+
it('returns defaultValue when current is null mid-path', () => {
|
|
76
|
+
const parser = makeParser();
|
|
77
|
+
expect(parser.getAt({ a: null }, ['a', 'b'], 'default')).toBe('default');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('returns defaultValue when key does not exist as own property', () => {
|
|
81
|
+
const parser = makeParser();
|
|
82
|
+
const data = Object.create({ inherited: true }) as Record<string, unknown>;
|
|
83
|
+
expect(parser.getAt(data, ['inherited'], 'fallback')).toBe('fallback');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns value when key is a direct own property', () => {
|
|
87
|
+
const parser = makeParser();
|
|
88
|
+
expect(parser.getAt({ key: 'value' }, ['key'])).toBe('value');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe(`${DotNotationParser.name} > removeAt empty segments`, () => {
|
|
93
|
+
// Kills line 189:36/13 - `segments.length === 0` early return
|
|
94
|
+
it('returns original data for empty segments', () => {
|
|
95
|
+
const parser = makeParser();
|
|
96
|
+
const data = { a: 1 };
|
|
97
|
+
expect(parser.removeAt(data, [])).toBe(data);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns a different object when segments are non-empty', () => {
|
|
101
|
+
const parser = makeParser();
|
|
102
|
+
const data = { a: 1, b: 2 };
|
|
103
|
+
const result = parser.removeAt(data, ['a']);
|
|
104
|
+
expect(result).not.toBe(data);
|
|
105
|
+
expect(result).toEqual({ b: 2 });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe(`${DotNotationParser.name} > merge existing is non-object`, () => {
|
|
110
|
+
// Kills line 214:45 - typeof existing === 'object' check in merge()
|
|
111
|
+
it('merges into empty object when existing path value is a primitive', () => {
|
|
112
|
+
const parser = makeParser();
|
|
113
|
+
// 'a' is a string (primitive), not an object - should merge into {}
|
|
114
|
+
const result = parser.merge({ a: 'string' }, 'a', { key: 'val' });
|
|
115
|
+
expect(result).toEqual({ a: { key: 'val' } });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('merges into empty object when existing path value is null', () => {
|
|
119
|
+
const parser = makeParser();
|
|
120
|
+
const result = parser.merge({ a: null }, 'a', { key: 'val' });
|
|
121
|
+
expect(result).toEqual({ a: { key: 'val' } });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe(`${DotNotationParser.name} > eraseAt hasOwnProperty check`, () => {
|
|
126
|
+
// Kills line 281:22 - hasOwnProperty check in eraseAt
|
|
127
|
+
it('does not remove inherited (non-own) properties via prototype chain', () => {
|
|
128
|
+
const parser = makeParser();
|
|
129
|
+
const proto = { inherited: 1 };
|
|
130
|
+
const data = Object.create(proto) as Record<string, unknown>;
|
|
131
|
+
data['own'] = 2;
|
|
132
|
+
const result = parser.removeAt(data, ['inherited']);
|
|
133
|
+
// 'inherited' is not an own property so eraseAt returns data unchanged
|
|
134
|
+
expect(Object.prototype.hasOwnProperty.call(result, 'inherited')).toBe(false);
|
|
135
|
+
expect(result['own']).toBe(2);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe(`${DotNotationParser.name} > eraseAt child null/non-object`, () => {
|
|
140
|
+
// Kills lines 290:13/30/42/60 - `typeof child !== 'object' || child === null`
|
|
141
|
+
it('returns copy unchanged when intermediate child is null', () => {
|
|
142
|
+
const parser = makeParser();
|
|
143
|
+
const data = { a: null };
|
|
144
|
+
const result = parser.removeAt(data as Record<string, unknown>, ['a', 'b']);
|
|
145
|
+
expect(result).toEqual({ a: null });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('returns copy unchanged when intermediate child is a number', () => {
|
|
149
|
+
const parser = makeParser();
|
|
150
|
+
const result = parser.removeAt({ a: 42 }, ['a', 'b']);
|
|
151
|
+
expect(result).toEqual({ a: 42 });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('returns copy unchanged when intermediate child is a string', () => {
|
|
155
|
+
const parser = makeParser();
|
|
156
|
+
const result = parser.removeAt({ a: 'text' }, ['a', 'b']);
|
|
157
|
+
expect(result).toEqual({ a: 'text' });
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe(`${DotNotationParser.name} > writeAt single segment`, () => {
|
|
162
|
+
// Kills lines 305:63/13 - `index === segments.length - 1` check (terminal condition)
|
|
163
|
+
it('sets value at a single-segment path correctly', () => {
|
|
164
|
+
const parser = makeParser();
|
|
165
|
+
const result = parser.setAt({}, ['key'], 'value');
|
|
166
|
+
expect(result).toEqual({ key: 'value' });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('sets value at a single-segment path, overwriting existing', () => {
|
|
170
|
+
const parser = makeParser();
|
|
171
|
+
const result = parser.setAt({ key: 'old' }, ['key'], 'new');
|
|
172
|
+
expect(result).toEqual({ key: 'new' });
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe(`${DotNotationParser.name} > writeAt child handling`, () => {
|
|
177
|
+
// Kills lines 317:13/42/58 - typeof child === 'object' check in writeAt
|
|
178
|
+
it('creates nested object when child is null', () => {
|
|
179
|
+
const parser = makeParser();
|
|
180
|
+
const result = parser.setAt({ a: null }, ['a', 'b'], 1);
|
|
181
|
+
expect(result).toEqual({ a: { b: 1 } });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('creates nested object when child is a primitive', () => {
|
|
185
|
+
const parser = makeParser();
|
|
186
|
+
const result = parser.setAt({ a: 42 }, ['a', 'b'], 1);
|
|
187
|
+
expect(result).toEqual({ a: { b: 1 } });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('creates nested object when child is an array', () => {
|
|
191
|
+
const parser = makeParser();
|
|
192
|
+
const result = parser.setAt({ a: [1, 2] }, ['a', 'b'], 1);
|
|
193
|
+
expect(result).toEqual({ a: { b: 1 } });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('preserves existing nested object when overwriting a key', () => {
|
|
197
|
+
const parser = makeParser();
|
|
198
|
+
const result = parser.setAt({ a: { x: 1, y: 2 } }, ['a', 'z'], 3);
|
|
199
|
+
expect(result).toEqual({ a: { x: 1, y: 2, z: 3 } });
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe(`${DotNotationParser.name} > write-path forbidden key validation`, () => {
|
|
204
|
+
it('throws SecurityException when setting a forbidden key via set', () => {
|
|
205
|
+
const parser = makeParser();
|
|
206
|
+
expect(() => parser.set({}, 'constructor', 'bad')).toThrow(SecurityException);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('throws SecurityException when setting a nested forbidden key via set', () => {
|
|
210
|
+
const parser = makeParser();
|
|
211
|
+
expect(() => parser.set({}, 'prototype.nested', 'bad')).toThrow(SecurityException);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('throws SecurityException when removing a forbidden key via remove', () => {
|
|
215
|
+
const parser = makeParser();
|
|
216
|
+
expect(() => parser.remove({ safe: 1 }, 'constructor')).toThrow(SecurityException);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('throws SecurityException when setting a forbidden key via setAt', () => {
|
|
220
|
+
const parser = makeParser();
|
|
221
|
+
expect(() => parser.setAt({}, ['__proto__'], 'bad')).toThrow(SecurityException);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('throws SecurityException when removing a forbidden key via removeAt', () => {
|
|
225
|
+
const parser = makeParser();
|
|
226
|
+
expect(() => parser.removeAt({ safe: 1 }, ['constructor'])).toThrow(SecurityException);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('throws SecurityException when merge source contains a forbidden key', () => {
|
|
230
|
+
const parser = makeParser();
|
|
231
|
+
expect(() => parser.merge({}, '', { hasOwnProperty: 'bad' })).toThrow(SecurityException);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('throws SecurityException when merge source contains a nested forbidden key', () => {
|
|
235
|
+
const parser = makeParser();
|
|
236
|
+
expect(() =>
|
|
237
|
+
parser.merge({ user: { name: 'Alice' } }, '', {
|
|
238
|
+
user: { prototype: 'bad' } as Record<string, unknown>,
|
|
239
|
+
}),
|
|
240
|
+
).toThrow(SecurityException);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('allows safe keys through write-path operations', () => {
|
|
244
|
+
const parser = makeParser();
|
|
245
|
+
expect(parser.set({}, 'username', 'Alice')).toEqual({ username: 'Alice' });
|
|
246
|
+
expect(parser.remove({ username: 'Alice' }, 'username')).toEqual({});
|
|
247
|
+
expect(parser.merge({}, '', { name: 'Bob' })).toEqual({ name: 'Bob' });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('write-path error message contains the forbidden key name', () => {
|
|
251
|
+
const parser = makeParser();
|
|
252
|
+
expect(() => parser.set({}, 'hasOwnProperty', 'bad')).toThrow(
|
|
253
|
+
"Forbidden key 'hasOwnProperty' detected.",
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('throws SecurityException for prototype pollution key via set', () => {
|
|
258
|
+
const parser = makeParser();
|
|
259
|
+
expect(() => parser.set({}, 'prototype', 'bad')).toThrow(SecurityException);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe(`${DotNotationParser.name} > deepMerge branch conditions`, () => {
|
|
264
|
+
it('recursively merges when both target and source values are objects', () => {
|
|
265
|
+
const parser = makeParser();
|
|
266
|
+
const result = parser.merge({ a: { x: 1 } }, 'a', { y: 2 });
|
|
267
|
+
expect(result).toEqual({ a: { x: 1, y: 2 } });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('overwrites when source value is null (not an object)', () => {
|
|
271
|
+
const parser = makeParser();
|
|
272
|
+
const result = parser.merge({ a: { x: 1 } }, 'a', {
|
|
273
|
+
x: null as unknown as Record<string, unknown>,
|
|
274
|
+
});
|
|
275
|
+
expect((result['a'] as Record<string, unknown>)['x']).toBeNull();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('overwrites when source value is an array (not a plain object)', () => {
|
|
279
|
+
const parser = makeParser();
|
|
280
|
+
const result = parser.merge({ a: { x: 1 } }, '', {
|
|
281
|
+
a: [1, 2, 3] as unknown as Record<string, unknown>,
|
|
282
|
+
});
|
|
283
|
+
expect(result['a']).toEqual([1, 2, 3]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('overwrites when target value is null (not an object) and source is an object', () => {
|
|
287
|
+
const parser = makeParser();
|
|
288
|
+
const result = parser.merge({ a: null }, '', {
|
|
289
|
+
a: { key: 'val' } as Record<string, unknown>,
|
|
290
|
+
});
|
|
291
|
+
expect(result['a']).toEqual({ key: 'val' });
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('overwrites when target value is an array and source is an object', () => {
|
|
295
|
+
const parser = makeParser();
|
|
296
|
+
const result = parser.merge({ a: [1, 2] }, '', {
|
|
297
|
+
a: { key: 'val' } as unknown as Record<string, unknown>,
|
|
298
|
+
});
|
|
299
|
+
expect(result['a']).toEqual({ key: 'val' });
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe(`${DotNotationParser.name} > getMaxKeys`, () => {
|
|
304
|
+
it('returns the max key count from the configured SecurityParser', () => {
|
|
305
|
+
const parser = new DotNotationParser(
|
|
306
|
+
new SecurityGuard(),
|
|
307
|
+
new SecurityParser({ maxKeys: 42 }),
|
|
308
|
+
);
|
|
309
|
+
expect(parser.getMaxKeys()).toBe(42);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('returns the default max key count when not overridden', () => {
|
|
313
|
+
expect(makeParser().getMaxKeys()).toBe(10_000);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe(`${DotNotationParser.name} > getStrict`, () => {
|
|
318
|
+
it('returns the value when the path exists', () => {
|
|
319
|
+
const parser = makeParser();
|
|
320
|
+
|
|
321
|
+
expect(parser.getStrict({ name: 'Alice' }, 'name')).toBe('Alice');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('throws PathNotFoundException when the path does not exist', () => {
|
|
325
|
+
const parser = makeParser();
|
|
326
|
+
|
|
327
|
+
expect(() => parser.getStrict({ name: 'Alice' }, 'missing')).toThrow(PathNotFoundException);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('throws PathNotFoundException with a message containing the path', () => {
|
|
331
|
+
const parser = makeParser();
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
parser.getStrict({ name: 'Alice' }, 'missing.path');
|
|
335
|
+
expect.fail('Should have thrown');
|
|
336
|
+
} catch (e) {
|
|
337
|
+
expect((e as Error).message).not.toBe('');
|
|
338
|
+
expect((e as Error).message).toContain('missing.path');
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe(`${DotNotationParser.name} > resolve maxResolveDepth enforcement`, () => {
|
|
344
|
+
it('throws SecurityException when resolve depth exceeds maxResolveDepth', () => {
|
|
345
|
+
const parser = new DotNotationParser(
|
|
346
|
+
new SecurityGuard(),
|
|
347
|
+
new SecurityParser({ maxResolveDepth: 2 }),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
expect(() => parser.get({ a: { b: { c: 1 } } }, 'a.b.c')).toThrow(SecurityException);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('resolves path within maxResolveDepth limit', () => {
|
|
354
|
+
const parser = new DotNotationParser(
|
|
355
|
+
new SecurityGuard(),
|
|
356
|
+
new SecurityParser({ maxResolveDepth: 5 }),
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
expect(parser.get({ a: { b: 1 } }, 'a.b')).toBe(1);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('uses maxResolveDepth, not maxDepth, for path resolution depth limit', () => {
|
|
363
|
+
const parser = new DotNotationParser(
|
|
364
|
+
new SecurityGuard(),
|
|
365
|
+
new SecurityParser({ maxDepth: 100, maxResolveDepth: 2 }),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
expect(() => parser.get({ a: { b: { c: 1 } } }, 'a.b.c')).toThrow(SecurityException);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('does not throw at exact maxResolveDepth boundary', () => {
|
|
372
|
+
const parser = new DotNotationParser(
|
|
373
|
+
new SecurityGuard(),
|
|
374
|
+
new SecurityParser({ maxResolveDepth: 3 }),
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
expect(parser.get({ a: { b: { c: 1 } } }, 'a.b.c')).toBe(1);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('throws when resolve depth is one above maxResolveDepth', () => {
|
|
381
|
+
const parser = new DotNotationParser(
|
|
382
|
+
new SecurityGuard(),
|
|
383
|
+
new SecurityParser({ maxResolveDepth: 3 }),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
expect(() => parser.get({ a: { b: { c: { d: 1 } } } }, 'a.b.c.d')).toThrow(
|
|
387
|
+
SecurityException,
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('exception message contains the depth value when resolve depth exceeded', () => {
|
|
392
|
+
const parser = new DotNotationParser(
|
|
393
|
+
new SecurityGuard(),
|
|
394
|
+
new SecurityParser({ maxResolveDepth: 2 }),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
parser.get({ a: { b: { c: 1 } } }, 'a.b.c');
|
|
399
|
+
expect.fail('Should have thrown');
|
|
400
|
+
} catch (e) {
|
|
401
|
+
expect((e as Error).message).toContain('3');
|
|
402
|
+
expect((e as Error).message).toContain('2');
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('get enforces maxResolveDepth on nested path resolution', () => {
|
|
407
|
+
const parser = new DotNotationParser(
|
|
408
|
+
new SecurityGuard(),
|
|
409
|
+
new SecurityParser({ maxResolveDepth: 1 }),
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
expect(parser.get({ a: 1 }, 'a')).toBe(1);
|
|
413
|
+
expect(() => parser.get({ a: { b: 1 } }, 'a.b')).toThrow(SecurityException);
|
|
414
|
+
});
|
|
415
|
+
});
|