216 lines
7.2 KiB
TypeScript
216 lines
7.2 KiB
TypeScript
/**
|
|
* PartyShare Entity Unit Tests
|
|
*/
|
|
|
|
import { PartyShare } from '../../../src/domain/entities/party-share.entity';
|
|
import {
|
|
ShareId,
|
|
PartyId,
|
|
SessionId,
|
|
ShareData,
|
|
PublicKey,
|
|
Threshold,
|
|
} from '../../../src/domain/value-objects';
|
|
import { PartyShareType, PartyShareStatus } from '../../../src/domain/enums';
|
|
import { ShareCreatedEvent, ShareRotatedEvent, ShareRevokedEvent } from '../../../src/domain/events';
|
|
|
|
describe('PartyShare Entity', () => {
|
|
// Test fixtures
|
|
const createTestShareData = () => ShareData.create(
|
|
Buffer.from('encrypted-share-data-here'),
|
|
Buffer.from('123456789012'), // 12 bytes IV
|
|
Buffer.from('1234567890123456'), // 16 bytes authTag
|
|
);
|
|
|
|
const createTestPublicKey = () => PublicKey.fromHex(
|
|
'03' + '0'.repeat(64), // Compressed public key format
|
|
);
|
|
|
|
const createTestThreshold = () => Threshold.create(3, 2);
|
|
|
|
const createTestPartyShare = () => PartyShare.create({
|
|
partyId: PartyId.create('user123-server'),
|
|
sessionId: SessionId.create('550e8400-e29b-41d4-a716-446655440000'),
|
|
shareType: PartyShareType.WALLET,
|
|
shareData: createTestShareData(),
|
|
publicKey: createTestPublicKey(),
|
|
threshold: createTestThreshold(),
|
|
});
|
|
|
|
describe('create', () => {
|
|
it('should create a valid party share', () => {
|
|
const share = createTestPartyShare();
|
|
|
|
expect(share.id).toBeDefined();
|
|
expect(share.id.value).toMatch(/^share_\d+_[a-z0-9]+$/);
|
|
expect(share.partyId.value).toBe('user123-server');
|
|
expect(share.sessionId.value).toBe('550e8400-e29b-41d4-a716-446655440000');
|
|
expect(share.shareType).toBe(PartyShareType.WALLET);
|
|
expect(share.status).toBe(PartyShareStatus.ACTIVE);
|
|
expect(share.threshold.toString()).toBe('2-of-3');
|
|
});
|
|
|
|
it('should emit ShareCreatedEvent', () => {
|
|
const share = createTestPartyShare();
|
|
const events = share.domainEvents;
|
|
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0]).toBeInstanceOf(ShareCreatedEvent);
|
|
|
|
const event = events[0] as ShareCreatedEvent;
|
|
expect(event.shareId).toBe(share.id.value);
|
|
expect(event.partyId).toBe('user123-server');
|
|
expect(event.shareType).toBe(PartyShareType.WALLET);
|
|
});
|
|
});
|
|
|
|
describe('reconstruct', () => {
|
|
it('should reconstruct share without emitting events', () => {
|
|
const share = PartyShare.reconstruct({
|
|
id: ShareId.create('share_1699887766123_abc123xyz'),
|
|
partyId: PartyId.create('user123-server'),
|
|
sessionId: SessionId.create('550e8400-e29b-41d4-a716-446655440000'),
|
|
shareType: PartyShareType.WALLET,
|
|
shareData: createTestShareData(),
|
|
publicKey: createTestPublicKey(),
|
|
threshold: createTestThreshold(),
|
|
status: PartyShareStatus.ACTIVE,
|
|
createdAt: new Date('2024-01-01'),
|
|
updatedAt: new Date('2024-01-01'),
|
|
});
|
|
|
|
expect(share.id.value).toBe('share_1699887766123_abc123xyz');
|
|
expect(share.domainEvents).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('markAsUsed', () => {
|
|
it('should update lastUsedAt timestamp', () => {
|
|
const share = createTestPartyShare();
|
|
share.clearDomainEvents();
|
|
|
|
expect(share.lastUsedAt).toBeUndefined();
|
|
|
|
share.markAsUsed();
|
|
|
|
expect(share.lastUsedAt).toBeDefined();
|
|
expect(share.lastUsedAt).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it('should throw error when share is not active', () => {
|
|
const share = createTestPartyShare();
|
|
share.clearDomainEvents();
|
|
share.revoke('test reason');
|
|
|
|
expect(() => share.markAsUsed()).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('rotate', () => {
|
|
it('should create new share with rotated status for old share', () => {
|
|
const share = createTestPartyShare();
|
|
share.clearDomainEvents();
|
|
|
|
const newShareData = ShareData.create(
|
|
Buffer.from('new-encrypted-share-data'),
|
|
Buffer.from('123456789012'),
|
|
Buffer.from('1234567890123456'),
|
|
);
|
|
const newSessionId = SessionId.create('660e8400-e29b-41d4-a716-446655440001');
|
|
|
|
const newShare = share.rotate(newShareData, newSessionId);
|
|
|
|
// Old share should be marked as rotated
|
|
expect(share.status).toBe(PartyShareStatus.ROTATED);
|
|
|
|
// New share should be active
|
|
expect(newShare.status).toBe(PartyShareStatus.ACTIVE);
|
|
expect(newShare.id.value).not.toBe(share.id.value);
|
|
expect(newShare.publicKey.equals(share.publicKey)).toBe(true);
|
|
|
|
// Event should be emitted on new share
|
|
const events = newShare.domainEvents;
|
|
expect(events.some(e => e instanceof ShareRotatedEvent)).toBe(true);
|
|
});
|
|
|
|
it('should throw error when share is not active', () => {
|
|
const share = createTestPartyShare();
|
|
share.revoke('test reason');
|
|
|
|
const newShareData = createTestShareData();
|
|
const newSessionId = SessionId.create('660e8400-e29b-41d4-a716-446655440001');
|
|
|
|
expect(() => share.rotate(newShareData, newSessionId)).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('revoke', () => {
|
|
it('should mark share as revoked', () => {
|
|
const share = createTestPartyShare();
|
|
share.clearDomainEvents();
|
|
|
|
share.revoke('Security concern');
|
|
|
|
expect(share.status).toBe(PartyShareStatus.REVOKED);
|
|
|
|
const events = share.domainEvents;
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0]).toBeInstanceOf(ShareRevokedEvent);
|
|
});
|
|
|
|
it('should throw error when share is already revoked', () => {
|
|
const share = createTestPartyShare();
|
|
share.revoke('First revocation');
|
|
|
|
expect(() => share.revoke('Second revocation')).toThrow('Share is already revoked');
|
|
});
|
|
});
|
|
|
|
describe('validateThreshold', () => {
|
|
it('should validate threshold correctly', () => {
|
|
const share = createTestPartyShare();
|
|
|
|
// 2-of-3 threshold
|
|
expect(share.validateThreshold(2)).toBe(true);
|
|
expect(share.validateThreshold(3)).toBe(true);
|
|
expect(share.validateThreshold(1)).toBe(false); // Less than t
|
|
expect(share.validateThreshold(4)).toBe(false); // More than n
|
|
});
|
|
});
|
|
|
|
describe('isActive', () => {
|
|
it('should return true for active share', () => {
|
|
const share = createTestPartyShare();
|
|
expect(share.isActive()).toBe(true);
|
|
});
|
|
|
|
it('should return false for revoked share', () => {
|
|
const share = createTestPartyShare();
|
|
share.revoke('test');
|
|
expect(share.isActive()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('belongsToParty', () => {
|
|
it('should return true for matching party', () => {
|
|
const share = createTestPartyShare();
|
|
expect(share.belongsToParty(PartyId.create('user123-server'))).toBe(true);
|
|
});
|
|
|
|
it('should return false for non-matching party', () => {
|
|
const share = createTestPartyShare();
|
|
expect(share.belongsToParty(PartyId.create('other-server'))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('clearDomainEvents', () => {
|
|
it('should clear all domain events', () => {
|
|
const share = createTestPartyShare();
|
|
expect(share.domainEvents.length).toBeGreaterThan(0);
|
|
|
|
share.clearDomainEvents();
|
|
expect(share.domainEvents).toHaveLength(0);
|
|
});
|
|
});
|
|
});
|