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