rwadurian/backend/services/mpc-service/tests/unit/domain/party-share.entity.spec.ts

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);
});
});
});