@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
|
@@ -1,1017 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { ArrayAccessor } from '../../src/accessors/formats/array-accessor.js';
|
|
3
|
-
import { EnvAccessor } from '../../src/accessors/formats/env-accessor.js';
|
|
4
|
-
import { IniAccessor } from '../../src/accessors/formats/ini-accessor.js';
|
|
5
|
-
import { NdjsonAccessor } from '../../src/accessors/formats/ndjson-accessor.js';
|
|
6
|
-
import { ObjectAccessor } from '../../src/accessors/formats/object-accessor.js';
|
|
7
|
-
import { XmlAccessor } from '../../src/accessors/formats/xml-accessor.js';
|
|
8
|
-
import { AnyAccessor } from '../../src/accessors/formats/any-accessor.js';
|
|
9
|
-
import { YamlAccessor } from '../../src/accessors/formats/yaml-accessor.js';
|
|
10
|
-
import { JsonAccessor } from '../../src/accessors/formats/json-accessor.js';
|
|
11
|
-
import { DotNotationParser } from '../../src/core/dot-notation-parser.js';
|
|
12
|
-
import { SecurityGuard } from '../../src/security/security-guard.js';
|
|
13
|
-
import { SecurityParser } from '../../src/security/security-parser.js';
|
|
14
|
-
import { InvalidFormatException } from '../../src/exceptions/invalid-format-exception.js';
|
|
15
|
-
import { SecurityException } from '../../src/exceptions/security-exception.js';
|
|
16
|
-
import { ReadonlyViolationException } from '../../src/exceptions/readonly-violation-exception.js';
|
|
17
|
-
import { PathNotFoundException } from '../../src/exceptions/path-not-found-exception.js';
|
|
18
|
-
import { FakeParseIntegration } from '../mocks/fake-parse-integration.js';
|
|
19
|
-
|
|
20
|
-
function makeParser(secParser?: SecurityParser): DotNotationParser {
|
|
21
|
-
return new DotNotationParser(new SecurityGuard(), secParser ?? new SecurityParser());
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ArrayAccessor
|
|
25
|
-
|
|
26
|
-
describe(ArrayAccessor.name, () => {
|
|
27
|
-
it('accepts a plain object', () => {
|
|
28
|
-
const a = new ArrayAccessor(makeParser()).from({ key: 'value' });
|
|
29
|
-
expect(a.get('key')).toBe('value');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('accepts an array, indexing by position', () => {
|
|
33
|
-
const a = new ArrayAccessor(makeParser()).from(['a', 'b', 'c']);
|
|
34
|
-
expect(a.get('0')).toBe('a');
|
|
35
|
-
expect(a.get('2')).toBe('c');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('throws InvalidFormatException for string input', () => {
|
|
39
|
-
expect(() => new ArrayAccessor(makeParser()).from('string')).toThrow(
|
|
40
|
-
InvalidFormatException,
|
|
41
|
-
);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('throws InvalidFormatException for null input', () => {
|
|
45
|
-
expect(() => new ArrayAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('throws InvalidFormatException for number input', () => {
|
|
49
|
-
expect(() => new ArrayAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('resolves a nested path in an array-ingested object', () => {
|
|
53
|
-
const a = new ArrayAccessor(makeParser()).from({ user: { name: 'Alice' } });
|
|
54
|
-
expect(a.get('user.name')).toBe('Alice');
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// EnvAccessor
|
|
59
|
-
|
|
60
|
-
describe(EnvAccessor.name, () => {
|
|
61
|
-
it('parses KEY=VALUE pairs', () => {
|
|
62
|
-
const a = new EnvAccessor(makeParser()).from('DB_HOST=localhost\nPORT=5432');
|
|
63
|
-
expect(a.get('DB_HOST')).toBe('localhost');
|
|
64
|
-
expect(a.get('PORT')).toBe('5432');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('skips comment lines', () => {
|
|
68
|
-
const a = new EnvAccessor(makeParser()).from('# comment\nKEY=value');
|
|
69
|
-
expect(a.has('# comment')).toBe(false);
|
|
70
|
-
expect(a.get('KEY')).toBe('value');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('skips blank lines', () => {
|
|
74
|
-
const a = new EnvAccessor(makeParser()).from('\nKEY=value\n');
|
|
75
|
-
expect(a.get('KEY')).toBe('value');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('strips double quotes from values', () => {
|
|
79
|
-
const a = new EnvAccessor(makeParser()).from('MSG="hello world"');
|
|
80
|
-
expect(a.get('MSG')).toBe('hello world');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('strips single quotes from values', () => {
|
|
84
|
-
const a = new EnvAccessor(makeParser()).from("MSG='hello world'");
|
|
85
|
-
expect(a.get('MSG')).toBe('hello world');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('skips lines without = sign', () => {
|
|
89
|
-
const a = new EnvAccessor(makeParser()).from('INVALID_LINE\nKEY=value');
|
|
90
|
-
expect(a.has('INVALID_LINE')).toBe(false);
|
|
91
|
-
expect(a.get('KEY')).toBe('value');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('throws InvalidFormatException for non-string input', () => {
|
|
95
|
-
expect(() => new EnvAccessor(makeParser()).from(123)).toThrow(InvalidFormatException);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('throws InvalidFormatException for null input', () => {
|
|
99
|
-
expect(() => new EnvAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('handles KEY= with no value (empty string)', () => {
|
|
103
|
-
const a = new EnvAccessor(makeParser()).from('EMPTY=');
|
|
104
|
-
expect(a.get('EMPTY')).toBe('');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('handles value with multiple = signs', () => {
|
|
108
|
-
const a = new EnvAccessor(makeParser()).from('JWT=a=b=c');
|
|
109
|
-
expect(a.get('JWT')).toBe('a=b=c');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Kills key trim MethodExpression: keys with surrounding whitespace are trimmed
|
|
113
|
-
it('trims whitespace from key names', () => {
|
|
114
|
-
const a = new EnvAccessor(makeParser()).from(' KEY =value');
|
|
115
|
-
expect(a.get('KEY')).toBe('value');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Kills value trim MethodExpression: values with surrounding whitespace are trimmed
|
|
119
|
-
it('trims whitespace from values', () => {
|
|
120
|
-
const a = new EnvAccessor(makeParser()).from('KEY= trimmed ');
|
|
121
|
-
expect(a.get('KEY')).toBe('trimmed');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// Kills startsWith('"') / endsWith('"') mutants: only strip when BOTH sides match
|
|
125
|
-
it('does not strip double quotes when only one side is present', () => {
|
|
126
|
-
const a = new EnvAccessor(makeParser()).from('KEY=hello"');
|
|
127
|
-
expect(a.get('KEY')).toBe('hello"');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('does not strip double quotes when value starts without quote', () => {
|
|
131
|
-
const a = new EnvAccessor(makeParser()).from('KEY="hello');
|
|
132
|
-
expect(a.get('KEY')).toBe('"hello');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Kills startsWith("'") / endsWith("'") mutants: only strip when BOTH sides match
|
|
136
|
-
it("does not strip single quotes when only one side is present", () => {
|
|
137
|
-
const a = new EnvAccessor(makeParser()).from("KEY=hello'");
|
|
138
|
-
expect(a.get("KEY")).toBe("hello'");
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("does not strip single quotes when value starts without quote", () => {
|
|
142
|
-
const a = new EnvAccessor(makeParser()).from("KEY='hello");
|
|
143
|
-
expect(a.get("KEY")).toBe("'hello");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Kills LogicalOperator (|| → &&): if both conditions needed, comment line with = would parse
|
|
147
|
-
it('skips lines starting with # even if they contain =', () => {
|
|
148
|
-
const a = new EnvAccessor(makeParser()).from('# KEY=value\nREAL=ok');
|
|
149
|
-
expect(a.has('# KEY')).toBe(false);
|
|
150
|
-
expect(a.get('REAL')).toBe('ok');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// Kills error message StringLiteral: error message contains typeof data
|
|
154
|
-
it('error message from from() includes the actual typeof data', () => {
|
|
155
|
-
expect(() => new EnvAccessor(makeParser()).from(42)).toThrow(/number/);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// IniAccessor
|
|
160
|
-
|
|
161
|
-
describe(IniAccessor.name, () => {
|
|
162
|
-
it('parses flat key=value pairs', () => {
|
|
163
|
-
const a = new IniAccessor(makeParser()).from('name=Alice\nage=30');
|
|
164
|
-
expect(a.get('name')).toBe('Alice');
|
|
165
|
-
expect(a.get('age')).toBe(30);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('parses sections as nested keys', () => {
|
|
169
|
-
const a = new IniAccessor(makeParser()).from('[db]\nhost=localhost\nport=5432');
|
|
170
|
-
expect(a.get('db.host')).toBe('localhost');
|
|
171
|
-
expect(a.get('db.port')).toBe(5432);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('throws InvalidFormatException for non-string input', () => {
|
|
175
|
-
expect(() => new IniAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('throws InvalidFormatException for number input', () => {
|
|
179
|
-
expect(() => new IniAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('skips comment lines starting with #', () => {
|
|
183
|
-
const a = new IniAccessor(makeParser()).from('# comment\nkey=value');
|
|
184
|
-
expect(a.has('# comment')).toBe(false);
|
|
185
|
-
expect(a.get('key')).toBe('value');
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('skips comment lines starting with ;', () => {
|
|
189
|
-
const a = new IniAccessor(makeParser()).from('; comment\nkey=value');
|
|
190
|
-
expect(a.get('key')).toBe('value');
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('skips blank lines', () => {
|
|
194
|
-
const a = new IniAccessor(makeParser()).from('\nkey=value\n');
|
|
195
|
-
expect(a.get('key')).toBe('value');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('casts true, yes, on to boolean true', () => {
|
|
199
|
-
const a = new IniAccessor(makeParser()).from('a=true\nb=yes\nc=on');
|
|
200
|
-
expect(a.get('a')).toBe(true);
|
|
201
|
-
expect(a.get('b')).toBe(true);
|
|
202
|
-
expect(a.get('c')).toBe(true);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('casts false, no, off, none to boolean false', () => {
|
|
206
|
-
const a = new IniAccessor(makeParser()).from('a=false\nb=no\nc=off\nd=none');
|
|
207
|
-
expect(a.get('a')).toBe(false);
|
|
208
|
-
expect(a.get('b')).toBe(false);
|
|
209
|
-
expect(a.get('c')).toBe(false);
|
|
210
|
-
expect(a.get('d')).toBe(false);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('casts null and empty string to null', () => {
|
|
214
|
-
const a = new IniAccessor(makeParser()).from('a=null\nb=');
|
|
215
|
-
expect(a.get('a')).toBeNull();
|
|
216
|
-
expect(a.get('b')).toBeNull();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('strips surrounding double quotes', () => {
|
|
220
|
-
const a = new IniAccessor(makeParser()).from('msg="hello world"');
|
|
221
|
-
expect(a.get('msg')).toBe('hello world');
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('strips surrounding single quotes', () => {
|
|
225
|
-
const a = new IniAccessor(makeParser()).from("msg='hello world'");
|
|
226
|
-
expect(a.get('msg')).toBe('hello world');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('casts integer values', () => {
|
|
230
|
-
const a = new IniAccessor(makeParser()).from('port=3306');
|
|
231
|
-
expect(a.get('port')).toBe(3306);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('casts float values', () => {
|
|
235
|
-
const a = new IniAccessor(makeParser()).from('ratio=3.14');
|
|
236
|
-
expect(a.get('ratio')).toBe(3.14);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('skips lines without = sign', () => {
|
|
240
|
-
const a = new IniAccessor(makeParser()).from('badline\nkey=value');
|
|
241
|
-
expect(a.has('badline')).toBe(false);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('parses multiple sections', () => {
|
|
245
|
-
const a = new IniAccessor(makeParser()).from('[app]\nname=MyApp\n[db]\nhost=localhost');
|
|
246
|
-
expect(a.get('app.name')).toBe('MyApp');
|
|
247
|
-
expect(a.get('db.host')).toBe('localhost');
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// Kills from() StringLiteral: error message includes typeof data
|
|
251
|
-
it('error message from from() includes the actual typeof data', () => {
|
|
252
|
-
expect(() => new IniAccessor(makeParser()).from(42)).toThrow(/number/);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// Kills hasOwnProperty ConditionalExpression: same section declared twice preserves first keys
|
|
256
|
-
it('preserves existing section keys when section header appears twice', () => {
|
|
257
|
-
const a = new IniAccessor(makeParser()).from('[db]\nhost=localhost\n[db]\nport=5432');
|
|
258
|
-
expect(a.get('db.host')).toBe('localhost');
|
|
259
|
-
expect(a.get('db.port')).toBe(5432);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
// Kills key/value trim MethodExpression: whitespace around key and value is trimmed
|
|
263
|
-
it('trims whitespace from key names and raw values', () => {
|
|
264
|
-
const a = new IniAccessor(makeParser()).from(' name = Alice ');
|
|
265
|
-
expect(a.get('name')).toBe('Alice');
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// Kills section regex Regex mutant: section header regex must match [section]
|
|
269
|
-
it('parses section with underscored name', () => {
|
|
270
|
-
const a = new IniAccessor(makeParser()).from('[my_section]\nkey=val');
|
|
271
|
-
expect(a.get('my_section.key')).toBe('val');
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// Kills quote-stripping MethodExpression/StringLiteral: partial quotes are not stripped
|
|
275
|
-
it('does not strip double quotes when only one side is present', () => {
|
|
276
|
-
const a = new IniAccessor(makeParser()).from('key=hello"');
|
|
277
|
-
expect(a.get('key')).toBe('hello"');
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it('does not strip single quotes when only one side is present', () => {
|
|
281
|
-
const a = new IniAccessor(makeParser()).from("key=hello'");
|
|
282
|
-
expect(a.get('key')).toBe("hello'");
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it('does not strip double quotes when value starts without quote', () => {
|
|
286
|
-
const a = new IniAccessor(makeParser()).from('key="hello');
|
|
287
|
-
expect(a.get('key')).toBe('"hello');
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('does not strip single quotes when value starts without quote', () => {
|
|
291
|
-
const a = new IniAccessor(makeParser()).from("key='hello");
|
|
292
|
-
expect(a.get('key')).toBe("'hello");
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
// Kills integer regex Regex mutant: partial-match strings should NOT become numbers
|
|
296
|
-
it('does not cast string with trailing non-digit characters as integer', () => {
|
|
297
|
-
const a = new IniAccessor(makeParser()).from('version=3.0-beta');
|
|
298
|
-
expect(typeof a.get('version')).toBe('string');
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('does not cast string with leading non-digit chars as integer', () => {
|
|
302
|
-
const a = new IniAccessor(makeParser()).from('version=v42');
|
|
303
|
-
expect(typeof a.get('version')).toBe('string');
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// Kills float regex Regex mutant: string with multiple dots is not a float
|
|
307
|
-
it('does not cast a version string like 3.1.4 as float', () => {
|
|
308
|
-
const a = new IniAccessor(makeParser()).from('ver=3.1.4');
|
|
309
|
-
expect(typeof a.get('ver')).toBe('string');
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
// Kills LogicalOperator in castIniValue(|| → &&): both alts must be tested
|
|
313
|
-
it('casts "yes" to boolean true', () => {
|
|
314
|
-
const a = new IniAccessor(makeParser()).from('flag=yes');
|
|
315
|
-
expect(a.get('flag')).toBe(true);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it('casts "no" to boolean false', () => {
|
|
319
|
-
const a = new IniAccessor(makeParser()).from('flag=no');
|
|
320
|
-
expect(a.get('flag')).toBe(false);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it('casts "none" to boolean false', () => {
|
|
324
|
-
const a = new IniAccessor(makeParser()).from('flag=none');
|
|
325
|
-
expect(a.get('flag')).toBe(false);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// Kills Regex survivor at line 61 (section header anchor ^ removed)
|
|
329
|
-
it('does not treat key=value containing [brackets] as section header', () => {
|
|
330
|
-
const a = new IniAccessor(makeParser()).from('key=value[brackets]\nother=1');
|
|
331
|
-
// Without ^, regex would match [brackets] inside value= line → wrong
|
|
332
|
-
expect(a.get('key')).toBe('value[brackets]');
|
|
333
|
-
expect(a.get('other')).toBe(1);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
// Kills Regex survivor at line 106 (float regex \d+ → \d)
|
|
337
|
-
it('casts a two-digit integer before decimal point to float', () => {
|
|
338
|
-
const a = new IniAccessor(makeParser()).from('ratio=10.5');
|
|
339
|
-
expect(a.get('ratio')).toBe(10.5);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it('casts negative float with two-digit integer part correctly', () => {
|
|
343
|
-
const a = new IniAccessor(makeParser()).from('offset=-12.75');
|
|
344
|
-
expect(a.get('offset')).toBe(-12.75);
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
// NdjsonAccessor
|
|
349
|
-
|
|
350
|
-
describe(NdjsonAccessor.name, () => {
|
|
351
|
-
it('parses two NDJSON lines', () => {
|
|
352
|
-
const a = new NdjsonAccessor(makeParser()).from('{"id":1}\n{"id":2}');
|
|
353
|
-
expect(a.get('0.id')).toBe(1);
|
|
354
|
-
expect(a.get('1.id')).toBe(2);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it('throws InvalidFormatException for non-string input', () => {
|
|
358
|
-
expect(() => new NdjsonAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('throws InvalidFormatException for number input', () => {
|
|
362
|
-
expect(() => new NdjsonAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it('returns empty object for blank-only input', () => {
|
|
366
|
-
const a = new NdjsonAccessor(makeParser()).from(' \n \n');
|
|
367
|
-
expect(a.all()).toEqual({});
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it('skips blank lines between valid lines', () => {
|
|
371
|
-
const a = new NdjsonAccessor(makeParser()).from('{"id":1}\n\n{"id":2}');
|
|
372
|
-
expect(a.get('0.id')).toBe(1);
|
|
373
|
-
expect(a.get('1.id')).toBe(2);
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
it('throws InvalidFormatException for malformed JSON line', () => {
|
|
377
|
-
expect(() => new NdjsonAccessor(makeParser()).from('{"ok":1}\n{not valid}')).toThrow(
|
|
378
|
-
InvalidFormatException,
|
|
379
|
-
);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it('error message contains the failing line number', () => {
|
|
383
|
-
expect(() => new NdjsonAccessor(makeParser()).from('{"ok":1}\n{not valid}')).toThrow(
|
|
384
|
-
/line 2/,
|
|
385
|
-
);
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
// ObjectAccessor
|
|
390
|
-
|
|
391
|
-
describe(ObjectAccessor.name, () => {
|
|
392
|
-
it('accepts a plain object', () => {
|
|
393
|
-
const a = new ObjectAccessor(makeParser()).from({ name: 'Alice' });
|
|
394
|
-
expect(a.get('name')).toBe('Alice');
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
it('throws InvalidFormatException for string input', () => {
|
|
398
|
-
expect(() => new ObjectAccessor(makeParser()).from('string')).toThrow(
|
|
399
|
-
InvalidFormatException,
|
|
400
|
-
);
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
it('throws InvalidFormatException for null input', () => {
|
|
404
|
-
expect(() => new ObjectAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it('throws InvalidFormatException for array input', () => {
|
|
408
|
-
expect(() => new ObjectAccessor(makeParser()).from([1, 2, 3])).toThrow(
|
|
409
|
-
InvalidFormatException,
|
|
410
|
-
);
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
it('throws InvalidFormatException with "array" in message for array input', () => {
|
|
414
|
-
expect(() => new ObjectAccessor(makeParser()).from([])).toThrow(/array/);
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it('resolves nested paths', () => {
|
|
418
|
-
const a = new ObjectAccessor(makeParser()).from({ user: { name: 'Bob' } });
|
|
419
|
-
expect(a.get('user.name')).toBe('Bob');
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
it('handles nested arrays of objects', () => {
|
|
423
|
-
const a = new ObjectAccessor(makeParser()).from({ items: [{ id: 1 }, { id: 2 }] });
|
|
424
|
-
expect(a.get('items')).toEqual([{ id: 1 }, { id: 2 }]);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
it('throws SecurityException when depth exceeds the limit', () => {
|
|
428
|
-
const secParser = new SecurityParser({ maxDepth: 1 });
|
|
429
|
-
const parser = makeParser(secParser);
|
|
430
|
-
expect(() => new ObjectAccessor(parser).from({ a: { b: { c: 1 } } })).toThrow(
|
|
431
|
-
SecurityException,
|
|
432
|
-
);
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it('does not throw for objects within the depth limit', () => {
|
|
436
|
-
const secParser = new SecurityParser({ maxDepth: 5 });
|
|
437
|
-
const parser = makeParser(secParser);
|
|
438
|
-
expect(() => new ObjectAccessor(parser).from({ a: { b: { c: 1 } } })).not.toThrow();
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it('throws SecurityException for deeply nested arrays exceeding depth', () => {
|
|
442
|
-
const secParser = new SecurityParser({ maxDepth: 0 });
|
|
443
|
-
const parser = makeParser(secParser);
|
|
444
|
-
expect(() => new ObjectAccessor(parser).from({ items: [{ id: 1 }] })).toThrow(
|
|
445
|
-
SecurityException,
|
|
446
|
-
);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it('handles nested arrays of primitives without throwing', () => {
|
|
450
|
-
const a = new ObjectAccessor(makeParser()).from({ tags: ['a', 'b', 'c'] });
|
|
451
|
-
expect(a.get('tags')).toEqual(['a', 'b', 'c']);
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
// Kills val !== null ConditionalExpression: null values should be preserved
|
|
455
|
-
it('preserves null values in object', () => {
|
|
456
|
-
const a = new ObjectAccessor(makeParser()).from({ key: null });
|
|
457
|
-
expect(a.get('key')).toBeNull();
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
// Kills !Array.isArray(val) check: arrays should not go to objectToRecord
|
|
461
|
-
it('handles array of objects at root level', () => {
|
|
462
|
-
const a = new ObjectAccessor(makeParser()).from({ list: [{ x: 1 }, { x: 2 }] });
|
|
463
|
-
expect(a.get('list')).toEqual([{ x: 1 }, { x: 2 }]);
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
// Kills NoCoverage: array of arrays (nested convertArrayValues recursion)
|
|
467
|
-
it('handles nested array of arrays', () => {
|
|
468
|
-
const a = new ObjectAccessor(makeParser()).from({ matrix: [[1, 2], [3, 4]] });
|
|
469
|
-
expect(a.get('matrix')).toEqual([[1, 2], [3, 4]]);
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
// Kills depth + 1 ArithmeticOperator: at maxDepth, direct child should throw
|
|
473
|
-
it('throws SecurityException at exactly maxDepth+1 nesting (strict=false so objectToRecord guard fires)', () => {
|
|
474
|
-
const secParser = new SecurityParser({ maxDepth: 2 });
|
|
475
|
-
const parser = makeParser(secParser);
|
|
476
|
-
// depth 0: root → depth 1: a → depth 2: b → depth 3: { d:1 } (3 > 2 = throw in objectToRecord)
|
|
477
|
-
expect(() => new ObjectAccessor(parser).strict(false).from({ a: { b: { c: { d: 1 } } } })).toThrow(
|
|
478
|
-
SecurityException,
|
|
479
|
-
);
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
it('does not throw at exactly maxDepth nesting (strict=false)', () => {
|
|
483
|
-
const secParser = new SecurityParser({ maxDepth: 2 });
|
|
484
|
-
const parser = makeParser(secParser);
|
|
485
|
-
// depth 0: root → depth 1: a → depth 2: b → depth 2: c (= maxDepth, processes c:1 as primitive)
|
|
486
|
-
expect(() => new ObjectAccessor(parser).strict(false).from({ a: { b: { c: 1 } } })).not.toThrow();
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
// Kills convertArrayValues depth + 1 ArithmeticOperator
|
|
490
|
-
it('throws SecurityException at exactly maxDepth+1 in nested array-of-objects (strict=false)', () => {
|
|
491
|
-
const secParser = new SecurityParser({ maxDepth: 1 });
|
|
492
|
-
const parser = makeParser(secParser);
|
|
493
|
-
// depth 0: root → depth 1: items array → depth 2: { id: 1 } object (2 > 1 = throw)
|
|
494
|
-
expect(() => new ObjectAccessor(parser).strict(false).from({ items: [{ id: 1 }] })).toThrow(
|
|
495
|
-
SecurityException,
|
|
496
|
-
);
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
// Kills convertArrayValues NoCoverage array-of-arrays: depth check in nested arrays
|
|
500
|
-
it('throws SecurityException when nested array of arrays exceeds depth (strict=false)', () => {
|
|
501
|
-
const secParser = new SecurityParser({ maxDepth: 0 });
|
|
502
|
-
const parser = makeParser(secParser);
|
|
503
|
-
// depth 0, 0 > 0? No. But then array items → depth 1 → inner array depth 2 > 0? Yes.
|
|
504
|
-
expect(() => new ObjectAccessor(parser).strict(false).from({ matrix: [[1, 2]] })).toThrow(
|
|
505
|
-
SecurityException,
|
|
506
|
-
);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
// Kills error message StringLiteral in security exception
|
|
510
|
-
it('security exception message contains depth value', () => {
|
|
511
|
-
const secParser = new SecurityParser({ maxDepth: 0 });
|
|
512
|
-
const parser = makeParser(secParser);
|
|
513
|
-
expect(() => new ObjectAccessor(parser).from({ a: { b: 1 } })).toThrow(/depth/i);
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
// Kills convertArrayValues depth > maxDepth vs >= maxDepth (EqualityOperator survivor)
|
|
517
|
-
it('does not throw convertArrayValues at exactly maxDepth (> not >=)', () => {
|
|
518
|
-
// maxDepth=1: convertArrayValues at depth=1 should NOT throw (1 > 1 is false)
|
|
519
|
-
// Items are primitives so no further recursion happens.
|
|
520
|
-
const secParser = new SecurityParser({ maxDepth: 1 });
|
|
521
|
-
const parser = makeParser(secParser);
|
|
522
|
-
// root(depth=0) -> items array -> convertArrayValues(depth=1) -> primitives
|
|
523
|
-
expect(() => new ObjectAccessor(parser).strict(false).from({ items: [1, 2, 3] })).not.toThrow();
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
// Kills val !== null ConditionalExpression in convertArrayValues (object path)
|
|
527
|
-
it('handles null values inside an array correctly', () => {
|
|
528
|
-
const a = new ObjectAccessor(makeParser()).from({ items: [null, 1, 'text'] });
|
|
529
|
-
const items = a.get('items') as unknown[];
|
|
530
|
-
expect(items[0]).toBeNull();
|
|
531
|
-
expect(items[1]).toBe(1);
|
|
532
|
-
expect(items[2]).toBe('text');
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
// Kills Array.isArray branch ConditionalExpression/BlockStatement in convertArrayValues
|
|
536
|
-
it('handles array-of-arrays with primitives and does not throw at valid depth', () => {
|
|
537
|
-
const a = new ObjectAccessor(makeParser()).from({ matrix: [[1, 2], [3, 4]] });
|
|
538
|
-
const matrix = a.get('matrix') as unknown[][];
|
|
539
|
-
expect(matrix[0]).toEqual([1, 2]);
|
|
540
|
-
expect(matrix[1]).toEqual([3, 4]);
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
// Kills ArithmeticOperator depth+1 in convertArrayValues for array arrays
|
|
544
|
-
it('throws when deeply nested arrays exceed maxDepth in convertArrayValues', () => {
|
|
545
|
-
const secParser = new SecurityParser({ maxDepth: 1 });
|
|
546
|
-
const parser = makeParser(secParser);
|
|
547
|
-
// depth 0: objectToRecord for root -> items: array -> convertArrayValues(depth=1)
|
|
548
|
-
// items = [[1,2]] -> convertArrayValues for inner array at depth=2 -> 2 > 1 = throw
|
|
549
|
-
expect(() => new ObjectAccessor(parser).strict(false).from({ items: [[1, 2]] })).toThrow(SecurityException);
|
|
550
|
-
});
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
// XmlAccessor
|
|
554
|
-
|
|
555
|
-
describe(XmlAccessor.name, () => {
|
|
556
|
-
it('throws InvalidFormatException for non-string input', () => {
|
|
557
|
-
expect(() => new XmlAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
it('throws InvalidFormatException for number input', () => {
|
|
561
|
-
expect(() => new XmlAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
it('throws SecurityException for DOCTYPE declarations (XXE prevention)', () => {
|
|
565
|
-
const xml = '<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><root/>';
|
|
566
|
-
expect(() => new XmlAccessor(makeParser()).from(xml)).toThrow(SecurityException);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
it('throws SecurityException for DOCTYPE regardless of case', () => {
|
|
570
|
-
const xml = '<!doctype foo><root/>';
|
|
571
|
-
expect(() => new XmlAccessor(makeParser()).from(xml)).toThrow(SecurityException);
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
it('parses a simple XML element', () => {
|
|
575
|
-
const a = new XmlAccessor(makeParser()).from('<root><name>Alice</name></root>');
|
|
576
|
-
expect(a.get('name')).toBe('Alice');
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
it('parses sibling elements under the root', () => {
|
|
580
|
-
const a = new XmlAccessor(makeParser()).from(
|
|
581
|
-
'<root><name>Alice</name><age>30</age></root>',
|
|
582
|
-
);
|
|
583
|
-
expect(a.get('name')).toBe('Alice');
|
|
584
|
-
expect(a.get('age')).toBe('30');
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
it('parses nested XML elements', () => {
|
|
588
|
-
const a = new XmlAccessor(makeParser()).from(
|
|
589
|
-
'<root><user><name>Alice</name></user></root>',
|
|
590
|
-
);
|
|
591
|
-
expect(a.get('user.name')).toBe('Alice');
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
it('throws InvalidFormatException for completely unparseable XML', () => {
|
|
595
|
-
expect(() => new XmlAccessor(makeParser()).from('not xml at all !@#')).toThrow(
|
|
596
|
-
InvalidFormatException,
|
|
597
|
-
);
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
it('returns empty object for self-closing root element', () => {
|
|
601
|
-
const a = new XmlAccessor(makeParser()).from('<root/>');
|
|
602
|
-
expect(a.all()).toEqual({});
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
it('merges duplicate sibling tags into an array', () => {
|
|
606
|
-
const a = new XmlAccessor(makeParser()).from('<root><item>a</item><item>b</item></root>');
|
|
607
|
-
const items = a.get('item');
|
|
608
|
-
expect(Array.isArray(items)).toBe(true);
|
|
609
|
-
expect((items as unknown[]).length).toBe(2);
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
it('parses with XML declaration header', () => {
|
|
613
|
-
const a = new XmlAccessor(makeParser()).from(
|
|
614
|
-
'<?xml version="1.0"?><root><key>value</key></root>',
|
|
615
|
-
);
|
|
616
|
-
expect(a.get('key')).toBe('value');
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
it('throws SecurityException when XML depth exceeds the limit', () => {
|
|
620
|
-
const secParser = new SecurityParser({ maxDepth: 1 });
|
|
621
|
-
const parser = makeParser(secParser);
|
|
622
|
-
const xml = '<root><a><b><c>deep</c></b></a></root>';
|
|
623
|
-
expect(() => new XmlAccessor(parser).from(xml)).toThrow(SecurityException);
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
// parseXmlChildren branch: inner content has elements (not just text)
|
|
627
|
-
it('parses deeply nested elements correctly', () => {
|
|
628
|
-
const a = new XmlAccessor(makeParser()).from(
|
|
629
|
-
'<root><level1><level2><value>deep</value></level2></level1></root>',
|
|
630
|
-
);
|
|
631
|
-
expect(a.get('level1.level2.value')).toBe('deep');
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
// parseXmlChildren branch: element with only text content (no child elements)
|
|
635
|
-
it('parses an element with plain text content', () => {
|
|
636
|
-
const a = new XmlAccessor(makeParser()).from('<root><text>hello world</text></root>');
|
|
637
|
-
expect(a.get('text')).toBe('hello world');
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
// parseXmlChildren branch: third duplicate tag becomes third array element
|
|
641
|
-
it('merges three duplicate sibling tags into an array of 3', () => {
|
|
642
|
-
const a = new XmlAccessor(makeParser()).from(
|
|
643
|
-
'<root><item>a</item><item>b</item><item>c</item></root>',
|
|
644
|
-
);
|
|
645
|
-
const items = a.get('item') as unknown[];
|
|
646
|
-
expect(Array.isArray(items)).toBe(true);
|
|
647
|
-
expect(items.length).toBe(3);
|
|
648
|
-
expect(items[2]).toBe('c');
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
// parseXmlChildren: self-closing child elements inside root
|
|
652
|
-
it('parses self-closing child element as empty string text', () => {
|
|
653
|
-
const a = new XmlAccessor(makeParser()).from('<root><empty/><name>Alice</name></root>');
|
|
654
|
-
expect(a.get('name')).toBe('Alice');
|
|
655
|
-
// empty self-closing tag present but name is parsed
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
// parseXmlManual: root with mixed child elements
|
|
659
|
-
it('parses multiple different child elements', () => {
|
|
660
|
-
const a = new XmlAccessor(makeParser()).from(
|
|
661
|
-
'<root><first>1</first><second>2</second><third>3</third></root>',
|
|
662
|
-
);
|
|
663
|
-
expect(a.get('first')).toBe('1');
|
|
664
|
-
expect(a.get('second')).toBe('2');
|
|
665
|
-
expect(a.get('third')).toBe('3');
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
// parseXmlChildren: content with no elements (pure text) at non-root level
|
|
669
|
-
it('returns empty object for root with only whitespace content', () => {
|
|
670
|
-
const a = new XmlAccessor(makeParser()).from('<root> </root>');
|
|
671
|
-
expect(a.all()).toEqual({});
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
// parseXmlChildren: complex inner structure (child has children)
|
|
675
|
-
it('returns nested structure for complex XML', () => {
|
|
676
|
-
const a = new XmlAccessor(makeParser()).from(
|
|
677
|
-
'<root><user><name>Alice</name><role>admin</role></user></root>',
|
|
678
|
-
);
|
|
679
|
-
expect(a.get('user.name')).toBe('Alice');
|
|
680
|
-
expect(a.get('user.role')).toBe('admin');
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
// Kill line 163/165: value selection when childResult has only '#text' key
|
|
684
|
-
it('returns plain string value when child has only text content', () => {
|
|
685
|
-
const a = new XmlAccessor(makeParser()).from(
|
|
686
|
-
'<root><title>Hello World</title></root>',
|
|
687
|
-
);
|
|
688
|
-
// childResult for <title> will be { '#text': 'Hello World' }
|
|
689
|
-
// value = childResult['#text'] = 'Hello World' (string, not object)
|
|
690
|
-
expect(typeof a.get('title')).toBe('string');
|
|
691
|
-
expect(a.get('title')).toBe('Hello World');
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
// Kill line 171/174: hasOwnProperty for array growth (third duplicate)
|
|
695
|
-
it('builds an array when the same tag appears 4 times', () => {
|
|
696
|
-
const a = new XmlAccessor(makeParser()).from(
|
|
697
|
-
'<root><k>1</k><k>2</k><k>3</k><k>4</k></root>',
|
|
698
|
-
);
|
|
699
|
-
const k = a.get('k') as unknown[];
|
|
700
|
-
expect(Array.isArray(k)).toBe(true);
|
|
701
|
-
expect(k.length).toBe(4);
|
|
702
|
-
expect(k[3]).toBe('4');
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
it('throws SecurityException when opening-tag count exceeds SecurityParser.maxKeys (getMaxKeys flows to XmlParser.maxElements)', () => {
|
|
706
|
-
const secParser = new SecurityParser({ maxKeys: 2 });
|
|
707
|
-
const parser = makeParser(secParser);
|
|
708
|
-
// <root> + 3 × <item> = 4 opening tags; 4 > maxKeys(2) → SecurityException
|
|
709
|
-
const xml = '<root>' + '<item>x</item>'.repeat(3) + '</root>';
|
|
710
|
-
expect(() => new XmlAccessor(parser).from(xml)).toThrow(SecurityException);
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
it('does not throw when opening-tag count is within SecurityParser.maxKeys', () => {
|
|
714
|
-
const secParser = new SecurityParser({ maxKeys: 10 });
|
|
715
|
-
const parser = makeParser(secParser);
|
|
716
|
-
const xml = '<root>' + '<item>x</item>'.repeat(3) + '</root>';
|
|
717
|
-
expect(() => new XmlAccessor(parser).from(xml)).not.toThrow();
|
|
718
|
-
});
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
// AnyAccessor
|
|
722
|
-
|
|
723
|
-
describe(AnyAccessor.name, () => {
|
|
724
|
-
it('accepts data when integration assertFormat returns true', () => {
|
|
725
|
-
const integration = new FakeParseIntegration(true, { key: 'value' });
|
|
726
|
-
const a = new AnyAccessor(makeParser(), integration).from('some data');
|
|
727
|
-
expect(a.get('key')).toBe('value');
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
it('throws InvalidFormatException when integration rejects format', () => {
|
|
731
|
-
const integration = new FakeParseIntegration(false, {});
|
|
732
|
-
expect(() => new AnyAccessor(makeParser(), integration).from('data')).toThrow(
|
|
733
|
-
InvalidFormatException,
|
|
734
|
-
);
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
it('validates string payloads through assertPayload', () => {
|
|
738
|
-
const secParser = new SecurityParser({ maxPayloadBytes: 3 });
|
|
739
|
-
const parser = makeParser(secParser);
|
|
740
|
-
const integration = new FakeParseIntegration(true, {});
|
|
741
|
-
expect(() => new AnyAccessor(parser, integration).from('1234')).toThrow(SecurityException);
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
it('does not call assertPayload for non-string data', () => {
|
|
745
|
-
const secParser = new SecurityParser({ maxPayloadBytes: 1 });
|
|
746
|
-
const parser = makeParser(secParser);
|
|
747
|
-
const integration = new FakeParseIntegration(true, { a: 1 });
|
|
748
|
-
expect(() => new AnyAccessor(parser, integration).from({ x: 1 })).not.toThrow();
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
it('resolves nested path from parsed data', () => {
|
|
752
|
-
const integration = new FakeParseIntegration(true, { user: { name: 'Alice' } });
|
|
753
|
-
const a = new AnyAccessor(makeParser(), integration).from('anything');
|
|
754
|
-
expect(a.get('user.name')).toBe('Alice');
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
it('error message mentions typeof when format is rejected', () => {
|
|
758
|
-
const integration = new FakeParseIntegration(false, {});
|
|
759
|
-
expect(() => new AnyAccessor(makeParser(), integration).from(42)).toThrow(/number/);
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
// Kills fake-parse-integration.ts:12:36 BooleanLiteral NoCoverage: default accepts=true
|
|
763
|
-
it('FakeParseIntegration default constructor accepts any input', () => {
|
|
764
|
-
const integration = new FakeParseIntegration();
|
|
765
|
-
expect(integration.assertFormat('test')).toBe(true);
|
|
766
|
-
});
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
// YamlAccessor
|
|
770
|
-
|
|
771
|
-
describe(YamlAccessor.name, () => {
|
|
772
|
-
it('parses a valid YAML string', () => {
|
|
773
|
-
const a = new YamlAccessor(makeParser()).from('name: Alice\nage: 30');
|
|
774
|
-
expect(a.get('name')).toBe('Alice');
|
|
775
|
-
expect(a.get('age')).toBe(30);
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
it('throws InvalidFormatException for non-string input', () => {
|
|
779
|
-
expect(() => new YamlAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
it('throws InvalidFormatException for number input', () => {
|
|
783
|
-
expect(() => new YamlAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
it('resolves a nested path', () => {
|
|
787
|
-
const a = new YamlAccessor(makeParser()).from('user:\n name: Bob');
|
|
788
|
-
expect(a.get('user.name')).toBe('Bob');
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
it('returns null for a missing path', () => {
|
|
792
|
-
const a = new YamlAccessor(makeParser()).from('key: value');
|
|
793
|
-
expect(a.get('missing')).toBeNull();
|
|
794
|
-
});
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
// AbstractAccessor (via JsonAccessor as concrete implementation)
|
|
798
|
-
|
|
799
|
-
describe('AbstractAccessor', () => {
|
|
800
|
-
it('strict mode default is enabled — validates on ingest', () => {
|
|
801
|
-
// __proto__ is forbidden → strict mode triggers SecurityException
|
|
802
|
-
expect(() => new JsonAccessor(makeParser()).from('{"__proto__": "bad"}')).toThrow(
|
|
803
|
-
SecurityException,
|
|
804
|
-
);
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
it('strict(false) disables validation', () => {
|
|
808
|
-
const a = new JsonAccessor(makeParser()).strict(false).from('{"__proto__": "ok"}');
|
|
809
|
-
expect(a.get('__proto__')).toBe('ok');
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
it('strict(true) re-enables validation', () => {
|
|
813
|
-
const accessor = new JsonAccessor(makeParser()).strict(false);
|
|
814
|
-
const strictAgain = accessor.strict(true);
|
|
815
|
-
expect(() => strictAgain.from('{"__proto__": "bad"}')).toThrow(SecurityException);
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
it('readonly(true) blocks set()', () => {
|
|
819
|
-
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
|
|
820
|
-
expect(() => a.set('x', 2)).toThrow(ReadonlyViolationException);
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
it('readonly(true) blocks remove()', () => {
|
|
824
|
-
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
|
|
825
|
-
expect(() => a.remove('x')).toThrow(ReadonlyViolationException);
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
it('readonly(false) allows mutation after readonly(true)', () => {
|
|
829
|
-
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true).readonly(false);
|
|
830
|
-
expect(() => a.set('x', 2)).not.toThrow();
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
it('merge() combines two objects at root level', () => {
|
|
834
|
-
const a = new JsonAccessor(makeParser()).from('{"a":1}');
|
|
835
|
-
const merged = a.merge('', { b: 2 });
|
|
836
|
-
expect(merged.get('a')).toBe(1);
|
|
837
|
-
expect(merged.get('b')).toBe(2);
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
it('merge() at a nested path', () => {
|
|
841
|
-
const a = new JsonAccessor(makeParser()).from('{"user":{"name":"Alice"}}');
|
|
842
|
-
const merged = a.merge('user', { role: 'admin' });
|
|
843
|
-
expect(merged.get('user.name')).toBe('Alice');
|
|
844
|
-
expect(merged.get('user.role')).toBe('admin');
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
it('all() returns all parsed data', () => {
|
|
848
|
-
const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2}');
|
|
849
|
-
expect(a.all()).toEqual({ a: 1, b: 2 });
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
it('keys() returns root-level keys', () => {
|
|
853
|
-
const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2}');
|
|
854
|
-
expect(a.keys()).toEqual(['a', 'b']);
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
it('count() returns number of root keys', () => {
|
|
858
|
-
const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2,"c":3}');
|
|
859
|
-
expect(a.count()).toBe(3);
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
it('getRaw() returns original input', () => {
|
|
863
|
-
const json = '{"name":"Alice"}';
|
|
864
|
-
expect(new JsonAccessor(makeParser()).from(json).getRaw()).toBe(json);
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
it('getOrFail() throws PathNotFoundException for missing path', () => {
|
|
868
|
-
const a = new JsonAccessor(makeParser()).from('{}');
|
|
869
|
-
expect(() => a.getOrFail('missing')).toThrow(PathNotFoundException);
|
|
870
|
-
});
|
|
871
|
-
|
|
872
|
-
// Kills line 87:36 — default parameter `= true` in readonly()
|
|
873
|
-
it('readonly() with no argument defaults to true (blocks mutations)', () => {
|
|
874
|
-
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly();
|
|
875
|
-
expect(() => a.set('x', 2)).toThrow(ReadonlyViolationException);
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
// Kills line ~102:30 — default parameter `= true` in strict()
|
|
879
|
-
it('strict() with no argument defaults to true (enables validation)', () => {
|
|
880
|
-
const accessor = new JsonAccessor(makeParser()).strict(false);
|
|
881
|
-
const strictAgain = accessor.strict(); // no-arg = true
|
|
882
|
-
expect(() => strictAgain.from('{"__proto__": "bad"}')).toThrow(SecurityException);
|
|
883
|
-
});
|
|
884
|
-
|
|
885
|
-
// Kills lines 150:70 — getAt() default parameter = null
|
|
886
|
-
it('getAt() returns null when path does not exist (default is null)', () => {
|
|
887
|
-
const a = new JsonAccessor(makeParser()).from('{"a":1}');
|
|
888
|
-
expect(a.getAt(['missing'])).toBeNull();
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
it('getAt() resolves a value using pre-parsed segments', () => {
|
|
892
|
-
const a = new JsonAccessor(makeParser()).from('{"user":{"name":"Alice"}}');
|
|
893
|
-
expect(a.getAt(['user', 'name'])).toBe('Alice');
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
// Kills line 170:40 — hasAt() return value
|
|
897
|
-
it('hasAt() returns true when segments resolve to a value', () => {
|
|
898
|
-
const a = new JsonAccessor(makeParser()).from('{"a":{"b":1}}');
|
|
899
|
-
expect(a.hasAt(['a', 'b'])).toBe(true);
|
|
900
|
-
});
|
|
901
|
-
|
|
902
|
-
it('hasAt() returns false when segments do not resolve', () => {
|
|
903
|
-
const a = new JsonAccessor(makeParser()).from('{"a":1}');
|
|
904
|
-
expect(a.hasAt(['missing'])).toBe(false);
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
// Kills line 195:53 — setAt() returns new instance with value set
|
|
908
|
-
it('setAt() sets a value using pre-parsed segments', () => {
|
|
909
|
-
const a = new JsonAccessor(makeParser()).from('{}');
|
|
910
|
-
const updated = a.setAt(['user', 'name'], 'Alice');
|
|
911
|
-
expect(updated.get('user.name')).toBe('Alice');
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
it('setAt() throws ReadonlyViolationException when readonly', () => {
|
|
915
|
-
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
|
|
916
|
-
expect(() => a.setAt(['x'], 2)).toThrow(ReadonlyViolationException);
|
|
917
|
-
});
|
|
918
|
-
|
|
919
|
-
// Kills lines 219/ removeAt()
|
|
920
|
-
it('removeAt() removes a value using pre-parsed segments', () => {
|
|
921
|
-
const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2}');
|
|
922
|
-
const updated = a.removeAt(['a']);
|
|
923
|
-
expect(updated.has('a')).toBe(false);
|
|
924
|
-
expect(updated.has('b')).toBe(true);
|
|
925
|
-
});
|
|
926
|
-
|
|
927
|
-
it('removeAt() throws ReadonlyViolationException when readonly', () => {
|
|
928
|
-
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
|
|
929
|
-
expect(() => a.removeAt(['x'])).toThrow(ReadonlyViolationException);
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
// Kills lines 230/232 — getMany()
|
|
933
|
-
it('getMany() returns map of paths to their values', () => {
|
|
934
|
-
const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2}');
|
|
935
|
-
expect(a.getMany({ a: 0, b: 0, missing: 'fallback' })).toEqual({ a: 1, b: 2, missing: 'fallback' });
|
|
936
|
-
});
|
|
937
|
-
|
|
938
|
-
// Kills lines 254/255 — count(path)
|
|
939
|
-
it('count(path) returns number of keys at a nested path', () => {
|
|
940
|
-
const a = new JsonAccessor(makeParser()).from('{"user":{"name":"Alice","age":30}}');
|
|
941
|
-
expect(a.count('user')).toBe(2);
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
it('count(path) returns 0 when path resolves to a non-object', () => {
|
|
945
|
-
const a = new JsonAccessor(makeParser()).from('{"a":"string"}');
|
|
946
|
-
expect(a.count('a')).toBe(0);
|
|
947
|
-
});
|
|
948
|
-
|
|
949
|
-
// Kills lines 268/269 — keys(path)
|
|
950
|
-
it('keys(path) returns keys at a nested path', () => {
|
|
951
|
-
const a = new JsonAccessor(makeParser()).from('{"user":{"name":"Alice","role":"admin"}}');
|
|
952
|
-
expect(a.keys('user')).toEqual(['name', 'role']);
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
it('keys(path) returns empty array when path resolves to a non-object', () => {
|
|
956
|
-
const a = new JsonAccessor(makeParser()).from('{"a":42}');
|
|
957
|
-
expect(a.keys('a')).toEqual([]);
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
// Kills line 295/297 — mergeAll()
|
|
961
|
-
it('mergeAll() deep-merges into root', () => {
|
|
962
|
-
const a = new JsonAccessor(makeParser()).from('{"a":1}');
|
|
963
|
-
const merged = a.mergeAll({ b: 2, c: 3 });
|
|
964
|
-
expect(merged.get('a')).toBe(1);
|
|
965
|
-
expect(merged.get('b')).toBe(2);
|
|
966
|
-
expect(merged.get('c')).toBe(3);
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
it('mergeAll() throws ReadonlyViolationException when readonly', () => {
|
|
970
|
-
const a = new JsonAccessor(makeParser()).from('{"a":1}').readonly(true);
|
|
971
|
-
expect(() => a.mergeAll({ b: 2 })).toThrow(ReadonlyViolationException);
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
// Kills line 310:23 ObjectLiteral mutation — copy._state = {} instead of {...this._state}
|
|
975
|
-
// Verify clone preserves the full state including readonly and strict options
|
|
976
|
-
it('set() clone preserves readonly state', () => {
|
|
977
|
-
const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
|
|
978
|
-
// After set, the new instance should still be readonly (state cloned properly)
|
|
979
|
-
const b = a.readonly(false).set('x', 2);
|
|
980
|
-
expect(b.get('x')).toBe(2);
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
// Kills ObjectLiteral: copy._state={} loses isStrict flag, allowing unsafe keys after set()
|
|
984
|
-
it('set() clone inherits strict mode — security validation still enforced', () => {
|
|
985
|
-
const a = new JsonAccessor(makeParser()).from('{"x":1}');
|
|
986
|
-
// a is strict (default). set() calls cloneInstance; if state is lost, new instance loses isStrict=true
|
|
987
|
-
// After set, calling .from() with unsafe data must still throw if strict is preserved
|
|
988
|
-
const b = a.set('x', 2);
|
|
989
|
-
// b should still be strict — .from(unsafe) must throw
|
|
990
|
-
expect(() => b.from('{"__proto__":"bad"}')).toThrow(SecurityException);
|
|
991
|
-
});
|
|
992
|
-
|
|
993
|
-
// Kills 269:13 ConditionalExpression — typeof target === 'object' → true (with null input)
|
|
994
|
-
it('keys() returns [] when path resolves to null (typeof null is object in JS)', () => {
|
|
995
|
-
const a = new JsonAccessor(makeParser()).from('{"a":null}');
|
|
996
|
-
// typeof null === 'object' is true in JS, so only null-check protects us
|
|
997
|
-
expect(a.keys('a')).toEqual([]);
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
it('count() returns 0 when path resolves to null (typeof null is object in JS)', () => {
|
|
1001
|
-
const a = new JsonAccessor(makeParser()).from('{"a":null}');
|
|
1002
|
-
expect(a.count('a')).toBe(0);
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
it('strict(false) bypasses payload size validation', () => {
|
|
1006
|
-
const tinyParser = new SecurityParser({ maxPayloadBytes: 5 });
|
|
1007
|
-
const parser = new DotNotationParser(new SecurityGuard(), tinyParser);
|
|
1008
|
-
const a = new JsonAccessor(parser).strict(false).from('{"name":"Alice"}');
|
|
1009
|
-
expect(a.get('name')).toBe('Alice');
|
|
1010
|
-
});
|
|
1011
|
-
|
|
1012
|
-
it('strict(true) enforces payload size validation', () => {
|
|
1013
|
-
const tinyParser = new SecurityParser({ maxPayloadBytes: 5 });
|
|
1014
|
-
const parser = new DotNotationParser(new SecurityGuard(), tinyParser);
|
|
1015
|
-
expect(() => new JsonAccessor(parser).from('{"name":"Alice"}')).toThrow(SecurityException);
|
|
1016
|
-
});
|
|
1017
|
-
});
|