super-productivity/packages/super-sync-server/tests/sync-compressed-body.routes.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

1487 lines
48 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';
import { randomBytes } from 'crypto';
import * as zlib from 'zlib';
import { promisify } from 'util';
const mocks = vi.hoisted(() => {
const syncService = {
isRateLimited: vi.fn(),
checkOpsRequestDedup: vi.fn(),
cacheOpsRequestResults: vi.fn(),
checkSnapshotRequestDedup: vi.fn(),
cacheSnapshotRequestResult: vi.fn(),
checkStorageQuota: vi.fn(),
uploadOps: vi.fn(),
cacheSnapshot: vi.fn(),
cacheSnapshotIfReplayable: vi.fn(),
prepareSnapshotCache: vi.fn(),
updateStorageUsage: vi.fn(),
incrementStorageUsage: vi.fn(),
decrementStorageUsage: vi.fn(),
runWithStorageUsageLock: vi.fn(),
freeStorageForUpload: vi.fn(),
getLatestSeq: vi.fn(),
getOpsSinceWithSeq: vi.fn(),
getStorageInfo: vi.fn(),
getCachedSnapshotBytes: vi.fn(),
markStorageNeedsReconcile: vi.fn(),
getMaxClockDriftMs: vi.fn(),
filterValidOpsForQuota: vi.fn(),
getPrevalidatedPayloadBytes: vi.fn(),
};
const prisma = {
operation: {
findFirst: vi.fn(),
findUnique: vi.fn(),
findMany: vi.fn(),
},
};
return {
syncService,
prisma,
notifyNewOps: vi.fn(),
};
});
vi.mock('../src/auth', () => ({
verifyToken: vi.fn().mockResolvedValue({
valid: true,
userId: 1,
email: 'test@test.com',
}),
}));
vi.mock('../src/sync/sync.service', () => ({
getSyncService: () => mocks.syncService,
}));
vi.mock('../src/sync/services/websocket-connection.service', () => ({
getWsConnectionService: () => ({
notifyNewOps: mocks.notifyNewOps,
}),
}));
vi.mock('../src/db', () => ({
prisma: mocks.prisma,
}));
import { syncRoutes } from '../src/sync/sync.routes';
import { SYNC_ERROR_CODES } from '../src/sync/sync.types';
import { computeOpStorageBytes } from '../src/sync/sync.const';
import { CURRENT_SCHEMA_VERSION, SUPER_SYNC_MAX_OPS_PER_UPLOAD } from '@sp/shared-schema';
const gzipAsync = promisify(zlib.gzip);
const createOp = (clientId: string) => ({
id: 'op-1',
clientId,
actionType: 'ADD_TASK',
opType: 'CRT',
entityType: 'TASK',
entityId: 'task-1',
payload: { title: 'Test Task' },
vectorClock: {},
timestamp: Date.now(),
schemaVersion: 1,
});
const createStoredDuplicateOp = (op: ReturnType<typeof createOp>) => ({
id: op.id,
userId: 1,
clientId: op.clientId,
actionType: op.actionType,
opType: op.opType,
entityType: op.entityType,
entityId: op.entityId,
entityIds: [],
payload: op.payload,
vectorClock: op.vectorClock,
schemaVersion: op.schemaVersion,
clientTimestamp: BigInt(op.timestamp),
receivedAt: BigInt(op.timestamp),
isPayloadEncrypted: false,
syncImportReason: null,
});
const MiB = 1024 * 1024;
describe('Sync compressed body routes', () => {
let app: FastifyInstance;
const authToken = 'mock-token';
beforeEach(async () => {
vi.clearAllMocks();
mocks.syncService.isRateLimited.mockReturnValue(false);
mocks.syncService.checkOpsRequestDedup.mockReturnValue(null);
mocks.syncService.checkSnapshotRequestDedup.mockReturnValue(null);
mocks.syncService.getMaxClockDriftMs.mockReturnValue(60_000);
mocks.syncService.filterValidOpsForQuota.mockImplementation((ops: unknown[]) => ops);
mocks.syncService.checkStorageQuota.mockResolvedValue({
allowed: true,
currentUsage: 0,
quota: 100 * 1024 * 1024,
});
mocks.syncService.uploadOps.mockResolvedValue([{ accepted: true, serverSeq: 1 }]);
mocks.syncService.cacheSnapshot.mockResolvedValue({
cached: true,
bytesWritten: 0,
previousBytes: 0,
deltaBytes: 0,
});
mocks.syncService.cacheSnapshotIfReplayable.mockResolvedValue({
cached: true,
bytesWritten: 0,
previousBytes: 0,
deltaBytes: 0,
});
mocks.syncService.prepareSnapshotCache.mockImplementation((state: unknown) => {
const serialized = JSON.stringify(state);
const data = zlib.gzipSync(serialized);
return {
data,
bytes: data.length,
stateBytes: Buffer.byteLength(serialized, 'utf8'),
cacheable: true,
};
});
mocks.syncService.getCachedSnapshotBytes.mockResolvedValue(0);
mocks.syncService.updateStorageUsage.mockResolvedValue(undefined);
mocks.syncService.incrementStorageUsage.mockResolvedValue(undefined);
mocks.syncService.decrementStorageUsage.mockResolvedValue(undefined);
mocks.syncService.runWithStorageUsageLock.mockImplementation(
async (_userId: number, fn: () => Promise<unknown>) => fn(),
);
mocks.syncService.freeStorageForUpload.mockResolvedValue({
success: false,
freedBytes: 0,
deletedRestorePoints: 0,
deletedOps: 0,
});
mocks.syncService.getLatestSeq.mockResolvedValue(1);
mocks.syncService.getOpsSinceWithSeq.mockResolvedValue({
ops: [],
latestSeq: 1,
});
mocks.syncService.getStorageInfo.mockResolvedValue({
storageUsedBytes: 0,
storageQuotaBytes: 100 * 1024 * 1024,
});
mocks.syncService.getCachedSnapshotBytes.mockResolvedValue(0);
mocks.prisma.operation.findFirst.mockResolvedValue(null);
mocks.prisma.operation.findUnique.mockReset().mockResolvedValue(null);
mocks.prisma.operation.findMany.mockResolvedValue([]);
app = Fastify();
await app.register(syncRoutes, { prefix: '/api/sync' });
await app.ready();
});
afterEach(async () => {
await app.close();
});
it('should accept plain JSON ops upload', async () => {
const clientId = 'plain-json-client';
const payload = {
ops: [createOp(clientId)],
clientId,
};
const jsonPayload = JSON.stringify(payload);
const payloadSize = Buffer.byteLength(jsonPayload, 'utf-8');
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
'content-length': String(payloadSize),
},
payload: jsonPayload,
});
expect(response.statusCode).toBe(200);
expect(response.json().results[0].accepted).toBe(true);
expect(mocks.syncService.uploadOps).toHaveBeenCalledOnce();
// Quota gate now accounts via computeOpsStorageBytes(ops), so the value is
// the per-op payload+vectorClock bytes rather than the full body size.
// Tests of compression handling stay value-agnostic — assert a finite,
// bounded delta was passed in.
const quotaCall = mocks.syncService.checkStorageQuota.mock.calls[0];
expect(quotaCall[0]).toBe(1);
expect(typeof quotaCall[1]).toBe('number');
expect(quotaCall[1]).toBeGreaterThan(0);
expect(quotaCall[1]).toBeLessThan(payloadSize);
});
it('should pass mixed schema versions to per-operation validation', async () => {
const clientId = 'mixed-schema-client';
const validOp = createOp(clientId);
const invalidOp = {
...createOp(clientId),
id: 'invalid-schema-op',
schemaVersion: 101,
};
mocks.syncService.uploadOps.mockResolvedValueOnce([
{ opId: validOp.id, accepted: true, serverSeq: 1 },
{
opId: invalidOp.id,
accepted: false,
error: 'Invalid schema version: 101',
errorCode: SYNC_ERROR_CODES.INVALID_SCHEMA_VERSION,
},
]);
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
},
payload: { ops: [validOp, invalidOp], clientId },
});
expect(response.statusCode).toBe(200);
expect(response.json().results).toEqual([
expect.objectContaining({ opId: validOp.id, accepted: true }),
expect.objectContaining({
opId: invalidOp.id,
accepted: false,
errorCode: SYNC_ERROR_CODES.INVALID_SCHEMA_VERSION,
}),
]);
expect(mocks.syncService.uploadOps).toHaveBeenCalledWith(
1,
clientId,
[validOp, invalidOp],
undefined,
new Set(),
);
});
it('should not let an invalid large sibling poison a near-quota upload', async () => {
const clientId = 'invalid-sibling-quota-client';
const validOp = {
...createOp(clientId),
id: 'valid-near-quota-op',
payload: { title: 'Valid task' },
};
const invalidLargeOp = {
...createOp(clientId),
id: 'invalid-large-op',
opType: 'UNKNOWN',
payload: { data: 'x'.repeat(10_000) },
};
const validBytes = computeOpStorageBytes(validOp).bytes;
mocks.syncService.filterValidOpsForQuota.mockReturnValueOnce([validOp]);
mocks.syncService.checkStorageQuota.mockImplementation(
async (_userId: number, additionalBytes: number) => ({
allowed: additionalBytes <= validBytes,
currentUsage: 1_000_000 - validBytes,
quota: 1_000_000,
}),
);
mocks.syncService.uploadOps.mockResolvedValueOnce([
{ opId: validOp.id, accepted: true, serverSeq: 1 },
{
opId: invalidLargeOp.id,
accepted: false,
error: 'Invalid opType',
errorCode: SYNC_ERROR_CODES.INVALID_OP_TYPE,
},
]);
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
},
payload: { ops: [validOp, invalidLargeOp], clientId },
});
expect(response.statusCode).toBe(200);
expect(response.json().results).toEqual([
expect.objectContaining({ opId: validOp.id, accepted: true }),
expect.objectContaining({
opId: invalidLargeOp.id,
accepted: false,
errorCode: SYNC_ERROR_CODES.INVALID_OP_TYPE,
}),
]);
expect(mocks.syncService.filterValidOpsForQuota).toHaveBeenCalledWith(
[validOp, invalidLargeOp],
clientId,
);
expect(mocks.syncService.checkStorageQuota).toHaveBeenCalledWith(1, validBytes);
expect(mocks.syncService.uploadOps).toHaveBeenCalledWith(
1,
clientId,
[validOp, invalidLargeOp],
undefined,
new Set(),
);
});
it('should subtract exact already-stored duplicate ops from the ops quota gate', async () => {
const clientId = 'known-duplicate-quota-client';
const duplicateOp = {
...createOp(clientId),
id: 'known-duplicate-op',
entityId: 'task-known-duplicate',
payload: { title: 'Already accepted task' },
};
const newOp = {
...createOp(clientId),
id: 'new-op',
entityId: 'task-new',
payload: { title: 'New task' },
};
mocks.prisma.operation.findMany.mockResolvedValueOnce([
createStoredDuplicateOp(duplicateOp),
]);
mocks.syncService.uploadOps.mockResolvedValueOnce([
{
opId: duplicateOp.id,
accepted: false,
error: 'Duplicate operation ID',
errorCode: SYNC_ERROR_CODES.DUPLICATE_OPERATION,
},
{ opId: newOp.id, accepted: true, serverSeq: 2 },
]);
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
},
payload: {
ops: [duplicateOp, newOp],
clientId,
},
});
expect(response.statusCode).toBe(200);
expect(mocks.prisma.operation.findMany).toHaveBeenCalledOnce();
expect(mocks.syncService.checkStorageQuota).toHaveBeenCalledWith(
1,
computeOpStorageBytes(newOp).bytes,
);
expect(mocks.syncService.uploadOps).toHaveBeenCalledWith(
1,
clientId,
[duplicateOp, newOp],
undefined,
new Set([duplicateOp.id]),
);
});
it('should not charge same-id different-content ops in the quota gate', async () => {
const clientId = 'id-collision-quota-client';
const incomingOp = {
...createOp(clientId),
id: 'colliding-op-id',
entityId: 'task-collision',
payload: { title: 'Incoming content' },
};
const storedDifferentOp = createStoredDuplicateOp({
...incomingOp,
payload: { title: 'Stored different content' },
});
mocks.prisma.operation.findMany.mockResolvedValueOnce([storedDifferentOp]);
mocks.syncService.uploadOps.mockResolvedValueOnce([
{
opId: incomingOp.id,
accepted: false,
error: 'Operation ID already belongs to a different operation',
errorCode: SYNC_ERROR_CODES.INVALID_OP_ID,
},
]);
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
},
payload: {
ops: [incomingOp],
clientId,
},
});
expect(response.statusCode).toBe(200);
expect(mocks.syncService.checkStorageQuota).toHaveBeenCalledWith(1, 0);
expect(mocks.prisma.operation.findMany).toHaveBeenCalledWith({
where: { id: { in: [incomingOp.id] } },
select: { id: true },
});
expect(mocks.syncService.uploadOps).toHaveBeenCalledWith(
1,
clientId,
[incomingOp],
undefined,
new Set([incomingOp.id]),
);
});
it('should charge only the first occurrence of a repeated new ID', async () => {
const clientId = 'intra-batch-duplicate-quota-client';
const firstOp = {
...createOp(clientId),
id: 'repeated-new-id',
entityId: 'task-first',
payload: { title: 'First content' },
};
const repeatedOp = {
...firstOp,
entityId: 'task-second',
payload: { title: 'Different repeated content' },
};
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
},
payload: { ops: [firstOp, repeatedOp], clientId },
});
expect(response.statusCode).toBe(200);
expect(mocks.syncService.checkStorageQuota).toHaveBeenCalledWith(
1,
computeOpStorageBytes(firstOp).bytes,
);
});
it('should not trigger cleanup for duplicate-only ops uploads near quota', async () => {
const clientId = 'duplicate-only-quota-client';
const duplicateOp = {
...createOp(clientId),
id: 'duplicate-only-op',
entityId: 'task-duplicate-only',
};
mocks.prisma.operation.findMany.mockResolvedValueOnce([
createStoredDuplicateOp(duplicateOp),
]);
mocks.syncService.checkStorageQuota.mockImplementation(
async (_userId: number, storageDeltaBytes: number) => ({
allowed: storageDeltaBytes === 0,
currentUsage: 100,
quota: 100,
}),
);
mocks.syncService.uploadOps.mockResolvedValueOnce([
{
opId: duplicateOp.id,
accepted: false,
error: 'Duplicate operation ID',
errorCode: SYNC_ERROR_CODES.DUPLICATE_OPERATION,
},
]);
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
},
payload: {
ops: [duplicateOp],
clientId,
},
});
expect(response.statusCode).toBe(200);
expect(mocks.syncService.checkStorageQuota).toHaveBeenCalledWith(1, 0);
expect(mocks.syncService.freeStorageForUpload).not.toHaveBeenCalled();
});
it('should reject oversized op batches before schema validation', async () => {
const clientId = 'too-many-ops-client';
const payload = {
ops: Array.from({ length: SUPER_SYNC_MAX_OPS_PER_UPLOAD + 1 }, (_, i) => ({
...createOp(clientId),
entityId: `task-${i}`,
timestamp: Date.now() + i,
})),
clientId,
};
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
},
payload: JSON.stringify(payload),
});
expect(response.statusCode).toBe(413);
expect(response.json()).toEqual(
expect.objectContaining({
errorCode: SYNC_ERROR_CODES.PAYLOAD_TOO_LARGE,
maxOpsPerBatch: SUPER_SYNC_MAX_OPS_PER_UPLOAD,
}),
);
expect(mocks.syncService.uploadOps).not.toHaveBeenCalled();
expect(mocks.syncService.checkStorageQuota).not.toHaveBeenCalled();
});
it('should fall back to UTF-8 JSON byte size for plain JSON without content-length', async () => {
const clientId = 'plain-json-client';
const payload = {
ops: [
createOp(clientId),
{
...createOp(clientId),
id: 'op-unicode',
entityId: 'task-unicode',
payload: { title: 'Übergrößenträger 🚀' },
},
],
clientId,
};
const jsonPayload = JSON.stringify(payload);
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
'content-length': 'not-a-number',
},
payload: jsonPayload,
});
expect(response.statusCode).toBe(200);
const quotaCall = mocks.syncService.checkStorageQuota.mock.calls[0];
expect(quotaCall[0]).toBe(1);
expect(typeof quotaCall[1]).toBe('number');
expect(quotaCall[1]).toBeGreaterThan(0);
// Multi-byte UTF-8 payload must measure larger in bytes than UTF-16 units
// to keep the quota gate accurate.
expect(Buffer.byteLength(jsonPayload, 'utf-8')).toBeGreaterThan(jsonPayload.length);
});
it('should accept base64 gzip ops upload', async () => {
const clientId = 'base64-gzip-client';
const payload = {
ops: [createOp(clientId)],
clientId,
};
const compressedPayload = await gzipAsync(
Buffer.from(JSON.stringify(payload), 'utf-8'),
);
const decompressedSize = Buffer.byteLength(JSON.stringify(payload), 'utf-8');
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
'content-encoding': 'gzip',
'content-transfer-encoding': 'base64',
},
payload: compressedPayload.toString('base64'),
});
expect(response.statusCode).toBe(200);
expect(response.json().results[0].accepted).toBe(true);
expect(mocks.syncService.uploadOps).toHaveBeenCalledOnce();
const quotaCall = mocks.syncService.checkStorageQuota.mock.calls[0];
expect(quotaCall[0]).toBe(1);
expect(typeof quotaCall[1]).toBe('number');
expect(quotaCall[1]).toBeGreaterThan(0);
expect(quotaCall[1]).toBeLessThan(decompressedSize);
expect(decompressedSize).not.toBe(
Buffer.byteLength(compressedPayload.toString('base64'), 'utf-8'),
);
});
it('should allow base64 gzip ops up to the binary compressed limit', async () => {
const clientId = 'base64-gzip-large-client';
const randomBlob = randomBytes(Math.floor(7.6 * MiB)).toString('base64');
const payload = {
ops: [
{
...createOp(clientId),
payload: { blob: randomBlob },
},
],
clientId,
};
const compressedPayload = await gzipAsync(
Buffer.from(JSON.stringify(payload), 'utf-8'),
);
const base64Payload = compressedPayload.toString('base64');
expect(compressedPayload.length).toBeLessThanOrEqual(10 * 1024 * 1024);
expect(Buffer.byteLength(base64Payload, 'utf-8')).toBeGreaterThan(10 * 1024 * 1024);
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
'content-encoding': 'gzip',
'content-transfer-encoding': 'base64',
},
payload: base64Payload,
});
expect(response.statusCode).toBe(200);
expect(mocks.syncService.uploadOps).toHaveBeenCalledOnce();
});
it('should keep plain JSON ops capped at the binary route limit', async () => {
const clientId = 'plain-json-large-client';
const jsonPayload = JSON.stringify({
ops: [
{
...createOp(clientId),
payload: { blob: 'x'.repeat(10 * MiB) },
},
],
clientId,
});
const payloadSize = Buffer.byteLength(jsonPayload, 'utf-8');
expect(payloadSize).toBeGreaterThan(10 * MiB);
expect(payloadSize).toBeLessThan(13 * MiB);
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
'content-length': String(payloadSize),
},
payload: jsonPayload,
});
expect(response.statusCode).toBe(413);
expect(mocks.syncService.uploadOps).not.toHaveBeenCalled();
});
it('should skip snapshot metadata for upload piggyback downloads', async () => {
const clientId = 'plain-json-client';
const payload = {
ops: [createOp(clientId)],
clientId,
lastKnownServerSeq: 0,
};
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
},
payload,
});
expect(response.statusCode).toBe(200);
expect(mocks.syncService.getOpsSinceWithSeq).toHaveBeenCalledWith(
1,
0,
clientId,
500,
false,
);
});
it('should skip snapshot metadata for deduplicated retry piggyback downloads', async () => {
const clientId = 'plain-json-client';
const cachedResults = [{ accepted: true, serverSeq: 1 }];
mocks.syncService.checkOpsRequestDedup.mockReturnValue(cachedResults);
mocks.syncService.getOpsSinceWithSeq.mockResolvedValue({
ops: [],
latestSeq: 4,
});
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
},
payload: {
ops: [createOp(clientId)],
clientId,
lastKnownServerSeq: 3,
requestId: 'retry-request',
},
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
results: cachedResults,
latestSeq: 4,
deduplicated: true,
});
expect(mocks.syncService.checkStorageQuota).not.toHaveBeenCalled();
expect(mocks.syncService.getOpsSinceWithSeq).toHaveBeenCalledWith(
1,
3,
clientId,
500,
false,
);
});
it('should charge compressed snapshot uploads by decompressed JSON size', async () => {
const clientId = 'base64-gzip-snapshot-client';
const payload = {
state: {
tasks: {
'task-1': { id: 'task-1', title: 'Snapshot Task' },
},
},
clientId,
reason: 'recovery',
vectorClock: { [clientId]: 1 },
schemaVersion: 1,
};
const jsonPayload = JSON.stringify(payload);
const compressedPayload = await gzipAsync(Buffer.from(jsonPayload, 'utf-8'));
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
'content-encoding': 'gzip',
'content-transfer-encoding': 'base64',
},
payload: compressedPayload.toString('base64'),
});
expect(response.statusCode).toBe(200);
expect(response.json().accepted).toBe(true);
expect(mocks.syncService.cacheSnapshotIfReplayable).toHaveBeenCalledWith(
1,
payload.state,
1,
false,
expect.anything(),
);
// Snapshot quota gate now accounts via estimated op + cache-delta bytes
// rather than the raw request body size. Stay value-agnostic; the
// compression-handling intent is still covered by the 200 + cacheSnapshot.
const quotaCall = mocks.syncService.checkStorageQuota.mock.calls[0];
expect(quotaCall[0]).toBe(1);
expect(typeof quotaCall[1]).toBe('number');
expect(quotaCall[1]).toBeGreaterThanOrEqual(0);
void jsonPayload;
});
it('should allow base64 gzip snapshots up to the binary compressed limit', async () => {
const clientId = 'base64-gzip-large-snapshot-client';
const randomBlob = randomBytes(Math.floor(22.6 * MiB)).toString('base64');
const payload = {
state: {
TASK: {
'task-1': { id: 'task-1', blob: randomBlob },
},
},
clientId,
reason: 'recovery',
vectorClock: { [clientId]: 1 },
schemaVersion: 1,
};
const compressedPayload = await gzipAsync(
Buffer.from(JSON.stringify(payload), 'utf-8'),
);
const base64Payload = compressedPayload.toString('base64');
expect(compressedPayload.length).toBeLessThanOrEqual(30 * MiB);
expect(Buffer.byteLength(base64Payload, 'utf-8')).toBeGreaterThan(30 * MiB);
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
'content-encoding': 'gzip',
'content-transfer-encoding': 'base64',
},
payload: base64Payload,
});
expect(response.statusCode).toBe(200);
expect(response.json().accepted).toBe(true);
expect(mocks.syncService.cacheSnapshotIfReplayable).toHaveBeenCalledWith(
1,
payload.state,
1,
false,
expect.anything(),
);
}, 15000);
it('should keep plain JSON snapshots capped at the binary route limit', async () => {
const clientId = 'plain-json-large-snapshot-client';
const jsonPayload = JSON.stringify({
state: { TASK: { 'task-1': { id: 'task-1', blob: 'x'.repeat(30 * MiB) } } },
clientId,
reason: 'recovery',
vectorClock: { [clientId]: 1 },
});
const payloadSize = Buffer.byteLength(jsonPayload, 'utf-8');
expect(payloadSize).toBeGreaterThan(30 * MiB);
expect(payloadSize).toBeLessThan(40 * MiB);
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
'content-length': String(payloadSize),
},
payload: jsonPayload,
});
expect(response.statusCode).toBe(413);
expect(mocks.syncService.uploadOps).not.toHaveBeenCalled();
});
it('should preserve the invalid gzip route response', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/sync/ops',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json',
'content-encoding': 'gzip',
},
payload: Buffer.from('not valid gzip data', 'utf-8'),
});
expect(response.statusCode).toBe(400);
expect(response.json().error).toBe('Failed to decompress gzip body');
expect(mocks.syncService.uploadOps).not.toHaveBeenCalled();
});
it('should reject clean-slate snapshot when replacement exceeds quota', async () => {
const clientId = 'clean-slate-quota-client';
mocks.syncService.prepareSnapshotCache.mockReturnValueOnce({
data: Buffer.from('cached-snapshot'),
bytes: 80,
stateBytes: 30,
cacheable: true,
});
mocks.syncService.getStorageInfo.mockResolvedValueOnce({
storageUsedBytes: 1000,
storageQuotaBytes: 100,
});
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: { 'task-1': { id: 'task-1' } } },
clientId,
reason: 'initial',
vectorClock: {},
opId: '018f2f0b-1c2d-7a1b-8c3d-123456789abc',
isCleanSlate: true,
},
});
expect(response.statusCode).toBe(413);
expect(response.json().errorCode).toBe('STORAGE_QUOTA_EXCEEDED');
expect(mocks.syncService.uploadOps).not.toHaveBeenCalled();
});
it('should return persistent success for a clean-slate retry before deleting data', async () => {
const opId = '018f2f0b-1c2d-7a1b-8c3d-123456789abc';
const clientId = 'clean-slate-retry-client';
const state = { TASK: { 'task-1': { id: 'task-1' } } };
const vectorClock = { [clientId]: 1 };
mocks.prisma.operation.findUnique.mockResolvedValueOnce({
id: opId,
userId: 1,
clientId,
actionType: '[SP_ALL] Load(import) all data',
opType: 'SYNC_IMPORT',
entityType: 'ALL',
entityId: null,
entityIds: [],
payload: state,
vectorClock,
schemaVersion: CURRENT_SCHEMA_VERSION,
clientTimestamp: BigInt(1),
receivedAt: BigInt(1),
isPayloadEncrypted: false,
syncImportReason: null,
serverSeq: 77,
});
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state,
clientId,
reason: 'recovery',
vectorClock,
schemaVersion: CURRENT_SCHEMA_VERSION,
opId,
isCleanSlate: true,
},
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ accepted: true, serverSeq: 77 });
expect(mocks.syncService.uploadOps).not.toHaveBeenCalled();
expect(mocks.syncService.checkStorageQuota).not.toHaveBeenCalled();
});
it('should reject a clean-slate retry whose opId belongs to different content', async () => {
const opId = '018f2f0b-1c2d-7a1b-8c3d-123456789abc';
const clientId = 'clean-slate-collision-client';
const vectorClock = { [clientId]: 1 };
mocks.prisma.operation.findUnique.mockResolvedValueOnce({
id: opId,
userId: 1,
clientId,
actionType: '[SP_ALL] Load(import) all data',
opType: 'SYNC_IMPORT',
entityType: 'ALL',
entityId: null,
entityIds: [],
payload: { TASK: { existing: { id: 'existing' } } },
vectorClock,
schemaVersion: CURRENT_SCHEMA_VERSION,
clientTimestamp: BigInt(1),
receivedAt: BigInt(1),
isPayloadEncrypted: false,
syncImportReason: null,
serverSeq: 77,
});
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: { replacement: { id: 'replacement' } } },
clientId,
reason: 'recovery',
vectorClock,
schemaVersion: CURRENT_SCHEMA_VERSION,
opId,
isCleanSlate: true,
},
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
accepted: false,
error: 'Operation ID already belongs to a different operation',
});
expect(mocks.syncService.uploadOps).not.toHaveBeenCalled();
expect(mocks.syncService.checkStorageQuota).not.toHaveBeenCalled();
});
it('should repeat initial snapshot duplicate detection inside the user lock', async () => {
const clientId = 'initial-race-client';
mocks.prisma.operation.findFirst
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({ id: 'existing-import', clientId: 'other-client' });
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: { 'task-1': { id: 'task-1' } } },
clientId,
reason: 'initial',
vectorClock: { [clientId]: 1 },
},
});
expect(response.statusCode).toBe(409);
expect(response.json()).toMatchObject({
errorCode: 'SYNC_IMPORT_EXISTS',
existingImportId: 'existing-import',
});
expect(mocks.prisma.operation.findFirst).toHaveBeenCalledTimes(2);
expect(mocks.syncService.checkStorageQuota).not.toHaveBeenCalled();
expect(mocks.syncService.uploadOps).not.toHaveBeenCalled();
});
it('should return cached snapshot upload response for retried requestId', async () => {
mocks.syncService.checkSnapshotRequestDedup.mockReturnValue({
accepted: true,
serverSeq: 42,
});
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: { 'task-1': { id: 'task-1' } } },
clientId: 'snapshot-retry-client',
reason: 'initial',
vectorClock: { 'snapshot-retry-client': 1 },
requestId: 'snapshot-v1-retry',
},
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ accepted: true, serverSeq: 42 });
expect(mocks.syncService.checkSnapshotRequestDedup).toHaveBeenCalledWith(
1,
'snapshot-v1-retry',
expect.any(Function),
);
expect(mocks.syncService.checkOpsRequestDedup).not.toHaveBeenCalled();
expect(mocks.prisma.operation.findFirst).not.toHaveBeenCalled();
expect(mocks.syncService.prepareSnapshotCache).not.toHaveBeenCalled();
expect(mocks.syncService.uploadOps).not.toHaveBeenCalled();
});
it('should convert a snapshot DUPLICATE_OPERATION rejection into an idempotent success when the op exists', async () => {
// Retry scenario: original snapshot was committed but its response was
// lost; the retry hits the duplicate-opId check inside uploadOps, and the
// route turns that into a success response carrying the original seq.
mocks.syncService.checkSnapshotRequestDedup.mockReturnValue(null);
mocks.syncService.prepareSnapshotCache.mockResolvedValue({
cacheable: true,
bytes: 0,
cleanSlate: false,
});
mocks.syncService.getCachedSnapshotBytes.mockResolvedValue(0);
mocks.syncService.getStorageInfo.mockResolvedValue({
currentUsage: 0,
quotaBytes: 100 * MiB,
});
mocks.syncService.runWithStorageUsageLock.mockImplementation(
async (_userId: number, fn: () => Promise<unknown>) => fn(),
);
mocks.syncService.uploadOps.mockResolvedValue([
{
opId: '018f2f0b-1c2d-7a1b-8c3d-123456789abc',
accepted: false,
error: 'Duplicate operation ID',
errorCode: 'DUPLICATE_OPERATION',
},
]);
// The first attempt actually persisted; the route looks it up via findFirst
// with a userId guard, so the mock must match the (id, userId) shape and
// return the original serverSeq for the conversion to succeed.
mocks.prisma.operation.findFirst.mockImplementation(
async ({ where }: { where: { id?: string; userId?: number } }) => {
if (where.id === '018f2f0b-1c2d-7a1b-8c3d-123456789abc' && where.userId === 1) {
return { serverSeq: 77 };
}
// SYNC_IMPORT_EXISTS pre-check (not exercised for reason='recovery',
// but kept defensively so an accidental call returns "no existing op").
return null;
},
);
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: {} },
clientId: 'dup-client',
reason: 'recovery',
vectorClock: { 'dup-client': 1 },
opId: '018f2f0b-1c2d-7a1b-8c3d-123456789abc',
requestId: 'snapshot-v1-dup-test',
},
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ accepted: true, serverSeq: 77 });
// The conversion should also re-arm the dedup cache so subsequent retries
// can short-circuit even more cheaply.
expect(mocks.syncService.cacheSnapshotRequestResult).toHaveBeenCalledWith(
1,
'snapshot-v1-dup-test',
{ accepted: true, serverSeq: 77 },
expect.any(String),
);
});
it('should return idempotent success for a retried SYNC_IMPORT whose opId matches the existing import', async () => {
mocks.syncService.checkSnapshotRequestDedup.mockReturnValue(null);
const retryOpId = '018f2f0b-1c2d-7a1b-8c3d-123456789abc';
// Existing SYNC_IMPORT for this user, same opId as the retry. The route
// looks the opId up directly via findUnique to keep the idempotency check
// deterministic when multiple full-state ops exist for the user.
mocks.prisma.operation.findUnique.mockResolvedValue({
id: retryOpId,
userId: 1,
clientId: 'dup-client',
serverSeq: 99,
opType: 'SYNC_IMPORT',
});
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: {} },
clientId: 'dup-client',
reason: 'initial',
vectorClock: { 'dup-client': 1 },
opId: retryOpId,
},
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ accepted: true, serverSeq: 99 });
// The pre-lock fast path should short-circuit before any work.
expect(mocks.syncService.prepareSnapshotCache).not.toHaveBeenCalled();
expect(mocks.syncService.uploadOps).not.toHaveBeenCalled();
});
it('should treat a retried SYNC_IMPORT idempotently even when other full-state ops exist for the user', async () => {
mocks.syncService.checkSnapshotRequestDedup.mockReturnValue(null);
const retryOpId = '018f2f0b-1c2d-7a1b-8c3d-fedcba987654';
// The opId-based lookup finds the exact retried op.
mocks.prisma.operation.findUnique.mockResolvedValue({
id: retryOpId,
userId: 1,
clientId: 'dup-client',
serverSeq: 42,
opType: 'SYNC_IMPORT',
});
// A later BACKUP_IMPORT also exists — without exact-match lookup,
// findFirst could return this instead and the idempotency check would
// incorrectly fail.
mocks.prisma.operation.findFirst.mockResolvedValue({
id: '018f2f0b-9999-7a1b-8c3d-aaaaaaaaaaaa',
clientId: 'other-client',
serverSeq: 142,
});
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: {} },
clientId: 'dup-client',
reason: 'initial',
vectorClock: { 'dup-client': 1 },
opId: retryOpId,
},
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ accepted: true, serverSeq: 42 });
expect(mocks.prisma.operation.findUnique).toHaveBeenCalledWith(
expect.objectContaining({ where: { id: retryOpId } }),
);
expect(mocks.syncService.uploadOps).not.toHaveBeenCalled();
});
it('should reject a SYNC_IMPORT whose opId belongs to a different user', async () => {
mocks.syncService.checkSnapshotRequestDedup.mockReturnValue(null);
const retryOpId = '018f2f0b-1c2d-7a1b-8c3d-cccccccccccc';
// Same opId exists but for a different user — must not be treated as an
// idempotent retry; the userId guard in findExistingSyncImport prevents
// cross-tenant leakage. Fall through to the (most-recent) full-state op
// for *this* user.
mocks.prisma.operation.findUnique.mockResolvedValue({
id: retryOpId,
userId: 999,
clientId: 'other-user-client',
serverSeq: 7,
opType: 'SYNC_IMPORT',
});
mocks.prisma.operation.findFirst.mockResolvedValue({
id: '018f2f0b-8888-7a1b-8c3d-bbbbbbbbbbbb',
clientId: 'this-user-client',
serverSeq: 17,
});
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: {} },
clientId: 'dup-client',
reason: 'initial',
vectorClock: { 'dup-client': 1 },
opId: retryOpId,
},
});
expect(response.statusCode).toBe(409);
expect(response.json()).toMatchObject({
errorCode: 'SYNC_IMPORT_EXISTS',
existingImportId: '018f2f0b-8888-7a1b-8c3d-bbbbbbbbbbbb',
});
});
it('should reconcile the counter before rejecting on the cheap snapshot pre-gate', async () => {
// Regression for W10: when the cached counter says we are at quota, the
// route reconciles once before rejecting — a stale-high counter would
// otherwise lock out users whose new snapshot would actually shrink
// their storage.
const clientId = 'pre-gate-reconcile-client';
const vectorClock = { [clientId]: 1 };
// 1st getStorageInfo returns stale-high (over quota). After reconcile,
// the 2nd call returns the corrected (under quota) value.
mocks.syncService.getStorageInfo
.mockResolvedValueOnce({
storageUsedBytes: 100 * 1024 * 1024,
storageQuotaBytes: 100 * 1024 * 1024,
})
.mockResolvedValue({
storageUsedBytes: 50_000,
storageQuotaBytes: 100 * 1024 * 1024,
});
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: { 'task-1': { id: 'task-1' } } },
clientId,
reason: 'recovery',
vectorClock,
},
});
expect(mocks.syncService.updateStorageUsage).toHaveBeenCalledWith(1);
expect(response.statusCode).toBe(200);
});
it('should still 413 on the snapshot pre-gate when reconcile confirms over-quota', async () => {
// Same path as above, but reconcile does not move the counter. The
// rejection now uses errorCode (not code) and routes through the unified
// 413 helper.
const clientId = 'pre-gate-no-reconcile-client';
mocks.syncService.getStorageInfo.mockResolvedValue({
storageUsedBytes: 100 * 1024 * 1024,
storageQuotaBytes: 100 * 1024 * 1024,
});
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: { 'task-1': { id: 'task-1' } } },
clientId,
reason: 'recovery',
vectorClock: { [clientId]: 1 },
},
});
expect(response.statusCode).toBe(413);
const body = response.json();
expect(body.errorCode).toBe('STORAGE_QUOTA_EXCEEDED');
// No legacy `code:` key — clients dispatch on errorCode.
expect(body.code).toBeUndefined();
expect(mocks.syncService.uploadOps).not.toHaveBeenCalled();
});
it('should fall through gracefully when the pre-gate reconcile throws', async () => {
// If the reconcile fails (DB hiccup), the route logs and uses the stale
// cached read. Either accept (cached < quota) or reject with 413 — but
// never bubble a 500.
const clientId = 'pre-gate-reconcile-throws';
mocks.syncService.getStorageInfo.mockResolvedValue({
storageUsedBytes: 100 * 1024 * 1024,
storageQuotaBytes: 100 * 1024 * 1024,
});
mocks.syncService.updateStorageUsage.mockRejectedValueOnce(new Error('db down'));
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: { 'task-1': { id: 'task-1' } } },
clientId,
reason: 'recovery',
vectorClock: { [clientId]: 1 },
},
});
expect(response.statusCode).toBe(413);
expect(response.json().errorCode).toBe('STORAGE_QUOTA_EXCEEDED');
});
it('should mark the user for forced reconcile when post-commit counter delta fails', async () => {
// Regression for W6: when applyStorageUsageDelta (called after a
// successful snapshot upload) fails, the user must be marked so the
// next quota check self-heals instead of waiting for daily cleanup.
const clientId = 'post-commit-counter-failure';
const vectorClock = { [clientId]: 1 };
mocks.syncService.prepareSnapshotCache.mockReturnValueOnce({
data: Buffer.from('cached-snapshot'),
bytes: 40,
stateBytes: 25,
cacheable: true,
});
mocks.syncService.uploadOps.mockResolvedValueOnce([{ accepted: true, serverSeq: 7 }]);
mocks.syncService.cacheSnapshotIfReplayable.mockResolvedValueOnce({
cached: true,
bytesWritten: 40,
previousBytes: 10,
deltaBytes: 30,
});
mocks.syncService.incrementStorageUsage.mockRejectedValueOnce(
new Error('counter down'),
);
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: { 'task-1': { id: 'task-1' } } },
clientId,
reason: 'recovery',
vectorClock,
},
});
// The data write is committed; the response must still succeed even
// though the counter is stale.
expect(response.statusCode).toBe(200);
expect(mocks.syncService.markStorageNeedsReconcile).toHaveBeenCalledWith(1);
});
it('should pre-gate the snapshot upload by op + cache-delta bytes', async () => {
// B3-route: the gate budget covers BOTH the op row (payload+vc) and the
// cache rewrite, so the user cannot squeeze through a snapshot whose
// op-row alone fits but whose cache delta would breach quota. The
// post-commit increment, however, only writes the cache portion — the
// op-row counter is now incremented inside `uploadOps`'s `$transaction`.
const clientId = 'snapshot-delta-client';
const vectorClock = { [clientId]: 1 };
const preparedSnapshot = {
data: Buffer.from('cached-snapshot'),
bytes: 40,
stateBytes: 25,
cacheable: true,
};
mocks.syncService.prepareSnapshotCache.mockReturnValueOnce(preparedSnapshot);
mocks.syncService.getCachedSnapshotBytes.mockResolvedValueOnce(10);
mocks.syncService.uploadOps.mockResolvedValueOnce([{ accepted: true, serverSeq: 7 }]);
mocks.syncService.cacheSnapshotIfReplayable.mockResolvedValueOnce({
cached: true,
bytesWritten: 40,
previousBytes: 10,
deltaBytes: 30,
});
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: { 'task-1': { id: 'task-1' } } },
clientId,
reason: 'recovery',
vectorClock,
},
});
const vectorClockBytes = Buffer.byteLength(JSON.stringify(vectorClock), 'utf8');
// Gate budget = op-row bytes + cache-delta bytes (40 - 10 = 30).
const expectedGate = preparedSnapshot.stateBytes + vectorClockBytes + 30;
expect(response.statusCode).toBe(200);
expect(mocks.syncService.checkStorageQuota).toHaveBeenCalledWith(1, expectedGate);
expect(mocks.syncService.cacheSnapshotIfReplayable).toHaveBeenCalledWith(
1,
{ TASK: { 'task-1': { id: 'task-1' } } },
7,
false,
preparedSnapshot,
);
// Post-commit increment only carries the snapshot-cache portion; the
// op-row counter is written atomically inside `uploadOps`'s transaction.
expect(mocks.syncService.incrementStorageUsage).toHaveBeenCalledWith(1, 30);
});
it('should subtract the old snapshot cache when a replacement is too large to cache', async () => {
const clientId = 'oversized-cache-replacement-client';
const vectorClock = { [clientId]: 1 };
const preparedSnapshot = {
data: Buffer.from('too-large-to-cache'),
bytes: 51 * 1024 * 1024,
stateBytes: 25,
cacheable: false,
};
mocks.syncService.prepareSnapshotCache.mockReturnValueOnce(preparedSnapshot);
mocks.syncService.getCachedSnapshotBytes.mockResolvedValueOnce(90);
mocks.syncService.uploadOps.mockResolvedValueOnce([{ accepted: true, serverSeq: 7 }]);
mocks.syncService.cacheSnapshotIfReplayable.mockResolvedValueOnce({
cached: false,
bytesWritten: 0,
previousBytes: 90,
deltaBytes: -90,
});
const response = await app.inject({
method: 'POST',
url: '/api/sync/snapshot',
headers: { authorization: `Bearer ${authToken}` },
payload: {
state: { TASK: { 'task-1': { id: 'task-1' } } },
clientId,
reason: 'recovery',
vectorClock,
},
});
const vectorClockBytes = Buffer.byteLength(JSON.stringify(vectorClock), 'utf8');
const expectedGateDelta = preparedSnapshot.stateBytes + vectorClockBytes - 90;
expect(response.statusCode).toBe(200);
expect(mocks.syncService.checkStorageQuota).toHaveBeenCalledWith(
1,
expectedGateDelta,
);
// Post-commit, the route only applies the snapshot-cache portion of the
// delta — the op-row portion is written atomically inside uploadOps's
// $transaction (wave-1 B3 / commit 9af17e460e). cacheSnapshotIfReplayable
// reported deltaBytes = -90 (the cleared cache), so the route's
// applyStorageUsageDelta path decrements by 90.
expect(mocks.syncService.decrementStorageUsage).toHaveBeenCalledWith(1, 90);
expect(mocks.syncService.incrementStorageUsage).not.toHaveBeenCalled();
});
});