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>
201 lines
6.5 KiB
TypeScript
201 lines
6.5 KiB
TypeScript
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
|
|
});
|
|
});
|
|
});
|