morphit/apps/indexer/test/handlers/chatRead.test.ts

216 lines
5.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import handler from '$indexer/handlers/chatRead';
import { makeCtx } from '../testutils/context';
import { makeMockClient } from '../testutils/mockClient';
// Default block time in makeCtx is 2026-04-19T12:00:00Z.
// Pick last_read_at values relative to that.
describe('chatRead handler', () => {
it('rejects non-object payload', async () => {
const mock = makeMockClient();
const r = await handler(makeCtx({ payload: 'not an object' as unknown }), mock.client);
expect(r).toEqual({ ok: false, reason: 'payload_not_object' });
});
it('rejects missing peer', async () => {
const mock = makeMockClient();
const r = await handler(
makeCtx({
payload: { last_read_at: '2026-04-19T11:00:00Z' }
}),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'peer_invalid' });
});
it('rejects malformed peer account name', async () => {
const mock = makeMockClient();
const r = await handler(
makeCtx({
payload: {
peer: 'NOT_LOWERCASE',
last_read_at: '2026-04-19T11:00:00Z'
}
}),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'peer_invalid' });
});
it('rejects self-chat', async () => {
const mock = makeMockClient();
const r = await handler(
makeCtx({
signer: 'alice',
payload: {
peer: 'alice',
last_read_at: '2026-04-19T11:00:00Z'
}
}),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'self_chat' });
});
it('rejects non-string last_read_at', async () => {
const mock = makeMockClient();
const r = await handler(
makeCtx({
payload: { peer: 'bob', last_read_at: 12345 }
}),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'last_read_at_not_string' });
});
// ─── §F.21 O3.5 — strict ISO-8601 shape ──────────────────────
it('O3.5: rejects US-style "12/31/2025" format', async () => {
const mock = makeMockClient();
const r = await handler(
makeCtx({
payload: { peer: 'bob', last_read_at: '12/31/2025' }
}),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'last_read_at_invalid' });
});
it('O3.5: rejects "December 15, 2025" format', async () => {
const mock = makeMockClient();
const r = await handler(
makeCtx({
payload: {
peer: 'bob',
last_read_at: 'December 15, 2025'
}
}),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'last_read_at_invalid' });
});
it('O3.5: rejects date-only "2026-04-15" without T-time', async () => {
const mock = makeMockClient();
const r = await handler(
makeCtx({
payload: { peer: 'bob', last_read_at: '2026-04-15' }
}),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'last_read_at_invalid' });
});
it('O3.5: rejects ISO without timezone marker', async () => {
// Strict regex requires Z or ±HH:MM; bare "T..:..:.." with
// no zone indicator gets rejected.
const mock = makeMockClient();
const r = await handler(
makeCtx({
payload: {
peer: 'bob',
last_read_at: '2026-04-19T11:00:00'
}
}),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'last_read_at_invalid' });
});
it('O3.5: accepts strict ISO with Z suffix', async () => {
const mock = makeMockClient([{ match: 'INSERT INTO chat_read_state', rowCount: 1 }]);
const r = await handler(
makeCtx({
signer: 'alice',
payload: {
peer: 'bob',
last_read_at: '2026-04-19T11:00:00Z'
}
}),
mock.client
);
expect(r).toEqual({ ok: true });
});
it('O3.5: accepts strict ISO with millisecond precision', async () => {
const mock = makeMockClient([{ match: 'INSERT INTO chat_read_state', rowCount: 1 }]);
const r = await handler(
makeCtx({
signer: 'alice',
payload: {
peer: 'bob',
last_read_at: '2026-04-19T11:00:00.123Z'
}
}),
mock.client
);
expect(r).toEqual({ ok: true });
});
it('O3.5: accepts strict ISO with offset timezone', async () => {
const mock = makeMockClient([{ match: 'INSERT INTO chat_read_state', rowCount: 1 }]);
const r = await handler(
makeCtx({
signer: 'alice',
payload: {
peer: 'bob',
last_read_at: '2026-04-19T13:00:00+02:00'
}
}),
mock.client
);
expect(r).toEqual({ ok: true });
});
// ─── Time-bound checks ───────────────────────────────────────
it('rejects last_read_at far in the future', async () => {
// Block time is 2026-04-19T12:00:00Z; 70s future is past the
// 60s skew budget.
const mock = makeMockClient();
const r = await handler(
makeCtx({
payload: {
peer: 'bob',
last_read_at: '2026-04-19T12:01:10Z'
}
}),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'last_read_at_in_future' });
});
it('rejects last_read_at before 2020', async () => {
const mock = makeMockClient();
const r = await handler(
makeCtx({
payload: {
peer: 'bob',
last_read_at: '2019-12-31T23:59:59Z'
}
}),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'last_read_at_too_old' });
});
it('happy path: upserts the read-state row', async () => {
const mock = makeMockClient([{ match: 'INSERT INTO chat_read_state', rowCount: 1 }]);
const r = await handler(
makeCtx({
signer: 'alice',
payload: {
peer: 'bob',
last_read_at: '2026-04-19T11:30:00Z'
}
}),
mock.client
);
expect(r).toEqual({ ok: true });
expect(mock.queries).toHaveLength(1);
expect(mock.queries[0]!.params[0]).toBe('alice');
expect(mock.queries[0]!.params[1]).toBe('bob');
});
});