feat: Security
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect
|
||||
} from 'vitest';
|
||||
import type { Message } from '../../../../shared-kernel';
|
||||
import {
|
||||
buildMessageRevision,
|
||||
materializeMessageFromRevision,
|
||||
revisionBeatsMessage
|
||||
} from './message-revision.builder.rules';
|
||||
|
||||
function createMessage(overrides: Partial<Message> = {}): Message {
|
||||
return {
|
||||
id: 'message-1',
|
||||
roomId: 'room-1',
|
||||
senderId: 'user-1',
|
||||
senderName: 'User 1',
|
||||
content: 'hello',
|
||||
timestamp: 1_000,
|
||||
reactions: [],
|
||||
isDeleted: false,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe('message-revision.builder.rules', () => {
|
||||
it('builds create revisions at revision zero', async () => {
|
||||
const message = createMessage();
|
||||
const revision = await buildMessageRevision({
|
||||
message,
|
||||
type: 'create',
|
||||
actorId: 'user-1',
|
||||
editedAt: 1_000
|
||||
});
|
||||
|
||||
expect(revision.revision).toBe(0);
|
||||
expect(revision.headHash).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(revision.type).toBe('create');
|
||||
});
|
||||
|
||||
it('increments revision for author edits', async () => {
|
||||
const message = createMessage({ revision: 0, headHash: 'abc' });
|
||||
const revision = await buildMessageRevision({
|
||||
message,
|
||||
type: 'author-edit',
|
||||
actorId: 'user-1',
|
||||
content: 'edited',
|
||||
editedAt: 2_000,
|
||||
prevRevisionHash: 'abc'
|
||||
});
|
||||
|
||||
expect(revision.revision).toBe(1);
|
||||
expect(revision.prevRevisionHash).toBe('abc');
|
||||
expect(revision.content).toBe('edited');
|
||||
});
|
||||
|
||||
it('materializes message state from a revision', async () => {
|
||||
const revision = await buildMessageRevision({
|
||||
message: createMessage(),
|
||||
type: 'author-edit',
|
||||
actorId: 'user-1',
|
||||
content: 'edited',
|
||||
editedAt: 2_000
|
||||
});
|
||||
const materialized = materializeMessageFromRevision(createMessage(), revision);
|
||||
|
||||
expect(materialized.revision).toBe(1);
|
||||
expect(materialized.content).toBe('edited');
|
||||
expect(materialized.headHash).toBe(revision.headHash);
|
||||
});
|
||||
|
||||
it('detects when a revision should replace local state', async () => {
|
||||
const local = materializeMessageFromRevision(
|
||||
createMessage({ revision: 0, headHash: 'old' }),
|
||||
await buildMessageRevision({
|
||||
message: createMessage(),
|
||||
type: 'create',
|
||||
actorId: 'user-1',
|
||||
editedAt: 1_000
|
||||
})
|
||||
);
|
||||
const incoming = await buildMessageRevision({
|
||||
message: createMessage({ revision: 0, headHash: local.headHash }),
|
||||
type: 'author-edit',
|
||||
actorId: 'user-1',
|
||||
content: 'edited',
|
||||
editedAt: 2_000,
|
||||
prevRevisionHash: local.headHash ?? ''
|
||||
});
|
||||
|
||||
expect(revisionBeatsMessage(incoming, local)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user