@auth-gate/testing 0.9.3 → 0.11.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.
@@ -0,0 +1,167 @@
1
+ import { RbacConfig, ResourceConfig, RoleConfig } from '@auth-gate/rbac';
2
+ export { GrantValue, RbacConfig, ResourceConfig, RoleConfig } from '@auth-gate/rbac';
3
+
4
+ /**
5
+ * Create a minimal valid RbacConfig for testing.
6
+ *
7
+ * Defaults:
8
+ * - Resource `documents` with actions `read`, `write`, `delete`
9
+ * - Role `admin` with all three grants
10
+ * - Role `viewer` with `read` only, marked as `isDefault`
11
+ *
12
+ * Pass `overrides` to merge/replace specific keys.
13
+ */
14
+ declare function createTestRbacConfig(overrides?: Partial<RbacConfig>): RbacConfig;
15
+ /**
16
+ * Create a single-resource record for use in test configs.
17
+ *
18
+ * @param key - The resource key (e.g. `"billing"`).
19
+ * @param actions - Array of action strings (e.g. `["read", "manage"]`).
20
+ * @param overrides - Additional ResourceConfig fields (e.g. `scopes`).
21
+ * @returns `Record<string, ResourceConfig>` keyed by `key`.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const config = createTestRbacConfig({
26
+ * resources: {
27
+ * ...createTestResource("billing", ["read", "manage"]),
28
+ * ...createTestResource("users", ["read", "invite", "remove"]),
29
+ * },
30
+ * });
31
+ * ```
32
+ */
33
+ declare function createTestResource(key: string, actions: string[], overrides?: Partial<ResourceConfig>): Record<string, ResourceConfig>;
34
+ /**
35
+ * Create a single-role record for use in test configs.
36
+ *
37
+ * @param key - The role key (e.g. `"editor"`).
38
+ * @param name - Human-readable display name (e.g. `"Editor"`).
39
+ * @param grants - The grants matrix.
40
+ * @param overrides - Additional RoleConfig fields (e.g. `inherits`, `isDefault`).
41
+ * @returns `Record<string, RoleConfig>` keyed by `key`.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const config = createTestRbacConfig({
46
+ * roles: {
47
+ * ...createTestRole("editor", "Editor", {
48
+ * documents: { read: true, write: true },
49
+ * }),
50
+ * },
51
+ * });
52
+ * ```
53
+ */
54
+ declare function createTestRole(key: string, name: string, grants: Record<string, Record<string, true | string | {
55
+ when: string;
56
+ scope?: string;
57
+ }>>, overrides?: Partial<Omit<RoleConfig, "name" | "grants">>): Record<string, RoleConfig>;
58
+
59
+ /**
60
+ * In-memory RBAC permission checker for testing.
61
+ *
62
+ * Resolves permissions from an RbacConfig, including role inheritance.
63
+ * All grant values (`true`, scope string, or conditional `{ when, scope }`)
64
+ * are treated as "granted" for the purpose of boolean permission checks.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * import { defineRbac } from "@auth-gate/rbac";
69
+ * import { RbacChecker } from "@auth-gate/testing/rbac";
70
+ *
71
+ * const rbac = defineRbac({ ... });
72
+ * const checker = new RbacChecker(rbac);
73
+ *
74
+ * checker.hasPermission("admin", "documents:read"); // true
75
+ * checker.getPermissions("viewer"); // Set { "documents:read" }
76
+ * checker.getRolesWithPermission("documents:delete"); // ["admin"]
77
+ * ```
78
+ */
79
+ declare class RbacChecker {
80
+ private readonly config;
81
+ /**
82
+ * @param input - Either a plain `RbacConfig` object, or a `TypedRbac`
83
+ * instance (from `defineRbac()`) which exposes `_config`.
84
+ */
85
+ constructor(input: RbacConfig | {
86
+ _config: RbacConfig;
87
+ });
88
+ /**
89
+ * Check whether a role has a specific permission.
90
+ *
91
+ * @param roleKey - The role key (e.g. `"admin"`).
92
+ * @param permission - A `"resource:action"` string (e.g. `"documents:read"`).
93
+ * @returns `true` if the role (or any ancestor via inheritance) grants the permission.
94
+ */
95
+ hasPermission(roleKey: string, permission: string): boolean;
96
+ /**
97
+ * Collect all permissions for a role, including inherited ones.
98
+ *
99
+ * @param roleKey - The role key.
100
+ * @returns A `Set<string>` of `"resource:action"` strings.
101
+ */
102
+ getPermissions(roleKey: string): Set<string>;
103
+ /**
104
+ * Find all roles that grant a specific permission.
105
+ *
106
+ * @param permission - A `"resource:action"` string.
107
+ * @returns Array of role keys that have the permission (directly or via inheritance).
108
+ */
109
+ getRolesWithPermission(permission: string): string[];
110
+ /**
111
+ * Recursively collect permissions for a role, following `inherits` chains.
112
+ * Uses a `visited` set to prevent infinite loops from circular inheritance.
113
+ */
114
+ private collectPermissions;
115
+ }
116
+
117
+ /**
118
+ * Assert that a role has a specific permission. Throws with a descriptive
119
+ * error message listing the role's actual permissions if the assertion fails.
120
+ *
121
+ * @param rbac - An `RbacConfig` or `TypedRbac` instance.
122
+ * @param roleKey - The role key to check.
123
+ * @param permission - The `"resource:action"` permission string expected.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * expectPermission(config, "admin", "documents:write");
128
+ * ```
129
+ */
130
+ declare function expectPermission(rbac: RbacConfig | {
131
+ _config: RbacConfig;
132
+ }, roleKey: string, permission: string): void;
133
+ /**
134
+ * Assert that a role does NOT have a specific permission. Throws if the
135
+ * role has the permission.
136
+ *
137
+ * @param rbac - An `RbacConfig` or `TypedRbac` instance.
138
+ * @param roleKey - The role key to check.
139
+ * @param permission - The `"resource:action"` permission string that must be absent.
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * expectNoPermission(config, "viewer", "documents:delete");
144
+ * ```
145
+ */
146
+ declare function expectNoPermission(rbac: RbacConfig | {
147
+ _config: RbacConfig;
148
+ }, roleKey: string, permission: string): void;
149
+ /**
150
+ * Assert that a role has exactly the specified set of permissions -- no more,
151
+ * no less. Throws with details about missing and extra permissions if the
152
+ * sets do not match.
153
+ *
154
+ * @param rbac - An `RbacConfig` or `TypedRbac` instance.
155
+ * @param roleKey - The role key to check.
156
+ * @param expected - The exact set of `"resource:action"` strings expected.
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * expectRolePermissions(config, "viewer", ["documents:read"]);
161
+ * ```
162
+ */
163
+ declare function expectRolePermissions(rbac: RbacConfig | {
164
+ _config: RbacConfig;
165
+ }, roleKey: string, expected: string[]): void;
166
+
167
+ export { RbacChecker, createTestRbacConfig, createTestResource, createTestRole, expectNoPermission, expectPermission, expectRolePermissions };
@@ -0,0 +1,167 @@
1
+ import { RbacConfig, ResourceConfig, RoleConfig } from '@auth-gate/rbac';
2
+ export { GrantValue, RbacConfig, ResourceConfig, RoleConfig } from '@auth-gate/rbac';
3
+
4
+ /**
5
+ * Create a minimal valid RbacConfig for testing.
6
+ *
7
+ * Defaults:
8
+ * - Resource `documents` with actions `read`, `write`, `delete`
9
+ * - Role `admin` with all three grants
10
+ * - Role `viewer` with `read` only, marked as `isDefault`
11
+ *
12
+ * Pass `overrides` to merge/replace specific keys.
13
+ */
14
+ declare function createTestRbacConfig(overrides?: Partial<RbacConfig>): RbacConfig;
15
+ /**
16
+ * Create a single-resource record for use in test configs.
17
+ *
18
+ * @param key - The resource key (e.g. `"billing"`).
19
+ * @param actions - Array of action strings (e.g. `["read", "manage"]`).
20
+ * @param overrides - Additional ResourceConfig fields (e.g. `scopes`).
21
+ * @returns `Record<string, ResourceConfig>` keyed by `key`.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const config = createTestRbacConfig({
26
+ * resources: {
27
+ * ...createTestResource("billing", ["read", "manage"]),
28
+ * ...createTestResource("users", ["read", "invite", "remove"]),
29
+ * },
30
+ * });
31
+ * ```
32
+ */
33
+ declare function createTestResource(key: string, actions: string[], overrides?: Partial<ResourceConfig>): Record<string, ResourceConfig>;
34
+ /**
35
+ * Create a single-role record for use in test configs.
36
+ *
37
+ * @param key - The role key (e.g. `"editor"`).
38
+ * @param name - Human-readable display name (e.g. `"Editor"`).
39
+ * @param grants - The grants matrix.
40
+ * @param overrides - Additional RoleConfig fields (e.g. `inherits`, `isDefault`).
41
+ * @returns `Record<string, RoleConfig>` keyed by `key`.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const config = createTestRbacConfig({
46
+ * roles: {
47
+ * ...createTestRole("editor", "Editor", {
48
+ * documents: { read: true, write: true },
49
+ * }),
50
+ * },
51
+ * });
52
+ * ```
53
+ */
54
+ declare function createTestRole(key: string, name: string, grants: Record<string, Record<string, true | string | {
55
+ when: string;
56
+ scope?: string;
57
+ }>>, overrides?: Partial<Omit<RoleConfig, "name" | "grants">>): Record<string, RoleConfig>;
58
+
59
+ /**
60
+ * In-memory RBAC permission checker for testing.
61
+ *
62
+ * Resolves permissions from an RbacConfig, including role inheritance.
63
+ * All grant values (`true`, scope string, or conditional `{ when, scope }`)
64
+ * are treated as "granted" for the purpose of boolean permission checks.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * import { defineRbac } from "@auth-gate/rbac";
69
+ * import { RbacChecker } from "@auth-gate/testing/rbac";
70
+ *
71
+ * const rbac = defineRbac({ ... });
72
+ * const checker = new RbacChecker(rbac);
73
+ *
74
+ * checker.hasPermission("admin", "documents:read"); // true
75
+ * checker.getPermissions("viewer"); // Set { "documents:read" }
76
+ * checker.getRolesWithPermission("documents:delete"); // ["admin"]
77
+ * ```
78
+ */
79
+ declare class RbacChecker {
80
+ private readonly config;
81
+ /**
82
+ * @param input - Either a plain `RbacConfig` object, or a `TypedRbac`
83
+ * instance (from `defineRbac()`) which exposes `_config`.
84
+ */
85
+ constructor(input: RbacConfig | {
86
+ _config: RbacConfig;
87
+ });
88
+ /**
89
+ * Check whether a role has a specific permission.
90
+ *
91
+ * @param roleKey - The role key (e.g. `"admin"`).
92
+ * @param permission - A `"resource:action"` string (e.g. `"documents:read"`).
93
+ * @returns `true` if the role (or any ancestor via inheritance) grants the permission.
94
+ */
95
+ hasPermission(roleKey: string, permission: string): boolean;
96
+ /**
97
+ * Collect all permissions for a role, including inherited ones.
98
+ *
99
+ * @param roleKey - The role key.
100
+ * @returns A `Set<string>` of `"resource:action"` strings.
101
+ */
102
+ getPermissions(roleKey: string): Set<string>;
103
+ /**
104
+ * Find all roles that grant a specific permission.
105
+ *
106
+ * @param permission - A `"resource:action"` string.
107
+ * @returns Array of role keys that have the permission (directly or via inheritance).
108
+ */
109
+ getRolesWithPermission(permission: string): string[];
110
+ /**
111
+ * Recursively collect permissions for a role, following `inherits` chains.
112
+ * Uses a `visited` set to prevent infinite loops from circular inheritance.
113
+ */
114
+ private collectPermissions;
115
+ }
116
+
117
+ /**
118
+ * Assert that a role has a specific permission. Throws with a descriptive
119
+ * error message listing the role's actual permissions if the assertion fails.
120
+ *
121
+ * @param rbac - An `RbacConfig` or `TypedRbac` instance.
122
+ * @param roleKey - The role key to check.
123
+ * @param permission - The `"resource:action"` permission string expected.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * expectPermission(config, "admin", "documents:write");
128
+ * ```
129
+ */
130
+ declare function expectPermission(rbac: RbacConfig | {
131
+ _config: RbacConfig;
132
+ }, roleKey: string, permission: string): void;
133
+ /**
134
+ * Assert that a role does NOT have a specific permission. Throws if the
135
+ * role has the permission.
136
+ *
137
+ * @param rbac - An `RbacConfig` or `TypedRbac` instance.
138
+ * @param roleKey - The role key to check.
139
+ * @param permission - The `"resource:action"` permission string that must be absent.
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * expectNoPermission(config, "viewer", "documents:delete");
144
+ * ```
145
+ */
146
+ declare function expectNoPermission(rbac: RbacConfig | {
147
+ _config: RbacConfig;
148
+ }, roleKey: string, permission: string): void;
149
+ /**
150
+ * Assert that a role has exactly the specified set of permissions -- no more,
151
+ * no less. Throws with details about missing and extra permissions if the
152
+ * sets do not match.
153
+ *
154
+ * @param rbac - An `RbacConfig` or `TypedRbac` instance.
155
+ * @param roleKey - The role key to check.
156
+ * @param expected - The exact set of `"resource:action"` strings expected.
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * expectRolePermissions(config, "viewer", ["documents:read"]);
161
+ * ```
162
+ */
163
+ declare function expectRolePermissions(rbac: RbacConfig | {
164
+ _config: RbacConfig;
165
+ }, roleKey: string, expected: string[]): void;
166
+
167
+ export { RbacChecker, createTestRbacConfig, createTestResource, createTestRole, expectNoPermission, expectPermission, expectRolePermissions };
@@ -0,0 +1,180 @@
1
+ import {
2
+ __spreadValues
3
+ } from "../chunk-MBTDPSN5.mjs";
4
+
5
+ // src/rbac/builders.ts
6
+ function createTestRbacConfig(overrides) {
7
+ return __spreadValues({
8
+ resources: {
9
+ documents: { actions: ["read", "write", "delete"] }
10
+ },
11
+ roles: {
12
+ admin: {
13
+ name: "Admin",
14
+ grants: {
15
+ documents: { read: true, write: true, delete: true }
16
+ }
17
+ },
18
+ viewer: {
19
+ name: "Viewer",
20
+ isDefault: true,
21
+ grants: {
22
+ documents: { read: true }
23
+ }
24
+ }
25
+ }
26
+ }, overrides);
27
+ }
28
+ function createTestResource(key, actions, overrides) {
29
+ return {
30
+ [key]: __spreadValues({
31
+ actions
32
+ }, overrides)
33
+ };
34
+ }
35
+ function createTestRole(key, name, grants, overrides) {
36
+ return {
37
+ [key]: __spreadValues({
38
+ name,
39
+ grants
40
+ }, overrides)
41
+ };
42
+ }
43
+
44
+ // src/rbac/checker.ts
45
+ var RbacChecker = class {
46
+ /**
47
+ * @param input - Either a plain `RbacConfig` object, or a `TypedRbac`
48
+ * instance (from `defineRbac()`) which exposes `_config`.
49
+ */
50
+ constructor(input) {
51
+ this.config = "_config" in input ? input._config : input;
52
+ }
53
+ /**
54
+ * Check whether a role has a specific permission.
55
+ *
56
+ * @param roleKey - The role key (e.g. `"admin"`).
57
+ * @param permission - A `"resource:action"` string (e.g. `"documents:read"`).
58
+ * @returns `true` if the role (or any ancestor via inheritance) grants the permission.
59
+ */
60
+ hasPermission(roleKey, permission) {
61
+ return this.getPermissions(roleKey).has(permission);
62
+ }
63
+ /**
64
+ * Collect all permissions for a role, including inherited ones.
65
+ *
66
+ * @param roleKey - The role key.
67
+ * @returns A `Set<string>` of `"resource:action"` strings.
68
+ */
69
+ getPermissions(roleKey) {
70
+ const permissions = /* @__PURE__ */ new Set();
71
+ this.collectPermissions(roleKey, permissions, /* @__PURE__ */ new Set());
72
+ return permissions;
73
+ }
74
+ /**
75
+ * Find all roles that grant a specific permission.
76
+ *
77
+ * @param permission - A `"resource:action"` string.
78
+ * @returns Array of role keys that have the permission (directly or via inheritance).
79
+ */
80
+ getRolesWithPermission(permission) {
81
+ const result = [];
82
+ for (const roleKey of Object.keys(this.config.roles)) {
83
+ if (this.hasPermission(roleKey, permission)) {
84
+ result.push(roleKey);
85
+ }
86
+ }
87
+ return result;
88
+ }
89
+ /**
90
+ * Recursively collect permissions for a role, following `inherits` chains.
91
+ * Uses a `visited` set to prevent infinite loops from circular inheritance.
92
+ */
93
+ collectPermissions(roleKey, permissions, visited) {
94
+ if (visited.has(roleKey)) return;
95
+ visited.add(roleKey);
96
+ const role = this.config.roles[roleKey];
97
+ if (!role) return;
98
+ for (const [resource, actions] of Object.entries(role.grants)) {
99
+ for (const [action, value] of Object.entries(
100
+ actions
101
+ )) {
102
+ if (isGranted(value)) {
103
+ permissions.add(`${resource}:${action}`);
104
+ }
105
+ }
106
+ }
107
+ if (role.inherits) {
108
+ for (const parentKey of role.inherits) {
109
+ this.collectPermissions(parentKey, permissions, visited);
110
+ }
111
+ }
112
+ }
113
+ };
114
+ function isGranted(value) {
115
+ if (value === true) return true;
116
+ if (typeof value === "string") return true;
117
+ if (typeof value === "object" && value !== null && "when" in value) return true;
118
+ return false;
119
+ }
120
+
121
+ // src/rbac/assertions.ts
122
+ function toChecker(input) {
123
+ return new RbacChecker(input);
124
+ }
125
+ function expectPermission(rbac, roleKey, permission) {
126
+ const checker = toChecker(rbac);
127
+ if (!checker.hasPermission(roleKey, permission)) {
128
+ const actual = checker.getPermissions(roleKey);
129
+ const permList = actual.size > 0 ? Array.from(actual).sort().join(", ") : "(none)";
130
+ throw new Error(
131
+ `Expected role "${roleKey}" to have permission "${permission}", but it does not. Actual permissions: [${permList}]`
132
+ );
133
+ }
134
+ }
135
+ function expectNoPermission(rbac, roleKey, permission) {
136
+ const checker = toChecker(rbac);
137
+ if (checker.hasPermission(roleKey, permission)) {
138
+ throw new Error(
139
+ `Expected role "${roleKey}" NOT to have permission "${permission}", but it does.`
140
+ );
141
+ }
142
+ }
143
+ function expectRolePermissions(rbac, roleKey, expected) {
144
+ const checker = toChecker(rbac);
145
+ const actual = checker.getPermissions(roleKey);
146
+ const expectedSet = new Set(expected);
147
+ const missing = [];
148
+ const extra = [];
149
+ for (const perm of expectedSet) {
150
+ if (!actual.has(perm)) {
151
+ missing.push(perm);
152
+ }
153
+ }
154
+ for (const perm of actual) {
155
+ if (!expectedSet.has(perm)) {
156
+ extra.push(perm);
157
+ }
158
+ }
159
+ if (missing.length > 0 || extra.length > 0) {
160
+ const parts = [];
161
+ if (missing.length > 0) {
162
+ parts.push(`Missing: [${missing.sort().join(", ")}]`);
163
+ }
164
+ if (extra.length > 0) {
165
+ parts.push(`Extra: [${extra.sort().join(", ")}]`);
166
+ }
167
+ throw new Error(
168
+ `Role "${roleKey}" permissions do not match expected set. ${parts.join(". ")}`
169
+ );
170
+ }
171
+ }
172
+ export {
173
+ RbacChecker,
174
+ createTestRbacConfig,
175
+ createTestResource,
176
+ createTestRole,
177
+ expectNoPermission,
178
+ expectPermission,
179
+ expectRolePermissions
180
+ };
package/icon.png ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auth-gate/testing",
3
- "version": "0.9.3",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -17,20 +17,33 @@
17
17
  "types": "./dist/cypress.d.ts",
18
18
  "import": "./dist/cypress.mjs",
19
19
  "require": "./dist/cypress.cjs"
20
+ },
21
+ "./billing": {
22
+ "types": "./dist/billing/index.d.ts",
23
+ "import": "./dist/billing/index.mjs",
24
+ "require": "./dist/billing/index.cjs"
25
+ },
26
+ "./rbac": {
27
+ "types": "./dist/rbac/index.d.ts",
28
+ "import": "./dist/rbac/index.mjs",
29
+ "require": "./dist/rbac/index.cjs"
20
30
  }
21
31
  },
22
32
  "main": "./dist/index.cjs",
23
33
  "module": "./dist/index.mjs",
24
34
  "types": "./dist/index.d.ts",
25
35
  "files": [
26
- "dist"
36
+ "dist",
37
+ "icon.png"
27
38
  ],
28
39
  "dependencies": {
29
- "@auth-gate/core": "0.9.3"
40
+ "@auth-gate/core": "0.11.0"
30
41
  },
31
42
  "peerDependencies": {
32
43
  "@playwright/test": ">=1.40",
33
- "cypress": ">=12"
44
+ "cypress": ">=12",
45
+ "@auth-gate/billing": ">=0.1.0",
46
+ "@auth-gate/rbac": ">=0.1.0"
34
47
  },
35
48
  "peerDependenciesMeta": {
36
49
  "@playwright/test": {
@@ -38,16 +51,26 @@
38
51
  },
39
52
  "cypress": {
40
53
  "optional": true
54
+ },
55
+ "@auth-gate/billing": {
56
+ "optional": true
57
+ },
58
+ "@auth-gate/rbac": {
59
+ "optional": true
41
60
  }
42
61
  },
43
62
  "devDependencies": {
44
63
  "@playwright/test": "^1.50.0",
45
64
  "cypress": "^13.0.0",
46
65
  "tsup": "^8.0.0",
47
- "typescript": "^5"
66
+ "typescript": "^5",
67
+ "vitest": "^3.0.0",
68
+ "@auth-gate/billing": "0.11.0",
69
+ "@auth-gate/rbac": "0.11.0"
48
70
  },
49
71
  "scripts": {
50
72
  "build": "tsup",
51
- "typecheck": "tsc --noEmit"
73
+ "typecheck": "tsc --noEmit",
74
+ "test": "vitest run"
52
75
  }
53
76
  }