@onesub/server 0.12.1 → 0.13.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 (118) hide show
  1. package/dist/__tests__/admin-customer-detail.test.js +146 -0
  2. package/dist/__tests__/admin-customer-detail.test.js.map +1 -0
  3. package/dist/__tests__/admin-list-subscriptions.test.js +192 -0
  4. package/dist/__tests__/admin-list-subscriptions.test.js.map +1 -0
  5. package/dist/__tests__/admin-subscription-detail.test.js +112 -0
  6. package/dist/__tests__/admin-subscription-detail.test.js.map +1 -0
  7. package/dist/__tests__/apple-root-ca.test.js +41 -0
  8. package/dist/__tests__/apple-root-ca.test.js.map +1 -0
  9. package/dist/__tests__/apple-status-api.test.js +345 -0
  10. package/dist/__tests__/apple-status-api.test.js.map +1 -0
  11. package/dist/__tests__/cache.test.js +34 -0
  12. package/dist/__tests__/cache.test.js.map +1 -0
  13. package/dist/__tests__/entitlements.test.js +268 -0
  14. package/dist/__tests__/entitlements.test.js.map +1 -0
  15. package/dist/__tests__/error-codes.test.js +168 -0
  16. package/dist/__tests__/error-codes.test.js.map +1 -0
  17. package/dist/__tests__/fetch-hardening.test.js +192 -0
  18. package/dist/__tests__/fetch-hardening.test.js.map +1 -0
  19. package/dist/__tests__/google-paused.test.js +126 -0
  20. package/dist/__tests__/google-paused.test.js.map +1 -0
  21. package/dist/__tests__/google-price-change.test.js +186 -0
  22. package/dist/__tests__/google-price-change.test.js.map +1 -0
  23. package/dist/__tests__/google-subscriptions-v2.test.js +206 -0
  24. package/dist/__tests__/google-subscriptions-v2.test.js.map +1 -0
  25. package/dist/__tests__/lifecycle-scenarios.test.js +579 -0
  26. package/dist/__tests__/lifecycle-scenarios.test.js.map +1 -0
  27. package/dist/__tests__/lifecycle-states.test.js +423 -0
  28. package/dist/__tests__/lifecycle-states.test.js.map +1 -0
  29. package/dist/__tests__/linked-purchase-token.test.js +227 -0
  30. package/dist/__tests__/linked-purchase-token.test.js.map +1 -0
  31. package/dist/__tests__/metrics.test.js +340 -0
  32. package/dist/__tests__/metrics.test.js.map +1 -0
  33. package/dist/__tests__/mock-provider.test.js +188 -0
  34. package/dist/__tests__/mock-provider.test.js.map +1 -0
  35. package/dist/__tests__/openapi.test.js +32 -0
  36. package/dist/__tests__/openapi.test.js.map +1 -0
  37. package/dist/__tests__/paused-auto-resume.test.js +162 -0
  38. package/dist/__tests__/paused-auto-resume.test.js.map +1 -0
  39. package/dist/__tests__/providers.test.js +321 -0
  40. package/dist/__tests__/providers.test.js.map +1 -0
  41. package/dist/__tests__/redis-store.test.js +117 -0
  42. package/dist/__tests__/redis-store.test.js.map +1 -0
  43. package/dist/__tests__/refund-policy.test.js +317 -0
  44. package/dist/__tests__/refund-policy.test.js.map +1 -0
  45. package/dist/__tests__/schema.test.js +46 -0
  46. package/dist/__tests__/schema.test.js.map +1 -0
  47. package/dist/__tests__/store.test.js +172 -0
  48. package/dist/__tests__/store.test.js.map +1 -0
  49. package/dist/__tests__/test-utils.js +30 -0
  50. package/dist/__tests__/test-utils.js.map +1 -0
  51. package/dist/__tests__/webhook-events.test.js +36 -0
  52. package/dist/__tests__/webhook-events.test.js.map +1 -0
  53. package/dist/__tests__/webhook-queue.test.js +26 -0
  54. package/dist/__tests__/webhook-queue.test.js.map +1 -0
  55. package/dist/__tests__/webhook-refund.test.js +313 -0
  56. package/dist/__tests__/webhook-refund.test.js.map +1 -0
  57. package/dist/cache.js +43 -0
  58. package/dist/cache.js.map +1 -0
  59. package/dist/errors.js +19 -0
  60. package/dist/errors.js.map +1 -0
  61. package/dist/http.js +36 -0
  62. package/dist/http.js.map +1 -0
  63. package/dist/index.cjs +231 -6
  64. package/dist/index.cjs.map +1 -1
  65. package/dist/index.d.ts +1 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +231 -8
  68. package/dist/index.js.map +1 -1
  69. package/dist/logger.js +20 -0
  70. package/dist/logger.js.map +1 -0
  71. package/dist/openapi.js +196 -0
  72. package/dist/openapi.js.map +1 -0
  73. package/dist/providers/apple-root-ca.js +38 -0
  74. package/dist/providers/apple-root-ca.js.map +1 -0
  75. package/dist/providers/apple.d.ts +76 -0
  76. package/dist/providers/apple.d.ts.map +1 -1
  77. package/dist/providers/apple.js +510 -0
  78. package/dist/providers/apple.js.map +1 -0
  79. package/dist/providers/google.d.ts +25 -0
  80. package/dist/providers/google.d.ts.map +1 -1
  81. package/dist/providers/google.js +522 -0
  82. package/dist/providers/google.js.map +1 -0
  83. package/dist/providers/mock.js +93 -0
  84. package/dist/providers/mock.js.map +1 -0
  85. package/dist/routes/admin.d.ts.map +1 -1
  86. package/dist/routes/admin.js +287 -0
  87. package/dist/routes/admin.js.map +1 -0
  88. package/dist/routes/apple-offer.d.ts +21 -0
  89. package/dist/routes/apple-offer.d.ts.map +1 -0
  90. package/dist/routes/entitlements.js +130 -0
  91. package/dist/routes/entitlements.js.map +1 -0
  92. package/dist/routes/metrics.js +281 -0
  93. package/dist/routes/metrics.js.map +1 -0
  94. package/dist/routes/purchase.js +187 -0
  95. package/dist/routes/purchase.js.map +1 -0
  96. package/dist/routes/status.js +55 -0
  97. package/dist/routes/status.js.map +1 -0
  98. package/dist/routes/validate.js +70 -0
  99. package/dist/routes/validate.js.map +1 -0
  100. package/dist/routes/webhook-apple.d.ts.map +1 -1
  101. package/dist/routes/webhook-google.d.ts.map +1 -1
  102. package/dist/routes/webhook.js +461 -0
  103. package/dist/routes/webhook.js.map +1 -0
  104. package/dist/store.js +145 -0
  105. package/dist/store.js.map +1 -0
  106. package/dist/stores/postgres.js +329 -0
  107. package/dist/stores/postgres.js.map +1 -0
  108. package/dist/stores/redis.js +255 -0
  109. package/dist/stores/redis.js.map +1 -0
  110. package/dist/stores/schema.js +67 -0
  111. package/dist/stores/schema.js.map +1 -0
  112. package/dist/tracing.js +74 -0
  113. package/dist/tracing.js.map +1 -0
  114. package/dist/webhook-events.js +57 -0
  115. package/dist/webhook-events.js.map +1 -0
  116. package/dist/webhook-queue.js +108 -0
  117. package/dist/webhook-queue.js.map +1 -0
  118. package/package.json +2 -2
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import request from 'supertest';
3
+ import express from 'express';
4
+ import { ONESUB_ERROR_CODE, SUBSCRIPTION_STATUS, MOCK_RECEIPT_PREFIX } from '@onesub/shared';
5
+ import { createOneSubMiddleware } from '../index.js';
6
+ import { InMemorySubscriptionStore, InMemoryPurchaseStore } from '../store.js';
7
+ import { classifyMockReceipt, mockValidateAppleSubscription, mockValidateAppleProduct, mockValidateGoogleSubscription, mockValidateGoogleProduct, } from '../providers/mock.js';
8
+ // ---------------------------------------------------------------------------
9
+ // Pure classifier
10
+ // ---------------------------------------------------------------------------
11
+ describe('classifyMockReceipt', () => {
12
+ const cases = [
13
+ [`${MOCK_RECEIPT_PREFIX.REVOKED}_tx1`, 'revoked'],
14
+ [`${MOCK_RECEIPT_PREFIX.EXPIRED}_tx1`, 'expired'],
15
+ [`${MOCK_RECEIPT_PREFIX.INVALID}_tx1`, 'invalid'],
16
+ [`${MOCK_RECEIPT_PREFIX.BAD_SIG}_tx1`, 'invalid'],
17
+ [`${MOCK_RECEIPT_PREFIX.NETWORK_ERROR}_tx1`, 'network-error'],
18
+ [`${MOCK_RECEIPT_PREFIX.SANDBOX}_tx1`, 'sandbox'],
19
+ ['MOCK_anything_else', 'valid'],
20
+ ['not_even_a_mock_prefix', 'valid'],
21
+ ];
22
+ for (const [receipt, expected] of cases) {
23
+ it(`classifies ${receipt} as ${expected}`, () => {
24
+ expect(classifyMockReceipt(receipt).kind).toBe(expected);
25
+ });
26
+ }
27
+ });
28
+ // ---------------------------------------------------------------------------
29
+ // Pure mock provider functions
30
+ // ---------------------------------------------------------------------------
31
+ describe('mockValidateAppleSubscription', () => {
32
+ it('returns ACTIVE subscription for valid receipt', () => {
33
+ const sub = mockValidateAppleSubscription('MOCK_VALID_sub_1');
34
+ expect(sub).toMatchObject({
35
+ platform: 'apple',
36
+ status: SUBSCRIPTION_STATUS.ACTIVE,
37
+ willRenew: true,
38
+ });
39
+ expect(sub?.originalTransactionId).toContain('mock_apple_orig_');
40
+ });
41
+ it('returns null for MOCK_REVOKED', () => {
42
+ expect(mockValidateAppleSubscription('MOCK_REVOKED_x')).toBeNull();
43
+ });
44
+ it('throws for MOCK_NETWORK_ERROR', () => {
45
+ expect(() => mockValidateAppleSubscription('MOCK_NETWORK_ERROR_x')).toThrow(/network error/);
46
+ });
47
+ it('produces deterministic transactionIds', () => {
48
+ const a = mockValidateAppleSubscription('MOCK_VALID_same');
49
+ const b = mockValidateAppleSubscription('MOCK_VALID_same');
50
+ expect(a?.originalTransactionId).toBe(b?.originalTransactionId);
51
+ });
52
+ it('sandbox receipts get shorter expiry (~1h)', () => {
53
+ const sub = mockValidateAppleSubscription('MOCK_SANDBOX_x');
54
+ const expiry = Date.parse(sub.expiresAt);
55
+ const ageMs = expiry - Date.now();
56
+ expect(ageMs).toBeGreaterThan(30 * 60 * 1000); // > 30 min
57
+ expect(ageMs).toBeLessThan(2 * 60 * 60 * 1000); // < 2 h
58
+ });
59
+ });
60
+ describe('mockValidateAppleProduct', () => {
61
+ it('returns product result with productId for valid receipt', () => {
62
+ const result = mockValidateAppleProduct('MOCK_VALID_x', 'premium');
63
+ expect(result).toMatchObject({ productId: 'premium' });
64
+ expect(result?.transactionId).toContain('mock_apple_premium_');
65
+ });
66
+ it('returns null for MOCK_EXPIRED', () => {
67
+ expect(mockValidateAppleProduct('MOCK_EXPIRED_x', 'premium')).toBeNull();
68
+ });
69
+ });
70
+ describe('mockValidateGoogleSubscription', () => {
71
+ it('returns ACTIVE subscription for valid token', () => {
72
+ const sub = mockValidateGoogleSubscription('MOCK_VALID_x', 'pro_monthly');
73
+ expect(sub).toMatchObject({
74
+ platform: 'google',
75
+ productId: 'pro_monthly',
76
+ status: SUBSCRIPTION_STATUS.ACTIVE,
77
+ });
78
+ });
79
+ it('throws for MOCK_NETWORK_ERROR', () => {
80
+ expect(() => mockValidateGoogleSubscription('MOCK_NETWORK_ERROR_x', 'pro'))
81
+ .toThrow(/network error/);
82
+ });
83
+ });
84
+ describe('mockValidateGoogleProduct', () => {
85
+ it('returns product result for valid token', () => {
86
+ const result = mockValidateGoogleProduct('MOCK_VALID_x', 'credits_100');
87
+ expect(result?.transactionId).toContain('mock_google_credits_100_');
88
+ });
89
+ it('returns null for MOCK_INVALID', () => {
90
+ expect(mockValidateGoogleProduct('MOCK_INVALID_x', 'credits_100')).toBeNull();
91
+ });
92
+ });
93
+ // ---------------------------------------------------------------------------
94
+ // HTTP integration — full onesub server with mockMode on both platforms.
95
+ // This is the key demo: a server standing up without real Apple / Google
96
+ // credentials, yet the full purchase flow completes end-to-end.
97
+ // ---------------------------------------------------------------------------
98
+ describe('full server in mockMode', () => {
99
+ function mockApp() {
100
+ const config = {
101
+ database: { url: '' },
102
+ apple: { bundleId: 'com.test.mock', mockMode: true },
103
+ google: { packageName: 'com.test.mock', mockMode: true },
104
+ };
105
+ const app = express();
106
+ app.use(createOneSubMiddleware({
107
+ ...config,
108
+ store: new InMemorySubscriptionStore(),
109
+ purchaseStore: new InMemoryPurchaseStore(),
110
+ }));
111
+ return app;
112
+ }
113
+ it('validates an Apple subscription without any real credentials', async () => {
114
+ const app = mockApp();
115
+ const res = await request(app).post('/onesub/validate').send({
116
+ platform: 'apple',
117
+ receipt: 'MOCK_VALID_apple_sub_1',
118
+ userId: 'user_1',
119
+ productId: 'pro_monthly',
120
+ });
121
+ expect(res.status).toBe(200);
122
+ expect(res.body.valid).toBe(true);
123
+ expect(res.body.subscription.status).toBe('active');
124
+ });
125
+ it('validates a Google non-consumable purchase without credentials', async () => {
126
+ const app = mockApp();
127
+ const res = await request(app).post('/onesub/purchase/validate').send({
128
+ platform: 'google',
129
+ receipt: 'MOCK_VALID_google_product_1',
130
+ userId: 'user_1',
131
+ productId: 'premium',
132
+ type: 'non_consumable',
133
+ });
134
+ expect(res.status).toBe(200);
135
+ expect(res.body.valid).toBe(true);
136
+ expect(res.body.action).toBe('new');
137
+ });
138
+ it('MOCK_REVOKED returns RECEIPT_VALIDATION_FAILED via normal error path', async () => {
139
+ const app = mockApp();
140
+ const res = await request(app).post('/onesub/purchase/validate').send({
141
+ platform: 'apple',
142
+ receipt: 'MOCK_REVOKED_x',
143
+ userId: 'user_1',
144
+ productId: 'premium',
145
+ type: 'non_consumable',
146
+ });
147
+ expect(res.status).toBe(422);
148
+ expect(res.body.errorCode).toBe(ONESUB_ERROR_CODE.RECEIPT_VALIDATION_FAILED);
149
+ });
150
+ it('throws when NODE_ENV=production + mockMode is set (fraud guard)', () => {
151
+ const original = process.env['NODE_ENV'];
152
+ process.env['NODE_ENV'] = 'production';
153
+ try {
154
+ expect(() => createOneSubMiddleware({
155
+ database: { url: '' },
156
+ apple: { bundleId: 'com.test', mockMode: true },
157
+ store: new InMemorySubscriptionStore(),
158
+ purchaseStore: new InMemoryPurchaseStore(),
159
+ })).toThrow(/mockMode cannot be enabled when NODE_ENV=production/);
160
+ }
161
+ finally {
162
+ if (original === undefined)
163
+ delete process.env['NODE_ENV'];
164
+ else
165
+ process.env['NODE_ENV'] = original;
166
+ }
167
+ });
168
+ it('replaying the same receipt returns action:restored (server idempotency)', async () => {
169
+ const app = mockApp();
170
+ const body = {
171
+ platform: 'apple',
172
+ receipt: 'MOCK_VALID_replay_test',
173
+ userId: 'user_1',
174
+ productId: 'premium',
175
+ type: 'non_consumable',
176
+ };
177
+ const first = await request(app).post('/onesub/purchase/validate').send(body);
178
+ expect(first.body.action).toBe('new');
179
+ // Wipe the user's record so hasPurchased() is false (NON_CONSUMABLE_ALREADY_OWNED
180
+ // would short-circuit otherwise). Use a different userId to hit the same
181
+ // transactionId path.
182
+ const second = await request(app).post('/onesub/purchase/validate').send({ ...body, userId: 'user_2' });
183
+ // non-consumable with same transactionId but different userId → auto-reassigned (0.6.1+)
184
+ expect(second.body.valid).toBe(true);
185
+ expect(second.body.action).toBe('restored');
186
+ });
187
+ });
188
+ //# sourceMappingURL=mock-provider.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-provider.test.js","sourceRoot":"","sources":["../../src/__tests__/mock-provider.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,OAAO,MAAM,WAAW,CAAC;AAChC,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC7F,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,EACL,mBAAmB,EACnB,6BAA6B,EAC7B,wBAAwB,EACxB,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAE9B,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAC9E,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,MAAM,KAAK,GAA4B;QACrC,CAAC,GAAG,mBAAmB,CAAC,OAAO,MAAM,EAAE,SAAS,CAAC;QACjD,CAAC,GAAG,mBAAmB,CAAC,OAAO,MAAM,EAAE,SAAS,CAAC;QACjD,CAAC,GAAG,mBAAmB,CAAC,OAAO,MAAM,EAAE,SAAS,CAAC;QACjD,CAAC,GAAG,mBAAmB,CAAC,OAAO,MAAM,EAAE,SAAS,CAAC;QACjD,CAAC,GAAG,mBAAmB,CAAC,aAAa,MAAM,EAAE,eAAe,CAAC;QAC7D,CAAC,GAAG,mBAAmB,CAAC,OAAO,MAAM,EAAE,SAAS,CAAC;QACjD,CAAC,oBAAoB,EAAE,OAAO,CAAC;QAC/B,CAAC,wBAAwB,EAAE,OAAO,CAAC;KACpC,CAAC;IACF,KAAK,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,KAAK,EAAE,CAAC;QACxC,EAAE,CAAC,cAAc,OAAO,OAAO,QAAQ,EAAE,EAAE,GAAG,EAAE;YAC9C,MAAM,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,8EAA8E;AAC9E,+BAA+B;AAC/B,8EAA8E;AAC9E,QAAQ,CAAC,+BAA+B,EAAE,GAAG,EAAE;IAC7C,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,GAAG,GAAG,6BAA6B,CAAC,kBAAkB,CAAC,CAAC;QAC9D,MAAM,CAAC,GAAG,CAAC,CAAC,aAAa,CAAC;YACxB,QAAQ,EAAE,OAAO;YACjB,MAAM,EAAE,mBAAmB,CAAC,MAAM;YAClC,SAAS,EAAE,IAAI;SAChB,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,6BAA6B,CAAC,gBAAgB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,GAAG,EAAE,CAAC,6BAA6B,CAAC,sBAAsB,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAC/F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,GAAG,6BAA6B,CAAC,iBAAiB,CAAC,CAAC;QAC3D,MAAM,CAAC,GAAG,6BAA6B,CAAC,iBAAiB,CAAC,CAAC;QAC3D,MAAM,CAAC,CAAC,EAAE,qBAAqB,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,qBAAqB,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,GAAG,GAAG,6BAA6B,CAAC,gBAAgB,CAAC,CAAC;QAC5D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAI,CAAC,SAAS,CAAC,CAAC;QAC1C,MAAM,KAAK,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAClC,MAAM,CAAC,KAAK,CAAC,CAAC,eAAe,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAO,WAAW;QAChE,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAM,QAAQ;IAC/D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,MAAM,GAAG,wBAAwB,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;QACnE,MAAM,CAAC,MAAM,CAAC,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC;QACvD,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,wBAAwB,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC3E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC9C,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,GAAG,GAAG,8BAA8B,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC;QAC1E,MAAM,CAAC,GAAG,CAAC,CAAC,aAAa,CAAC;YACxB,QAAQ,EAAE,QAAQ;YAClB,SAAS,EAAE,aAAa;YACxB,MAAM,EAAE,mBAAmB,CAAC,MAAM;SACnC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,GAAG,EAAE,CAAC,8BAA8B,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;aACxE,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,MAAM,GAAG,yBAAyB,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC;QACxE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,SAAS,CAAC,0BAA0B,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,yBAAyB,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAChF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,8EAA8E;AAC9E,yEAAyE;AACzE,yEAAyE;AACzE,gEAAgE;AAChE,8EAA8E;AAC9E,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,SAAS,OAAO;QACd,MAAM,MAAM,GAAuB;YACjC,QAAQ,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;YACrB,KAAK,EAAE,EAAE,QAAQ,EAAE,eAAe,EAAE,QAAQ,EAAE,IAAI,EAAE;YACpD,MAAM,EAAE,EAAE,WAAW,EAAE,eAAe,EAAE,QAAQ,EAAE,IAAI,EAAE;SACzD,CAAC;QACF,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;QACtB,GAAG,CAAC,GAAG,CAAC,sBAAsB,CAAC;YAC7B,GAAG,MAAM;YACT,KAAK,EAAE,IAAI,yBAAyB,EAAE;YACtC,aAAa,EAAE,IAAI,qBAAqB,EAAE;SAC3C,CAAC,CAAC,CAAC;QACJ,OAAO,GAAG,CAAC;IACb,CAAC;IAED,EAAE,CAAC,8DAA8D,EAAE,KAAK,IAAI,EAAE;QAC5E,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC;YAC3D,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,wBAAwB;YACjC,MAAM,EAAE,QAAQ;YAChB,SAAS,EAAE,aAAa;SACzB,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7B,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;QAC9E,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC,IAAI,CAAC;YACpE,QAAQ,EAAE,QAAQ;YAClB,OAAO,EAAE,6BAA6B;YACtC,MAAM,EAAE,QAAQ;YAChB,SAAS,EAAE,SAAS;YACpB,IAAI,EAAE,gBAAgB;SACvB,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7B,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;QACpF,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC,IAAI,CAAC;YACpE,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,gBAAgB;YACzB,MAAM,EAAE,QAAQ;YAChB,SAAS,EAAE,SAAS;YACpB,IAAI,EAAE,gBAAgB;SACvB,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7B,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,yBAAyB,CAAC,CAAC;IAC/E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACzC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,YAAY,CAAC;QACvC,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,EAAE,CAAC,sBAAsB,CAAC;gBAClC,QAAQ,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;gBACrB,KAAK,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE;gBAC/C,KAAK,EAAE,IAAI,yBAAyB,EAAE;gBACtC,aAAa,EAAE,IAAI,qBAAqB,EAAE;aAC3C,CAAC,CAAC,CAAC,OAAO,CAAC,qDAAqD,CAAC,CAAC;QACrE,CAAC;gBAAS,CAAC;YACT,IAAI,QAAQ,KAAK,SAAS;gBAAE,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;;gBACtD,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC;QAC1C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yEAAyE,EAAE,KAAK,IAAI,EAAE;QACvF,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG;YACX,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,wBAAwB;YACjC,MAAM,EAAE,QAAQ;YAChB,SAAS,EAAE,SAAS;YACpB,IAAI,EAAE,gBAAgB;SACvB,CAAC;QACF,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9E,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAEtC,kFAAkF;QAClF,yEAAyE;QACzE,sBAAsB;QACtB,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;QACxG,yFAAyF;QACzF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ONESUB_OPENAPI } from '../openapi.js';
3
+ import { ROUTES } from '@onesub/shared';
4
+ describe('OpenAPI document', () => {
5
+ it('declares every public route', () => {
6
+ const declared = Object.keys(ONESUB_OPENAPI.paths);
7
+ // Spot-check the main public routes — the spec is hand-written, so this
8
+ // is the parity guard. Routes added to ROUTES that aren't in the spec
9
+ // should fail this test.
10
+ for (const path of [
11
+ ROUTES.VALIDATE,
12
+ ROUTES.STATUS,
13
+ ROUTES.WEBHOOK_APPLE,
14
+ ROUTES.WEBHOOK_GOOGLE,
15
+ ROUTES.ENTITLEMENTS,
16
+ ROUTES.ADMIN_SUBSCRIPTIONS,
17
+ ]) {
18
+ expect(declared).toContain(path);
19
+ }
20
+ });
21
+ it('uses 3.1.0', () => {
22
+ expect(ONESUB_OPENAPI.openapi).toBe('3.1.0');
23
+ });
24
+ it('declares the AdminSecret security scheme', () => {
25
+ expect(ONESUB_OPENAPI.components.securitySchemes.AdminSecret).toMatchObject({
26
+ type: 'apiKey',
27
+ in: 'header',
28
+ name: 'X-Admin-Secret',
29
+ });
30
+ });
31
+ });
32
+ //# sourceMappingURL=openapi.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.test.js","sourceRoot":"","sources":["../../src/__tests__/openapi.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAExC,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QACnD,wEAAwE;QACxE,sEAAsE;QACtE,yBAAyB;QACzB,KAAK,MAAM,IAAI,IAAI;YACjB,MAAM,CAAC,QAAQ;YACf,MAAM,CAAC,MAAM;YACb,MAAM,CAAC,aAAa;YACpB,MAAM,CAAC,cAAc;YACrB,MAAM,CAAC,YAAY;YACnB,MAAM,CAAC,mBAAmB;SAC3B,EAAE,CAAC;YACF,MAAM,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EAAE,GAAG,EAAE;QACpB,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC,aAAa,CAAC;YAC1E,IAAI,EAAE,QAAQ;YACd,EAAE,EAAE,QAAQ;YACZ,IAAI,EAAE,gBAAgB;SACvB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Tests for SubscriptionInfo.autoResumeTime — Google paused subscriptions
3
+ * surface the auto-resume timestamp from v2 pausedStateContext, so host apps
4
+ * can render "재개 예정: YYYY-MM-DD" UX.
5
+ */
6
+ import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
7
+ import express from 'express';
8
+ import { generateKeyPairSync } from 'crypto';
9
+ import { SUBSCRIPTION_STATUS } from '@onesub/shared';
10
+ import { validateGoogleReceipt } from '../providers/google.js';
11
+ import { createStatusRouter } from '../routes/status.js';
12
+ import { InMemorySubscriptionStore } from '../store.js';
13
+ import { isLocalhostUrl, urlHost } from './test-utils.js';
14
+ let testPrivateKey;
15
+ beforeAll(() => {
16
+ const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
17
+ testPrivateKey = privateKey.export({ type: 'pkcs8', format: 'pem' });
18
+ });
19
+ beforeEach(() => {
20
+ vi.restoreAllMocks();
21
+ });
22
+ function googleConfig() {
23
+ return {
24
+ packageName: 'com.example.app',
25
+ serviceAccountKey: JSON.stringify({
26
+ client_email: `test-${Math.random()}@test.iam.gserviceaccount.com`,
27
+ private_key: testPrivateKey,
28
+ token_uri: 'https://oauth2.googleapis.com/token',
29
+ }),
30
+ };
31
+ }
32
+ function v2Paused(opts) {
33
+ return {
34
+ startTime: '2026-01-01T00:00:00Z',
35
+ subscriptionState: 'SUBSCRIPTION_STATE_PAUSED',
36
+ latestOrderId: 'GPA.paused_order',
37
+ pausedStateContext: opts.autoResumeTime ? { autoResumeTime: opts.autoResumeTime } : undefined,
38
+ lineItems: [{
39
+ productId: opts.productId ?? 'pro_monthly',
40
+ expiryTime: '2027-01-01T00:00:00Z',
41
+ autoRenewingPlan: { autoRenewEnabled: true },
42
+ }],
43
+ };
44
+ }
45
+ function v2Active(productId = 'pro_monthly') {
46
+ return {
47
+ startTime: '2026-01-01T00:00:00Z',
48
+ subscriptionState: 'SUBSCRIPTION_STATE_ACTIVE',
49
+ latestOrderId: 'GPA.active_order',
50
+ lineItems: [{
51
+ productId,
52
+ expiryTime: '2027-01-01T00:00:00Z',
53
+ autoRenewingPlan: { autoRenewEnabled: true },
54
+ }],
55
+ };
56
+ }
57
+ function mockV2Fetch(responseBody) {
58
+ const originalFetch = global.fetch;
59
+ vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
60
+ if (isLocalhostUrl(url))
61
+ return originalFetch(url, init);
62
+ const host = urlHost(url);
63
+ if (host === 'oauth2.googleapis.com') {
64
+ return {
65
+ ok: true,
66
+ json: async () => ({ access_token: 'tok', expires_in: 3600 }),
67
+ text: async () => '',
68
+ };
69
+ }
70
+ if (host === 'androidpublisher.googleapis.com') {
71
+ return {
72
+ ok: true,
73
+ json: async () => responseBody,
74
+ text: async () => JSON.stringify(responseBody),
75
+ };
76
+ }
77
+ throw new Error(`[test] Unexpected URL: ${String(url)}`);
78
+ });
79
+ }
80
+ // ── validateGoogleReceipt surfaces autoResumeTime ───────────────────────────
81
+ describe('validateGoogleReceipt — autoResumeTime passthrough', () => {
82
+ it('surfaces autoResumeTime when subscription is paused', async () => {
83
+ mockV2Fetch(v2Paused({ autoResumeTime: '2026-08-15T00:00:00Z' }));
84
+ const result = await validateGoogleReceipt('tok_paused', 'pro_monthly', googleConfig());
85
+ expect(result?.status).toBe(SUBSCRIPTION_STATUS.PAUSED);
86
+ expect(result?.autoResumeTime).toBe('2026-08-15T00:00:00Z');
87
+ });
88
+ it('autoResumeTime is undefined when paused but Google omits the timestamp', async () => {
89
+ // Google sometimes returns SUBSCRIPTION_STATE_PAUSED without
90
+ // pausedStateContext (rare, but the API allows it).
91
+ mockV2Fetch(v2Paused({}));
92
+ const result = await validateGoogleReceipt('tok_paused_no_ctx', 'pro_monthly', googleConfig());
93
+ expect(result?.status).toBe(SUBSCRIPTION_STATUS.PAUSED);
94
+ expect(result?.autoResumeTime).toBeUndefined();
95
+ });
96
+ it('autoResumeTime is undefined for active subscription (no pausedStateContext)', async () => {
97
+ mockV2Fetch(v2Active('pro_monthly'));
98
+ const result = await validateGoogleReceipt('tok_active', 'pro_monthly', googleConfig());
99
+ expect(result?.status).toBe(SUBSCRIPTION_STATUS.ACTIVE);
100
+ expect(result?.autoResumeTime).toBeUndefined();
101
+ });
102
+ it('autoResumeTime is undefined even if pausedStateContext appears on a non-paused state (defensive)', async () => {
103
+ // Google shouldn't send pausedStateContext for non-paused states, but if
104
+ // they ever do, we should not surface it as if the user were paused.
105
+ mockV2Fetch({
106
+ ...v2Active('pro_monthly'),
107
+ pausedStateContext: { autoResumeTime: '2099-01-01T00:00:00Z' },
108
+ });
109
+ const result = await validateGoogleReceipt('tok_defensive', 'pro_monthly', googleConfig());
110
+ expect(result?.status).toBe(SUBSCRIPTION_STATUS.ACTIVE);
111
+ expect(result?.autoResumeTime).toBeUndefined();
112
+ });
113
+ });
114
+ function spinUpStatus() {
115
+ const store = new InMemorySubscriptionStore();
116
+ const app = express();
117
+ app.use(express.json());
118
+ app.use(createStatusRouter(store));
119
+ return {
120
+ store,
121
+ server: {
122
+ async request(path) {
123
+ const httpServer = app.listen(0);
124
+ const address = httpServer.address();
125
+ const port = typeof address === 'object' && address ? address.port : 0;
126
+ try {
127
+ const resp = await fetch(`http://127.0.0.1:${port}${path}`);
128
+ const body = await resp.json();
129
+ return { status: resp.status, body };
130
+ }
131
+ finally {
132
+ await new Promise((resolve) => httpServer.close(() => resolve()));
133
+ }
134
+ },
135
+ },
136
+ };
137
+ }
138
+ const sampleSub = (overrides) => ({
139
+ userId: 'u_pause',
140
+ productId: 'pro_monthly',
141
+ platform: 'google',
142
+ status: 'paused',
143
+ expiresAt: '2099-01-01T00:00:00.000Z',
144
+ originalTransactionId: 'tok_p',
145
+ purchasedAt: '2026-01-01T00:00:00.000Z',
146
+ willRenew: true,
147
+ ...overrides,
148
+ });
149
+ describe('status route — autoResumeTime in subscription payload', () => {
150
+ it('returns the saved autoResumeTime so the host can show "재개 예정" UX', async () => {
151
+ const { store, server } = spinUpStatus();
152
+ await store.save(sampleSub({
153
+ userId: 'u_with_resume',
154
+ autoResumeTime: '2026-09-01T00:00:00.000Z',
155
+ }));
156
+ const resp = await server.request('/onesub/status?userId=u_with_resume');
157
+ const body = resp.body;
158
+ expect(body.active).toBe(false); // paused stays revoked
159
+ expect(body.subscription?.autoResumeTime).toBe('2026-09-01T00:00:00.000Z');
160
+ });
161
+ });
162
+ //# sourceMappingURL=paused-auto-resume.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paused-auto-resume.test.js","sourceRoot":"","sources":["../../src/__tests__/paused-auto-resume.test.ts"],"names":[],"mappings":"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"}