@bsv/sdk
Version:
BSV Blockchain Software Development Kit
279 lines (252 loc) • 9.9 kB
text/typescript
import { WalletCertificate, WalletInterface } from '../../wallet/index'
import { IdentityClient } from '../IdentityClient'
import { Certificate } from '../../auth/certificates/index.js'
import { KNOWN_IDENTITY_TYPES, defaultIdentity } from '../types/index.js'
// ----- Mocks for external dependencies -----
jest.mock('../../script', () => {
return {
PushDrop: jest.fn().mockImplementation(() => ({
lock: jest.fn().mockResolvedValue({
toHex: () => 'lockingScriptHex'
}),
unlock: jest.fn()
}))
}
})
jest.mock('../../overlay-tools/index.js', () => {
return {
TopicBroadcaster: jest.fn().mockImplementation(() => ({
broadcast: jest.fn().mockResolvedValue('broadcastResult')
}))
}
})
jest.mock('../../transaction/index.js', () => {
return {
Transaction: {
fromAtomicBEEF: jest.fn().mockImplementation((tx) => ({
toHexBEEF: () => 'transactionHex'
})),
fromBEEF: jest.fn()
}
}
})
// ----- Begin Test Suite -----
describe('IdentityClient', () => {
let walletMock: Partial<WalletInterface>
let identityClient: IdentityClient
beforeEach(() => {
// Create a fake wallet implementing the methods used by IdentityClient.
walletMock = {
proveCertificate: jest.fn().mockResolvedValue({ keyringForVerifier: 'fakeKeyring' }),
createAction: jest.fn().mockResolvedValue({
tx: [1, 2, 3],
signableTransaction: { tx: [1, 2, 3], reference: 'ref' }
}),
listCertificates: jest.fn().mockResolvedValue({ certificates: [] }),
acquireCertificate: jest.fn().mockResolvedValue({
fields: { name: 'Alice' },
verify: jest.fn().mockResolvedValue(true)
}),
signAction: jest.fn().mockResolvedValue({ tx: [4, 5, 6] }),
getNetwork: jest.fn().mockResolvedValue({ network: 'testnet' }),
discoverByIdentityKey: jest.fn(),
discoverByAttributes: jest.fn()
}
identityClient = new IdentityClient(walletMock as WalletInterface)
// Clear any previous calls/spies.
jest.clearAllMocks()
})
describe('publiclyRevealAttributes', () => {
it('should throw an error if certificate has no fields', async () => {
const certificate = {
fields: {},
verify: jest.fn().mockResolvedValue(true)
} as any as WalletCertificate
const fieldsToReveal = ['name']
await expect(
identityClient.publiclyRevealAttributes(certificate, fieldsToReveal)
).rejects.toThrow('Certificate has no fields to reveal!')
})
it('should throw an error if fieldsToReveal is empty', async () => {
const certificate = {
fields: { name: 'Alice' },
verify: jest.fn().mockResolvedValue(true)
} as any as WalletCertificate
const fieldsToReveal: string[] = []
await expect(
identityClient.publiclyRevealAttributes(certificate, fieldsToReveal)
).rejects.toThrow('You must reveal at least one field!')
})
it('should throw an error if certificate verification fails', async () => {
const certificate = {
fields: { name: 'Alice' },
verify: jest.fn().mockRejectedValue(new Error('Verification error')),
type: 'dummyType',
serialNumber: 'dummySerial',
subject: 'dummySubject',
certifier: 'dummyCertifier',
revocationOutpoint: 'dummyRevocation',
signature: 'dummySignature'
} as any as WalletCertificate
const fieldsToReveal = ['name']
await expect(
identityClient.publiclyRevealAttributes(certificate, fieldsToReveal)
).rejects.toThrow('Certificate verification failed!')
})
it('should publicly reveal attributes successfully', async () => {
// Prepare a dummy certificate with all required properties.
const certificate = {
fields: { name: 'Alice' },
verify: jest.fn().mockResolvedValue(true), // this property is not used since the Certificate is re-instantiated
type: 'xCert',
serialNumber: '12345',
subject: 'abcdef1234567890',
certifier: 'CertifierX',
revocationOutpoint: 'outpoint1',
signature: 'signature1'
} as any as WalletCertificate
// Ensure that Certificate.verify (called on the re-instantiated Certificate)
// resolves successfully.
jest.spyOn(Certificate.prototype, 'verify').mockResolvedValue(false)
const fieldsToReveal = ['name']
const result = await identityClient.publiclyRevealAttributes(certificate, fieldsToReveal)
expect(result).toEqual('broadcastResult')
// Validate that proveCertificate was called with the proper arguments.
expect(walletMock.proveCertificate).toHaveBeenCalledWith({
certificate,
fieldsToReveal,
verifier: expect.any(String)
})
// Validate that createAction was called.
expect(walletMock.createAction).toHaveBeenCalled()
})
})
describe('resolveByIdentityKey', () => {
it('should return parsed identities from discovered certificates', async () => {
const dummyCertificate = {
type: KNOWN_IDENTITY_TYPES.xCert,
subject: 'abcdef1234567890',
decryptedFields: {
userName: 'Alice',
profilePhoto: 'alicePhotoUrl'
},
certifierInfo: {
name: 'CertifierX',
iconUrl: 'certifierIconUrl'
}
}
// Mock discoverByIdentityKey to return a certificate list.
walletMock.discoverByIdentityKey = jest.fn().mockResolvedValue({ certificates: [dummyCertificate] })
const identities = await identityClient.resolveByIdentityKey({ identityKey: 'dummyKey' })
expect(walletMock.discoverByIdentityKey).toHaveBeenCalledWith({ identityKey: 'dummyKey' }, undefined)
expect(identities).toHaveLength(1)
expect(identities[0]).toEqual({
name: 'Alice',
avatarURL: 'alicePhotoUrl',
abbreviatedKey: 'abcdef1234...',
identityKey: 'abcdef1234567890',
badgeLabel: 'X account certified by CertifierX',
badgeIconURL: 'certifierIconUrl',
badgeClickURL: 'https://socialcert.net'
})
})
})
it('should throw if createAction returns no tx', async () => {
const certificate = {
fields: { name: 'Alice' },
verify: jest.fn().mockResolvedValue(true),
type: 'xCert',
serialNumber: '12345',
subject: 'abcdef1234567890',
certifier: 'CertifierX',
revocationOutpoint: 'outpoint1',
signature: 'signature1'
} as any as WalletCertificate
jest.spyOn(Certificate.prototype, 'verify').mockResolvedValue(false)
// Simulate createAction returning an object with tx = undefined
walletMock.createAction = jest.fn().mockResolvedValue({
tx: undefined,
signableTransaction: { tx: undefined, reference: 'ref' }
})
const fieldsToReveal = ['name']
await expect(
identityClient.publiclyRevealAttributes(certificate, fieldsToReveal)
).rejects.toThrow('Public reveal failed: failed to create action!')
})
describe('resolveByAttributes', () => {
it('should return parsed identities from discovered certificates', async () => {
const dummyCertificate = {
type: KNOWN_IDENTITY_TYPES.emailCert,
subject: 'emailSubject1234',
decryptedFields: {
email: 'alice@example.com',
profilePhoto: 'ignored' // not used for email type
},
certifierInfo: {
name: 'EmailCertifier',
iconUrl: 'emailIconUrl'
}
}
// Mock discoverByAttributes to return a certificate list.
walletMock.discoverByAttributes = jest.fn().mockResolvedValue({ certificates: [dummyCertificate] })
const identities = await identityClient.resolveByAttributes({ attributes: { email: 'alice@example.com' } })
expect(walletMock.discoverByAttributes).toHaveBeenCalledWith({ attributes: { email: 'alice@example.com' } }, undefined)
expect(identities).toHaveLength(1)
expect(identities[0]).toEqual({
name: 'alice@example.com',
avatarURL: 'XUTZxep7BBghAJbSBwTjNfmcsDdRFs5EaGEgkESGSgjJVYgMEizu',
abbreviatedKey: 'emailSubje...',
identityKey: 'emailSubject1234',
badgeLabel: 'Email certified by EmailCertifier',
badgeIconURL: 'emailIconUrl',
badgeClickURL: 'https://socialcert.net'
})
})
})
describe('parseIdentity', () => {
it('should correctly parse an xCert identity', () => {
const dummyCertificate = {
type: KNOWN_IDENTITY_TYPES.xCert,
subject: 'abcdef1234567890',
decryptedFields: {
userName: 'Alice',
profilePhoto: 'alicePhotoUrl'
},
certifierInfo: {
name: 'CertifierX',
iconUrl: 'certifierIconUrl'
}
}
const identity = IdentityClient.parseIdentity(dummyCertificate as unknown as any)
expect(identity).toEqual({
name: 'Alice',
avatarURL: 'alicePhotoUrl',
abbreviatedKey: 'abcdef1234...',
identityKey: 'abcdef1234567890',
badgeLabel: 'X account certified by CertifierX',
badgeIconURL: 'certifierIconUrl',
badgeClickURL: 'https://socialcert.net'
})
})
it('should return default identity for unknown type', () => {
const dummyCertificate = {
type: 'unknownType',
subject: '',
decryptedFields: {
profilePhoto: 'defaultPhoto'
},
certifierInfo: {}
}
const identity = IdentityClient.parseIdentity(dummyCertificate as any)
expect(identity).toEqual({
name: defaultIdentity.name,
avatarURL: 'defaultPhoto',
abbreviatedKey: '',
identityKey: '',
badgeLabel: defaultIdentity.badgeLabel,
badgeIconURL: defaultIdentity.badgeIconURL,
badgeClickURL: defaultIdentity.badgeClickURL
})
})
})
})