Skip to content

Commit 81b4cb8

Browse files
authored
Merge pull request #329 from Prz-droid/feature/wallet-adapter-tests
Feature/wallet adapter tests
2 parents 4162d67 + 7767854 commit 81b4cb8

2 files changed

Lines changed: 548 additions & 0 deletions

File tree

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
import { FreighterAdapter } from './freighter.adapter';
2+
import { WalletName } from './wallet.interface';
3+
import { TikkaSdkError, TikkaSdkErrorCode } from '../utils/errors';
4+
import { Networks } from '@stellar/stellar-sdk';
5+
6+
// Mock the @stellar/freighter-api module
7+
jest.mock('@stellar/freighter-api', () => {
8+
const mockApi = {
9+
getAddress: jest.fn(),
10+
signTransaction: jest.fn(),
11+
signMessage: jest.fn(),
12+
getNetworkDetails: jest.fn(),
13+
};
14+
return mockApi;
15+
}, { virtual: true });
16+
17+
describe('FreighterAdapter', () => {
18+
let adapter: FreighterAdapter;
19+
let mockFreighterApi: any;
20+
21+
beforeEach(async () => {
22+
// Get the mocked module
23+
mockFreighterApi = await import('@stellar/freighter-api');
24+
25+
// Reset all mocks
26+
jest.clearAllMocks();
27+
28+
// Mock window.freighter as well (fallback)
29+
(globalThis as any).freighter = mockFreighterApi;
30+
31+
adapter = new FreighterAdapter({
32+
networkPassphrase: Networks.TESTNET,
33+
});
34+
});
35+
36+
afterEach(() => {
37+
delete (globalThis as any).freighter;
38+
});
39+
40+
describe('name', () => {
41+
it('should return correct wallet name', () => {
42+
expect(adapter.name).toBe(WalletName.Freighter);
43+
});
44+
});
45+
46+
describe('isAvailable', () => {
47+
it('should return true when Freighter is installed', () => {
48+
(globalThis as any).freighter = {};
49+
expect(adapter.isAvailable()).toBe(true);
50+
});
51+
52+
it('should return false when Freighter is not installed', () => {
53+
delete (globalThis as any).freighter;
54+
expect(adapter.isAvailable()).toBe(false);
55+
});
56+
});
57+
58+
describe('getPublicKey', () => {
59+
it('should return public key string', async () => {
60+
const expectedAddress = 'GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
61+
mockFreighterApi.getAddress.mockResolvedValue({ address: expectedAddress });
62+
63+
const publicKey = await adapter.getPublicKey();
64+
65+
expect(publicKey).toBe(expectedAddress);
66+
expect(mockFreighterApi.getAddress).toHaveBeenCalledTimes(1);
67+
});
68+
69+
it('should throw WalletNotInstalled error when Freighter is not available', async () => {
70+
delete (globalThis as any).freighter;
71+
const freshAdapter = new FreighterAdapter();
72+
73+
await expect(freshAdapter.getPublicKey()).rejects.toThrow(TikkaSdkError);
74+
await expect(freshAdapter.getPublicKey()).rejects.toMatchObject({
75+
code: TikkaSdkErrorCode.WalletNotInstalled,
76+
message: expect.stringContaining('Freighter wallet extension is not installed'),
77+
});
78+
});
79+
80+
it('should throw UserRejected error when user declines', async () => {
81+
mockFreighterApi.getAddress.mockRejectedValue(new Error('User declined to share public key'));
82+
83+
await expect(adapter.getPublicKey()).rejects.toThrow(TikkaSdkError);
84+
await expect(adapter.getPublicKey()).rejects.toMatchObject({
85+
code: TikkaSdkErrorCode.UserRejected,
86+
message: 'User rejected public key request',
87+
});
88+
});
89+
90+
it('should throw Unknown error for other failures', async () => {
91+
mockFreighterApi.getAddress.mockRejectedValue(new Error('Network error'));
92+
93+
await expect(adapter.getPublicKey()).rejects.toThrow(TikkaSdkError);
94+
await expect(adapter.getPublicKey()).rejects.toMatchObject({
95+
code: TikkaSdkErrorCode.Unknown,
96+
message: expect.stringContaining('Freighter getPublicKey failed'),
97+
});
98+
});
99+
});
100+
101+
describe('signTransaction', () => {
102+
const mockXdr = 'AAAAAgAAAABqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxQ==';
103+
const mockSignedXdr = 'AAAAAgAAAABqyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyQ==';
104+
105+
it('should sign transaction and return signed XDR', async () => {
106+
mockFreighterApi.signTransaction.mockResolvedValue({ signedTxXdr: mockSignedXdr });
107+
108+
const result = await adapter.signTransaction(mockXdr);
109+
110+
expect(result.signedXdr).toBe(mockSignedXdr);
111+
expect(mockFreighterApi.signTransaction).toHaveBeenCalledWith(mockXdr, {
112+
networkPassphrase: Networks.TESTNET,
113+
accountToSign: undefined,
114+
});
115+
});
116+
117+
it('should use provided network passphrase', async () => {
118+
mockFreighterApi.signTransaction.mockResolvedValue({ signedTxXdr: mockSignedXdr });
119+
120+
await adapter.signTransaction(mockXdr, {
121+
networkPassphrase: Networks.PUBLIC,
122+
});
123+
124+
expect(mockFreighterApi.signTransaction).toHaveBeenCalledWith(mockXdr, {
125+
networkPassphrase: Networks.PUBLIC,
126+
accountToSign: undefined,
127+
});
128+
});
129+
130+
it('should pass accountToSign parameter', async () => {
131+
const accountToSign = 'GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
132+
mockFreighterApi.signTransaction.mockResolvedValue({ signedTxXdr: mockSignedXdr });
133+
134+
await adapter.signTransaction(mockXdr, {
135+
accountToSign,
136+
});
137+
138+
expect(mockFreighterApi.signTransaction).toHaveBeenCalledWith(mockXdr, {
139+
networkPassphrase: Networks.TESTNET,
140+
accountToSign,
141+
});
142+
});
143+
144+
it('should throw WalletNotInstalled error when Freighter is not available', async () => {
145+
delete (globalThis as any).freighter;
146+
const freshAdapter = new FreighterAdapter();
147+
148+
await expect(freshAdapter.signTransaction(mockXdr)).rejects.toThrow(TikkaSdkError);
149+
await expect(freshAdapter.signTransaction(mockXdr)).rejects.toMatchObject({
150+
code: TikkaSdkErrorCode.WalletNotInstalled,
151+
});
152+
});
153+
154+
it('should throw UserRejected error when user cancels', async () => {
155+
mockFreighterApi.signTransaction.mockRejectedValue(new Error('User rejected the transaction'));
156+
157+
await expect(adapter.signTransaction(mockXdr)).rejects.toThrow(TikkaSdkError);
158+
await expect(adapter.signTransaction(mockXdr)).rejects.toMatchObject({
159+
code: TikkaSdkErrorCode.UserRejected,
160+
message: 'User rejected transaction signing',
161+
});
162+
});
163+
164+
it('should map user declined error to UserRejected', async () => {
165+
mockFreighterApi.signTransaction.mockRejectedValue(new Error('User declined to sign'));
166+
167+
await expect(adapter.signTransaction(mockXdr)).rejects.toMatchObject({
168+
code: TikkaSdkErrorCode.UserRejected,
169+
});
170+
});
171+
172+
it('should map cancelled error to UserRejected', async () => {
173+
mockFreighterApi.signTransaction.mockRejectedValue(new Error('Transaction cancelled by user'));
174+
175+
await expect(adapter.signTransaction(mockXdr)).rejects.toMatchObject({
176+
code: TikkaSdkErrorCode.UserRejected,
177+
});
178+
});
179+
180+
it('should throw Unknown error for other failures', async () => {
181+
mockFreighterApi.signTransaction.mockRejectedValue(new Error('Invalid XDR'));
182+
183+
await expect(adapter.signTransaction(mockXdr)).rejects.toThrow(TikkaSdkError);
184+
await expect(adapter.signTransaction(mockXdr)).rejects.toMatchObject({
185+
code: TikkaSdkErrorCode.Unknown,
186+
message: expect.stringContaining('Freighter signTransaction failed'),
187+
});
188+
});
189+
});
190+
191+
describe('signMessage', () => {
192+
const mockMessage = 'Sign this message for authentication';
193+
const mockSignedMessage = 'signed_message_base64';
194+
195+
it('should sign message and return signature', async () => {
196+
mockFreighterApi.signMessage = jest.fn().mockResolvedValue({ signedMessage: mockSignedMessage });
197+
198+
const signature = await adapter.signMessage(mockMessage);
199+
200+
expect(signature).toBe(mockSignedMessage);
201+
expect(mockFreighterApi.signMessage).toHaveBeenCalledWith(mockMessage);
202+
});
203+
204+
it('should throw error when signMessage is not supported', async () => {
205+
delete mockFreighterApi.signMessage;
206+
207+
await expect(adapter.signMessage(mockMessage)).rejects.toThrow(
208+
'signMessage not supported by this Freighter version',
209+
);
210+
});
211+
212+
it('should throw UserRejected error when user declines', async () => {
213+
mockFreighterApi.signMessage = jest.fn().mockRejectedValue(new Error('User rejected message signing'));
214+
215+
await expect(adapter.signMessage(mockMessage)).rejects.toMatchObject({
216+
code: TikkaSdkErrorCode.UserRejected,
217+
message: 'User rejected message signing',
218+
});
219+
});
220+
221+
it('should throw WalletNotInstalled error when Freighter is not available', async () => {
222+
delete (globalThis as any).freighter;
223+
const freshAdapter = new FreighterAdapter();
224+
225+
await expect(freshAdapter.signMessage(mockMessage)).rejects.toMatchObject({
226+
code: TikkaSdkErrorCode.WalletNotInstalled,
227+
});
228+
});
229+
});
230+
231+
describe('getNetwork', () => {
232+
it('should return network passphrase from wallet', async () => {
233+
mockFreighterApi.getNetworkDetails.mockResolvedValue({ networkPassphrase: Networks.PUBLIC });
234+
235+
const network = await adapter.getNetwork();
236+
237+
expect(network).toBe(Networks.PUBLIC);
238+
expect(mockFreighterApi.getNetworkDetails).toHaveBeenCalledTimes(1);
239+
});
240+
241+
it('should return undefined on error', async () => {
242+
mockFreighterApi.getNetworkDetails.mockRejectedValue(new Error('Failed to get network'));
243+
244+
const network = await adapter.getNetwork();
245+
246+
expect(network).toBeUndefined();
247+
});
248+
249+
it('should return testnet passphrase', async () => {
250+
mockFreighterApi.getNetworkDetails.mockResolvedValue({ networkPassphrase: Networks.TESTNET });
251+
252+
const network = await adapter.getNetwork();
253+
254+
expect(network).toBe(Networks.TESTNET);
255+
});
256+
});
257+
258+
describe('interface consistency', () => {
259+
it('should have all required WalletAdapter methods', () => {
260+
expect(typeof adapter.isAvailable).toBe('function');
261+
expect(typeof adapter.getPublicKey).toBe('function');
262+
expect(typeof adapter.signTransaction).toBe('function');
263+
expect(typeof adapter.signMessage).toBe('function');
264+
expect(typeof adapter.getNetwork).toBe('function');
265+
});
266+
267+
it('should have name property', () => {
268+
expect(adapter.name).toBeDefined();
269+
expect(typeof adapter.name).toBe('string');
270+
});
271+
272+
it('should return SignTransactionResult with signedXdr property', async () => {
273+
const mockSignedXdr = 'AAAAAgAAAABqyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyQ==';
274+
mockFreighterApi.signTransaction.mockResolvedValue({ signedTxXdr: mockSignedXdr });
275+
276+
const result = await adapter.signTransaction('mockXdr');
277+
278+
expect(result).toHaveProperty('signedXdr');
279+
expect(typeof result.signedXdr).toBe('string');
280+
});
281+
});
282+
283+
describe('error mapping consistency', () => {
284+
it('should map user declined errors consistently', async () => {
285+
// Test getPublicKey
286+
mockFreighterApi.getAddress.mockRejectedValueOnce(new Error('User declined'));
287+
await expect(adapter.getPublicKey()).rejects.toMatchObject({
288+
code: TikkaSdkErrorCode.UserRejected,
289+
});
290+
291+
// Test signTransaction
292+
mockFreighterApi.signTransaction.mockRejectedValueOnce(new Error('User declined'));
293+
await expect(adapter.signTransaction('xdr')).rejects.toMatchObject({
294+
code: TikkaSdkErrorCode.UserRejected,
295+
});
296+
});
297+
298+
it('should map cancelled errors consistently', async () => {
299+
// Test getPublicKey
300+
mockFreighterApi.getAddress.mockRejectedValueOnce(new Error('Cancelled by user'));
301+
await expect(adapter.getPublicKey()).rejects.toMatchObject({
302+
code: TikkaSdkErrorCode.UserRejected,
303+
});
304+
305+
// Test signTransaction
306+
mockFreighterApi.signTransaction.mockRejectedValueOnce(new Error('Cancelled'));
307+
await expect(adapter.signTransaction('xdr')).rejects.toMatchObject({
308+
code: TikkaSdkErrorCode.UserRejected,
309+
});
310+
});
311+
});
312+
});

0 commit comments

Comments
 (0)