Skip to content

Commit d42306c

Browse files
authored
Merge pull request #799 from Mimah97/feat/fix-simulation-typing
fix(backend): use SDK discriminated unions for simulate responses in …
2 parents b4c30fb + 18701be commit d42306c

2 files changed

Lines changed: 316 additions & 145 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/**
2+
* Unit tests for SubscriptionChainReaderService simulation typing.
3+
*
4+
* Covers every branch of the SDK discriminated union:
5+
* SimulateTransactionErrorResponse → isSimulationError
6+
* SimulateTransactionSuccessResponse → isSimulationSuccess
7+
* SimulateTransactionRestoreResponse → isSimulationRestore
8+
*
9+
* Also verifies graceful handling of: network errors, missing retval,
10+
* and the restore (archived entry) path.
11+
*/
12+
import { rpc, xdr, nativeToScVal } from '@stellar/stellar-sdk';
13+
import { SubscriptionChainReaderService } from './subscription-chain-reader.service';
14+
import { LedgerClockService } from './ledger-clock.service';
15+
16+
// ── helpers ──────────────────────────────────────────────────────────────────
17+
18+
const CONTRACT_ID = 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM';
19+
const FAN = 'GFAN1111111111111111111111111111111111111111111111111111';
20+
const CREATOR = 'GCREATOR111111111111111111111111111111111111111111111111';
21+
22+
function makeService(simResult: rpc.Api.SimulateTransactionResponse): SubscriptionChainReaderService {
23+
const ledgerClock = {
24+
fetchSnapshot: jest.fn().mockResolvedValue({
25+
ledgerSeq: 1000,
26+
ledgerCloseTimeUnix: 1_700_000_000,
27+
capturedAtMs: 1_700_000_000_000,
28+
skewMs: -200,
29+
}),
30+
ledgerSeqToUnix: jest.fn().mockReturnValue(1_700_000_000 + 30 * 24 * 3600),
31+
} as unknown as LedgerClockService;
32+
33+
const svc = new SubscriptionChainReaderService(ledgerClock);
34+
35+
// Stub the private makeServer() so no real network call is made
36+
jest.spyOn(svc as any, 'makeServer').mockReturnValue({
37+
simulateTransaction: jest.fn().mockResolvedValue(simResult),
38+
});
39+
40+
return svc;
41+
}
42+
43+
/** Build a minimal success response */
44+
function successSim(retval: xdr.ScVal): rpc.Api.SimulateTransactionSuccessResponse {
45+
return {
46+
_parsed: true,
47+
id: '1',
48+
latestLedger: 1000,
49+
events: [],
50+
transactionData: {} as any,
51+
minResourceFee: '100',
52+
result: { auth: [], retval },
53+
} as rpc.Api.SimulateTransactionSuccessResponse;
54+
}
55+
56+
/** Build a minimal error response */
57+
function errorSim(error: string): rpc.Api.SimulateTransactionErrorResponse {
58+
return {
59+
_parsed: true,
60+
id: '1',
61+
latestLedger: 1000,
62+
events: [],
63+
error,
64+
} as rpc.Api.SimulateTransactionErrorResponse;
65+
}
66+
67+
/** Build a minimal restore response */
68+
function restoreSim(): rpc.Api.SimulateTransactionRestoreResponse {
69+
return {
70+
_parsed: true,
71+
id: '1',
72+
latestLedger: 1000,
73+
events: [],
74+
transactionData: {} as any,
75+
minResourceFee: '100',
76+
result: { auth: [], retval: nativeToScVal(true) },
77+
restorePreamble: { minResourceFee: '200', transactionData: {} as any },
78+
} as rpc.Api.SimulateTransactionRestoreResponse;
79+
}
80+
81+
// ── readIsSubscriber ──────────────────────────────────────────────────────────
82+
83+
describe('SubscriptionChainReaderService.readIsSubscriber', () => {
84+
it('returns ok=true isSubscriber=true on success sim with true retval', async () => {
85+
const svc = makeService(successSim(nativeToScVal(true)));
86+
const result = await svc.readIsSubscriber(CONTRACT_ID, FAN, CREATOR);
87+
expect(result).toEqual({ ok: true, isSubscriber: true });
88+
});
89+
90+
it('returns ok=true isSubscriber=false on success sim with false retval', async () => {
91+
const svc = makeService(successSim(nativeToScVal(false)));
92+
const result = await svc.readIsSubscriber(CONTRACT_ID, FAN, CREATOR);
93+
expect(result).toEqual({ ok: true, isSubscriber: false });
94+
});
95+
96+
it('returns ok=false with error string on error sim', async () => {
97+
const svc = makeService(errorSim('contract not found'));
98+
const result = await svc.readIsSubscriber(CONTRACT_ID, FAN, CREATOR);
99+
expect(result.ok).toBe(false);
100+
expect((result as any).error).toMatch(/contract not found/i);
101+
});
102+
103+
it('returns ok=false with restore message on restore sim', async () => {
104+
const svc = makeService(restoreSim());
105+
const result = await svc.readIsSubscriber(CONTRACT_ID, FAN, CREATOR);
106+
expect(result.ok).toBe(false);
107+
expect((result as any).error).toMatch(/restoration/i);
108+
});
109+
110+
it('returns ok=false when simulateTransaction rejects (network error)', async () => {
111+
const svc = new SubscriptionChainReaderService({
112+
fetchSnapshot: jest.fn(),
113+
ledgerSeqToUnix: jest.fn(),
114+
} as unknown as LedgerClockService);
115+
jest.spyOn(svc as any, 'makeServer').mockReturnValue({
116+
simulateTransaction: jest.fn().mockRejectedValue(new Error('connection refused')),
117+
});
118+
const result = await svc.readIsSubscriber(CONTRACT_ID, FAN, CREATOR);
119+
expect(result.ok).toBe(false);
120+
expect((result as any).error).toMatch(/connection refused/i);
121+
});
122+
123+
it('returns ok=false when success sim has no retval', async () => {
124+
const sim: rpc.Api.SimulateTransactionSuccessResponse = {
125+
_parsed: true,
126+
id: '1',
127+
latestLedger: 1000,
128+
events: [],
129+
transactionData: {} as any,
130+
minResourceFee: '100',
131+
// result intentionally absent
132+
} as rpc.Api.SimulateTransactionSuccessResponse;
133+
const svc = makeService(sim);
134+
const result = await svc.readIsSubscriber(CONTRACT_ID, FAN, CREATOR);
135+
expect(result.ok).toBe(false);
136+
expect((result as any).error).toMatch(/no retval/i);
137+
});
138+
});
139+
140+
// ── readExpiryUnix ────────────────────────────────────────────────────────────
141+
142+
describe('SubscriptionChainReaderService.readExpiryUnix', () => {
143+
it('returns ok=true with expiryUnix from contract when non-zero', async () => {
144+
const contractExpiry = 1_800_000_000n;
145+
const retval = nativeToScVal([1200n, contractExpiry] as any);
146+
const svc = makeService(successSim(retval));
147+
const result = await svc.readExpiryUnix(CONTRACT_ID, FAN, CREATOR);
148+
expect(result.ok).toBe(true);
149+
if (result.ok) {
150+
expect(result.expiryUnix).toBe(Number(contractExpiry));
151+
expect(typeof result.skewMs).toBe('number');
152+
}
153+
});
154+
155+
it('returns ok=false on error sim', async () => {
156+
const svc = makeService(errorSim('host function error'));
157+
const result = await svc.readExpiryUnix(CONTRACT_ID, FAN, CREATOR);
158+
expect(result.ok).toBe(false);
159+
});
160+
161+
it('returns ok=false on restore sim', async () => {
162+
const svc = makeService(restoreSim());
163+
const result = await svc.readExpiryUnix(CONTRACT_ID, FAN, CREATOR);
164+
expect(result.ok).toBe(false);
165+
expect((result as any).error).toMatch(/restoration/i);
166+
});
167+
});
168+
169+
// ── readPlan ──────────────────────────────────────────────────────────────────
170+
171+
describe('SubscriptionChainReaderService.readPlan', () => {
172+
it('returns ok=true with plan fields on success sim', async () => {
173+
const planNative = { creator: CREATOR, asset: 'XLM', amount: 10_000_000n, interval_days: 30 };
174+
const retval = nativeToScVal(planNative as any);
175+
const svc = makeService(successSim(retval));
176+
const result = await svc.readPlan(CONTRACT_ID, 1);
177+
expect(result.ok).toBe(true);
178+
if (result.ok) {
179+
expect(result.plan.creator).toBe(CREATOR);
180+
expect(result.plan.amount).toBe('10000000');
181+
expect(result.plan.intervalDays).toBe(30);
182+
}
183+
});
184+
185+
it('returns ok=false on error sim', async () => {
186+
const svc = makeService(errorSim('plan not found'));
187+
const result = await svc.readPlan(CONTRACT_ID, 99);
188+
expect(result.ok).toBe(false);
189+
});
190+
191+
it('returns ok=false on restore sim', async () => {
192+
const svc = makeService(restoreSim());
193+
const result = await svc.readPlan(CONTRACT_ID, 1);
194+
expect(result.ok).toBe(false);
195+
expect((result as any).error).toMatch(/restoration/i);
196+
});
197+
});
198+
199+
// ── readPlanCount ─────────────────────────────────────────────────────────────
200+
201+
describe('SubscriptionChainReaderService.readPlanCount', () => {
202+
it('returns ok=true with count on success sim', async () => {
203+
const svc = makeService(successSim(nativeToScVal(5, { type: 'u32' })));
204+
const result = await svc.readPlanCount(CONTRACT_ID);
205+
expect(result).toEqual({ ok: true, count: 5 });
206+
});
207+
208+
it('returns ok=false on error sim', async () => {
209+
const svc = makeService(errorSim('contract error'));
210+
const result = await svc.readPlanCount(CONTRACT_ID);
211+
expect(result.ok).toBe(false);
212+
});
213+
214+
it('returns ok=false on restore sim', async () => {
215+
const svc = makeService(restoreSim());
216+
const result = await svc.readPlanCount(CONTRACT_ID);
217+
expect(result.ok).toBe(false);
218+
});
219+
});

0 commit comments

Comments
 (0)