Implement DDD refactoring phases 1-4 with domain layer and ubiquitous language
Phase 1-3: Domain Layer Foundation - Add value objects: IdentityId, PermissionId, RelayId, WalletId, Nickname, NostrKeyPair - Add rich domain entities: Identity, Permission, Relay with behavior - Add domain events: IdentityCreated, IdentityRenamed, IdentitySelected, etc. - Add repository interfaces for Identity, Permission, Relay - Add infrastructure layer with repository implementations - Add EncryptionService abstraction Phase 4: Ubiquitous Language Cleanup - Rename BrowserSyncData → EncryptedVault (encrypted vault storage) - Rename BrowserSessionData → VaultSession (decrypted session state) - Rename SignerMetaData → ExtensionSettings (extension configuration) - Rename Identity_ENCRYPTED → StoredIdentity (storage DTO) - Rename Identity_DECRYPTED → IdentityData (session DTO) - Similar renames for Permission, Relay, NwcConnection, CashuMint - Add backwards compatibility aliases with @deprecated markers Test Coverage - Add comprehensive tests for all value objects - Add tests for domain entities and their behavior - Add tests for domain events - Fix PermissionChecker to prioritize kind-specific rules over blanket rules - Fix pre-existing component test issues (IconButton, Pubkey) All 113 tests pass. Both Chrome and Firefox builds succeed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
200
projects/common/src/lib/domain/entities/identity.spec.ts
Normal file
200
projects/common/src/lib/domain/entities/identity.spec.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { Identity, UnsignedEvent, SignedEvent, SigningFunction } from './identity';
|
||||
import { IdentityCreated, IdentityRenamed, IdentitySigned } from '../events';
|
||||
|
||||
describe('Identity Entity', () => {
|
||||
const TEST_PRIVATE_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
|
||||
describe('create', () => {
|
||||
it('should create identity with generated keypair when no private key provided', () => {
|
||||
const identity = Identity.create('Alice');
|
||||
|
||||
expect(identity.nickname).toEqual('Alice');
|
||||
expect(identity.publicKey).toBeTruthy();
|
||||
expect(identity.publicKey.length).toBe(64);
|
||||
});
|
||||
|
||||
it('should create identity with provided private key', () => {
|
||||
const identity = Identity.create('Bob', TEST_PRIVATE_KEY);
|
||||
|
||||
expect(identity.nickname).toEqual('Bob');
|
||||
expect(identity.publicKey).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should raise IdentityCreated event', () => {
|
||||
const identity = Identity.create('Charlie');
|
||||
const events = identity.pullDomainEvents();
|
||||
|
||||
expect(events.length).toBe(1);
|
||||
expect(events[0]).toBeInstanceOf(IdentityCreated);
|
||||
|
||||
const createdEvent = events[0] as IdentityCreated;
|
||||
expect(createdEvent.identityId).toEqual(identity.id.toString());
|
||||
expect(createdEvent.publicKey).toEqual(identity.publicKey);
|
||||
expect(createdEvent.nickname).toEqual('Charlie');
|
||||
});
|
||||
|
||||
it('should set createdAt timestamp', () => {
|
||||
const before = new Date();
|
||||
const identity = Identity.create('Dana');
|
||||
const after = new Date();
|
||||
|
||||
expect(identity.createdAt.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
||||
expect(identity.createdAt.getTime()).toBeLessThanOrEqual(after.getTime());
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromSnapshot', () => {
|
||||
it('should reconstruct identity from snapshot', () => {
|
||||
const original = Identity.create('Eve', TEST_PRIVATE_KEY);
|
||||
original.pullDomainEvents(); // Clear creation event
|
||||
|
||||
const snapshot = original.toSnapshot();
|
||||
const restored = Identity.fromSnapshot(snapshot);
|
||||
|
||||
expect(restored.id.toString()).toEqual(original.id.toString());
|
||||
expect(restored.nickname).toEqual('Eve');
|
||||
expect(restored.publicKey).toEqual(original.publicKey);
|
||||
});
|
||||
|
||||
it('should not raise events when loading from snapshot', () => {
|
||||
const original = Identity.create('Frank');
|
||||
const snapshot = original.toSnapshot();
|
||||
|
||||
const restored = Identity.fromSnapshot(snapshot);
|
||||
const events = restored.pullDomainEvents();
|
||||
|
||||
expect(events.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rename', () => {
|
||||
it('should update nickname', () => {
|
||||
const identity = Identity.create('OldName');
|
||||
identity.pullDomainEvents(); // Clear creation event
|
||||
|
||||
identity.rename('NewName');
|
||||
|
||||
expect(identity.nickname).toEqual('NewName');
|
||||
});
|
||||
|
||||
it('should raise IdentityRenamed event', () => {
|
||||
const identity = Identity.create('OldName');
|
||||
identity.pullDomainEvents(); // Clear creation event
|
||||
|
||||
identity.rename('NewName');
|
||||
const events = identity.pullDomainEvents();
|
||||
|
||||
expect(events.length).toBe(1);
|
||||
expect(events[0]).toBeInstanceOf(IdentityRenamed);
|
||||
|
||||
const renamedEvent = events[0] as IdentityRenamed;
|
||||
expect(renamedEvent.identityId).toEqual(identity.id.toString());
|
||||
expect(renamedEvent.oldNickname).toEqual('OldName');
|
||||
expect(renamedEvent.newNickname).toEqual('NewName');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sign', () => {
|
||||
it('should call signing function with event and return signed event', () => {
|
||||
const identity = Identity.create('Signer', TEST_PRIVATE_KEY);
|
||||
identity.pullDomainEvents();
|
||||
|
||||
const unsignedEvent: UnsignedEvent = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: 'Hello, Nostr!',
|
||||
};
|
||||
|
||||
const mockSignFn: SigningFunction = (event, privateKeyBytes) => {
|
||||
expect(privateKeyBytes).toBeInstanceOf(Uint8Array);
|
||||
expect(privateKeyBytes.length).toBe(32);
|
||||
|
||||
return {
|
||||
...event,
|
||||
id: 'mock-event-id',
|
||||
pubkey: identity.publicKey,
|
||||
sig: 'mock-signature',
|
||||
} as SignedEvent;
|
||||
};
|
||||
|
||||
const signedEvent = identity.sign(unsignedEvent, mockSignFn);
|
||||
|
||||
expect(signedEvent.id).toEqual('mock-event-id');
|
||||
expect(signedEvent.pubkey).toEqual(identity.publicKey);
|
||||
expect(signedEvent.sig).toEqual('mock-signature');
|
||||
});
|
||||
|
||||
it('should raise IdentitySigned event', () => {
|
||||
const identity = Identity.create('Signer', TEST_PRIVATE_KEY);
|
||||
identity.pullDomainEvents();
|
||||
|
||||
const unsignedEvent: UnsignedEvent = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: 'Test',
|
||||
};
|
||||
|
||||
const mockSignFn: SigningFunction = (event) => ({
|
||||
...event,
|
||||
id: 'signed-event-id',
|
||||
pubkey: identity.publicKey,
|
||||
sig: 'sig',
|
||||
} as SignedEvent);
|
||||
|
||||
identity.sign(unsignedEvent, mockSignFn);
|
||||
const events = identity.pullDomainEvents();
|
||||
|
||||
expect(events.length).toBe(1);
|
||||
expect(events[0]).toBeInstanceOf(IdentitySigned);
|
||||
|
||||
const signedEvt = events[0] as IdentitySigned;
|
||||
expect(signedEvt.identityId).toEqual(identity.id.toString());
|
||||
expect(signedEvt.eventKind).toBe(1);
|
||||
expect(signedEvt.signedEventId).toEqual('signed-event-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toSnapshot', () => {
|
||||
it('should create complete snapshot for storage', () => {
|
||||
const identity = Identity.create('Snapshot Test', TEST_PRIVATE_KEY);
|
||||
const snapshot = identity.toSnapshot();
|
||||
|
||||
expect(snapshot.id).toEqual(identity.id.toString());
|
||||
expect(snapshot.nick).toEqual('Snapshot Test');
|
||||
expect(snapshot.privkey).toBeTruthy();
|
||||
expect(snapshot.createdAt).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('npub', () => {
|
||||
it('should return bech32 encoded public key', () => {
|
||||
const identity = Identity.create('NpubTest');
|
||||
|
||||
expect(identity.npub).toMatch(/^npub1[a-z0-9]+$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pullDomainEvents', () => {
|
||||
it('should clear events after pulling', () => {
|
||||
const identity = Identity.create('Test');
|
||||
|
||||
const firstPull = identity.pullDomainEvents();
|
||||
const secondPull = identity.pullDomainEvents();
|
||||
|
||||
expect(firstPull.length).toBe(1);
|
||||
expect(secondPull.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should accumulate multiple events', () => {
|
||||
const identity = Identity.create('Multi');
|
||||
identity.rename('Name1');
|
||||
identity.rename('Name2');
|
||||
|
||||
const events = identity.pullDomainEvents();
|
||||
|
||||
expect(events.length).toBe(3); // Created + 2 renames
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user