Skip to content

Commit

Permalink
test: unit tests for SatsConnectConnector (#3344)
Browse files Browse the repository at this point in the history
  • Loading branch information
zoruka authored Nov 29, 2024
1 parent 9ca2e70 commit cd16595
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 7 deletions.
3 changes: 2 additions & 1 deletion packages/adapters/bitcoin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"build": "tsc --build tsconfig.build.json",
"watch": "tsc --watch",
"typecheck": "tsc --noEmit",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"test": "vitest run --coverage.enabled=true -- coverage.reporter=json --coverage.reporter=json-summary --coverage.reportOnFailure=true"
},
"exports": {
".": {
Expand Down
13 changes: 7 additions & 6 deletions packages/adapters/bitcoin/src/connectors/SatsConnectConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class SatsConnectConnector extends ProviderEventEmitter implements Bitcoi
}

public get imageUrl(): string {
return this.wallet.icon || ''
return this.wallet.icon
}

public get chains() {
Expand Down Expand Up @@ -79,10 +79,6 @@ export class SatsConnectConnector extends ProviderEventEmitter implements Bitcoi
message: 'Connect to your wallet'
})

if (response.addresses.length === 0) {
throw new Error('No address available')
}

return response.addresses
}

Expand Down Expand Up @@ -121,7 +117,12 @@ export class SatsConnectConnector extends ProviderEventEmitter implements Bitcoi
amount,
recipient
}: BitcoinConnector.SendTransferParams): Promise<string> {
const parsedAmount = isNaN(Number(amount)) ? 0 : Number(amount)
const parsedAmount = Number(amount)

if (isNaN(parsedAmount)) {
throw new Error('Invalid amount')
}

const res = await this.internalRequest('sendTransfer', {
recipients: [{ address: recipient, amount: parsedAmount }]
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { SatsConnectConnector } from '../../src/connectors/SatsConnectConnector'
import { mockSatsConnectProvider } from '../mocks/mockSatsConnect'
import type { CaipNetwork } from '@reown/appkit-common'
import { MessageSigningProtocols } from 'sats-connect'

describe('SatsConnectConnector', () => {
let connector: SatsConnectConnector
let mocks: ReturnType<typeof mockSatsConnectProvider>
let requestedChains: CaipNetwork[]

beforeEach(() => {
requestedChains = []
mocks = mockSatsConnectProvider()
connector = new SatsConnectConnector({ provider: mocks.provider, requestedChains })
})

it('should validate the test fixture', async () => {
expect((window as any)[mocks.provider.id]).toBeDefined()
expect(window.btc_providers).to.include(mocks.provider)
expect(connector).toBeDefined()
})

it('should get wallets correctly', async () => {
const wallets = SatsConnectConnector.getWallets({ requestedChains })

expect(wallets instanceof Array).toBeTruthy()
wallets.forEach(wallet => expect(wallet instanceof SatsConnectConnector).toBeTruthy())
})

it('should get metadata correctly', async () => {
expect(connector.id).toBe(mocks.provider.name)
expect(connector.name).toBe(mocks.provider.name)
expect(connector.imageUrl).toBe(mocks.provider.icon)
expect(connector.chains).toEqual(requestedChains)
})

it('should disconnect correctly', async () => {
await connector.disconnect()
expect(mocks.wallet.request).toHaveBeenCalledWith('wallet_disconnect', null)
})

it('should request correctly', async () => {
const args = { method: 'getAddresses', params: {} }
await connector.request(args)
expect(mocks.wallet.request).toHaveBeenCalledWith(args.method, args.params)
})

it('should connect correctly with wallet already connected', async () => {
const spy = vi.spyOn(mocks.wallet, 'request')

spy.mockResolvedValueOnce(
mockSatsConnectProvider.mockRequestResolve({
addresses: [
{
address: 'mock_address',
purpose: 'receive',
addressType: 'p2pkh',
gaiaAppKey: 'mock_gaia_app_key',
gaiaHubUrl: 'mock_gaia_hub_url',
publicKey: 'mock_public_key'
}
]
})
)

const result = await connector.connect()

expect(result).toBe('mock_address')
expect(mocks.wallet.request).toHaveBeenCalledWith('getAddresses', {
purposes: expect.arrayContaining(['payment', 'ordinals', 'stacks']),
message: 'Connect to your wallet'
})
})

it('should connect correctly with wallet not connected', async () => {
const spy = vi.spyOn(mocks.wallet, 'request')

spy.mockResolvedValueOnce(
mockSatsConnectProvider.mockRequestReject({ message: 'Unauthorized' })
)

spy.mockResolvedValueOnce(
mockSatsConnectProvider.mockRequestResolve({
addresses: [
{
address: 'mock_address',
purpose: 'payment',
addressType: 'p2pkh',
gaiaAppKey: 'mock_gaia_app_key',
gaiaHubUrl: 'mock_gaia_hub_url',
publicKey: 'mock_public_key'
}
]
})
)

const result = await connector.connect()

expect(result).toBe('mock_address')
expect(mocks.wallet.request).toHaveBeenNthCalledWith(1, 'getAddresses', {
purposes: expect.arrayContaining(['payment', 'ordinals', 'stacks']),
message: 'Connect to your wallet'
})
expect(mocks.wallet.request).toHaveBeenNthCalledWith(2, 'wallet_connect', null)
})

it('should throw if connect with empty addresses', async () => {
const spy = vi.spyOn(mocks.wallet, 'request')

spy.mockResolvedValueOnce(mockSatsConnectProvider.mockRequestResolve({ addresses: [] }))

await expect(connector.connect()).rejects.toThrow('No address available')
})

it('should signMessage correctly', async () => {
const params = { message: 'mock_message', address: 'mock_address' }
const spy = vi.spyOn(mocks.wallet, 'request')

spy.mockResolvedValueOnce(
mockSatsConnectProvider.mockRequestResolve({
signature: 'mock_signature',
address: 'mock_address',
protocol: MessageSigningProtocols.BIP322,
messageHash: 'mock_message_hash'
})
)

const result = await connector.signMessage(params)

expect(result).toBe('mock_signature')
expect(mocks.wallet.request).toHaveBeenCalledWith('signMessage', params)
})

it('should sendTransfer correctly', async () => {
const params = {
amount: '1000',
recipient: 'mock_recipient'
}
const spy = vi.spyOn(mocks.wallet, 'request')

spy.mockResolvedValueOnce(mockSatsConnectProvider.mockRequestResolve({ txid: 'mock_txid' }))

const result = await connector.sendTransfer(params)

expect(result).toBe('mock_txid')
expect(mocks.wallet.request).toHaveBeenCalledWith('sendTransfer', {
recipients: [{ address: params.recipient, amount: 1000 }]
})
})

it('should throw if sendTransfer with invalid amount', async () => {
const params = {
amount: 'invalid',
recipient: 'mock_recipient'
}

await expect(connector.sendTransfer(params)).rejects.toThrow('Invalid amount')
})

it('should throw correct error if internalRequest fails', async () => {
const args = {
method: 'signMessage',
params: { message: 'mock_message', address: 'mock_address' }
}
vi.spyOn(mocks.wallet, 'request').mockResolvedValueOnce(
mockSatsConnectProvider.mockRequestReject({
message: 'mock_error'
})
)

await expect(connector.request(args)).rejects.toThrow('mock_error')
})
})
76 changes: 76 additions & 0 deletions packages/adapters/bitcoin/tests/mocks/mockSatsConnect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { Provider as SatsConnectProvider, BitcoinProvider, RpcError } from 'sats-connect'
import { vi } from 'vitest'

export function mockSatsConnectProvider(replaces: Partial<SatsConnectProvider> = {}): {
provider: SatsConnectProvider
wallet: BitcoinProvider
} {
const id = replaces.id || 'mock_provider_id'

const wallet = mockSatsConnectWindowProvider(id)

const provider: SatsConnectProvider = {
id,
icon: 'mock_icon',
name: 'mock_provider_name',
chromeWebStoreUrl: '',
googlePlayStoreUrl: '',
iOSAppStoreUrl: '',
methods: ['getAddresses', 'signPsbt', 'sendTransfer', 'signMessage'],
mozillaAddOnsUrl: '',
webUrl: '',
...replaces
}

if (window.btc_providers) {
window.btc_providers = window.btc_providers.filter(p => p.id !== id)
window.btc_providers.push(provider)
} else {
window.btc_providers = [provider]
}

return { provider, wallet }
}

mockSatsConnectProvider.mockRequestResolve = <T>(result: T) => ({
id: 'mock_request_id',
jsonrpc: '2.0' as const,
result
})

mockSatsConnectProvider.mockRequestReject = (error: Partial<RpcError> = {}) => ({
id: 'mock_request_id',
jsonrpc: '2.0' as const,
error: {
code: -32000,
message: 'Mocked error',
...error
} satisfies RpcError
})

export function mockSatsConnectWindowProvider(id: string) {
const windowProvider: BitcoinProvider = {
addListener: vi.fn(),
connect: vi.fn(),
createInscription: vi.fn(),
createRepeatInscriptions: vi.fn(),
request: vi.fn(() =>
Promise.resolve({
id: 'mock_request_id',
jsonrpc: '2.0' as const,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result: {} as any
})
),
sendBtcTransaction: vi.fn(),
signMessage: vi.fn(),
signMultipleTransactions: vi.fn(),
signTransaction: vi.fn(),
getCapabilities: vi.fn()
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any)[id] = windowProvider

return windowProvider
}
7 changes: 7 additions & 0 deletions packages/adapters/bitcoin/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
environment: 'jsdom'
}
})

0 comments on commit cd16595

Please sign in to comment.