@onesub/server 0.11.4 → 0.12.1
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/README.md +119 -2
- package/dist/__tests__/cache.test.d.ts +2 -0
- package/dist/__tests__/cache.test.d.ts.map +1 -0
- package/dist/__tests__/openapi.test.d.ts +2 -0
- package/dist/__tests__/openapi.test.d.ts.map +1 -0
- package/dist/__tests__/redis-store.test.d.ts +2 -0
- package/dist/__tests__/redis-store.test.d.ts.map +1 -0
- package/dist/__tests__/webhook-apple.test.d.ts +12 -0
- package/dist/__tests__/webhook-apple.test.d.ts.map +1 -0
- package/dist/__tests__/webhook-events.test.d.ts +2 -0
- package/dist/__tests__/webhook-events.test.d.ts.map +1 -0
- package/dist/__tests__/webhook-google.test.d.ts +13 -0
- package/dist/__tests__/webhook-google.test.d.ts.map +1 -0
- package/dist/__tests__/webhook-queue.test.d.ts +2 -0
- package/dist/__tests__/webhook-queue.test.d.ts.map +1 -0
- package/dist/cache.d.ts +32 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/index.cjs +3084 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +34 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3045 -118
- package/dist/index.js.map +1 -1
- package/dist/openapi.d.ts +41 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/providers/apple.d.ts +1 -1
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/routes/admin.d.ts +2 -1
- package/dist/routes/admin.d.ts.map +1 -1
- package/dist/routes/webhook-apple.d.ts +6 -0
- package/dist/routes/webhook-apple.d.ts.map +1 -0
- package/dist/routes/webhook-google.d.ts +7 -0
- package/dist/routes/webhook-google.d.ts.map +1 -0
- package/dist/routes/webhook.d.ts +6 -18
- package/dist/routes/webhook.d.ts.map +1 -1
- package/dist/stores/redis.d.ts +60 -0
- package/dist/stores/redis.d.ts.map +1 -0
- package/dist/stores/schema.d.ts.map +1 -1
- package/dist/tracing.d.ts +32 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/webhook-events.d.ts +58 -0
- package/dist/webhook-events.d.ts.map +1 -0
- package/dist/webhook-queue.d.ts +92 -0
- package/dist/webhook-queue.d.ts.map +1 -0
- package/package.json +33 -4
- package/sql/schema.sql +10 -0
- package/dist/__tests__/admin-customer-detail.test.js +0 -146
- package/dist/__tests__/admin-customer-detail.test.js.map +0 -1
- package/dist/__tests__/admin-list-subscriptions.test.js +0 -192
- package/dist/__tests__/admin-list-subscriptions.test.js.map +0 -1
- package/dist/__tests__/admin-subscription-detail.test.js +0 -112
- package/dist/__tests__/admin-subscription-detail.test.js.map +0 -1
- package/dist/__tests__/apple-root-ca.test.js +0 -41
- package/dist/__tests__/apple-root-ca.test.js.map +0 -1
- package/dist/__tests__/apple-status-api.test.js +0 -345
- package/dist/__tests__/apple-status-api.test.js.map +0 -1
- package/dist/__tests__/entitlements.test.js +0 -268
- package/dist/__tests__/entitlements.test.js.map +0 -1
- package/dist/__tests__/error-codes.test.js +0 -168
- package/dist/__tests__/error-codes.test.js.map +0 -1
- package/dist/__tests__/fetch-hardening.test.js +0 -192
- package/dist/__tests__/fetch-hardening.test.js.map +0 -1
- package/dist/__tests__/google-paused.test.js +0 -126
- package/dist/__tests__/google-paused.test.js.map +0 -1
- package/dist/__tests__/google-price-change.test.js +0 -186
- package/dist/__tests__/google-price-change.test.js.map +0 -1
- package/dist/__tests__/google-subscriptions-v2.test.js +0 -206
- package/dist/__tests__/google-subscriptions-v2.test.js.map +0 -1
- package/dist/__tests__/lifecycle-scenarios.test.js +0 -579
- package/dist/__tests__/lifecycle-scenarios.test.js.map +0 -1
- package/dist/__tests__/lifecycle-states.test.js +0 -423
- package/dist/__tests__/lifecycle-states.test.js.map +0 -1
- package/dist/__tests__/linked-purchase-token.test.js +0 -227
- package/dist/__tests__/linked-purchase-token.test.js.map +0 -1
- package/dist/__tests__/metrics.test.js +0 -340
- package/dist/__tests__/metrics.test.js.map +0 -1
- package/dist/__tests__/mock-provider.test.js +0 -188
- package/dist/__tests__/mock-provider.test.js.map +0 -1
- package/dist/__tests__/paused-auto-resume.test.js +0 -162
- package/dist/__tests__/paused-auto-resume.test.js.map +0 -1
- package/dist/__tests__/providers.test.js +0 -321
- package/dist/__tests__/providers.test.js.map +0 -1
- package/dist/__tests__/refund-policy.test.js +0 -317
- package/dist/__tests__/refund-policy.test.js.map +0 -1
- package/dist/__tests__/schema.test.js +0 -46
- package/dist/__tests__/schema.test.js.map +0 -1
- package/dist/__tests__/store.test.js +0 -172
- package/dist/__tests__/store.test.js.map +0 -1
- package/dist/__tests__/test-utils.js +0 -30
- package/dist/__tests__/test-utils.js.map +0 -1
- package/dist/__tests__/webhook-refund.test.js +0 -313
- package/dist/__tests__/webhook-refund.test.js.map +0 -1
- package/dist/errors.js +0 -19
- package/dist/errors.js.map +0 -1
- package/dist/http.js +0 -36
- package/dist/http.js.map +0 -1
- package/dist/logger.js +0 -20
- package/dist/logger.js.map +0 -1
- package/dist/providers/apple-root-ca.js +0 -38
- package/dist/providers/apple-root-ca.js.map +0 -1
- package/dist/providers/apple.js +0 -499
- package/dist/providers/apple.js.map +0 -1
- package/dist/providers/google.js +0 -493
- package/dist/providers/google.js.map +0 -1
- package/dist/providers/mock.js +0 -93
- package/dist/providers/mock.js.map +0 -1
- package/dist/routes/admin.js +0 -251
- package/dist/routes/admin.js.map +0 -1
- package/dist/routes/entitlements.js +0 -130
- package/dist/routes/entitlements.js.map +0 -1
- package/dist/routes/metrics.js +0 -281
- package/dist/routes/metrics.js.map +0 -1
- package/dist/routes/purchase.js +0 -187
- package/dist/routes/purchase.js.map +0 -1
- package/dist/routes/status.js +0 -55
- package/dist/routes/status.js.map +0 -1
- package/dist/routes/validate.js +0 -70
- package/dist/routes/validate.js.map +0 -1
- package/dist/routes/webhook.js +0 -441
- package/dist/routes/webhook.js.map +0 -1
- package/dist/store.js +0 -145
- package/dist/store.js.map +0 -1
- package/dist/stores/postgres.js +0 -329
- package/dist/stores/postgres.js.map +0 -1
- package/dist/stores/schema.js +0 -57
- package/dist/stores/schema.js.map +0 -1
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for Apple and Google product receipt validators.
|
|
3
|
-
* Covers: validateAppleConsumableReceipt, validateGoogleProductReceipt
|
|
4
|
-
*/
|
|
5
|
-
import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest';
|
|
6
|
-
import { generateKeyPairSync } from 'crypto';
|
|
7
|
-
import { validateAppleConsumableReceipt } from '../providers/apple.js';
|
|
8
|
-
import { validateGoogleProductReceipt, acknowledgeGoogleSubscription, acknowledgeGoogleProduct, } from '../providers/google.js';
|
|
9
|
-
import { urlHost } from './test-utils.js';
|
|
10
|
-
// ── Apple helpers ──────────────────────────────────────────────────────────
|
|
11
|
-
/**
|
|
12
|
-
* Build a minimal JWS token whose payload is the given object.
|
|
13
|
-
* With skipJwsVerification=true, decodeJwt() only base64url-decodes the payload.
|
|
14
|
-
*/
|
|
15
|
-
function makeJws(payload) {
|
|
16
|
-
const header = Buffer.from(JSON.stringify({ alg: 'ES256' })).toString('base64url');
|
|
17
|
-
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
18
|
-
return `${header}.${body}.fakesig`;
|
|
19
|
-
}
|
|
20
|
-
const APPLE_CONFIG = { bundleId: 'com.example.app', skipJwsVerification: true };
|
|
21
|
-
function validApplePayload(overrides) {
|
|
22
|
-
return {
|
|
23
|
-
bundleId: 'com.example.app',
|
|
24
|
-
type: 'Consumable',
|
|
25
|
-
productId: 'credits_100',
|
|
26
|
-
transactionId: 'txn_apple_001',
|
|
27
|
-
originalTransactionId: 'orig_apple_001',
|
|
28
|
-
purchaseDate: Date.now(),
|
|
29
|
-
...overrides,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
// ── Google helpers ──────────────────────────────────────────────────────────
|
|
33
|
-
let testPrivateKey;
|
|
34
|
-
beforeAll(() => {
|
|
35
|
-
// Generate a real RSA key pair once — needed for the JWT assertion in getAccessToken().
|
|
36
|
-
// 2048-bit (CodeQL minimum); generated once per suite so test-time cost is negligible.
|
|
37
|
-
const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
|
|
38
|
-
testPrivateKey = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
39
|
-
});
|
|
40
|
-
beforeEach(() => {
|
|
41
|
-
vi.restoreAllMocks();
|
|
42
|
-
});
|
|
43
|
-
/**
|
|
44
|
-
* Create a fresh Google config with a unique client_email per call.
|
|
45
|
-
* The module-level token cache in google.ts is keyed by the full JSON string,
|
|
46
|
-
* so varying the email ensures each test starts with a cold cache.
|
|
47
|
-
*/
|
|
48
|
-
function makeGoogleConfig() {
|
|
49
|
-
return {
|
|
50
|
-
packageName: 'com.example.app',
|
|
51
|
-
serviceAccountKey: JSON.stringify({
|
|
52
|
-
client_email: `test-${Math.random()}@test.iam.gserviceaccount.com`,
|
|
53
|
-
private_key: testPrivateKey,
|
|
54
|
-
token_uri: 'https://oauth2.googleapis.com/token',
|
|
55
|
-
}),
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Mock global fetch with URL-based routing:
|
|
60
|
-
* - oauth2.googleapis.com → returns a fake access token
|
|
61
|
-
* - androidpublisher.googleapis.com → returns the given product purchase object
|
|
62
|
-
*/
|
|
63
|
-
function mockGoogleFetch(productPurchase) {
|
|
64
|
-
vi.spyOn(global, 'fetch').mockImplementation(async (url) => {
|
|
65
|
-
const host = urlHost(url);
|
|
66
|
-
if (host === 'oauth2.googleapis.com') {
|
|
67
|
-
return {
|
|
68
|
-
ok: true,
|
|
69
|
-
json: async () => ({ access_token: 'test_access_token', expires_in: 3600 }),
|
|
70
|
-
text: async () => '',
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
if (host === 'androidpublisher.googleapis.com') {
|
|
74
|
-
return {
|
|
75
|
-
ok: true,
|
|
76
|
-
json: async () => productPurchase,
|
|
77
|
-
text: async () => JSON.stringify(productPurchase),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
throw new Error(`[test] Unexpected fetch URL: ${String(url)}`);
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
// ============================================================================
|
|
84
|
-
// validateAppleConsumableReceipt
|
|
85
|
-
// ============================================================================
|
|
86
|
-
describe('validateAppleConsumableReceipt', () => {
|
|
87
|
-
it('returns null when bundleId is missing in receipt', async () => {
|
|
88
|
-
const jws = makeJws(validApplePayload({ bundleId: undefined }));
|
|
89
|
-
expect(await validateAppleConsumableReceipt(jws, APPLE_CONFIG)).toBeNull();
|
|
90
|
-
});
|
|
91
|
-
it('returns null when bundleId mismatches config', async () => {
|
|
92
|
-
const jws = makeJws(validApplePayload({ bundleId: 'com.attacker.app' }));
|
|
93
|
-
expect(await validateAppleConsumableReceipt(jws, APPLE_CONFIG)).toBeNull();
|
|
94
|
-
});
|
|
95
|
-
it('returns null for Auto-Renewable Subscription type (not a consumable)', async () => {
|
|
96
|
-
const jws = makeJws(validApplePayload({ type: 'Auto-Renewable Subscription' }));
|
|
97
|
-
expect(await validateAppleConsumableReceipt(jws, APPLE_CONFIG)).toBeNull();
|
|
98
|
-
});
|
|
99
|
-
it('returns null for NonRenewingSubscription type', async () => {
|
|
100
|
-
const jws = makeJws(validApplePayload({ type: 'Non-Renewing Subscription' }));
|
|
101
|
-
expect(await validateAppleConsumableReceipt(jws, APPLE_CONFIG)).toBeNull();
|
|
102
|
-
});
|
|
103
|
-
it('returns null when the purchase is revoked (refunded)', async () => {
|
|
104
|
-
const jws = makeJws(validApplePayload({ revocationDate: Date.now() - 1000 }));
|
|
105
|
-
expect(await validateAppleConsumableReceipt(jws, APPLE_CONFIG)).toBeNull();
|
|
106
|
-
});
|
|
107
|
-
it('returns null when receipt is older than 72 hours', async () => {
|
|
108
|
-
const jws = makeJws(validApplePayload({ purchaseDate: Date.now() - 73 * 60 * 60 * 1000 }));
|
|
109
|
-
expect(await validateAppleConsumableReceipt(jws, APPLE_CONFIG)).toBeNull();
|
|
110
|
-
});
|
|
111
|
-
it('accepts a receipt that is exactly 71 hours old', async () => {
|
|
112
|
-
const jws = makeJws(validApplePayload({ purchaseDate: Date.now() - 71 * 60 * 60 * 1000 }));
|
|
113
|
-
expect(await validateAppleConsumableReceipt(jws, APPLE_CONFIG)).not.toBeNull();
|
|
114
|
-
});
|
|
115
|
-
it('returns null when expectedProductId mismatches receipt', async () => {
|
|
116
|
-
const jws = makeJws(validApplePayload({ productId: 'credits_200' }));
|
|
117
|
-
expect(await validateAppleConsumableReceipt(jws, APPLE_CONFIG, 'credits_100')).toBeNull();
|
|
118
|
-
});
|
|
119
|
-
it('validates productId when expectedProductId matches', async () => {
|
|
120
|
-
const jws = makeJws(validApplePayload({ productId: 'credits_100' }));
|
|
121
|
-
expect(await validateAppleConsumableReceipt(jws, APPLE_CONFIG, 'credits_100')).not.toBeNull();
|
|
122
|
-
});
|
|
123
|
-
it('returns null when JWS is malformed', async () => {
|
|
124
|
-
expect(await validateAppleConsumableReceipt('not.a.valid.jws.format', APPLE_CONFIG)).toBeNull();
|
|
125
|
-
});
|
|
126
|
-
it('returns valid result with correct fields for a Consumable purchase', async () => {
|
|
127
|
-
const now = Date.now();
|
|
128
|
-
const jws = makeJws(validApplePayload({
|
|
129
|
-
transactionId: 'txn_abc',
|
|
130
|
-
originalTransactionId: 'orig_abc',
|
|
131
|
-
purchaseDate: now,
|
|
132
|
-
}));
|
|
133
|
-
const result = await validateAppleConsumableReceipt(jws, APPLE_CONFIG);
|
|
134
|
-
expect(result).not.toBeNull();
|
|
135
|
-
expect(result?.transactionId).toBe('txn_abc');
|
|
136
|
-
expect(result?.productId).toBe('credits_100');
|
|
137
|
-
expect(result?.purchasedAt).toBe(new Date(now).toISOString());
|
|
138
|
-
});
|
|
139
|
-
it('uses transactionId (not originalTransactionId) for consumables', async () => {
|
|
140
|
-
const jws = makeJws(validApplePayload({
|
|
141
|
-
transactionId: 'per_tx_id',
|
|
142
|
-
originalTransactionId: 'shared_orig_id',
|
|
143
|
-
}));
|
|
144
|
-
const result = await validateAppleConsumableReceipt(jws, APPLE_CONFIG);
|
|
145
|
-
// per-transaction ID must be used — originalTransactionId is shared across re-purchases
|
|
146
|
-
expect(result?.transactionId).toBe('per_tx_id');
|
|
147
|
-
expect(result?.transactionId).not.toBe('shared_orig_id');
|
|
148
|
-
});
|
|
149
|
-
it('falls back to originalTransactionId when transactionId is absent', async () => {
|
|
150
|
-
const jws = makeJws(validApplePayload({
|
|
151
|
-
transactionId: undefined,
|
|
152
|
-
originalTransactionId: 'orig_only',
|
|
153
|
-
}));
|
|
154
|
-
const result = await validateAppleConsumableReceipt(jws, APPLE_CONFIG);
|
|
155
|
-
expect(result?.transactionId).toBe('orig_only');
|
|
156
|
-
});
|
|
157
|
-
it('accepts Non-Consumable type', async () => {
|
|
158
|
-
const jws = makeJws(validApplePayload({ type: 'Non-Consumable', productId: 'premium_unlock' }));
|
|
159
|
-
const result = await validateAppleConsumableReceipt(jws, APPLE_CONFIG);
|
|
160
|
-
expect(result).not.toBeNull();
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
// ============================================================================
|
|
164
|
-
// validateGoogleProductReceipt
|
|
165
|
-
// ============================================================================
|
|
166
|
-
describe('validateGoogleProductReceipt', () => {
|
|
167
|
-
it('returns null when serviceAccountKey is not provided', async () => {
|
|
168
|
-
const result = await validateGoogleProductReceipt('some_token', 'credits_100', { packageName: 'com.example.app' });
|
|
169
|
-
expect(result).toBeNull();
|
|
170
|
-
});
|
|
171
|
-
it('returns null when purchaseState is 1 (canceled)', async () => {
|
|
172
|
-
mockGoogleFetch({ purchaseState: 1, orderId: 'GPA.canceled' });
|
|
173
|
-
const result = await validateGoogleProductReceipt('token_canceled', 'credits_100', makeGoogleConfig());
|
|
174
|
-
expect(result).toBeNull();
|
|
175
|
-
});
|
|
176
|
-
it('returns null when purchaseState is 2 (pending)', async () => {
|
|
177
|
-
mockGoogleFetch({ purchaseState: 2, orderId: 'GPA.pending' });
|
|
178
|
-
const result = await validateGoogleProductReceipt('token_pending', 'credits_100', makeGoogleConfig());
|
|
179
|
-
expect(result).toBeNull();
|
|
180
|
-
});
|
|
181
|
-
it('returns null for consumable when consumptionState is 1 (already consumed)', async () => {
|
|
182
|
-
mockGoogleFetch({
|
|
183
|
-
purchaseState: 0,
|
|
184
|
-
consumptionState: 1,
|
|
185
|
-
purchaseTimeMillis: String(Date.now()),
|
|
186
|
-
orderId: 'GPA.already_consumed',
|
|
187
|
-
});
|
|
188
|
-
const result = await validateGoogleProductReceipt('token_consumed', 'credits_100', makeGoogleConfig(), 'consumable');
|
|
189
|
-
expect(result).toBeNull();
|
|
190
|
-
});
|
|
191
|
-
it('does NOT block non-consumable when consumptionState is 1', async () => {
|
|
192
|
-
// Non-consumables may have consumptionState=1 after acknowledgement — this is normal
|
|
193
|
-
mockGoogleFetch({
|
|
194
|
-
purchaseState: 0,
|
|
195
|
-
consumptionState: 1,
|
|
196
|
-
purchaseTimeMillis: String(Date.now()),
|
|
197
|
-
orderId: 'GPA.nc_acknowledged',
|
|
198
|
-
});
|
|
199
|
-
const result = await validateGoogleProductReceipt('token_nc', 'premium_unlock', makeGoogleConfig(), 'non_consumable');
|
|
200
|
-
expect(result).not.toBeNull();
|
|
201
|
-
expect(result?.transactionId).toBe('GPA.nc_acknowledged');
|
|
202
|
-
});
|
|
203
|
-
it('returns null when receipt is older than 72 hours', async () => {
|
|
204
|
-
const oldMs = String(Date.now() - 73 * 60 * 60 * 1000);
|
|
205
|
-
mockGoogleFetch({
|
|
206
|
-
purchaseState: 0,
|
|
207
|
-
purchaseTimeMillis: oldMs,
|
|
208
|
-
orderId: 'GPA.old',
|
|
209
|
-
});
|
|
210
|
-
const result = await validateGoogleProductReceipt('token_old', 'credits_100', makeGoogleConfig());
|
|
211
|
-
expect(result).toBeNull();
|
|
212
|
-
});
|
|
213
|
-
it('accepts a receipt that is exactly 71 hours old', async () => {
|
|
214
|
-
const recentMs = String(Date.now() - 71 * 60 * 60 * 1000);
|
|
215
|
-
mockGoogleFetch({
|
|
216
|
-
purchaseState: 0,
|
|
217
|
-
consumptionState: 0,
|
|
218
|
-
purchaseTimeMillis: recentMs,
|
|
219
|
-
orderId: 'GPA.recent',
|
|
220
|
-
});
|
|
221
|
-
const result = await validateGoogleProductReceipt('token_recent', 'credits_100', makeGoogleConfig(), 'consumable');
|
|
222
|
-
expect(result).not.toBeNull();
|
|
223
|
-
});
|
|
224
|
-
it('returns null when orderId is missing', async () => {
|
|
225
|
-
mockGoogleFetch({
|
|
226
|
-
purchaseState: 0,
|
|
227
|
-
purchaseTimeMillis: String(Date.now()),
|
|
228
|
-
// orderId absent
|
|
229
|
-
});
|
|
230
|
-
const result = await validateGoogleProductReceipt('token_no_order', 'credits_100', makeGoogleConfig());
|
|
231
|
-
expect(result).toBeNull();
|
|
232
|
-
});
|
|
233
|
-
it('returns valid result with orderId as transactionId on success', async () => {
|
|
234
|
-
const now = Date.now();
|
|
235
|
-
mockGoogleFetch({
|
|
236
|
-
purchaseState: 0,
|
|
237
|
-
consumptionState: 0,
|
|
238
|
-
purchaseTimeMillis: String(now),
|
|
239
|
-
orderId: 'GPA.valid_order_123',
|
|
240
|
-
});
|
|
241
|
-
const result = await validateGoogleProductReceipt('token_valid', 'credits_100', makeGoogleConfig(), 'consumable');
|
|
242
|
-
expect(result).not.toBeNull();
|
|
243
|
-
expect(result?.transactionId).toBe('GPA.valid_order_123');
|
|
244
|
-
expect(result?.purchasedAt).toBe(new Date(now).toISOString());
|
|
245
|
-
});
|
|
246
|
-
it('returns null when Play API returns an error response', async () => {
|
|
247
|
-
vi.spyOn(global, 'fetch').mockImplementation(async (url) => {
|
|
248
|
-
if (urlHost(url) === 'oauth2.googleapis.com') {
|
|
249
|
-
return {
|
|
250
|
-
ok: true,
|
|
251
|
-
json: async () => ({ access_token: 'tok', expires_in: 3600 }),
|
|
252
|
-
text: async () => '',
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
return { ok: false, text: async () => 'Not Found', json: async () => ({}) };
|
|
256
|
-
});
|
|
257
|
-
const result = await validateGoogleProductReceipt('token_bad', 'credits_100', makeGoogleConfig());
|
|
258
|
-
expect(result).toBeNull();
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
// ============================================================================
|
|
262
|
-
// acknowledgeGoogleSubscription / acknowledgeGoogleProduct
|
|
263
|
-
// ============================================================================
|
|
264
|
-
describe('acknowledgeGoogleSubscription', () => {
|
|
265
|
-
it('POSTs to subscriptions/:acknowledge with empty body', async () => {
|
|
266
|
-
const calls = [];
|
|
267
|
-
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
|
|
268
|
-
calls.push({ url: String(url), method: init?.method, body: init?.body });
|
|
269
|
-
if (urlHost(url) === 'oauth2.googleapis.com') {
|
|
270
|
-
return { ok: true, json: async () => ({ access_token: 'tok', expires_in: 3600 }), text: async () => '' };
|
|
271
|
-
}
|
|
272
|
-
return { ok: true, json: async () => ({}), text: async () => '' };
|
|
273
|
-
});
|
|
274
|
-
await acknowledgeGoogleSubscription('purchase_token_xyz', 'pro_monthly', makeGoogleConfig());
|
|
275
|
-
const ackCall = calls.find((c) => c.url.includes(':acknowledge'));
|
|
276
|
-
expect(ackCall).toBeDefined();
|
|
277
|
-
expect(ackCall?.url).toContain('/purchases/subscriptions/pro_monthly/tokens/purchase_token_xyz:acknowledge');
|
|
278
|
-
expect(ackCall?.method).toBe('POST');
|
|
279
|
-
expect(ackCall?.body).toBe('{}');
|
|
280
|
-
});
|
|
281
|
-
it('does not throw when Play API returns an error (fire-and-forget)', async () => {
|
|
282
|
-
vi.spyOn(global, 'fetch').mockImplementation(async (url) => {
|
|
283
|
-
if (urlHost(url) === 'oauth2.googleapis.com') {
|
|
284
|
-
return { ok: true, json: async () => ({ access_token: 'tok', expires_in: 3600 }), text: async () => '' };
|
|
285
|
-
}
|
|
286
|
-
return { ok: false, status: 500, json: async () => ({}), text: async () => 'oops' };
|
|
287
|
-
});
|
|
288
|
-
await expect(acknowledgeGoogleSubscription('tok', 'pro_monthly', makeGoogleConfig())).resolves.toBeUndefined();
|
|
289
|
-
});
|
|
290
|
-
it('skips when serviceAccountKey is missing', async () => {
|
|
291
|
-
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
292
|
-
await acknowledgeGoogleSubscription('tok', 'pro', { packageName: 'com.example.app' });
|
|
293
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
294
|
-
});
|
|
295
|
-
it('skips when mockMode is true', async () => {
|
|
296
|
-
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
297
|
-
await acknowledgeGoogleSubscription('tok', 'pro', { ...makeGoogleConfig(), mockMode: true });
|
|
298
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
describe('acknowledgeGoogleProduct', () => {
|
|
302
|
-
it('POSTs to products/:acknowledge with empty body', async () => {
|
|
303
|
-
const calls = [];
|
|
304
|
-
vi.spyOn(global, 'fetch').mockImplementation(async (url) => {
|
|
305
|
-
calls.push({ url: String(url) });
|
|
306
|
-
if (urlHost(url) === 'oauth2.googleapis.com') {
|
|
307
|
-
return { ok: true, json: async () => ({ access_token: 'tok', expires_in: 3600 }), text: async () => '' };
|
|
308
|
-
}
|
|
309
|
-
return { ok: true, json: async () => ({}), text: async () => '' };
|
|
310
|
-
});
|
|
311
|
-
await acknowledgeGoogleProduct('tok_nc', 'premium_unlock', makeGoogleConfig());
|
|
312
|
-
const ackCall = calls.find((c) => c.url.includes(':acknowledge'));
|
|
313
|
-
expect(ackCall?.url).toContain('/purchases/products/premium_unlock/tokens/tok_nc:acknowledge');
|
|
314
|
-
});
|
|
315
|
-
it('skips when serviceAccountKey is missing', async () => {
|
|
316
|
-
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
317
|
-
await acknowledgeGoogleProduct('tok', 'p', { packageName: 'com.example.app' });
|
|
318
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
//# sourceMappingURL=providers.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"providers.test.js","sourceRoot":"","sources":["../../src/__tests__/providers.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for OneSubServerConfig.refundPolicy.
|
|
3
|
-
*
|
|
4
|
-
* Covers:
|
|
5
|
-
* - Default ('immediate'): subscription REFUND/REVOKE/voided sets status=canceled
|
|
6
|
-
* - 'until_expiry': keeps status + expiresAt, only flips willRenew=false
|
|
7
|
-
* - IAP refund is unaffected by refundPolicy (always immediate)
|
|
8
|
-
* - status route's stale-record check (active && expiresAt > now)
|
|
9
|
-
*/
|
|
10
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
11
|
-
import express from 'express';
|
|
12
|
-
import { SUBSCRIPTION_STATUS } from '@onesub/shared';
|
|
13
|
-
import { createWebhookRouter } from '../routes/webhook.js';
|
|
14
|
-
import { createStatusRouter } from '../routes/status.js';
|
|
15
|
-
import { InMemorySubscriptionStore, InMemoryPurchaseStore } from '../store.js';
|
|
16
|
-
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
17
|
-
function makeJws(payload) {
|
|
18
|
-
const header = Buffer.from(JSON.stringify({ alg: 'ES256' })).toString('base64url');
|
|
19
|
-
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
20
|
-
return `${header}.${body}.fakesig`;
|
|
21
|
-
}
|
|
22
|
-
function spinUp(handler) {
|
|
23
|
-
return {
|
|
24
|
-
async request(method, path, body) {
|
|
25
|
-
const server = handler.listen(0);
|
|
26
|
-
const address = server.address();
|
|
27
|
-
const port = typeof address === 'object' && address ? address.port : 0;
|
|
28
|
-
try {
|
|
29
|
-
const url = `http://127.0.0.1:${port}${path}`;
|
|
30
|
-
const init = { method };
|
|
31
|
-
if (body !== undefined) {
|
|
32
|
-
init.headers = { 'Content-Type': 'application/json' };
|
|
33
|
-
init.body = JSON.stringify(body);
|
|
34
|
-
}
|
|
35
|
-
const resp = await fetch(url, init);
|
|
36
|
-
const text = await resp.text();
|
|
37
|
-
let parsed = text;
|
|
38
|
-
try {
|
|
39
|
-
parsed = JSON.parse(text);
|
|
40
|
-
}
|
|
41
|
-
catch { /* keep as text */ }
|
|
42
|
-
return { status: resp.status, body: parsed };
|
|
43
|
-
}
|
|
44
|
-
finally {
|
|
45
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
function buildWebhookServer(config) {
|
|
51
|
-
const store = new InMemorySubscriptionStore();
|
|
52
|
-
const purchaseStore = new InMemoryPurchaseStore();
|
|
53
|
-
const app = express();
|
|
54
|
-
app.use(express.json());
|
|
55
|
-
app.use(createWebhookRouter(config, store, purchaseStore));
|
|
56
|
-
return { store, purchaseStore, server: spinUp(app) };
|
|
57
|
-
}
|
|
58
|
-
function buildStatusServer() {
|
|
59
|
-
const store = new InMemorySubscriptionStore();
|
|
60
|
-
const app = express();
|
|
61
|
-
app.use(express.json());
|
|
62
|
-
app.use(createStatusRouter(store));
|
|
63
|
-
return { store, server: spinUp(app) };
|
|
64
|
-
}
|
|
65
|
-
const futureExpiry = '2099-01-01T00:00:00.000Z';
|
|
66
|
-
const pastExpiry = '2024-01-01T00:00:00.000Z';
|
|
67
|
-
const sampleSub = (overrides) => ({
|
|
68
|
-
userId: 'user_x',
|
|
69
|
-
productId: 'pro_monthly',
|
|
70
|
-
platform: 'apple',
|
|
71
|
-
status: 'active',
|
|
72
|
-
expiresAt: futureExpiry,
|
|
73
|
-
originalTransactionId: 'orig_x',
|
|
74
|
-
purchasedAt: '2026-01-01T00:00:00.000Z',
|
|
75
|
-
willRenew: true,
|
|
76
|
-
...overrides,
|
|
77
|
-
});
|
|
78
|
-
function appleSubRefundPayload(originalTransactionId) {
|
|
79
|
-
const signedTransactionInfo = makeJws({
|
|
80
|
-
bundleId: 'com.example.app',
|
|
81
|
-
type: 'Auto-Renewable Subscription',
|
|
82
|
-
productId: 'pro_monthly',
|
|
83
|
-
transactionId: 'tx_refund',
|
|
84
|
-
originalTransactionId,
|
|
85
|
-
purchaseDate: Date.now() - 30 * 86400000,
|
|
86
|
-
expiresDate: Date.now() + 30 * 86400000,
|
|
87
|
-
});
|
|
88
|
-
return {
|
|
89
|
-
signedPayload: makeJws({
|
|
90
|
-
notificationType: 'REFUND',
|
|
91
|
-
data: { signedTransactionInfo, signedRenewalInfo: makeJws({}) },
|
|
92
|
-
}),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
function appleConsumableRefundPayload(transactionId) {
|
|
96
|
-
const signedTransactionInfo = makeJws({
|
|
97
|
-
bundleId: 'com.example.app',
|
|
98
|
-
type: 'Consumable',
|
|
99
|
-
productId: 'credits_100',
|
|
100
|
-
transactionId,
|
|
101
|
-
originalTransactionId: transactionId,
|
|
102
|
-
purchaseDate: Date.now(),
|
|
103
|
-
});
|
|
104
|
-
return {
|
|
105
|
-
signedPayload: makeJws({
|
|
106
|
-
notificationType: 'REFUND',
|
|
107
|
-
data: { signedTransactionInfo, signedRenewalInfo: makeJws({}) },
|
|
108
|
-
}),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
function googleVoidedSubPayload(purchaseToken) {
|
|
112
|
-
const json = JSON.stringify({
|
|
113
|
-
version: '1.0',
|
|
114
|
-
packageName: 'com.example.app',
|
|
115
|
-
eventTimeMillis: String(Date.now()),
|
|
116
|
-
voidedPurchaseNotification: {
|
|
117
|
-
purchaseToken,
|
|
118
|
-
orderId: 'GPA.refunded',
|
|
119
|
-
productType: 1,
|
|
120
|
-
refundType: 1,
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
return {
|
|
124
|
-
message: { data: Buffer.from(json).toString('base64'), messageId: '1' },
|
|
125
|
-
subscription: 's',
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
const appleConfig = {
|
|
129
|
-
bundleId: 'com.example.app',
|
|
130
|
-
skipJwsVerification: true,
|
|
131
|
-
};
|
|
132
|
-
const googleConfig = {
|
|
133
|
-
packageName: 'com.example.app',
|
|
134
|
-
};
|
|
135
|
-
// ── refundPolicy default ('immediate') ──────────────────────────────────────
|
|
136
|
-
describe('refundPolicy default (immediate) — Apple subscription REFUND', () => {
|
|
137
|
-
let store;
|
|
138
|
-
let server;
|
|
139
|
-
beforeEach(() => {
|
|
140
|
-
const built = buildWebhookServer({ apple: appleConfig, database: { url: '' } });
|
|
141
|
-
store = built.store;
|
|
142
|
-
server = built.server;
|
|
143
|
-
});
|
|
144
|
-
it('marks status=canceled on REFUND', async () => {
|
|
145
|
-
await store.save(sampleSub({ originalTransactionId: 'orig_immed' }));
|
|
146
|
-
const resp = await server.request('POST', '/onesub/webhook/apple', appleSubRefundPayload('orig_immed'));
|
|
147
|
-
expect(resp.status).toBe(200);
|
|
148
|
-
expect((await store.getByTransactionId('orig_immed'))?.status).toBe(SUBSCRIPTION_STATUS.CANCELED);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
describe('refundPolicy default (immediate) — Google voided subscription', () => {
|
|
152
|
-
it('marks status=canceled on voidedPurchaseNotification productType=1', async () => {
|
|
153
|
-
const { store, server } = buildWebhookServer({ google: googleConfig, database: { url: '' } });
|
|
154
|
-
await store.save(sampleSub({ platform: 'google', originalTransactionId: 'tok_immed' }));
|
|
155
|
-
const resp = await server.request('POST', '/onesub/webhook/google', googleVoidedSubPayload('tok_immed'));
|
|
156
|
-
expect(resp.status).toBe(200);
|
|
157
|
-
expect((await store.getByTransactionId('tok_immed'))?.status).toBe(SUBSCRIPTION_STATUS.CANCELED);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
// ── refundPolicy 'until_expiry' ─────────────────────────────────────────────
|
|
161
|
-
describe("refundPolicy 'until_expiry' — Apple subscription REFUND", () => {
|
|
162
|
-
let store;
|
|
163
|
-
let server;
|
|
164
|
-
beforeEach(() => {
|
|
165
|
-
const built = buildWebhookServer({
|
|
166
|
-
apple: appleConfig,
|
|
167
|
-
database: { url: '' },
|
|
168
|
-
refundPolicy: 'until_expiry',
|
|
169
|
-
});
|
|
170
|
-
store = built.store;
|
|
171
|
-
server = built.server;
|
|
172
|
-
});
|
|
173
|
-
it('keeps status=active and expiresAt; only flips willRenew=false', async () => {
|
|
174
|
-
const original = sampleSub({
|
|
175
|
-
originalTransactionId: 'orig_keep',
|
|
176
|
-
status: 'active',
|
|
177
|
-
expiresAt: futureExpiry,
|
|
178
|
-
willRenew: true,
|
|
179
|
-
});
|
|
180
|
-
await store.save(original);
|
|
181
|
-
await server.request('POST', '/onesub/webhook/apple', appleSubRefundPayload('orig_keep'));
|
|
182
|
-
const updated = await store.getByTransactionId('orig_keep');
|
|
183
|
-
expect(updated?.status).toBe(SUBSCRIPTION_STATUS.ACTIVE);
|
|
184
|
-
expect(updated?.expiresAt).toBe(futureExpiry);
|
|
185
|
-
expect(updated?.willRenew).toBe(false);
|
|
186
|
-
});
|
|
187
|
-
it('still routes Apple REVOKE the same way (refund-class signal)', async () => {
|
|
188
|
-
await store.save(sampleSub({ originalTransactionId: 'orig_revoke' }));
|
|
189
|
-
const signedTransactionInfo = makeJws({
|
|
190
|
-
bundleId: 'com.example.app',
|
|
191
|
-
type: 'Auto-Renewable Subscription',
|
|
192
|
-
productId: 'pro_monthly',
|
|
193
|
-
transactionId: 'tx_revoke',
|
|
194
|
-
originalTransactionId: 'orig_revoke',
|
|
195
|
-
purchaseDate: Date.now(),
|
|
196
|
-
expiresDate: Date.now() + 86400000,
|
|
197
|
-
});
|
|
198
|
-
const signedPayload = makeJws({
|
|
199
|
-
notificationType: 'REVOKE',
|
|
200
|
-
data: { signedTransactionInfo, signedRenewalInfo: makeJws({}) },
|
|
201
|
-
});
|
|
202
|
-
await server.request('POST', '/onesub/webhook/apple', { signedPayload });
|
|
203
|
-
const updated = await store.getByTransactionId('orig_revoke');
|
|
204
|
-
expect(updated?.status).toBe(SUBSCRIPTION_STATUS.ACTIVE);
|
|
205
|
-
expect(updated?.willRenew).toBe(false);
|
|
206
|
-
});
|
|
207
|
-
it('still applies normal expiry mapping for non-refund notifications (DID_RENEW)', async () => {
|
|
208
|
-
await store.save(sampleSub({ originalTransactionId: 'orig_renew', willRenew: false }));
|
|
209
|
-
const signedTransactionInfo = makeJws({
|
|
210
|
-
bundleId: 'com.example.app',
|
|
211
|
-
type: 'Auto-Renewable Subscription',
|
|
212
|
-
productId: 'pro_monthly',
|
|
213
|
-
transactionId: 'tx_renew',
|
|
214
|
-
originalTransactionId: 'orig_renew',
|
|
215
|
-
purchaseDate: Date.now(),
|
|
216
|
-
expiresDate: Date.now() + 30 * 86400000,
|
|
217
|
-
});
|
|
218
|
-
const signedPayload = makeJws({
|
|
219
|
-
notificationType: 'DID_RENEW',
|
|
220
|
-
data: { signedTransactionInfo, signedRenewalInfo: makeJws({ autoRenewStatus: 1 }) },
|
|
221
|
-
});
|
|
222
|
-
await server.request('POST', '/onesub/webhook/apple', { signedPayload });
|
|
223
|
-
const updated = await store.getByTransactionId('orig_renew');
|
|
224
|
-
expect(updated?.status).toBe(SUBSCRIPTION_STATUS.ACTIVE);
|
|
225
|
-
// willRenew should reflect the renewal info (true), not stay false
|
|
226
|
-
expect(updated?.willRenew).toBe(true);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
describe("refundPolicy 'until_expiry' — Google voided subscription", () => {
|
|
230
|
-
it('keeps status=active and only flips willRenew=false', async () => {
|
|
231
|
-
const { store, server } = buildWebhookServer({
|
|
232
|
-
google: googleConfig,
|
|
233
|
-
database: { url: '' },
|
|
234
|
-
refundPolicy: 'until_expiry',
|
|
235
|
-
});
|
|
236
|
-
await store.save(sampleSub({
|
|
237
|
-
platform: 'google',
|
|
238
|
-
originalTransactionId: 'tok_keep',
|
|
239
|
-
status: 'active',
|
|
240
|
-
willRenew: true,
|
|
241
|
-
}));
|
|
242
|
-
await server.request('POST', '/onesub/webhook/google', googleVoidedSubPayload('tok_keep'));
|
|
243
|
-
const updated = await store.getByTransactionId('tok_keep');
|
|
244
|
-
expect(updated?.status).toBe(SUBSCRIPTION_STATUS.ACTIVE);
|
|
245
|
-
expect(updated?.willRenew).toBe(false);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
// ── IAP refund is unaffected by refundPolicy ────────────────────────────────
|
|
249
|
-
describe('refundPolicy does NOT apply to one-time IAP refunds', () => {
|
|
250
|
-
const iap = (overrides) => ({
|
|
251
|
-
userId: 'u',
|
|
252
|
-
productId: 'credits_100',
|
|
253
|
-
platform: 'apple',
|
|
254
|
-
type: 'consumable',
|
|
255
|
-
transactionId: 'iap_tx',
|
|
256
|
-
purchasedAt: '2026-01-01T00:00:00.000Z',
|
|
257
|
-
quantity: 1,
|
|
258
|
-
...overrides,
|
|
259
|
-
});
|
|
260
|
-
it("Apple IAP REFUND removes the row even when refundPolicy='until_expiry'", async () => {
|
|
261
|
-
const { purchaseStore, server } = buildWebhookServer({
|
|
262
|
-
apple: appleConfig,
|
|
263
|
-
database: { url: '' },
|
|
264
|
-
refundPolicy: 'until_expiry',
|
|
265
|
-
});
|
|
266
|
-
await purchaseStore.savePurchase(iap({ transactionId: 'iap_keep_test' }));
|
|
267
|
-
await server.request('POST', '/onesub/webhook/apple', appleConsumableRefundPayload('iap_keep_test'));
|
|
268
|
-
expect(await purchaseStore.getPurchaseByTransactionId('iap_keep_test')).toBeNull();
|
|
269
|
-
});
|
|
270
|
-
it("Google voided IAP (productType=2) deletes the row even when refundPolicy='until_expiry'", async () => {
|
|
271
|
-
const { purchaseStore, server } = buildWebhookServer({
|
|
272
|
-
google: googleConfig,
|
|
273
|
-
database: { url: '' },
|
|
274
|
-
refundPolicy: 'until_expiry',
|
|
275
|
-
});
|
|
276
|
-
await purchaseStore.savePurchase(iap({ platform: 'google', transactionId: 'GPA.iap_keep' }));
|
|
277
|
-
const json = JSON.stringify({
|
|
278
|
-
version: '1.0',
|
|
279
|
-
packageName: 'com.example.app',
|
|
280
|
-
eventTimeMillis: String(Date.now()),
|
|
281
|
-
voidedPurchaseNotification: {
|
|
282
|
-
purchaseToken: 'tok',
|
|
283
|
-
orderId: 'GPA.iap_keep',
|
|
284
|
-
productType: 2,
|
|
285
|
-
refundType: 1,
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
await server.request('POST', '/onesub/webhook/google', {
|
|
289
|
-
message: { data: Buffer.from(json).toString('base64'), messageId: '1' },
|
|
290
|
-
subscription: 's',
|
|
291
|
-
});
|
|
292
|
-
expect(await purchaseStore.getPurchaseByTransactionId('GPA.iap_keep')).toBeNull();
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
// ── status route stale-record safety ────────────────────────────────────────
|
|
296
|
-
describe('status route — stale active record', () => {
|
|
297
|
-
it('returns active=false when status=active but expiresAt is in the past', async () => {
|
|
298
|
-
const { store, server } = buildStatusServer();
|
|
299
|
-
await store.save(sampleSub({ userId: 'u_stale', status: 'active', expiresAt: pastExpiry }));
|
|
300
|
-
const resp = await server.request('GET', '/onesub/status?userId=u_stale');
|
|
301
|
-
expect(resp.status).toBe(200);
|
|
302
|
-
expect(resp.body.active).toBe(false);
|
|
303
|
-
});
|
|
304
|
-
it('returns active=true when status=active and expiresAt is in the future (normal case)', async () => {
|
|
305
|
-
const { store, server } = buildStatusServer();
|
|
306
|
-
await store.save(sampleSub({ userId: 'u_ok', status: 'active', expiresAt: futureExpiry }));
|
|
307
|
-
const resp = await server.request('GET', '/onesub/status?userId=u_ok');
|
|
308
|
-
expect(resp.body.active).toBe(true);
|
|
309
|
-
});
|
|
310
|
-
it('returns active=false for grace_period record past expiresAt', async () => {
|
|
311
|
-
const { store, server } = buildStatusServer();
|
|
312
|
-
await store.save(sampleSub({ userId: 'u_grace_stale', status: 'grace_period', expiresAt: pastExpiry }));
|
|
313
|
-
const resp = await server.request('GET', '/onesub/status?userId=u_grace_stale');
|
|
314
|
-
expect(resp.body.active).toBe(false);
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
//# sourceMappingURL=refund-policy.test.js.map
|