@auth-gate/testing 0.9.2 → 0.10.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/README.md +182 -0
- package/dist/billing/index.cjs +282 -0
- package/dist/billing/index.d.cts +129 -0
- package/dist/billing/index.d.ts +129 -0
- package/dist/billing/index.mjs +252 -0
- package/dist/{chunk-OV3AFWVB.mjs → chunk-7QV2KZWA.mjs} +0 -8
- package/dist/chunk-MBTDPSN5.mjs +27 -0
- package/dist/cypress.mjs +2 -1
- package/dist/index.mjs +2 -1
- package/dist/playwright.mjs +4 -2
- package/dist/rbac/index.cjs +223 -0
- package/dist/rbac/index.d.cts +167 -0
- package/dist/rbac/index.d.ts +167 -0
- package/dist/rbac/index.mjs +180 -0
- package/icon.png +0 -0
- package/package.json +29 -6
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="icon.png" alt="AuthGate" width="120" height="120" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# @auth-gate/testing
|
|
6
|
+
|
|
7
|
+
Testing utilities for [AuthGate](https://www.authgate.dev) — E2E helpers (Playwright/Cypress), in-memory billing test harness, and RBAC permission testing.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -D @auth-gate/testing
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Optional peer dependencies:** `@playwright/test >= 1.40`, `cypress >= 12`, `@auth-gate/billing >= 0.1.0`, `@auth-gate/rbac >= 0.1.0`
|
|
16
|
+
|
|
17
|
+
## Core API
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { AuthGateTest } from "@auth-gate/testing";
|
|
21
|
+
|
|
22
|
+
const testing = new AuthGateTest({
|
|
23
|
+
apiKey: process.env.AUTHGATE_API_KEY!,
|
|
24
|
+
baseUrl: process.env.AUTHGATE_URL!,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Create a test user with an immediate session
|
|
28
|
+
const user = await testing.createUser({
|
|
29
|
+
email: "alice@test.authgate.dev",
|
|
30
|
+
password: "test-password-123",
|
|
31
|
+
name: "Alice Test",
|
|
32
|
+
});
|
|
33
|
+
// Returns: { id, email, name, token, refreshToken, expiresAt }
|
|
34
|
+
|
|
35
|
+
// Create a new session for an existing user
|
|
36
|
+
const session = await testing.createSession(user.id);
|
|
37
|
+
|
|
38
|
+
// Create a test organization
|
|
39
|
+
const org = await testing.createOrg({ name: "Acme Corp" });
|
|
40
|
+
|
|
41
|
+
// Create a test role
|
|
42
|
+
const role = await testing.createRole({
|
|
43
|
+
key: "editor",
|
|
44
|
+
name: "Editor",
|
|
45
|
+
permissions: ["documents:read", "documents:write"],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Add a member to an organization
|
|
49
|
+
const member = await testing.addMember(org.id, user.id, "editor");
|
|
50
|
+
|
|
51
|
+
// Clean up all test users
|
|
52
|
+
const result = await testing.cleanup();
|
|
53
|
+
// { deleted: 3 }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Playwright
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { AuthGateTest } from "@auth-gate/testing/playwright";
|
|
60
|
+
|
|
61
|
+
// In your test setup
|
|
62
|
+
const testing = new AuthGateTest({
|
|
63
|
+
apiKey: process.env.AUTHGATE_API_KEY!,
|
|
64
|
+
baseUrl: process.env.AUTHGATE_URL!,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("authenticated user sees dashboard", async ({ page }) => {
|
|
68
|
+
const user = await testing.createUser({
|
|
69
|
+
email: "test@test.authgate.dev",
|
|
70
|
+
password: "password123",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Inject the session cookie
|
|
74
|
+
await testing.injectSession(page, user);
|
|
75
|
+
|
|
76
|
+
await page.goto("/dashboard");
|
|
77
|
+
await expect(page.locator("h1")).toContainText("Dashboard");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test.afterAll(async () => {
|
|
81
|
+
await testing.cleanup();
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Cypress
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { AuthGateTest } from "@auth-gate/testing/cypress";
|
|
89
|
+
|
|
90
|
+
const testing = new AuthGateTest({
|
|
91
|
+
apiKey: Cypress.env("AUTHGATE_API_KEY"),
|
|
92
|
+
baseUrl: Cypress.env("AUTHGATE_URL"),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
cy.wrap(
|
|
97
|
+
testing.createUser({
|
|
98
|
+
email: "test@test.authgate.dev",
|
|
99
|
+
password: "password123",
|
|
100
|
+
})
|
|
101
|
+
).then((user) => {
|
|
102
|
+
testing.injectSession(user);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
cy.wrap(testing.cleanup());
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Test Users
|
|
112
|
+
|
|
113
|
+
Test user emails must end with `@test.authgate.dev`. The OTP code for test users is always `000000`.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { TEST_EMAIL_DOMAIN, TEST_OTP_CODE } from "@auth-gate/testing";
|
|
117
|
+
|
|
118
|
+
// TEST_EMAIL_DOMAIN = "test.authgate.dev"
|
|
119
|
+
// TEST_OTP_CODE = "000000"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API Reference
|
|
123
|
+
|
|
124
|
+
### `AuthGateTest`
|
|
125
|
+
|
|
126
|
+
| Method | Description |
|
|
127
|
+
|--------|-------------|
|
|
128
|
+
| `createUser(options)` | Create a test user with immediate session |
|
|
129
|
+
| `createSession(userId)` | Create a new session for an existing user |
|
|
130
|
+
| `deleteUser(userId)` | Delete a single test user |
|
|
131
|
+
| `cleanup()` | Delete all test users in the project |
|
|
132
|
+
| `createOrg(options)` | Create a test organization |
|
|
133
|
+
| `createRole(options)` | Create a test role with permissions |
|
|
134
|
+
| `addMember(orgId, userId, roleKey)` | Add a user to an organization |
|
|
135
|
+
|
|
136
|
+
## Billing test utilities (`@auth-gate/testing/billing`)
|
|
137
|
+
|
|
138
|
+
In-memory billing harness for unit-testing subscription logic, entitlement checks, and usage-based billing.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npm install -D @auth-gate/testing @auth-gate/billing
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { createTestBilling } from "@auth-gate/testing/billing";
|
|
146
|
+
import { billing } from "./authgate.billing";
|
|
147
|
+
|
|
148
|
+
const t = createTestBilling({ billing });
|
|
149
|
+
|
|
150
|
+
t.subscribe("user_1", "pro");
|
|
151
|
+
t.reportUsage("user_1", "api_calls", 500);
|
|
152
|
+
t.advanceTime({ months: 1 });
|
|
153
|
+
|
|
154
|
+
const check = t.checkEntitlement("user_1", "api_calls");
|
|
155
|
+
// { type: "metered", allowed: true, limit: 100000, used: 0, remaining: 100000 }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## RBAC test utilities (`@auth-gate/testing/rbac`)
|
|
159
|
+
|
|
160
|
+
Config builders, in-memory permission checker, and assertion helpers for RBAC configurations.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npm install -D @auth-gate/testing @auth-gate/rbac
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { defineRbac } from "@auth-gate/rbac";
|
|
168
|
+
import { RbacChecker, expectPermission, expectNoPermission } from "@auth-gate/testing/rbac";
|
|
169
|
+
|
|
170
|
+
const rbac = defineRbac({ /* ... */ });
|
|
171
|
+
const checker = new RbacChecker(rbac);
|
|
172
|
+
|
|
173
|
+
checker.hasPermission("admin", "documents:delete"); // true
|
|
174
|
+
checker.getPermissions("viewer"); // Set { "documents:read" }
|
|
175
|
+
|
|
176
|
+
expectPermission(rbac, "admin", "documents:write");
|
|
177
|
+
expectNoPermission(rbac, "viewer", "documents:delete");
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/billing/index.ts
|
|
21
|
+
var billing_exports = {};
|
|
22
|
+
__export(billing_exports, {
|
|
23
|
+
EntitlementChecker: () => EntitlementChecker,
|
|
24
|
+
InMemoryStore: () => InMemoryStore,
|
|
25
|
+
TestClock: () => TestClock,
|
|
26
|
+
computeDiff: () => import_billing.computeDiff,
|
|
27
|
+
createTestBilling: () => createTestBilling,
|
|
28
|
+
priceConfigKey: () => import_billing.priceConfigKey
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(billing_exports);
|
|
31
|
+
|
|
32
|
+
// src/billing/clock.ts
|
|
33
|
+
var TestClock = class {
|
|
34
|
+
constructor(startDate) {
|
|
35
|
+
this._now = startDate ? new Date(startDate) : /* @__PURE__ */ new Date("2025-01-01T00:00:00Z");
|
|
36
|
+
}
|
|
37
|
+
now() {
|
|
38
|
+
return new Date(this._now);
|
|
39
|
+
}
|
|
40
|
+
advance(options) {
|
|
41
|
+
const next = new Date(this._now);
|
|
42
|
+
if (options.days) {
|
|
43
|
+
next.setUTCDate(next.getUTCDate() + options.days);
|
|
44
|
+
}
|
|
45
|
+
if (options.months) {
|
|
46
|
+
next.setUTCMonth(next.getUTCMonth() + options.months);
|
|
47
|
+
}
|
|
48
|
+
this._now = next;
|
|
49
|
+
return this.now();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if advancing from `before` to `after` crosses a billing boundary.
|
|
53
|
+
*/
|
|
54
|
+
crossesBoundary(before, after, boundary) {
|
|
55
|
+
if (boundary === "month_end") {
|
|
56
|
+
return before.getUTCMonth() !== after.getUTCMonth() || before.getUTCFullYear() !== after.getUTCFullYear();
|
|
57
|
+
}
|
|
58
|
+
if (boundary === "year_end") {
|
|
59
|
+
return before.getUTCFullYear() !== after.getUTCFullYear();
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// src/billing/store.ts
|
|
66
|
+
var InMemoryStore = class {
|
|
67
|
+
constructor() {
|
|
68
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
69
|
+
this.usage = [];
|
|
70
|
+
this.nextId = 1;
|
|
71
|
+
}
|
|
72
|
+
createSubscription(params) {
|
|
73
|
+
var _a, _b;
|
|
74
|
+
const id = `sub_${this.nextId++}`;
|
|
75
|
+
const sub = {
|
|
76
|
+
id,
|
|
77
|
+
userId: params.userId,
|
|
78
|
+
planKey: params.planKey,
|
|
79
|
+
priceIndex: (_a = params.priceIndex) != null ? _a : 0,
|
|
80
|
+
status: (_b = params.status) != null ? _b : "active",
|
|
81
|
+
currentPeriodStart: params.periodStart,
|
|
82
|
+
currentPeriodEnd: params.periodEnd,
|
|
83
|
+
cancelAtPeriodEnd: false,
|
|
84
|
+
createdAt: new Date(params.periodStart)
|
|
85
|
+
};
|
|
86
|
+
this.subscriptions.set(id, sub);
|
|
87
|
+
return sub;
|
|
88
|
+
}
|
|
89
|
+
getSubscription(id) {
|
|
90
|
+
return this.subscriptions.get(id);
|
|
91
|
+
}
|
|
92
|
+
getSubscriptionByUser(userId) {
|
|
93
|
+
for (const sub of this.subscriptions.values()) {
|
|
94
|
+
if (sub.userId === userId && sub.status !== "canceled") return sub;
|
|
95
|
+
}
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
updateSubscription(id, updates) {
|
|
99
|
+
const sub = this.subscriptions.get(id);
|
|
100
|
+
if (!sub) return void 0;
|
|
101
|
+
Object.assign(sub, updates);
|
|
102
|
+
return sub;
|
|
103
|
+
}
|
|
104
|
+
listSubscriptions(filter) {
|
|
105
|
+
let subs = [...this.subscriptions.values()];
|
|
106
|
+
if (filter == null ? void 0 : filter.status) subs = subs.filter((s) => s.status === filter.status);
|
|
107
|
+
if (filter == null ? void 0 : filter.planKey) subs = subs.filter((s) => s.planKey === filter.planKey);
|
|
108
|
+
return subs;
|
|
109
|
+
}
|
|
110
|
+
recordUsage(userId, metric, quantity, timestamp) {
|
|
111
|
+
this.usage.push({ userId, metric, quantity, timestamp });
|
|
112
|
+
}
|
|
113
|
+
getUsage(userId, metric, periodStart, periodEnd) {
|
|
114
|
+
return this.usage.filter(
|
|
115
|
+
(r) => r.userId === userId && r.metric === metric && r.timestamp >= periodStart && r.timestamp < periodEnd
|
|
116
|
+
).reduce((sum, r) => sum + r.quantity, 0);
|
|
117
|
+
}
|
|
118
|
+
subscriberCount(planKey) {
|
|
119
|
+
return this.listSubscriptions({ planKey, status: "active" }).length;
|
|
120
|
+
}
|
|
121
|
+
clear() {
|
|
122
|
+
this.subscriptions.clear();
|
|
123
|
+
this.usage = [];
|
|
124
|
+
this.nextId = 1;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// src/billing/entitlements.ts
|
|
129
|
+
var EntitlementChecker = class {
|
|
130
|
+
constructor(config, store) {
|
|
131
|
+
this.config = config;
|
|
132
|
+
this.store = store;
|
|
133
|
+
}
|
|
134
|
+
check(userId, feature) {
|
|
135
|
+
var _a;
|
|
136
|
+
const sub = this.store.getSubscriptionByUser(userId);
|
|
137
|
+
if (!sub) {
|
|
138
|
+
return { type: "boolean", feature, allowed: false };
|
|
139
|
+
}
|
|
140
|
+
const plan = this.config.plans[sub.planKey];
|
|
141
|
+
if (!plan) {
|
|
142
|
+
return { type: "boolean", feature, allowed: false };
|
|
143
|
+
}
|
|
144
|
+
if (plan.entitlements) {
|
|
145
|
+
const value = plan.entitlements[feature];
|
|
146
|
+
if (value === void 0) {
|
|
147
|
+
return { type: "boolean", feature, allowed: false };
|
|
148
|
+
}
|
|
149
|
+
if (value === true) {
|
|
150
|
+
return { type: "boolean", feature, allowed: true };
|
|
151
|
+
}
|
|
152
|
+
if (typeof value === "object" && "limit" in value) {
|
|
153
|
+
const used = this.store.getUsage(
|
|
154
|
+
userId,
|
|
155
|
+
feature,
|
|
156
|
+
sub.currentPeriodStart,
|
|
157
|
+
sub.currentPeriodEnd
|
|
158
|
+
);
|
|
159
|
+
const limit = value.limit;
|
|
160
|
+
return {
|
|
161
|
+
type: "metered",
|
|
162
|
+
feature,
|
|
163
|
+
allowed: used < limit,
|
|
164
|
+
used,
|
|
165
|
+
limit,
|
|
166
|
+
remaining: Math.max(0, limit - used)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if ((_a = plan.features) == null ? void 0 : _a.includes(feature)) {
|
|
171
|
+
return { type: "boolean", feature, allowed: true };
|
|
172
|
+
}
|
|
173
|
+
return { type: "boolean", feature, allowed: false };
|
|
174
|
+
}
|
|
175
|
+
checkAll(userId) {
|
|
176
|
+
const sub = this.store.getSubscriptionByUser(userId);
|
|
177
|
+
if (!sub) return {};
|
|
178
|
+
const plan = this.config.plans[sub.planKey];
|
|
179
|
+
if (!plan) return {};
|
|
180
|
+
const result = {};
|
|
181
|
+
if (plan.entitlements) {
|
|
182
|
+
for (const feature of Object.keys(plan.entitlements)) {
|
|
183
|
+
result[feature] = this.check(userId, feature);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (plan.features) {
|
|
187
|
+
for (const feature of plan.features) {
|
|
188
|
+
if (!result[feature]) {
|
|
189
|
+
result[feature] = { type: "boolean", feature, allowed: true };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// src/billing/test-billing.ts
|
|
198
|
+
function getPeriodEnd(start, interval) {
|
|
199
|
+
const end = new Date(start);
|
|
200
|
+
if (interval === "monthly") {
|
|
201
|
+
end.setUTCMonth(end.getUTCMonth() + 1);
|
|
202
|
+
} else {
|
|
203
|
+
end.setUTCFullYear(end.getUTCFullYear() + 1);
|
|
204
|
+
}
|
|
205
|
+
return end;
|
|
206
|
+
}
|
|
207
|
+
function buildTestBilling(config, startDate) {
|
|
208
|
+
const clock = new TestClock(startDate);
|
|
209
|
+
const store = new InMemoryStore();
|
|
210
|
+
const checker = new EntitlementChecker(config, store);
|
|
211
|
+
return {
|
|
212
|
+
clock,
|
|
213
|
+
store,
|
|
214
|
+
checker,
|
|
215
|
+
subscribe(userId, planKey, priceIndex = 0) {
|
|
216
|
+
const plan = config.plans[planKey];
|
|
217
|
+
if (!plan) throw new Error(`Plan "${planKey}" not found in config`);
|
|
218
|
+
if (!plan.prices[priceIndex]) throw new Error(`Price index ${priceIndex} not found on plan "${planKey}"`);
|
|
219
|
+
const existing = store.getSubscriptionByUser(userId);
|
|
220
|
+
if (existing) throw new Error(`User "${userId}" already has an active subscription (${existing.planKey})`);
|
|
221
|
+
const now = clock.now();
|
|
222
|
+
const interval = plan.prices[priceIndex].interval;
|
|
223
|
+
return store.createSubscription({
|
|
224
|
+
userId,
|
|
225
|
+
planKey,
|
|
226
|
+
priceIndex,
|
|
227
|
+
periodStart: now,
|
|
228
|
+
periodEnd: getPeriodEnd(now, interval)
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
changePlan(userId, newPlanKey, priceIndex = 0) {
|
|
232
|
+
const sub = store.getSubscriptionByUser(userId);
|
|
233
|
+
if (!sub) throw new Error(`User "${userId}" has no active subscription`);
|
|
234
|
+
const plan = config.plans[newPlanKey];
|
|
235
|
+
if (!plan) throw new Error(`Plan "${newPlanKey}" not found in config`);
|
|
236
|
+
if (!plan.prices[priceIndex]) throw new Error(`Price index ${priceIndex} not found on plan "${newPlanKey}"`);
|
|
237
|
+
store.updateSubscription(sub.id, { planKey: newPlanKey, priceIndex });
|
|
238
|
+
return store.getSubscription(sub.id);
|
|
239
|
+
},
|
|
240
|
+
cancel(userId) {
|
|
241
|
+
const sub = store.getSubscriptionByUser(userId);
|
|
242
|
+
if (!sub) throw new Error(`User "${userId}" has no active subscription`);
|
|
243
|
+
store.updateSubscription(sub.id, { cancelAtPeriodEnd: true });
|
|
244
|
+
return store.getSubscription(sub.id);
|
|
245
|
+
},
|
|
246
|
+
reportUsage(userId, metric, quantity) {
|
|
247
|
+
store.recordUsage(userId, metric, quantity, clock.now());
|
|
248
|
+
},
|
|
249
|
+
checkEntitlement(userId, feature) {
|
|
250
|
+
return checker.check(userId, feature);
|
|
251
|
+
},
|
|
252
|
+
getSubscription(userId) {
|
|
253
|
+
return store.getSubscriptionByUser(userId);
|
|
254
|
+
},
|
|
255
|
+
advanceTime(options) {
|
|
256
|
+
return clock.advance(options);
|
|
257
|
+
},
|
|
258
|
+
subscriberCounts() {
|
|
259
|
+
const counts = {};
|
|
260
|
+
for (const planKey of Object.keys(config.plans)) {
|
|
261
|
+
counts[planKey] = store.subscriberCount(planKey);
|
|
262
|
+
}
|
|
263
|
+
return counts;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function createTestBilling(options) {
|
|
268
|
+
const config = "billing" in options ? options.billing._config : options.config;
|
|
269
|
+
return buildTestBilling(config, options.startDate);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/billing/index.ts
|
|
273
|
+
var import_billing = require("@auth-gate/billing");
|
|
274
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
275
|
+
0 && (module.exports = {
|
|
276
|
+
EntitlementChecker,
|
|
277
|
+
InMemoryStore,
|
|
278
|
+
TestClock,
|
|
279
|
+
computeDiff,
|
|
280
|
+
createTestBilling,
|
|
281
|
+
priceConfigKey
|
|
282
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { BillingConfig, TypedBilling, PlanKey, FeatureKey } from '@auth-gate/billing';
|
|
2
|
+
export { computeDiff, priceConfigKey } from '@auth-gate/billing';
|
|
3
|
+
|
|
4
|
+
interface AdvanceOptions {
|
|
5
|
+
days?: number;
|
|
6
|
+
months?: number;
|
|
7
|
+
}
|
|
8
|
+
type BillingBoundary = "month_end" | "year_end";
|
|
9
|
+
declare class TestClock {
|
|
10
|
+
private _now;
|
|
11
|
+
constructor(startDate?: Date);
|
|
12
|
+
now(): Date;
|
|
13
|
+
advance(options: AdvanceOptions): Date;
|
|
14
|
+
/**
|
|
15
|
+
* Check if advancing from `before` to `after` crosses a billing boundary.
|
|
16
|
+
*/
|
|
17
|
+
crossesBoundary(before: Date, after: Date, boundary: BillingBoundary): boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Subscription {
|
|
21
|
+
id: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
planKey: string;
|
|
24
|
+
priceIndex: number;
|
|
25
|
+
status: "active" | "canceled" | "past_due" | "trialing";
|
|
26
|
+
currentPeriodStart: Date;
|
|
27
|
+
currentPeriodEnd: Date;
|
|
28
|
+
cancelAtPeriodEnd: boolean;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
}
|
|
31
|
+
interface UsageRecord {
|
|
32
|
+
userId: string;
|
|
33
|
+
metric: string;
|
|
34
|
+
quantity: number;
|
|
35
|
+
timestamp: Date;
|
|
36
|
+
}
|
|
37
|
+
declare class InMemoryStore {
|
|
38
|
+
private subscriptions;
|
|
39
|
+
private usage;
|
|
40
|
+
private nextId;
|
|
41
|
+
createSubscription(params: {
|
|
42
|
+
userId: string;
|
|
43
|
+
planKey: string;
|
|
44
|
+
priceIndex?: number;
|
|
45
|
+
status?: Subscription["status"];
|
|
46
|
+
periodStart: Date;
|
|
47
|
+
periodEnd: Date;
|
|
48
|
+
}): Subscription;
|
|
49
|
+
getSubscription(id: string): Subscription | undefined;
|
|
50
|
+
getSubscriptionByUser(userId: string): Subscription | undefined;
|
|
51
|
+
updateSubscription(id: string, updates: Partial<Pick<Subscription, "planKey" | "priceIndex" | "status" | "cancelAtPeriodEnd" | "currentPeriodStart" | "currentPeriodEnd">>): Subscription | undefined;
|
|
52
|
+
listSubscriptions(filter?: {
|
|
53
|
+
status?: Subscription["status"];
|
|
54
|
+
planKey?: string;
|
|
55
|
+
}): Subscription[];
|
|
56
|
+
recordUsage(userId: string, metric: string, quantity: number, timestamp: Date): void;
|
|
57
|
+
getUsage(userId: string, metric: string, periodStart: Date, periodEnd: Date): number;
|
|
58
|
+
subscriberCount(planKey: string): number;
|
|
59
|
+
clear(): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface BooleanCheck {
|
|
63
|
+
type: "boolean";
|
|
64
|
+
feature: string;
|
|
65
|
+
allowed: boolean;
|
|
66
|
+
}
|
|
67
|
+
interface MeteredCheck {
|
|
68
|
+
type: "metered";
|
|
69
|
+
feature: string;
|
|
70
|
+
allowed: boolean;
|
|
71
|
+
used: number;
|
|
72
|
+
limit: number;
|
|
73
|
+
remaining: number;
|
|
74
|
+
}
|
|
75
|
+
type EntitlementCheck = BooleanCheck | MeteredCheck;
|
|
76
|
+
declare class EntitlementChecker {
|
|
77
|
+
private config;
|
|
78
|
+
private store;
|
|
79
|
+
constructor(config: BillingConfig, store: InMemoryStore);
|
|
80
|
+
check(userId: string, feature: string): EntitlementCheck;
|
|
81
|
+
checkAll(userId: string): Record<string, EntitlementCheck>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface TestBillingOptions {
|
|
85
|
+
config: BillingConfig;
|
|
86
|
+
startDate?: Date;
|
|
87
|
+
}
|
|
88
|
+
interface TestBilling {
|
|
89
|
+
clock: TestClock;
|
|
90
|
+
store: InMemoryStore;
|
|
91
|
+
checker: EntitlementChecker;
|
|
92
|
+
subscribe(userId: string, planKey: string, priceIndex?: number): Subscription;
|
|
93
|
+
changePlan(userId: string, newPlanKey: string, priceIndex?: number): Subscription;
|
|
94
|
+
cancel(userId: string): Subscription;
|
|
95
|
+
reportUsage(userId: string, metric: string, quantity: number): void;
|
|
96
|
+
checkEntitlement(userId: string, feature: string): EntitlementCheck;
|
|
97
|
+
getSubscription(userId: string): Subscription | undefined;
|
|
98
|
+
advanceTime(options: {
|
|
99
|
+
days?: number;
|
|
100
|
+
months?: number;
|
|
101
|
+
}): Date;
|
|
102
|
+
subscriberCounts(): Record<string, number>;
|
|
103
|
+
}
|
|
104
|
+
interface TypedTestBillingOptions<B extends TypedBilling<any>> {
|
|
105
|
+
billing: B;
|
|
106
|
+
startDate?: Date;
|
|
107
|
+
}
|
|
108
|
+
interface TypedTestBilling<B extends TypedBilling<any>> {
|
|
109
|
+
clock: TestClock;
|
|
110
|
+
store: InMemoryStore;
|
|
111
|
+
checker: EntitlementChecker;
|
|
112
|
+
subscribe(userId: string, planKey: PlanKey<B>, priceIndex?: number): Subscription;
|
|
113
|
+
changePlan(userId: string, newPlanKey: PlanKey<B>, priceIndex?: number): Subscription;
|
|
114
|
+
cancel(userId: string): Subscription;
|
|
115
|
+
reportUsage(userId: string, metric: FeatureKey<B>, quantity: number): void;
|
|
116
|
+
checkEntitlement(userId: string, feature: FeatureKey<B>): EntitlementCheck;
|
|
117
|
+
getSubscription(userId: string): Subscription | undefined;
|
|
118
|
+
advanceTime(options: {
|
|
119
|
+
days?: number;
|
|
120
|
+
months?: number;
|
|
121
|
+
}): Date;
|
|
122
|
+
subscriberCounts(): Record<PlanKey<B>, number>;
|
|
123
|
+
}
|
|
124
|
+
/** Create typed test billing from a `defineBilling()` config. */
|
|
125
|
+
declare function createTestBilling<B extends TypedBilling<any>>(options: TypedTestBillingOptions<B>): TypedTestBilling<B>;
|
|
126
|
+
/** Create untyped test billing (legacy). */
|
|
127
|
+
declare function createTestBilling(options: TestBillingOptions): TestBilling;
|
|
128
|
+
|
|
129
|
+
export { type AdvanceOptions, type BillingBoundary, type BooleanCheck, type EntitlementCheck, EntitlementChecker, InMemoryStore, type MeteredCheck, type Subscription, type TestBilling, type TestBillingOptions, TestClock, type TypedTestBilling, type TypedTestBillingOptions, type UsageRecord, createTestBilling };
|