@onesub/server 0.7.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/apple-status-api.test.d.ts +14 -0
- package/dist/__tests__/apple-status-api.test.d.ts.map +1 -0
- package/dist/__tests__/apple-status-api.test.js +345 -0
- package/dist/__tests__/apple-status-api.test.js.map +1 -0
- package/dist/__tests__/fetch-hardening.test.d.ts +7 -0
- package/dist/__tests__/fetch-hardening.test.d.ts.map +1 -0
- package/dist/__tests__/fetch-hardening.test.js +192 -0
- package/dist/__tests__/fetch-hardening.test.js.map +1 -0
- package/dist/__tests__/google-paused.test.d.ts +10 -0
- package/dist/__tests__/google-paused.test.d.ts.map +1 -0
- package/dist/__tests__/google-paused.test.js +126 -0
- package/dist/__tests__/google-paused.test.js.map +1 -0
- package/dist/__tests__/google-price-change.test.d.ts +6 -0
- package/dist/__tests__/google-price-change.test.d.ts.map +1 -0
- package/dist/__tests__/google-price-change.test.js +186 -0
- package/dist/__tests__/google-price-change.test.js.map +1 -0
- package/dist/__tests__/google-subscriptions-v2.test.d.ts +13 -0
- package/dist/__tests__/google-subscriptions-v2.test.d.ts.map +1 -0
- package/dist/__tests__/google-subscriptions-v2.test.js +206 -0
- package/dist/__tests__/google-subscriptions-v2.test.js.map +1 -0
- package/dist/__tests__/lifecycle-states.test.d.ts +12 -0
- package/dist/__tests__/lifecycle-states.test.d.ts.map +1 -0
- package/dist/__tests__/lifecycle-states.test.js +423 -0
- package/dist/__tests__/lifecycle-states.test.js.map +1 -0
- package/dist/__tests__/linked-purchase-token.test.d.ts +9 -0
- package/dist/__tests__/linked-purchase-token.test.d.ts.map +1 -0
- package/dist/__tests__/linked-purchase-token.test.js +227 -0
- package/dist/__tests__/linked-purchase-token.test.js.map +1 -0
- package/dist/__tests__/paused-auto-resume.test.d.ts +7 -0
- package/dist/__tests__/paused-auto-resume.test.d.ts.map +1 -0
- package/dist/__tests__/paused-auto-resume.test.js +162 -0
- package/dist/__tests__/paused-auto-resume.test.js.map +1 -0
- package/dist/__tests__/providers.test.js +69 -9
- package/dist/__tests__/providers.test.js.map +1 -1
- package/dist/__tests__/refund-policy.test.d.ts +11 -0
- package/dist/__tests__/refund-policy.test.d.ts.map +1 -0
- package/dist/__tests__/refund-policy.test.js +317 -0
- package/dist/__tests__/refund-policy.test.js.map +1 -0
- package/dist/__tests__/schema.test.js +4 -0
- package/dist/__tests__/schema.test.js.map +1 -1
- package/dist/__tests__/store.test.js +17 -0
- package/dist/__tests__/store.test.js.map +1 -1
- package/dist/__tests__/test-utils.d.ts +20 -0
- package/dist/__tests__/test-utils.d.ts.map +1 -0
- package/dist/__tests__/test-utils.js +30 -0
- package/dist/__tests__/test-utils.js.map +1 -0
- package/dist/__tests__/webhook-refund.test.d.ts +13 -0
- package/dist/__tests__/webhook-refund.test.d.ts.map +1 -0
- package/dist/__tests__/webhook-refund.test.js +313 -0
- package/dist/__tests__/webhook-refund.test.js.map +1 -0
- package/dist/http.d.ts +17 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +36 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/providers/apple.d.ts +50 -2
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/providers/apple.js +235 -2
- package/dist/providers/apple.js.map +1 -1
- package/dist/providers/google.d.ts +71 -6
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +211 -36
- package/dist/providers/google.js.map +1 -1
- package/dist/routes/purchase.d.ts.map +1 -1
- package/dist/routes/purchase.js +15 -6
- package/dist/routes/purchase.js.map +1 -1
- package/dist/routes/status.d.ts.map +1 -1
- package/dist/routes/status.js +14 -1
- package/dist/routes/status.js.map +1 -1
- package/dist/routes/validate.d.ts.map +1 -1
- package/dist/routes/validate.js +7 -1
- package/dist/routes/validate.js.map +1 -1
- package/dist/routes/webhook.d.ts +2 -2
- package/dist/routes/webhook.d.ts.map +1 -1
- package/dist/routes/webhook.js +219 -27
- package/dist/routes/webhook.js.map +1 -1
- package/dist/store.d.ts +9 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +12 -0
- package/dist/store.js.map +1 -1
- package/dist/stores/postgres.d.ts +1 -0
- package/dist/stores/postgres.d.ts.map +1 -1
- package/dist/stores/postgres.js +14 -2
- package/dist/stores/postgres.js.map +1 -1
- package/dist/stores/schema.d.ts.map +1 -1
- package/dist/stores/schema.js +7 -0
- package/dist/stores/schema.js.map +1 -1
- package/package.json +2 -2
- package/sql/schema.sql +7 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for fetchAppleSubscriptionStatus + the Apple webhook unknown-tx fallback.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - status code 1..5 → SubscriptionStatus mapping
|
|
6
|
+
* - sandbox vs production host routing
|
|
7
|
+
* - missing API credentials → returns null (no throw)
|
|
8
|
+
* - HTTP error / empty data → returns null
|
|
9
|
+
* - webhook receives notification for unknown originalTransactionId →
|
|
10
|
+
* calls Status API + saves the returned record
|
|
11
|
+
* - webhook unknown tx without API creds → no fetch, just logs
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=apple-status-api.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple-status-api.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/apple-status-api.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for fetchAppleSubscriptionStatus + the Apple webhook unknown-tx fallback.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - status code 1..5 → SubscriptionStatus mapping
|
|
6
|
+
* - sandbox vs production host routing
|
|
7
|
+
* - missing API credentials → returns null (no throw)
|
|
8
|
+
* - HTTP error / empty data → returns null
|
|
9
|
+
* - webhook receives notification for unknown originalTransactionId →
|
|
10
|
+
* calls Status API + saves the returned record
|
|
11
|
+
* - webhook unknown tx without API creds → no fetch, just logs
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
|
|
14
|
+
import express from 'express';
|
|
15
|
+
import { generateKeyPairSync } from 'crypto';
|
|
16
|
+
import { SUBSCRIPTION_STATUS } from '@onesub/shared';
|
|
17
|
+
import { fetchAppleSubscriptionStatus } from '../providers/apple.js';
|
|
18
|
+
import { createWebhookRouter } from '../routes/webhook.js';
|
|
19
|
+
import { InMemorySubscriptionStore, InMemoryPurchaseStore } from '../store.js';
|
|
20
|
+
import { isLocalhostUrl } from './test-utils.js';
|
|
21
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
22
|
+
function makeJws(payload) {
|
|
23
|
+
const header = Buffer.from(JSON.stringify({ alg: 'ES256' })).toString('base64url');
|
|
24
|
+
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
25
|
+
return `${header}.${body}.fakesig`;
|
|
26
|
+
}
|
|
27
|
+
let testEcKey;
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });
|
|
30
|
+
testEcKey = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
31
|
+
});
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.restoreAllMocks();
|
|
34
|
+
});
|
|
35
|
+
function appleConfigWithApi(overrides) {
|
|
36
|
+
return {
|
|
37
|
+
bundleId: 'com.example.app',
|
|
38
|
+
skipJwsVerification: true,
|
|
39
|
+
keyId: 'TESTKEY1',
|
|
40
|
+
issuerId: '12345678-1234-1234-1234-123456789012',
|
|
41
|
+
privateKey: testEcKey,
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Mock global fetch with localhost pass-through. Apple Status API requests
|
|
47
|
+
* resolve to the provided body; localhost (our test http server) is forwarded
|
|
48
|
+
* to the real fetch implementation.
|
|
49
|
+
*/
|
|
50
|
+
function mockAppleStatusFetch(responseBody, opts) {
|
|
51
|
+
const originalFetch = global.fetch;
|
|
52
|
+
const calls = [];
|
|
53
|
+
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
|
|
54
|
+
if (isLocalhostUrl(url)) {
|
|
55
|
+
return originalFetch(url, init);
|
|
56
|
+
}
|
|
57
|
+
const urlStr = String(url);
|
|
58
|
+
calls.push({
|
|
59
|
+
url: urlStr,
|
|
60
|
+
method: init?.method,
|
|
61
|
+
headers: init?.headers,
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
ok: (opts?.status ?? 200) < 400,
|
|
65
|
+
status: opts?.status ?? 200,
|
|
66
|
+
json: async () => responseBody,
|
|
67
|
+
text: async () => JSON.stringify(responseBody),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
return calls;
|
|
71
|
+
}
|
|
72
|
+
function makeStatusResponse(opts) {
|
|
73
|
+
const tx = makeJws({
|
|
74
|
+
bundleId: 'com.example.app',
|
|
75
|
+
type: 'Auto-Renewable Subscription',
|
|
76
|
+
productId: opts.productId ?? 'pro_monthly',
|
|
77
|
+
transactionId: 'tx_inner',
|
|
78
|
+
originalTransactionId: opts.originalTransactionId,
|
|
79
|
+
purchaseDate: Date.now() - 30 * 86400000,
|
|
80
|
+
originalPurchaseDate: Date.now() - 30 * 86400000,
|
|
81
|
+
expiresDate: opts.expiresDate ?? Date.now() + 86400000,
|
|
82
|
+
});
|
|
83
|
+
const renewal = makeJws({ autoRenewStatus: opts.autoRenewStatus ?? 1 });
|
|
84
|
+
return {
|
|
85
|
+
bundleId: 'com.example.app',
|
|
86
|
+
environment: 'Production',
|
|
87
|
+
data: [{
|
|
88
|
+
subscriptionGroupIdentifier: 'group_a',
|
|
89
|
+
lastTransactions: [{
|
|
90
|
+
originalTransactionId: opts.originalTransactionId,
|
|
91
|
+
status: opts.status,
|
|
92
|
+
signedTransactionInfo: tx,
|
|
93
|
+
signedRenewalInfo: renewal,
|
|
94
|
+
}],
|
|
95
|
+
}],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// ── fetchAppleSubscriptionStatus unit tests ─────────────────────────────────
|
|
99
|
+
describe('fetchAppleSubscriptionStatus', () => {
|
|
100
|
+
it('returns null when API credentials are missing', async () => {
|
|
101
|
+
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
102
|
+
const result = await fetchAppleSubscriptionStatus('orig_xyz', {
|
|
103
|
+
bundleId: 'com.example.app',
|
|
104
|
+
skipJwsVerification: true,
|
|
105
|
+
});
|
|
106
|
+
expect(result).toBeNull();
|
|
107
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
it('returns null when mockMode is enabled', async () => {
|
|
110
|
+
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
111
|
+
const result = await fetchAppleSubscriptionStatus('orig_xyz', appleConfigWithApi({ mockMode: true }));
|
|
112
|
+
expect(result).toBeNull();
|
|
113
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
it('GET .../inApps/v1/subscriptions/<id> with Bearer JWT', async () => {
|
|
116
|
+
const calls = mockAppleStatusFetch(makeStatusResponse({
|
|
117
|
+
originalTransactionId: 'orig_active',
|
|
118
|
+
status: 1,
|
|
119
|
+
}));
|
|
120
|
+
await fetchAppleSubscriptionStatus('orig_active', appleConfigWithApi());
|
|
121
|
+
expect(calls).toHaveLength(1);
|
|
122
|
+
expect(calls[0].url).toContain('api.storekit.itunes.apple.com');
|
|
123
|
+
expect(calls[0].url).toContain('/inApps/v1/subscriptions/orig_active');
|
|
124
|
+
expect(calls[0].headers?.Authorization).toMatch(/^Bearer /);
|
|
125
|
+
});
|
|
126
|
+
it('routes to sandbox host when options.sandbox is true', async () => {
|
|
127
|
+
const calls = mockAppleStatusFetch(makeStatusResponse({
|
|
128
|
+
originalTransactionId: 'orig_sb',
|
|
129
|
+
status: 1,
|
|
130
|
+
}));
|
|
131
|
+
await fetchAppleSubscriptionStatus('orig_sb', appleConfigWithApi(), { sandbox: true });
|
|
132
|
+
expect(calls[0].url).toContain('api.storekit-sandbox.itunes.apple.com');
|
|
133
|
+
});
|
|
134
|
+
it('maps status code 1 → active', async () => {
|
|
135
|
+
mockAppleStatusFetch(makeStatusResponse({ originalTransactionId: 'o1', status: 1 }));
|
|
136
|
+
const result = await fetchAppleSubscriptionStatus('o1', appleConfigWithApi());
|
|
137
|
+
expect(result?.status).toBe(SUBSCRIPTION_STATUS.ACTIVE);
|
|
138
|
+
});
|
|
139
|
+
it('maps status code 2 → expired', async () => {
|
|
140
|
+
mockAppleStatusFetch(makeStatusResponse({ originalTransactionId: 'o2', status: 2 }));
|
|
141
|
+
const result = await fetchAppleSubscriptionStatus('o2', appleConfigWithApi());
|
|
142
|
+
expect(result?.status).toBe(SUBSCRIPTION_STATUS.EXPIRED);
|
|
143
|
+
});
|
|
144
|
+
it('maps status code 3 → on_hold (billing retry)', async () => {
|
|
145
|
+
mockAppleStatusFetch(makeStatusResponse({ originalTransactionId: 'o3', status: 3 }));
|
|
146
|
+
const result = await fetchAppleSubscriptionStatus('o3', appleConfigWithApi());
|
|
147
|
+
expect(result?.status).toBe(SUBSCRIPTION_STATUS.ON_HOLD);
|
|
148
|
+
});
|
|
149
|
+
it('maps status code 4 → grace_period', async () => {
|
|
150
|
+
mockAppleStatusFetch(makeStatusResponse({ originalTransactionId: 'o4', status: 4 }));
|
|
151
|
+
const result = await fetchAppleSubscriptionStatus('o4', appleConfigWithApi());
|
|
152
|
+
expect(result?.status).toBe(SUBSCRIPTION_STATUS.GRACE_PERIOD);
|
|
153
|
+
});
|
|
154
|
+
it('maps status code 5 → canceled (revoked)', async () => {
|
|
155
|
+
mockAppleStatusFetch(makeStatusResponse({ originalTransactionId: 'o5', status: 5 }));
|
|
156
|
+
const result = await fetchAppleSubscriptionStatus('o5', appleConfigWithApi());
|
|
157
|
+
expect(result?.status).toBe(SUBSCRIPTION_STATUS.CANCELED);
|
|
158
|
+
});
|
|
159
|
+
it('reads autoRenewStatus from signedRenewalInfo for willRenew', async () => {
|
|
160
|
+
mockAppleStatusFetch(makeStatusResponse({
|
|
161
|
+
originalTransactionId: 'o_renew',
|
|
162
|
+
status: 1,
|
|
163
|
+
autoRenewStatus: 0,
|
|
164
|
+
}));
|
|
165
|
+
const result = await fetchAppleSubscriptionStatus('o_renew', appleConfigWithApi());
|
|
166
|
+
expect(result?.willRenew).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
it('returns null on HTTP 404', async () => {
|
|
169
|
+
mockAppleStatusFetch({ errorCode: 4040010 }, { status: 404 });
|
|
170
|
+
const result = await fetchAppleSubscriptionStatus('not_found', appleConfigWithApi());
|
|
171
|
+
expect(result).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
it('returns null when data array is empty', async () => {
|
|
174
|
+
mockAppleStatusFetch({ bundleId: 'com.example.app', environment: 'Production', data: [] });
|
|
175
|
+
const result = await fetchAppleSubscriptionStatus('orig_empty', appleConfigWithApi());
|
|
176
|
+
expect(result).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
it('returns null when matching originalTransactionId not in lastTransactions', async () => {
|
|
179
|
+
mockAppleStatusFetch({
|
|
180
|
+
bundleId: 'com.example.app',
|
|
181
|
+
environment: 'Production',
|
|
182
|
+
data: [{
|
|
183
|
+
subscriptionGroupIdentifier: 'g',
|
|
184
|
+
lastTransactions: [{
|
|
185
|
+
originalTransactionId: 'different_id',
|
|
186
|
+
status: 1,
|
|
187
|
+
signedTransactionInfo: makeJws({ productId: 'p', expiresDate: Date.now() }),
|
|
188
|
+
}],
|
|
189
|
+
}],
|
|
190
|
+
});
|
|
191
|
+
const result = await fetchAppleSubscriptionStatus('expected_id', appleConfigWithApi());
|
|
192
|
+
expect(result).toBeNull();
|
|
193
|
+
});
|
|
194
|
+
it('preserves originalTransactionId from caller (not from inner JWS)', async () => {
|
|
195
|
+
mockAppleStatusFetch(makeStatusResponse({
|
|
196
|
+
originalTransactionId: 'orig_caller',
|
|
197
|
+
status: 1,
|
|
198
|
+
}));
|
|
199
|
+
const result = await fetchAppleSubscriptionStatus('orig_caller', appleConfigWithApi());
|
|
200
|
+
expect(result?.originalTransactionId).toBe('orig_caller');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
function buildAppleWebhookServer(config) {
|
|
204
|
+
const store = new InMemorySubscriptionStore();
|
|
205
|
+
const purchaseStore = new InMemoryPurchaseStore();
|
|
206
|
+
const app = express();
|
|
207
|
+
app.use(express.json());
|
|
208
|
+
app.use(createWebhookRouter(config, store, purchaseStore));
|
|
209
|
+
return {
|
|
210
|
+
store,
|
|
211
|
+
server: {
|
|
212
|
+
async request(path, body) {
|
|
213
|
+
const httpServer = app.listen(0);
|
|
214
|
+
const address = httpServer.address();
|
|
215
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
216
|
+
try {
|
|
217
|
+
const resp = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers: { 'Content-Type': 'application/json' },
|
|
220
|
+
body: JSON.stringify(body),
|
|
221
|
+
});
|
|
222
|
+
const text = await resp.text();
|
|
223
|
+
let parsed = text;
|
|
224
|
+
try {
|
|
225
|
+
parsed = JSON.parse(text);
|
|
226
|
+
}
|
|
227
|
+
catch { /* keep as text */ }
|
|
228
|
+
return { status: resp.status, body: parsed };
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
await new Promise((resolve) => httpServer.close(() => resolve()));
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function buildUnknownTxNotification(originalTransactionId, opts) {
|
|
238
|
+
const signedTransactionInfo = makeJws({
|
|
239
|
+
bundleId: 'com.example.app',
|
|
240
|
+
type: 'Auto-Renewable Subscription',
|
|
241
|
+
productId: 'pro_monthly',
|
|
242
|
+
transactionId: 'tx_notify',
|
|
243
|
+
originalTransactionId,
|
|
244
|
+
purchaseDate: Date.now(),
|
|
245
|
+
expiresDate: Date.now() + 86400000,
|
|
246
|
+
environment: opts?.sandbox ? 'Sandbox' : 'Production',
|
|
247
|
+
});
|
|
248
|
+
return {
|
|
249
|
+
signedPayload: makeJws({
|
|
250
|
+
notificationType: 'DID_RENEW',
|
|
251
|
+
data: {
|
|
252
|
+
signedTransactionInfo,
|
|
253
|
+
signedRenewalInfo: makeJws({ autoRenewStatus: 1 }),
|
|
254
|
+
},
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
describe('Apple webhook — unknown originalTransactionId fallback', () => {
|
|
259
|
+
it('fetches from Status API and saves a placeholder record when API creds present', async () => {
|
|
260
|
+
mockAppleStatusFetch(makeStatusResponse({
|
|
261
|
+
originalTransactionId: 'orig_unknown',
|
|
262
|
+
status: 1,
|
|
263
|
+
}));
|
|
264
|
+
const config = {
|
|
265
|
+
apple: appleConfigWithApi(),
|
|
266
|
+
database: { url: '' },
|
|
267
|
+
};
|
|
268
|
+
const { store, server } = buildAppleWebhookServer(config);
|
|
269
|
+
const resp = await server.request('/onesub/webhook/apple', buildUnknownTxNotification('orig_unknown'));
|
|
270
|
+
expect(resp.status).toBe(200);
|
|
271
|
+
const saved = await store.getByTransactionId('orig_unknown');
|
|
272
|
+
expect(saved).not.toBeNull();
|
|
273
|
+
expect(saved?.status).toBe(SUBSCRIPTION_STATUS.ACTIVE);
|
|
274
|
+
// userId is unknown at webhook time — placeholder is the originalTransactionId
|
|
275
|
+
expect(saved?.userId).toBe('orig_unknown');
|
|
276
|
+
});
|
|
277
|
+
it('routes to sandbox host when notification environment is Sandbox', async () => {
|
|
278
|
+
const calls = mockAppleStatusFetch(makeStatusResponse({
|
|
279
|
+
originalTransactionId: 'orig_sb_unknown',
|
|
280
|
+
status: 1,
|
|
281
|
+
}));
|
|
282
|
+
const config = {
|
|
283
|
+
apple: appleConfigWithApi(),
|
|
284
|
+
database: { url: '' },
|
|
285
|
+
};
|
|
286
|
+
const { server } = buildAppleWebhookServer(config);
|
|
287
|
+
await server.request('/onesub/webhook/apple', buildUnknownTxNotification('orig_sb_unknown', { sandbox: true }));
|
|
288
|
+
const apiCall = calls.find((c) => c.url.includes('/inApps/v1/subscriptions/'));
|
|
289
|
+
expect(apiCall?.url).toContain('api.storekit-sandbox.itunes.apple.com');
|
|
290
|
+
});
|
|
291
|
+
it('does not call Status API when issuerId/keyId/privateKey are missing', async () => {
|
|
292
|
+
const originalFetch = global.fetch;
|
|
293
|
+
const outboundCalls = [];
|
|
294
|
+
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
|
|
295
|
+
if (isLocalhostUrl(url)) {
|
|
296
|
+
return originalFetch(url, init);
|
|
297
|
+
}
|
|
298
|
+
outboundCalls.push({ url: String(url) });
|
|
299
|
+
return { ok: true, status: 200, json: async () => ({}), text: async () => '' };
|
|
300
|
+
});
|
|
301
|
+
const config = {
|
|
302
|
+
apple: { bundleId: 'com.example.app', skipJwsVerification: true }, // no API creds
|
|
303
|
+
database: { url: '' },
|
|
304
|
+
};
|
|
305
|
+
const { store, server } = buildAppleWebhookServer(config);
|
|
306
|
+
const resp = await server.request('/onesub/webhook/apple', buildUnknownTxNotification('orig_no_creds'));
|
|
307
|
+
expect(resp.status).toBe(200);
|
|
308
|
+
expect(await store.getByTransactionId('orig_no_creds')).toBeNull();
|
|
309
|
+
expect(outboundCalls).toHaveLength(0);
|
|
310
|
+
});
|
|
311
|
+
it('does not overwrite an existing record (still the normal update path)', async () => {
|
|
312
|
+
const originalFetch = global.fetch;
|
|
313
|
+
const outboundCalls = [];
|
|
314
|
+
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
|
|
315
|
+
if (isLocalhostUrl(url)) {
|
|
316
|
+
return originalFetch(url, init);
|
|
317
|
+
}
|
|
318
|
+
outboundCalls.push({ url: String(url) });
|
|
319
|
+
return { ok: true, status: 200, json: async () => ({}), text: async () => '' };
|
|
320
|
+
});
|
|
321
|
+
const config = {
|
|
322
|
+
apple: appleConfigWithApi(),
|
|
323
|
+
database: { url: '' },
|
|
324
|
+
};
|
|
325
|
+
const { store, server } = buildAppleWebhookServer(config);
|
|
326
|
+
const existing = {
|
|
327
|
+
userId: 'real_user',
|
|
328
|
+
productId: 'pro_monthly',
|
|
329
|
+
platform: 'apple',
|
|
330
|
+
status: SUBSCRIPTION_STATUS.ACTIVE,
|
|
331
|
+
expiresAt: '2027-01-01T00:00:00.000Z',
|
|
332
|
+
originalTransactionId: 'orig_known',
|
|
333
|
+
purchasedAt: '2026-01-01T00:00:00.000Z',
|
|
334
|
+
willRenew: true,
|
|
335
|
+
};
|
|
336
|
+
await store.save(existing);
|
|
337
|
+
const resp = await server.request('/onesub/webhook/apple', buildUnknownTxNotification('orig_known'));
|
|
338
|
+
expect(resp.status).toBe(200);
|
|
339
|
+
// Existing record's userId preserved (not replaced with placeholder)
|
|
340
|
+
expect((await store.getByTransactionId('orig_known'))?.userId).toBe('real_user');
|
|
341
|
+
// No outbound Status API call since we already had the record
|
|
342
|
+
expect(outboundCalls.find((c) => c.url.includes('/inApps/v1/subscriptions/'))).toBeUndefined();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
//# sourceMappingURL=apple-status-api.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple-status-api.test.js","sourceRoot":"","sources":["../../src/__tests__/apple-status-api.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for outbound fetch hardening:
|
|
3
|
+
* - Apple Server API JWT cache (re-use within TTL, mint dedup under concurrent calls)
|
|
4
|
+
* - fetchWithTimeout: AbortController fires when upstream hangs longer than the budget
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=fetch-hardening.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-hardening.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/fetch-hardening.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for outbound fetch hardening:
|
|
3
|
+
* - Apple Server API JWT cache (re-use within TTL, mint dedup under concurrent calls)
|
|
4
|
+
* - fetchWithTimeout: AbortController fires when upstream hangs longer than the budget
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
|
|
7
|
+
import { generateKeyPairSync } from 'crypto';
|
|
8
|
+
import { fetchAppleSubscriptionStatus, __testing as appleTesting } from '../providers/apple.js';
|
|
9
|
+
import { fetchWithTimeout } from '../http.js';
|
|
10
|
+
import { isLocalhostUrl } from './test-utils.js';
|
|
11
|
+
let testEcKey;
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });
|
|
14
|
+
testEcKey = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
15
|
+
});
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
appleTesting.clearAppleJwtCacheForTests();
|
|
19
|
+
});
|
|
20
|
+
function appleConfig() {
|
|
21
|
+
return {
|
|
22
|
+
bundleId: 'com.example.app',
|
|
23
|
+
skipJwsVerification: true,
|
|
24
|
+
keyId: 'KEY1',
|
|
25
|
+
issuerId: 'iss-uuid',
|
|
26
|
+
privateKey: testEcKey,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function makeJws(payload) {
|
|
30
|
+
const header = Buffer.from(JSON.stringify({ alg: 'ES256' })).toString('base64url');
|
|
31
|
+
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
32
|
+
return `${header}.${body}.fakesig`;
|
|
33
|
+
}
|
|
34
|
+
function statusResponse(orig) {
|
|
35
|
+
return {
|
|
36
|
+
bundleId: 'com.example.app',
|
|
37
|
+
environment: 'Production',
|
|
38
|
+
data: [{
|
|
39
|
+
lastTransactions: [{
|
|
40
|
+
originalTransactionId: orig,
|
|
41
|
+
status: 1,
|
|
42
|
+
signedTransactionInfo: makeJws({
|
|
43
|
+
bundleId: 'com.example.app',
|
|
44
|
+
productId: 'pro_monthly',
|
|
45
|
+
transactionId: 'tx',
|
|
46
|
+
originalTransactionId: orig,
|
|
47
|
+
purchaseDate: Date.now(),
|
|
48
|
+
originalPurchaseDate: Date.now(),
|
|
49
|
+
expiresDate: Date.now() + 86400000,
|
|
50
|
+
}),
|
|
51
|
+
signedRenewalInfo: makeJws({ autoRenewStatus: 1 }),
|
|
52
|
+
}],
|
|
53
|
+
}],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ── Apple JWT cache ─────────────────────────────────────────────────────────
|
|
57
|
+
describe('makeAppleApiJwt cache', () => {
|
|
58
|
+
it('mints a JWT once per cache window when called sequentially', async () => {
|
|
59
|
+
const originalFetch = global.fetch;
|
|
60
|
+
const calls = [];
|
|
61
|
+
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
|
|
62
|
+
if (isLocalhostUrl(url))
|
|
63
|
+
return originalFetch(url, init);
|
|
64
|
+
const headers = init?.headers;
|
|
65
|
+
calls.push({ url: String(url), auth: headers?.Authorization });
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
status: 200,
|
|
69
|
+
json: async () => statusResponse('orig_a'),
|
|
70
|
+
text: async () => '',
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
await fetchAppleSubscriptionStatus('orig_a', appleConfig());
|
|
74
|
+
await fetchAppleSubscriptionStatus('orig_a', appleConfig());
|
|
75
|
+
await fetchAppleSubscriptionStatus('orig_a', appleConfig());
|
|
76
|
+
// 3 outbound API calls, but the Authorization header (Bearer <jwt>) should
|
|
77
|
+
// be the same JWT in all of them — cache hit on calls 2 and 3.
|
|
78
|
+
expect(calls).toHaveLength(3);
|
|
79
|
+
expect(calls[0].auth).toBeDefined();
|
|
80
|
+
expect(calls[1].auth).toBe(calls[0].auth);
|
|
81
|
+
expect(calls[2].auth).toBe(calls[0].auth);
|
|
82
|
+
});
|
|
83
|
+
it('dedups concurrent JWT mints (single in-flight Promise under burst)', async () => {
|
|
84
|
+
// Use a real key pair sign spy through jose? Indirect — just count fetch
|
|
85
|
+
// Authorization headers across many concurrent calls. With dedup, all
|
|
86
|
+
// share the same JWT.
|
|
87
|
+
const originalFetch = global.fetch;
|
|
88
|
+
const auths = [];
|
|
89
|
+
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
|
|
90
|
+
if (isLocalhostUrl(url))
|
|
91
|
+
return originalFetch(url, init);
|
|
92
|
+
const headers = init?.headers;
|
|
93
|
+
auths.push(headers?.Authorization ?? '');
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
status: 200,
|
|
97
|
+
json: async () => statusResponse('orig_burst'),
|
|
98
|
+
text: async () => '',
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
await Promise.all(Array.from({ length: 10 }, () => fetchAppleSubscriptionStatus('orig_burst', appleConfig())));
|
|
102
|
+
expect(auths).toHaveLength(10);
|
|
103
|
+
const unique = new Set(auths);
|
|
104
|
+
expect(unique.size).toBe(1); // all 10 used the same JWT
|
|
105
|
+
});
|
|
106
|
+
it('mints a fresh JWT after the cache is cleared (e.g. credential rotation)', async () => {
|
|
107
|
+
const originalFetch = global.fetch;
|
|
108
|
+
const auths = [];
|
|
109
|
+
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
|
|
110
|
+
if (isLocalhostUrl(url))
|
|
111
|
+
return originalFetch(url, init);
|
|
112
|
+
const headers = init?.headers;
|
|
113
|
+
auths.push(headers?.Authorization ?? '');
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
status: 200,
|
|
117
|
+
json: async () => statusResponse('orig_clear'),
|
|
118
|
+
text: async () => '',
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
await fetchAppleSubscriptionStatus('orig_clear', appleConfig());
|
|
122
|
+
appleTesting.clearAppleJwtCacheForTests();
|
|
123
|
+
await fetchAppleSubscriptionStatus('orig_clear', appleConfig());
|
|
124
|
+
expect(auths).toHaveLength(2);
|
|
125
|
+
expect(auths[0]).not.toBe(auths[1]); // cache miss → fresh JWT
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
// ── fetchWithTimeout ────────────────────────────────────────────────────────
|
|
129
|
+
describe('fetchWithTimeout', () => {
|
|
130
|
+
it('returns the response when upstream replies before the timeout', async () => {
|
|
131
|
+
vi.spyOn(global, 'fetch').mockImplementation(async () => {
|
|
132
|
+
return { ok: true, status: 200, text: async () => 'hi' };
|
|
133
|
+
});
|
|
134
|
+
const resp = await fetchWithTimeout('https://example.com', undefined, 1000);
|
|
135
|
+
expect(resp.ok).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
it('aborts when upstream hangs longer than the timeout', async () => {
|
|
138
|
+
vi.spyOn(global, 'fetch').mockImplementation((_input, init) => {
|
|
139
|
+
// Simulate a hung server — never resolves until the AbortController fires
|
|
140
|
+
return new Promise((_resolve, reject) => {
|
|
141
|
+
const signal = init?.signal;
|
|
142
|
+
if (signal) {
|
|
143
|
+
signal.addEventListener('abort', () => {
|
|
144
|
+
const reason = signal.reason;
|
|
145
|
+
reject(reason instanceof Error ? reason : new Error('aborted'));
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
const start = Date.now();
|
|
151
|
+
await expect(fetchWithTimeout('https://hung.example', undefined, 100)).rejects.toThrow(/timed out|aborted/i);
|
|
152
|
+
const elapsed = Date.now() - start;
|
|
153
|
+
expect(elapsed).toBeGreaterThanOrEqual(90);
|
|
154
|
+
expect(elapsed).toBeLessThan(500);
|
|
155
|
+
});
|
|
156
|
+
it('respects a caller-provided AbortSignal (composes with timeout)', async () => {
|
|
157
|
+
vi.spyOn(global, 'fetch').mockImplementation((_input, init) => {
|
|
158
|
+
return new Promise((_resolve, reject) => {
|
|
159
|
+
init?.signal?.addEventListener('abort', () => reject(new Error('aborted')));
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
const ac = new AbortController();
|
|
163
|
+
setTimeout(() => ac.abort(new Error('caller cancelled')), 30);
|
|
164
|
+
await expect(fetchWithTimeout('https://hung.example', { signal: ac.signal }, 5000)).rejects.toThrow();
|
|
165
|
+
});
|
|
166
|
+
it('clears its timer on success (no leaked handles)', async () => {
|
|
167
|
+
vi.spyOn(global, 'fetch').mockImplementation(async () => {
|
|
168
|
+
return { ok: true, status: 200, text: async () => 'ok' };
|
|
169
|
+
});
|
|
170
|
+
// 100s timeout, but resolves immediately. If the timer leaked, vitest
|
|
171
|
+
// would hold the process open after the test — vitest's `--detectLeaks`
|
|
172
|
+
// would catch that. Smoke test here is just that the call returns and the
|
|
173
|
+
// process doesn't hang the test runner.
|
|
174
|
+
await fetchWithTimeout('https://example.com', undefined, 100_000);
|
|
175
|
+
// Reaching this line means the timer either fired (it shouldn't) or was
|
|
176
|
+
// cleared. We can't easily inspect `setTimeout` internals, but the suite
|
|
177
|
+
// completing in normal time is the implicit assertion.
|
|
178
|
+
expect(true).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
it('uses the default 10s budget when no timeoutMs is passed', async () => {
|
|
181
|
+
let observedTimeout;
|
|
182
|
+
vi.spyOn(global, 'fetch').mockImplementation((_input, init) => {
|
|
183
|
+
// Snapshot any abort behaviour: we can't read the timeout directly,
|
|
184
|
+
// but we can verify a signal is attached.
|
|
185
|
+
observedTimeout = init?.signal ? 1 : 0;
|
|
186
|
+
return Promise.resolve({ ok: true, status: 200, text: async () => '' });
|
|
187
|
+
});
|
|
188
|
+
await fetchWithTimeout('https://example.com');
|
|
189
|
+
expect(observedTimeout).toBe(1); // signal attached → timeout active
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
//# sourceMappingURL=fetch-hardening.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-hardening.test.js","sourceRoot":"","sources":["../../src/__tests__/fetch-hardening.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the new 'paused' SubscriptionStatus + Google webhook
|
|
3
|
+
* SUBSCRIPTION_PAUSED notification handling.
|
|
4
|
+
*
|
|
5
|
+
* Background: Google supports user-voluntary pause (different from on_hold,
|
|
6
|
+
* which is involuntary payment failure). Surfaced as a distinct status so
|
|
7
|
+
* the host app can render the right UX ("재개 예정" vs "결제 정보 업데이트").
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=google-paused.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google-paused.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/google-paused.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|