@auth-gate/testing 0.9.3 → 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 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 };