@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.
Files changed (140) hide show
  1. package/dist/__tests__/google-subscriptions-v2.test.d.ts +1 -1
  2. package/dist/__tests__/receipt-age.test.d.ts +7 -0
  3. package/dist/__tests__/receipt-age.test.d.ts.map +1 -0
  4. package/dist/__tests__/tracing.test.d.ts +2 -0
  5. package/dist/__tests__/tracing.test.d.ts.map +1 -0
  6. package/dist/cache.d.ts +5 -0
  7. package/dist/cache.d.ts.map +1 -1
  8. package/dist/index.cjs +178 -55
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.js +178 -55
  11. package/dist/index.js.map +1 -1
  12. package/dist/providers/apple.d.ts.map +1 -1
  13. package/dist/providers/google.d.ts.map +1 -1
  14. package/dist/providers/mock.d.ts.map +1 -1
  15. package/dist/routes/admin.d.ts.map +1 -1
  16. package/dist/routes/purchase.d.ts.map +1 -1
  17. package/dist/routes/validate.d.ts.map +1 -1
  18. package/dist/routes/webhook-apple.d.ts.map +1 -1
  19. package/dist/routes/webhook-google.d.ts +1 -1
  20. package/dist/routes/webhook-google.d.ts.map +1 -1
  21. package/dist/store.d.ts.map +1 -1
  22. package/dist/stores/postgres.d.ts.map +1 -1
  23. package/dist/stores/redis.d.ts +1 -0
  24. package/dist/stores/redis.d.ts.map +1 -1
  25. package/dist/tracing.d.ts.map +1 -1
  26. package/dist/webhook-events.d.ts +10 -0
  27. package/dist/webhook-events.d.ts.map +1 -1
  28. package/package.json +5 -3
  29. package/dist/__tests__/admin-customer-detail.test.js +0 -146
  30. package/dist/__tests__/admin-customer-detail.test.js.map +0 -1
  31. package/dist/__tests__/admin-list-subscriptions.test.js +0 -192
  32. package/dist/__tests__/admin-list-subscriptions.test.js.map +0 -1
  33. package/dist/__tests__/admin-subscription-detail.test.js +0 -112
  34. package/dist/__tests__/admin-subscription-detail.test.js.map +0 -1
  35. package/dist/__tests__/apple-root-ca.test.js +0 -41
  36. package/dist/__tests__/apple-root-ca.test.js.map +0 -1
  37. package/dist/__tests__/apple-status-api.test.js +0 -345
  38. package/dist/__tests__/apple-status-api.test.js.map +0 -1
  39. package/dist/__tests__/cache.test.js +0 -34
  40. package/dist/__tests__/cache.test.js.map +0 -1
  41. package/dist/__tests__/entitlements.test.js +0 -268
  42. package/dist/__tests__/entitlements.test.js.map +0 -1
  43. package/dist/__tests__/error-codes.test.js +0 -173
  44. package/dist/__tests__/error-codes.test.js.map +0 -1
  45. package/dist/__tests__/fetch-hardening.test.js +0 -192
  46. package/dist/__tests__/fetch-hardening.test.js.map +0 -1
  47. package/dist/__tests__/google-paused.test.js +0 -126
  48. package/dist/__tests__/google-paused.test.js.map +0 -1
  49. package/dist/__tests__/google-price-change.test.js +0 -186
  50. package/dist/__tests__/google-price-change.test.js.map +0 -1
  51. package/dist/__tests__/google-subscriptions-v2.test.js +0 -206
  52. package/dist/__tests__/google-subscriptions-v2.test.js.map +0 -1
  53. package/dist/__tests__/lifecycle-scenarios.test.js +0 -579
  54. package/dist/__tests__/lifecycle-scenarios.test.js.map +0 -1
  55. package/dist/__tests__/lifecycle-states.test.js +0 -423
  56. package/dist/__tests__/lifecycle-states.test.js.map +0 -1
  57. package/dist/__tests__/linked-purchase-token.test.js +0 -227
  58. package/dist/__tests__/linked-purchase-token.test.js.map +0 -1
  59. package/dist/__tests__/metrics.test.js +0 -340
  60. package/dist/__tests__/metrics.test.js.map +0 -1
  61. package/dist/__tests__/mock-provider.test.js +0 -188
  62. package/dist/__tests__/mock-provider.test.js.map +0 -1
  63. package/dist/__tests__/openapi.test.js +0 -32
  64. package/dist/__tests__/openapi.test.js.map +0 -1
  65. package/dist/__tests__/paused-auto-resume.test.js +0 -162
  66. package/dist/__tests__/paused-auto-resume.test.js.map +0 -1
  67. package/dist/__tests__/providers.test.js +0 -321
  68. package/dist/__tests__/providers.test.js.map +0 -1
  69. package/dist/__tests__/redis-store.test.js +0 -166
  70. package/dist/__tests__/redis-store.test.js.map +0 -1
  71. package/dist/__tests__/refund-policy.test.js +0 -317
  72. package/dist/__tests__/refund-policy.test.js.map +0 -1
  73. package/dist/__tests__/schema.test.js +0 -46
  74. package/dist/__tests__/schema.test.js.map +0 -1
  75. package/dist/__tests__/store.test.js +0 -172
  76. package/dist/__tests__/store.test.js.map +0 -1
  77. package/dist/__tests__/test-utils.js +0 -30
  78. package/dist/__tests__/test-utils.js.map +0 -1
  79. package/dist/__tests__/webhook-apple.test.js +0 -247
  80. package/dist/__tests__/webhook-apple.test.js.map +0 -1
  81. package/dist/__tests__/webhook-events.test.js +0 -36
  82. package/dist/__tests__/webhook-events.test.js.map +0 -1
  83. package/dist/__tests__/webhook-google.test.js +0 -255
  84. package/dist/__tests__/webhook-google.test.js.map +0 -1
  85. package/dist/__tests__/webhook-queue.test.js +0 -26
  86. package/dist/__tests__/webhook-queue.test.js.map +0 -1
  87. package/dist/__tests__/webhook-refund.test.js +0 -313
  88. package/dist/__tests__/webhook-refund.test.js.map +0 -1
  89. package/dist/cache.js +0 -39
  90. package/dist/cache.js.map +0 -1
  91. package/dist/errors.js +0 -19
  92. package/dist/errors.js.map +0 -1
  93. package/dist/http.js +0 -36
  94. package/dist/http.js.map +0 -1
  95. package/dist/logger.js +0 -20
  96. package/dist/logger.js.map +0 -1
  97. package/dist/openapi.js +0 -196
  98. package/dist/openapi.js.map +0 -1
  99. package/dist/providers/apple-root-ca.js +0 -38
  100. package/dist/providers/apple-root-ca.js.map +0 -1
  101. package/dist/providers/apple.js +0 -630
  102. package/dist/providers/apple.js.map +0 -1
  103. package/dist/providers/google.js +0 -564
  104. package/dist/providers/google.js.map +0 -1
  105. package/dist/providers/mock.js +0 -93
  106. package/dist/providers/mock.js.map +0 -1
  107. package/dist/routes/admin.js +0 -327
  108. package/dist/routes/admin.js.map +0 -1
  109. package/dist/routes/apple-offer.js +0 -72
  110. package/dist/routes/apple-offer.js.map +0 -1
  111. package/dist/routes/entitlements.js +0 -130
  112. package/dist/routes/entitlements.js.map +0 -1
  113. package/dist/routes/metrics.js +0 -281
  114. package/dist/routes/metrics.js.map +0 -1
  115. package/dist/routes/purchase.js +0 -204
  116. package/dist/routes/purchase.js.map +0 -1
  117. package/dist/routes/status.js +0 -55
  118. package/dist/routes/status.js.map +0 -1
  119. package/dist/routes/validate.js +0 -70
  120. package/dist/routes/validate.js.map +0 -1
  121. package/dist/routes/webhook-apple.js +0 -145
  122. package/dist/routes/webhook-apple.js.map +0 -1
  123. package/dist/routes/webhook-google.js +0 -204
  124. package/dist/routes/webhook-google.js.map +0 -1
  125. package/dist/routes/webhook.js +0 -19
  126. package/dist/routes/webhook.js.map +0 -1
  127. package/dist/store.js +0 -145
  128. package/dist/store.js.map +0 -1
  129. package/dist/stores/postgres.js +0 -329
  130. package/dist/stores/postgres.js.map +0 -1
  131. package/dist/stores/redis.js +0 -297
  132. package/dist/stores/redis.js.map +0 -1
  133. package/dist/stores/schema.js +0 -67
  134. package/dist/stores/schema.js.map +0 -1
  135. package/dist/tracing.js +0 -74
  136. package/dist/tracing.js.map +0 -1
  137. package/dist/webhook-events.js +0 -58
  138. package/dist/webhook-events.js.map +0 -1
  139. package/dist/webhook-queue.js +0 -108
  140. 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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"}