@onesub/server 0.14.0 → 0.16.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__/google-subscriptions-v2.test.d.ts +1 -1
- package/dist/__tests__/receipt-age.test.d.ts +7 -0
- package/dist/__tests__/receipt-age.test.d.ts.map +1 -0
- package/dist/__tests__/tracing.test.d.ts +2 -0
- package/dist/__tests__/tracing.test.d.ts.map +1 -0
- package/dist/cache.d.ts +5 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/index.cjs +178 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +178 -55
- package/dist/index.js.map +1 -1
- package/dist/providers/apple.d.ts.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/mock.d.ts.map +1 -1
- package/dist/routes/admin.d.ts.map +1 -1
- package/dist/routes/purchase.d.ts.map +1 -1
- package/dist/routes/validate.d.ts.map +1 -1
- package/dist/routes/webhook-apple.d.ts.map +1 -1
- package/dist/routes/webhook-google.d.ts +1 -1
- package/dist/routes/webhook-google.d.ts.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/stores/postgres.d.ts.map +1 -1
- package/dist/stores/redis.d.ts +1 -0
- package/dist/stores/redis.d.ts.map +1 -1
- package/dist/tracing.d.ts.map +1 -1
- package/dist/webhook-events.d.ts +10 -0
- package/dist/webhook-events.d.ts.map +1 -1
- package/package.json +5 -3
- 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__/cache.test.js +0 -34
- package/dist/__tests__/cache.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 -173
- 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__/openapi.test.js +0 -32
- package/dist/__tests__/openapi.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__/redis-store.test.js +0 -166
- package/dist/__tests__/redis-store.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-apple.test.js +0 -247
- package/dist/__tests__/webhook-apple.test.js.map +0 -1
- package/dist/__tests__/webhook-events.test.js +0 -36
- package/dist/__tests__/webhook-events.test.js.map +0 -1
- package/dist/__tests__/webhook-google.test.js +0 -255
- package/dist/__tests__/webhook-google.test.js.map +0 -1
- package/dist/__tests__/webhook-queue.test.js +0 -26
- package/dist/__tests__/webhook-queue.test.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/cache.js +0 -39
- package/dist/cache.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/openapi.js +0 -196
- package/dist/openapi.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 -630
- package/dist/providers/apple.js.map +0 -1
- package/dist/providers/google.js +0 -564
- 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 -327
- package/dist/routes/admin.js.map +0 -1
- package/dist/routes/apple-offer.js +0 -72
- package/dist/routes/apple-offer.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 -204
- 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-apple.js +0 -145
- package/dist/routes/webhook-apple.js.map +0 -1
- package/dist/routes/webhook-google.js +0 -204
- package/dist/routes/webhook-google.js.map +0 -1
- package/dist/routes/webhook.js +0 -19
- 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/redis.js +0 -297
- package/dist/stores/redis.js.map +0 -1
- package/dist/stores/schema.js +0 -67
- package/dist/stores/schema.js.map +0 -1
- package/dist/tracing.js +0 -74
- package/dist/tracing.js.map +0 -1
- package/dist/webhook-events.js +0 -58
- package/dist/webhook-events.js.map +0 -1
- package/dist/webhook-queue.js +0 -108
- package/dist/webhook-queue.js.map +0 -1
|
@@ -1,192 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-hardening.test.js","sourceRoot":"","sources":["../../src/__tests__/fetch-hardening.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,126 +0,0 @@
|
|
|
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
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
10
|
-
import express from 'express';
|
|
11
|
-
import { SUBSCRIPTION_STATUS } from '@onesub/shared';
|
|
12
|
-
import { createWebhookRouter } from '../routes/webhook.js';
|
|
13
|
-
import { createStatusRouter } from '../routes/status.js';
|
|
14
|
-
import { InMemorySubscriptionStore, InMemoryPurchaseStore } from '../store.js';
|
|
15
|
-
function spinUp(handler) {
|
|
16
|
-
return {
|
|
17
|
-
async request(method, path, body) {
|
|
18
|
-
const server = handler.listen(0);
|
|
19
|
-
const address = server.address();
|
|
20
|
-
const port = typeof address === 'object' && address ? address.port : 0;
|
|
21
|
-
try {
|
|
22
|
-
const init = { method };
|
|
23
|
-
if (body !== undefined) {
|
|
24
|
-
init.headers = { 'Content-Type': 'application/json' };
|
|
25
|
-
init.body = JSON.stringify(body);
|
|
26
|
-
}
|
|
27
|
-
const resp = await fetch(`http://127.0.0.1:${port}${path}`, init);
|
|
28
|
-
const text = await resp.text();
|
|
29
|
-
let parsed = text;
|
|
30
|
-
try {
|
|
31
|
-
parsed = JSON.parse(text);
|
|
32
|
-
}
|
|
33
|
-
catch { /* keep as text */ }
|
|
34
|
-
return { status: resp.status, body: parsed };
|
|
35
|
-
}
|
|
36
|
-
finally {
|
|
37
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
const sampleSub = (overrides) => ({
|
|
43
|
-
userId: 'user_pause',
|
|
44
|
-
productId: 'pro_monthly',
|
|
45
|
-
platform: 'google',
|
|
46
|
-
status: 'active',
|
|
47
|
-
expiresAt: '2099-01-01T00:00:00.000Z',
|
|
48
|
-
originalTransactionId: 'tok_pause',
|
|
49
|
-
purchasedAt: '2026-01-01T00:00:00.000Z',
|
|
50
|
-
willRenew: true,
|
|
51
|
-
...overrides,
|
|
52
|
-
});
|
|
53
|
-
function googlePushBody(notificationType, purchaseToken = 'tok_pause') {
|
|
54
|
-
const json = JSON.stringify({
|
|
55
|
-
version: '1.0',
|
|
56
|
-
packageName: 'com.example.app',
|
|
57
|
-
eventTimeMillis: String(Date.now()),
|
|
58
|
-
subscriptionNotification: {
|
|
59
|
-
version: '1.0',
|
|
60
|
-
notificationType,
|
|
61
|
-
purchaseToken,
|
|
62
|
-
subscriptionId: 'pro_monthly',
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
return {
|
|
66
|
-
message: { data: Buffer.from(json).toString('base64'), messageId: '1' },
|
|
67
|
-
subscription: 's',
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
// ── webhook PAUSED → status=paused ──────────────────────────────────────────
|
|
71
|
-
describe('Google webhook — SUBSCRIPTION_PAUSED (10)', () => {
|
|
72
|
-
let store;
|
|
73
|
-
let server;
|
|
74
|
-
beforeEach(() => {
|
|
75
|
-
const config = {
|
|
76
|
-
google: { packageName: 'com.example.app' }, // no serviceAccountKey → no Play API re-fetch
|
|
77
|
-
database: { url: '' },
|
|
78
|
-
};
|
|
79
|
-
store = new InMemorySubscriptionStore();
|
|
80
|
-
const purchaseStore = new InMemoryPurchaseStore();
|
|
81
|
-
const app = express();
|
|
82
|
-
app.use(express.json());
|
|
83
|
-
app.use(createWebhookRouter(config, store, purchaseStore));
|
|
84
|
-
server = spinUp(app);
|
|
85
|
-
});
|
|
86
|
-
it('maps PAUSED notification to status=paused', async () => {
|
|
87
|
-
await store.save(sampleSub({ originalTransactionId: 'tok_pause' }));
|
|
88
|
-
const resp = await server.request('POST', '/onesub/webhook/google', googlePushBody(10));
|
|
89
|
-
expect(resp.status).toBe(200);
|
|
90
|
-
expect((await store.getByTransactionId('tok_pause'))?.status).toBe(SUBSCRIPTION_STATUS.PAUSED);
|
|
91
|
-
});
|
|
92
|
-
it('paused is distinct from on_hold (different lifecycle signal)', async () => {
|
|
93
|
-
await store.save(sampleSub({ originalTransactionId: 'tok_a' }));
|
|
94
|
-
await store.save(sampleSub({ userId: 'user_hold', originalTransactionId: 'tok_b' }));
|
|
95
|
-
await server.request('POST', '/onesub/webhook/google', googlePushBody(10, 'tok_a')); // PAUSED
|
|
96
|
-
await server.request('POST', '/onesub/webhook/google', googlePushBody(5, 'tok_b')); // ON_HOLD
|
|
97
|
-
expect((await store.getByTransactionId('tok_a'))?.status).toBe(SUBSCRIPTION_STATUS.PAUSED);
|
|
98
|
-
expect((await store.getByTransactionId('tok_b'))?.status).toBe(SUBSCRIPTION_STATUS.ON_HOLD);
|
|
99
|
-
});
|
|
100
|
-
it('SUBSCRIPTION_RESTARTED (7) recovers entitlement after PAUSED', async () => {
|
|
101
|
-
await store.save(sampleSub({ originalTransactionId: 'tok_resume', status: 'paused' }));
|
|
102
|
-
await server.request('POST', '/onesub/webhook/google', googlePushBody(7, 'tok_resume'));
|
|
103
|
-
expect((await store.getByTransactionId('tok_resume'))?.status).toBe(SUBSCRIPTION_STATUS.ACTIVE);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
// ── status route entitlement ────────────────────────────────────────────────
|
|
107
|
-
describe('status route — paused', () => {
|
|
108
|
-
let store;
|
|
109
|
-
let server;
|
|
110
|
-
beforeEach(() => {
|
|
111
|
-
store = new InMemorySubscriptionStore();
|
|
112
|
-
const app = express();
|
|
113
|
-
app.use(express.json());
|
|
114
|
-
app.use(createStatusRouter(store));
|
|
115
|
-
server = spinUp(app);
|
|
116
|
-
});
|
|
117
|
-
it('paused → active=false (entitlement revoked while paused)', async () => {
|
|
118
|
-
await store.save(sampleSub({ userId: 'u_paused', status: 'paused' }));
|
|
119
|
-
const resp = await server.request('GET', '/onesub/status?userId=u_paused');
|
|
120
|
-
expect(resp.status).toBe(200);
|
|
121
|
-
expect(resp.body.active).toBe(false);
|
|
122
|
-
// subscription record still returned so the host can show "재개 예정" UX
|
|
123
|
-
expect(resp.body.subscription?.status).toBe('paused');
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
//# sourceMappingURL=google-paused.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"google-paused.test.js","sourceRoot":"","sources":["../../src/__tests__/google-paused.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Google webhook SUBSCRIPTION_PRICE_CHANGE_CONFIRMED (8) handling
|
|
3
|
-
* + the onPriceChangeConfirmed hook.
|
|
4
|
-
*/
|
|
5
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
6
|
-
import express from 'express';
|
|
7
|
-
import { SUBSCRIPTION_STATUS } from '@onesub/shared';
|
|
8
|
-
import { createWebhookRouter } from '../routes/webhook.js';
|
|
9
|
-
import { InMemorySubscriptionStore, InMemoryPurchaseStore } from '../store.js';
|
|
10
|
-
function spinUp(handler) {
|
|
11
|
-
return {
|
|
12
|
-
async request(path, body) {
|
|
13
|
-
const server = handler.listen(0);
|
|
14
|
-
const address = server.address();
|
|
15
|
-
const port = typeof address === 'object' && address ? address.port : 0;
|
|
16
|
-
try {
|
|
17
|
-
const resp = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
18
|
-
method: 'POST',
|
|
19
|
-
headers: { 'Content-Type': 'application/json' },
|
|
20
|
-
body: JSON.stringify(body),
|
|
21
|
-
});
|
|
22
|
-
const text = await resp.text();
|
|
23
|
-
let parsed = text;
|
|
24
|
-
try {
|
|
25
|
-
parsed = JSON.parse(text);
|
|
26
|
-
}
|
|
27
|
-
catch { /* keep as text */ }
|
|
28
|
-
return { status: resp.status, body: parsed };
|
|
29
|
-
}
|
|
30
|
-
finally {
|
|
31
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
const sampleSub = (overrides) => ({
|
|
37
|
-
userId: 'user_pc',
|
|
38
|
-
productId: 'pro_monthly',
|
|
39
|
-
platform: 'google',
|
|
40
|
-
status: 'active',
|
|
41
|
-
expiresAt: '2099-01-01T00:00:00.000Z',
|
|
42
|
-
originalTransactionId: 'tok_pc',
|
|
43
|
-
purchasedAt: '2026-01-01T00:00:00.000Z',
|
|
44
|
-
willRenew: true,
|
|
45
|
-
...overrides,
|
|
46
|
-
});
|
|
47
|
-
function priceChangePushBody(purchaseToken = 'tok_pc') {
|
|
48
|
-
const json = JSON.stringify({
|
|
49
|
-
version: '1.0',
|
|
50
|
-
packageName: 'com.example.app',
|
|
51
|
-
eventTimeMillis: String(Date.now()),
|
|
52
|
-
subscriptionNotification: {
|
|
53
|
-
version: '1.0',
|
|
54
|
-
notificationType: 8, // SUBSCRIPTION_PRICE_CHANGE_CONFIRMED
|
|
55
|
-
purchaseToken,
|
|
56
|
-
subscriptionId: 'pro_monthly',
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
return {
|
|
60
|
-
message: { data: Buffer.from(json).toString('base64'), messageId: '1' },
|
|
61
|
-
subscription: 's',
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
function buildServer(config) {
|
|
65
|
-
const store = new InMemorySubscriptionStore();
|
|
66
|
-
const purchaseStore = new InMemoryPurchaseStore();
|
|
67
|
-
const app = express();
|
|
68
|
-
app.use(express.json());
|
|
69
|
-
app.use(createWebhookRouter(config, store, purchaseStore));
|
|
70
|
-
return { store, server: spinUp(app) };
|
|
71
|
-
}
|
|
72
|
-
describe('Google webhook — SUBSCRIPTION_PRICE_CHANGE_CONFIRMED (8)', () => {
|
|
73
|
-
it('keeps status=active for the existing record', async () => {
|
|
74
|
-
const { store, server } = buildServer({
|
|
75
|
-
google: { packageName: 'com.example.app' },
|
|
76
|
-
database: { url: '' },
|
|
77
|
-
});
|
|
78
|
-
await store.save(sampleSub({ originalTransactionId: 'tok_keep' }));
|
|
79
|
-
const resp = await server.request('/onesub/webhook/google', priceChangePushBody('tok_keep'));
|
|
80
|
-
expect(resp.status).toBe(200);
|
|
81
|
-
expect((await store.getByTransactionId('tok_keep'))?.status).toBe(SUBSCRIPTION_STATUS.ACTIVE);
|
|
82
|
-
});
|
|
83
|
-
it('invokes onPriceChangeConfirmed hook with correct context', async () => {
|
|
84
|
-
const captured = [];
|
|
85
|
-
const { store, server } = buildServer({
|
|
86
|
-
google: {
|
|
87
|
-
packageName: 'com.example.app',
|
|
88
|
-
onPriceChangeConfirmed: (ctx) => {
|
|
89
|
-
captured.push(ctx);
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
database: { url: '' },
|
|
93
|
-
});
|
|
94
|
-
await store.save(sampleSub({ originalTransactionId: 'tok_hook' }));
|
|
95
|
-
await server.request('/onesub/webhook/google', priceChangePushBody('tok_hook'));
|
|
96
|
-
// Hook is fire-and-forget — give microtask queue a tick
|
|
97
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
98
|
-
expect(captured).toHaveLength(1);
|
|
99
|
-
expect(captured[0]).toEqual({
|
|
100
|
-
purchaseToken: 'tok_hook',
|
|
101
|
-
subscriptionId: 'pro_monthly',
|
|
102
|
-
packageName: 'com.example.app',
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
it('async hook is awaited internally without blocking the webhook response', async () => {
|
|
106
|
-
let hookResolved = false;
|
|
107
|
-
const { store, server } = buildServer({
|
|
108
|
-
google: {
|
|
109
|
-
packageName: 'com.example.app',
|
|
110
|
-
onPriceChangeConfirmed: async () => {
|
|
111
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
112
|
-
hookResolved = true;
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
database: { url: '' },
|
|
116
|
-
});
|
|
117
|
-
await store.save(sampleSub({ originalTransactionId: 'tok_async' }));
|
|
118
|
-
const start = Date.now();
|
|
119
|
-
const resp = await server.request('/onesub/webhook/google', priceChangePushBody('tok_async'));
|
|
120
|
-
const responseLatency = Date.now() - start;
|
|
121
|
-
// Webhook returned 200 fast — host's slow hook didn't block
|
|
122
|
-
expect(resp.status).toBe(200);
|
|
123
|
-
expect(responseLatency).toBeLessThan(50);
|
|
124
|
-
expect(hookResolved).toBe(false);
|
|
125
|
-
// Hook still completes eventually
|
|
126
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
127
|
-
expect(hookResolved).toBe(true);
|
|
128
|
-
});
|
|
129
|
-
it('hook errors are swallowed (webhook still returns 200)', async () => {
|
|
130
|
-
const { store, server } = buildServer({
|
|
131
|
-
google: {
|
|
132
|
-
packageName: 'com.example.app',
|
|
133
|
-
onPriceChangeConfirmed: () => {
|
|
134
|
-
throw new Error('boom');
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
database: { url: '' },
|
|
138
|
-
});
|
|
139
|
-
await store.save(sampleSub({ originalTransactionId: 'tok_throw' }));
|
|
140
|
-
const resp = await server.request('/onesub/webhook/google', priceChangePushBody('tok_throw'));
|
|
141
|
-
expect(resp.status).toBe(200);
|
|
142
|
-
});
|
|
143
|
-
it('does nothing extra when hook is not configured', async () => {
|
|
144
|
-
const { store, server } = buildServer({
|
|
145
|
-
google: { packageName: 'com.example.app' },
|
|
146
|
-
database: { url: '' },
|
|
147
|
-
});
|
|
148
|
-
await store.save(sampleSub({ originalTransactionId: 'tok_nohook' }));
|
|
149
|
-
const resp = await server.request('/onesub/webhook/google', priceChangePushBody('tok_nohook'));
|
|
150
|
-
expect(resp.status).toBe(200);
|
|
151
|
-
// Subscription still active — webhook didn't fail just because no hook
|
|
152
|
-
expect((await store.getByTransactionId('tok_nohook'))?.status).toBe(SUBSCRIPTION_STATUS.ACTIVE);
|
|
153
|
-
});
|
|
154
|
-
it('hook is NOT invoked for other notification types (e.g. DID_RENEW)', async () => {
|
|
155
|
-
const hookSpy = vi.fn();
|
|
156
|
-
const { store, server } = buildServer({
|
|
157
|
-
google: {
|
|
158
|
-
packageName: 'com.example.app',
|
|
159
|
-
onPriceChangeConfirmed: hookSpy,
|
|
160
|
-
},
|
|
161
|
-
database: { url: '' },
|
|
162
|
-
});
|
|
163
|
-
await store.save(sampleSub({ originalTransactionId: 'tok_other' }));
|
|
164
|
-
const renewBody = {
|
|
165
|
-
message: {
|
|
166
|
-
data: Buffer.from(JSON.stringify({
|
|
167
|
-
version: '1.0',
|
|
168
|
-
packageName: 'com.example.app',
|
|
169
|
-
eventTimeMillis: String(Date.now()),
|
|
170
|
-
subscriptionNotification: {
|
|
171
|
-
version: '1.0',
|
|
172
|
-
notificationType: 2, // SUBSCRIPTION_RENEWED
|
|
173
|
-
purchaseToken: 'tok_other',
|
|
174
|
-
subscriptionId: 'pro_monthly',
|
|
175
|
-
},
|
|
176
|
-
})).toString('base64'),
|
|
177
|
-
messageId: '1',
|
|
178
|
-
},
|
|
179
|
-
subscription: 's',
|
|
180
|
-
};
|
|
181
|
-
await server.request('/onesub/webhook/google', renewBody);
|
|
182
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
183
|
-
expect(hookSpy).not.toHaveBeenCalled();
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
//# sourceMappingURL=google-price-change.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"google-price-change.test.js","sourceRoot":"","sources":["../../src/__tests__/google-price-change.test.ts"],"names":[],"mappings":"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"}
|