super-productivity/packages/shared-schema/tests/supersync-http-contract.spec.ts
Johannes Millan 12145ed6b2 fix(supersync): isolate duplicate upload ids and make clean-slate idempotent
- A request repeating one operation id no longer double-charges quota or
  wedges the batch: the first occurrence reserves the id and later
  siblings are terminally rejected (DUPLICATE_OPERATION for an exact
  retry, INVALID_OP_ID otherwise), in both batch and serial paths.
- Clean-slate snapshot uploads become durably idempotent: the request
  cache is process-local and expiring, so the client-supplied opId is
  now required (contract superRefine) and checked against the stored
  operation inside the per-user lock before any data is deleted. An
  exact retry returns the original serverSeq; a colliding opId is
  rejected without touching existing data.
- Audit logging validates id/entityType/opType/clientId against the
  known-safe charsets before embedding them in log lines, and the six
  duplicated reject-audit blocks collapse into one rejectedUploadResult
  helper; the ops handler logs rejected error codes instead of whole
  operation objects.
2026-07-11 18:04:23 +02:00

232 lines
7.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
SUPER_SYNC_MAX_ENTITY_IDS_PER_OP,
SUPER_SYNC_MAX_OPS_PER_UPLOAD,
SuperSyncDownloadOpsQuerySchema,
SuperSyncDownloadOpsResponseSchema,
SuperSyncOperationSchema,
SuperSyncUploadOpsRequestSchema,
SuperSyncUploadSnapshotRequestSchema,
} from '../src/supersync-http-contract';
const createValidOperation = (clientId: string = 'client_1') => ({
id: 'op-1',
clientId,
actionType: '[Task] Add task',
opType: 'CRT',
entityType: 'TASK',
entityId: 'task-1',
payload: { title: 'Test' },
vectorClock: { [clientId]: 1 },
timestamp: 1234567890,
schemaVersion: 1,
});
describe('SuperSync HTTP contract schemas', () => {
it('validates ops upload requests with the shared server limit', () => {
const parsed = SuperSyncUploadOpsRequestSchema.parse({
ops: [createValidOperation()],
clientId: 'client_1',
lastKnownServerSeq: 12,
requestId: 'request-1',
isCleanSlate: false,
});
expect(parsed.ops.length).toBe(1);
expect(SUPER_SYNC_MAX_OPS_PER_UPLOAD).toBe(100);
});
it('preserves server request behavior by stripping unknown upload fields', () => {
const parsed = SuperSyncUploadOpsRequestSchema.parse({
ops: [{ ...createValidOperation(), extraOpField: true }],
clientId: 'client_1',
extraRequestField: true,
});
expect('extraRequestField' in parsed).toBe(false);
expect('extraOpField' in parsed.ops[0]).toBe(false);
});
it('passes oversized entityIds through for per-operation validation', () => {
const operation = {
...createValidOperation(),
entityIds: Array.from(
{ length: SUPER_SYNC_MAX_ENTITY_IDS_PER_OP + 1 },
(_, i) => `task-${i}`,
),
};
const parsed = SuperSyncUploadOpsRequestSchema.parse({
ops: [operation],
clientId: 'client_1',
});
expect(parsed.ops[0].entityIds).toHaveLength(SUPER_SYNC_MAX_ENTITY_IDS_PER_OP + 1);
expect(() => SuperSyncOperationSchema.parse(operation)).toThrow();
});
it('rejects invalid client IDs in upload requests', () => {
expect(() =>
SuperSyncUploadOpsRequestSchema.parse({
ops: [createValidOperation('invalid client')],
clientId: 'invalid client',
}),
).toThrow();
});
it.each([0, 1.5, -1, 101])(
'passes operation schema version %s through the upload transport schema',
(schemaVersion) => {
const parsed = SuperSyncUploadOpsRequestSchema.parse({
ops: [{ ...createValidOperation(), schemaVersion }],
clientId: 'client_1',
});
expect(parsed.ops[0].schemaVersion).toBe(schemaVersion);
expect(() =>
SuperSyncOperationSchema.parse({
...createValidOperation(),
schemaVersion,
}),
).toThrow();
},
);
it.each([
['empty operation ID', { id: '' }],
['overlong operation ID', { id: 'x'.repeat(256) }],
['mismatched operation client ID', { clientId: 'other client' }],
['unknown operation type', { opType: 'UNKNOWN' }],
['unknown entity type', { entityType: 'UNKNOWN' }],
['overlong entity ID', { entityId: 'x'.repeat(256) }],
['invalid vector-clock entry', { vectorClock: { client_1: 'invalid' } }],
])('passes semantic %s through for per-operation validation', (_label, override) => {
const operation = { ...createValidOperation(), ...override };
const parsed = SuperSyncUploadOpsRequestSchema.parse({
ops: [operation],
clientId: 'client_1',
});
expect(parsed.ops).toHaveLength(1);
});
it.each([
['non-string operation ID', { id: 123 }],
['empty action type', { actionType: '' }],
['non-string operation type', { opType: 123 }],
['non-string entity type', { entityType: 123 }],
['non-string entity ID', { entityId: 123 }],
['non-string entityIds member', { entityIds: ['task-1', 123] }],
['non-object vector clock', { vectorClock: [] }],
['non-numeric timestamp', { timestamp: '123' }],
['non-numeric schema version', { schemaVersion: '1' }],
['non-boolean encryption flag', { isPayloadEncrypted: 'true' }],
['unknown import reason', { syncImportReason: 'UNKNOWN' }],
])('keeps the upload transport constraint for %s', (_label, override) => {
expect(() =>
SuperSyncUploadOpsRequestSchema.parse({
ops: [{ ...createValidOperation(), ...override }],
clientId: 'client_1',
}),
).toThrow();
});
it('rejects semantically invalid identifiers beyond the absolute transport cap', () => {
expect(() =>
SuperSyncUploadOpsRequestSchema.parse({
ops: [{ ...createValidOperation(), id: 'x'.repeat(4097) }],
clientId: 'client_1',
}),
).toThrow();
});
it.each([0, 1.5, -1, 101])(
'rejects malformed snapshot schema version %s',
(schemaVersion) => {
expect(() =>
SuperSyncUploadSnapshotRequestSchema.parse({
state: {},
clientId: 'client_1',
reason: 'recovery',
vectorClock: { client_1: 1 },
schemaVersion,
}),
).toThrow();
},
);
it('coerces download query numbers like the route-level schema', () => {
const parsed = SuperSyncDownloadOpsQuerySchema.parse({
sinceSeq: '5',
limit: '50',
excludeClient: 'client-1',
});
expect(parsed).toEqual({
sinceSeq: 5,
limit: 50,
excludeClient: 'client-1',
});
});
it('validates snapshot upload requests', () => {
const parsed = SuperSyncUploadSnapshotRequestSchema.parse({
state: { tasks: {} },
clientId: 'client_1',
reason: 'recovery',
vectorClock: { client_1: 2 },
schemaVersion: 1,
isPayloadEncrypted: true,
syncImportReason: 'BACKUP_RESTORE',
opId: '018f2f0b-1c2d-7a1b-8c3d-123456789abc',
isCleanSlate: true,
snapshotOpType: 'BACKUP_IMPORT',
requestId: 'snapshot-v1-request',
});
expect(parsed.snapshotOpType).toBe('BACKUP_IMPORT');
expect(parsed.requestId).toBe('snapshot-v1-request');
});
it('requires an operation ID for destructive clean-slate snapshots', () => {
expect(() =>
SuperSyncUploadSnapshotRequestSchema.parse({
state: {},
clientId: 'client_1',
reason: 'recovery',
vectorClock: { client_1: 1 },
schemaVersion: 1,
isCleanSlate: true,
}),
).toThrow();
});
it('rejects requestIds containing characters outside the safe-log charset', () => {
// Control character — would be unsafe to embed in server log lines.
expect(() =>
SuperSyncUploadOpsRequestSchema.parse({
ops: [createValidOperation()],
clientId: 'client_1',
requestId: 'has\nnewline-injected',
}),
).toThrow();
// Space is not part of the allowed charset.
expect(() =>
SuperSyncUploadOpsRequestSchema.parse({
ops: [createValidOperation()],
clientId: 'client_1',
requestId: 'has space',
}),
).toThrow();
// Longer than 64 chars — exceeds the documented bound.
expect(() =>
SuperSyncUploadOpsRequestSchema.parse({
ops: [createValidOperation()],
clientId: 'client_1',
requestId: 'x'.repeat(65),
}),
).toThrow();
});
});