fix(net): serialize validated wire snapshots
This commit is contained in:
@@ -199,6 +199,8 @@ test('send rejects every non-JSON data graph edge with its exact path', async ()
|
||||
sparse.length = 1;
|
||||
const arrayWithExtra = [] as unknown[] & { extra?: number };
|
||||
arrayWithExtra.extra = 1;
|
||||
const arrayWithSymbol = [] as unknown[];
|
||||
Object.defineProperty(arrayWithSymbol, Symbol('extra'), { value: 1 });
|
||||
const cyclic: Record<string, unknown> = {};
|
||||
cyclic.self = cyclic;
|
||||
const withAccessor: Record<string, unknown> = {};
|
||||
@@ -220,9 +222,11 @@ test('send rejects every non-JSON data graph edge with its exact path', async ()
|
||||
[{ value: 1n }, /\$\.data\.value.*bigint/i],
|
||||
[{ value: Number.NaN }, /\$\.data\.value.*finite number/i],
|
||||
[{ value: Number.POSITIVE_INFINITY }, /\$\.data\.value.*finite number/i],
|
||||
[{ value: -0 }, /\$\.data\.value.*negative zero/i],
|
||||
[{ nested: sparse }, /\$\.data\.nested\[0\].*array hole/i],
|
||||
[{ nested: arrayWithExtra }, /\$\.data\.nested\.extra.*extra array property/i],
|
||||
[{ nested: new Date(0) }, /\$\.data\.nested.*plain object/i],
|
||||
[{ nested: arrayWithSymbol }, /\$\.data\.nested.*symbol key/i],
|
||||
[{ nested: new Date(0) }, /\$\.data\.nested.*inherited toJSON/i],
|
||||
[{ nested: new Exotic() }, /\$\.data\.nested.*plain object/i],
|
||||
[{ nested: withAccessor }, /\$\.data\.nested\.secret.*accessor/i],
|
||||
[{ nested: withHidden }, /\$\.data\.nested\.hidden.*enumerable/i],
|
||||
@@ -240,6 +244,149 @@ test('send rejects every non-JSON data graph edge with its exact path', async ()
|
||||
assert.deepEqual(transport.sent, []);
|
||||
});
|
||||
|
||||
test('send preserves a dense non-enumerable array index in the validated wire bytes', async () => {
|
||||
const { client, transports } = makeHarness();
|
||||
client.start();
|
||||
const transport = transports[0]!;
|
||||
await transport.flush();
|
||||
|
||||
const values: unknown[] = [];
|
||||
Object.defineProperty(values, '0', {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: 'descriptor value',
|
||||
writable: true,
|
||||
});
|
||||
const completed = {
|
||||
app: 'youle',
|
||||
route: 'agent',
|
||||
rpc: 'array_descriptor',
|
||||
data: { values },
|
||||
} as OutboundEnvelope;
|
||||
|
||||
client.send(completed);
|
||||
|
||||
assert.equal(transport.sent[0], JSON.stringify(completed));
|
||||
assert.equal(
|
||||
transport.sent[0],
|
||||
'{"app":"youle","route":"agent","rpc":"array_descriptor","data":{"values":["descriptor value"]}}',
|
||||
);
|
||||
});
|
||||
|
||||
test('send rejects inherited toJSON without invoking it', async () => {
|
||||
const { client, transports } = makeHarness();
|
||||
client.start();
|
||||
const transport = transports[0]!;
|
||||
await transport.flush();
|
||||
const original = Object.getOwnPropertyDescriptor(Object.prototype, 'toJSON');
|
||||
let calls = 0;
|
||||
Object.defineProperty(Object.prototype, 'toJSON', {
|
||||
configurable: true,
|
||||
value() {
|
||||
calls++;
|
||||
return { changed: true };
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
assert.throws(
|
||||
() => client.send({ app: 'youle', route: 'agent', rpc: 'x', data: { value: 1 } }),
|
||||
/\$.*inherited toJSON/i,
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
assert.deepEqual(transport.sent, []);
|
||||
} finally {
|
||||
if (original) Object.defineProperty(Object.prototype, 'toJSON', original);
|
||||
else delete (Object.prototype as { toJSON?: unknown }).toJSON;
|
||||
}
|
||||
});
|
||||
|
||||
test('send serializes stable descriptor snapshots without invoking Proxy get traps', async () => {
|
||||
const { client, transports } = makeHarness();
|
||||
client.start();
|
||||
const transport = transports[0]!;
|
||||
await transport.flush();
|
||||
const target = { value: 1 };
|
||||
let getCalls = 0;
|
||||
const proxied = new Proxy(target, {
|
||||
get(source, key, receiver) {
|
||||
getCalls++;
|
||||
if (key === 'value') return 2;
|
||||
return Reflect.get(source, key, receiver);
|
||||
},
|
||||
});
|
||||
|
||||
client.send({ app: 'youle', route: 'agent', rpc: 'proxy', data: { proxied } });
|
||||
|
||||
assert.equal(getCalls, 0);
|
||||
assert.equal(
|
||||
transport.sent[0],
|
||||
'{"app":"youle","route":"agent","rpc":"proxy","data":{"proxied":{"value":1}}}',
|
||||
);
|
||||
});
|
||||
|
||||
test('send rejects source graphs whose descriptors, own-key order, or prototype change', async () => {
|
||||
const cases: ReadonlyArray<readonly [string, () => object, RegExp]> = [
|
||||
[
|
||||
'descriptor',
|
||||
() => {
|
||||
const target = { value: 1 };
|
||||
let calls = 0;
|
||||
return new Proxy(target, {
|
||||
getOwnPropertyDescriptor(source, key) {
|
||||
const descriptor = Reflect.getOwnPropertyDescriptor(source, key);
|
||||
if (key !== 'value' || !descriptor) return descriptor;
|
||||
calls++;
|
||||
return { ...descriptor, value: calls === 1 ? 1 : 2 };
|
||||
},
|
||||
});
|
||||
},
|
||||
/\$\.data\.unstable\.value.*descriptor.*changed/i,
|
||||
],
|
||||
[
|
||||
'own keys',
|
||||
() => {
|
||||
const target = { first: 1, second: 2 };
|
||||
let calls = 0;
|
||||
return new Proxy(target, {
|
||||
ownKeys() {
|
||||
calls++;
|
||||
return calls === 1 ? ['first', 'second'] : ['second', 'first'];
|
||||
},
|
||||
});
|
||||
},
|
||||
/\$\.data\.unstable.*own keys.*changed/i,
|
||||
],
|
||||
[
|
||||
'prototype',
|
||||
() => {
|
||||
const target = { value: 1 };
|
||||
let calls = 0;
|
||||
return new Proxy(target, {
|
||||
getPrototypeOf() {
|
||||
calls++;
|
||||
return calls === 1 ? Object.prototype : null;
|
||||
},
|
||||
});
|
||||
},
|
||||
/\$\.data\.unstable.*prototype.*changed/i,
|
||||
],
|
||||
];
|
||||
|
||||
for (const [name, makeUnstable, expected] of cases) {
|
||||
const { client, transports } = makeHarness();
|
||||
client.start();
|
||||
const transport = transports[0]!;
|
||||
await transport.flush();
|
||||
|
||||
assert.throws(
|
||||
() => client.send({ app: 'youle', route: 'agent', rpc: name, data: { unstable: makeUnstable() } }),
|
||||
expected,
|
||||
);
|
||||
assert.deepEqual(transport.sent, []);
|
||||
}
|
||||
});
|
||||
|
||||
test('send rejects top-level symbol, non-enumerable and accessor properties', async () => {
|
||||
const { client, transports } = makeHarness();
|
||||
client.start();
|
||||
|
||||
Reference in New Issue
Block a user