@jperezmart/nest-casl-testing 0.1.1 → 0.2.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 +69 -0
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +12 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @jperezmart/nest-casl-testing
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@jperezmart/nest-casl-testing)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
Testing utilities for [`@jperezmart/nest-casl`](https://github.com/jperezmart/nest-casl/tree/main/packages/core).
|
|
7
|
+
Build a CASL ability straight from a permissions map and a user, so permission
|
|
8
|
+
definitions can be unit-tested without booting a Nest application.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add -D @jperezmart/nest-casl-testing
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`@casl/ability` and `@jperezmart/nest-casl` are peer dependencies.
|
|
17
|
+
|
|
18
|
+
## `buildAbilityForTest(permissions, user, options?)`
|
|
19
|
+
|
|
20
|
+
Takes the same `permissions` map you pass to `CaslModule.forFeature` and the
|
|
21
|
+
user you would otherwise have to authenticate, and returns the ability CASL
|
|
22
|
+
would have built:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { buildAbilityForTest } from '@jperezmart/nest-casl-testing';
|
|
26
|
+
import type { RolePermissions } from '@jperezmart/nest-casl';
|
|
27
|
+
import { subject } from '@casl/ability';
|
|
28
|
+
|
|
29
|
+
interface AppUser {
|
|
30
|
+
id: string;
|
|
31
|
+
roles: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const permissions: RolePermissions<'author', AppUser> = {
|
|
35
|
+
author: (user, { can }) => {
|
|
36
|
+
can('read', 'Article');
|
|
37
|
+
can('update', 'Article', { authorId: user.id });
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const ability = buildAbilityForTest(permissions, {
|
|
42
|
+
id: '1',
|
|
43
|
+
roles: ['author'],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(ability.can('read', 'Article')).toBe(true);
|
|
47
|
+
expect(ability.can('update', subject('Article', { authorId: '1' }))).toBe(true);
|
|
48
|
+
expect(ability.can('update', subject('Article', { authorId: '2' }))).toBe(
|
|
49
|
+
false,
|
|
50
|
+
);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Options
|
|
54
|
+
|
|
55
|
+
Both mirror `CaslModule.forRoot`, so a test builds the same ability the running
|
|
56
|
+
application would:
|
|
57
|
+
|
|
58
|
+
| Option | Type | Effect |
|
|
59
|
+
| ------------------- | ----------------------------- | ------------------------------------------------------------------------------------ |
|
|
60
|
+
| `superuserRole` | `string` | A user holding this role gets `can('manage', 'all')` and no other rule is evaluated. |
|
|
61
|
+
| `detectSubjectType` | `(subject: object) => string` | Custom subject-type detection, for subjects CASL cannot name on its own. |
|
|
62
|
+
|
|
63
|
+
A role whose definition is `true` rather than a function also grants
|
|
64
|
+
`manage all`. A user whose `roles` is missing or not an array is treated as
|
|
65
|
+
having no roles, which is what `AbilityFactory` does.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -2,9 +2,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let _casl_ability = require("@casl/ability");
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
/**
|
|
5
|
-
* Build a CASL ability directly from
|
|
5
|
+
* Build a CASL ability directly from Role permissions and a user, without
|
|
6
6
|
* booting a Nest application. Intended for unit-testing permission definitions
|
|
7
|
-
* in projects that consume `@jperezmart/nest-casl`.
|
|
7
|
+
* in projects that consume `@jperezmart/nest-casl`. The role union, user and
|
|
8
|
+
* ability types are inferred from `permissions`; the user may carry roles the
|
|
9
|
+
* map does not declare.
|
|
8
10
|
*
|
|
9
11
|
* @example
|
|
10
12
|
* ```ts
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["AbilityBuilder","createMongoAbility"],"sources":["../src/index.ts"],"sourcesContent":["import type { AnyAbility, MongoAbility } from '@casl/ability';\nimport { AbilityBuilder, createMongoAbility } from '@casl/ability';\nimport type {\n AuthorizableUser,\n
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["AbilityBuilder","createMongoAbility"],"sources":["../src/index.ts"],"sourcesContent":["import type { AnyAbility, MongoAbility } from '@casl/ability';\nimport { AbilityBuilder, createMongoAbility } from '@casl/ability';\nimport type {\n AuthorizableUser,\n DefineRolePermissions,\n RolePermissions,\n} from '@jperezmart/nest-casl';\n\n/** Extra options accepted by {@link buildAbilityForTest}. */\nexport interface BuildAbilityForTestOptions {\n /**\n * Role that bypasses all checks (mirrors `CaslModule.forRoot`). Any string:\n * the superuser role usually has no entry in the Role permissions.\n */\n superuserRole?: string;\n\n /** Custom subject-type detection (mirrors `CaslModule.forRoot`). */\n detectSubjectType?: (subject: object) => string;\n}\n\n/**\n * Build a CASL ability directly from Role permissions and a user, without\n * booting a Nest application. Intended for unit-testing permission definitions\n * in projects that consume `@jperezmart/nest-casl`. The role union, user and\n * ability types are inferred from `permissions`; the user may carry roles the\n * map does not declare.\n *\n * @example\n * ```ts\n * const ability = buildAbilityForTest(permissions, { id: \"1\", roles: [\"author\"] });\n * expect(ability.can(\"update\", subject(\"Article\", { authorId: \"1\" }))).toBe(true);\n * ```\n */\nexport function buildAbilityForTest<\n Roles extends string = string,\n TUser extends AuthorizableUser = AuthorizableUser,\n TAbility extends AnyAbility = AnyAbility,\n>(\n permissions: RolePermissions<Roles, TUser, TAbility>,\n user: TUser,\n options: BuildAbilityForTestOptions = {},\n): TAbility {\n const builder = new AbilityBuilder<MongoAbility>(createMongoAbility);\n const buildOptions = options.detectSubjectType\n ? { detectSubjectType: options.detectSubjectType }\n : undefined;\n\n // Mirror AbilityFactory: a missing / non-array `roles` means \"no roles\".\n const roles: readonly string[] = Array.isArray(user.roles) ? user.roles : [];\n\n if (\n options.superuserRole !== undefined &&\n roles.includes(options.superuserRole)\n ) {\n builder.can('manage', 'all');\n return builder.build(buildOptions) as unknown as TAbility;\n }\n\n for (const role of roles) {\n const definition = (permissions as Record<string, unknown>)[role];\n if (definition === true) {\n builder.can('manage', 'all');\n } else if (typeof definition === 'function') {\n (definition as DefineRolePermissions<TUser, AnyAbility>)(\n user,\n builder as unknown as AbilityBuilder<AnyAbility>,\n );\n }\n }\n\n return builder.build(buildOptions) as unknown as TAbility;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiCA,SAAgB,oBAKd,aACA,MACA,UAAsC,CAAC,GAC7B;CACV,MAAM,UAAU,IAAIA,cAAAA,eAA6BC,cAAAA,kBAAkB;CACnE,MAAM,eAAe,QAAQ,oBACzB,EAAE,mBAAmB,QAAQ,kBAAkB,IAC/C,KAAA;CAGJ,MAAM,QAA2B,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC;CAE3E,IACE,QAAQ,kBAAkB,KAAA,KAC1B,MAAM,SAAS,QAAQ,aAAa,GACpC;EACA,QAAQ,IAAI,UAAU,KAAK;EAC3B,OAAO,QAAQ,MAAM,YAAY;CACnC;CAEA,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,aAAc,YAAwC;EAC5D,IAAI,eAAe,MACjB,QAAQ,IAAI,UAAU,KAAK;OACtB,IAAI,OAAO,eAAe,YAC/B,WACE,MACA,OACF;CAEJ;CAEA,OAAO,QAAQ,MAAM,YAAY;AACnC"}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { AnyAbility } from "@casl/ability";
|
|
2
|
-
import { AuthorizableUser,
|
|
2
|
+
import { AuthorizableUser, RolePermissions } from "@jperezmart/nest-casl";
|
|
3
3
|
|
|
4
4
|
//#region src/index.d.ts
|
|
5
5
|
/** Extra options accepted by {@link buildAbilityForTest}. */
|
|
6
|
-
interface BuildAbilityForTestOptions
|
|
7
|
-
/**
|
|
8
|
-
|
|
6
|
+
interface BuildAbilityForTestOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Role that bypasses all checks (mirrors `CaslModule.forRoot`). Any string:
|
|
9
|
+
* the superuser role usually has no entry in the Role permissions.
|
|
10
|
+
*/
|
|
11
|
+
superuserRole?: string;
|
|
9
12
|
/** Custom subject-type detection (mirrors `CaslModule.forRoot`). */
|
|
10
13
|
detectSubjectType?: (subject: object) => string;
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
13
|
-
* Build a CASL ability directly from
|
|
16
|
+
* Build a CASL ability directly from Role permissions and a user, without
|
|
14
17
|
* booting a Nest application. Intended for unit-testing permission definitions
|
|
15
|
-
* in projects that consume `@jperezmart/nest-casl`.
|
|
18
|
+
* in projects that consume `@jperezmart/nest-casl`. The role union, user and
|
|
19
|
+
* ability types are inferred from `permissions`; the user may carry roles the
|
|
20
|
+
* map does not declare.
|
|
16
21
|
*
|
|
17
22
|
* @example
|
|
18
23
|
* ```ts
|
|
@@ -20,7 +25,7 @@ interface BuildAbilityForTestOptions<Roles extends string = string> {
|
|
|
20
25
|
* expect(ability.can("update", subject("Article", { authorId: "1" }))).toBe(true);
|
|
21
26
|
* ```
|
|
22
27
|
*/
|
|
23
|
-
declare function buildAbilityForTest<Roles extends string = string, TUser extends AuthorizableUser
|
|
28
|
+
declare function buildAbilityForTest<Roles extends string = string, TUser extends AuthorizableUser = AuthorizableUser, TAbility extends AnyAbility = AnyAbility>(permissions: RolePermissions<Roles, TUser, TAbility>, user: TUser, options?: BuildAbilityForTestOptions): TAbility;
|
|
24
29
|
//#endregion
|
|
25
30
|
export { BuildAbilityForTestOptions, buildAbilityForTest };
|
|
26
31
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;UASiB,0BAAA;EAAA
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;UASiB,0BAAA;EAAA;;;;EAKf,aAAA;EAGA;EAAA,iBAAA,IAAqB,OAAA;AAAA;AAAe;AAgBtC;;;;;;;;;;;;AAhBsC,iBAgBtB,mBAAA,8CAEA,gBAAA,GAAmB,gBAAA,mBAChB,UAAA,GAAa,UAAA,EAE9B,WAAA,EAAa,eAAA,CAAgB,KAAA,EAAO,KAAA,EAAO,QAAA,GAC3C,IAAA,EAAM,KAAA,EACN,OAAA,GAAS,0BAAA,GACR,QAAA"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { AnyAbility } from "@casl/ability";
|
|
2
|
-
import { AuthorizableUser,
|
|
2
|
+
import { AuthorizableUser, RolePermissions } from "@jperezmart/nest-casl";
|
|
3
3
|
|
|
4
4
|
//#region src/index.d.ts
|
|
5
5
|
/** Extra options accepted by {@link buildAbilityForTest}. */
|
|
6
|
-
interface BuildAbilityForTestOptions
|
|
7
|
-
/**
|
|
8
|
-
|
|
6
|
+
interface BuildAbilityForTestOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Role that bypasses all checks (mirrors `CaslModule.forRoot`). Any string:
|
|
9
|
+
* the superuser role usually has no entry in the Role permissions.
|
|
10
|
+
*/
|
|
11
|
+
superuserRole?: string;
|
|
9
12
|
/** Custom subject-type detection (mirrors `CaslModule.forRoot`). */
|
|
10
13
|
detectSubjectType?: (subject: object) => string;
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
13
|
-
* Build a CASL ability directly from
|
|
16
|
+
* Build a CASL ability directly from Role permissions and a user, without
|
|
14
17
|
* booting a Nest application. Intended for unit-testing permission definitions
|
|
15
|
-
* in projects that consume `@jperezmart/nest-casl`.
|
|
18
|
+
* in projects that consume `@jperezmart/nest-casl`. The role union, user and
|
|
19
|
+
* ability types are inferred from `permissions`; the user may carry roles the
|
|
20
|
+
* map does not declare.
|
|
16
21
|
*
|
|
17
22
|
* @example
|
|
18
23
|
* ```ts
|
|
@@ -20,7 +25,7 @@ interface BuildAbilityForTestOptions<Roles extends string = string> {
|
|
|
20
25
|
* expect(ability.can("update", subject("Article", { authorId: "1" }))).toBe(true);
|
|
21
26
|
* ```
|
|
22
27
|
*/
|
|
23
|
-
declare function buildAbilityForTest<Roles extends string = string, TUser extends AuthorizableUser
|
|
28
|
+
declare function buildAbilityForTest<Roles extends string = string, TUser extends AuthorizableUser = AuthorizableUser, TAbility extends AnyAbility = AnyAbility>(permissions: RolePermissions<Roles, TUser, TAbility>, user: TUser, options?: BuildAbilityForTestOptions): TAbility;
|
|
24
29
|
//#endregion
|
|
25
30
|
export { BuildAbilityForTestOptions, buildAbilityForTest };
|
|
26
31
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;UASiB,0BAAA;EAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;UASiB,0BAAA;EAAA;;;;EAKf,aAAA;EAGA;EAAA,iBAAA,IAAqB,OAAA;AAAA;AAAe;AAgBtC;;;;;;;;;;;;AAhBsC,iBAgBtB,mBAAA,8CAEA,gBAAA,GAAmB,gBAAA,mBAChB,UAAA,GAAa,UAAA,EAE9B,WAAA,EAAa,eAAA,CAAgB,KAAA,EAAO,KAAA,EAAO,QAAA,GAC3C,IAAA,EAAM,KAAA,EACN,OAAA,GAAS,0BAAA,GACR,QAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { AbilityBuilder, createMongoAbility } from "@casl/ability";
|
|
2
2
|
//#region src/index.ts
|
|
3
3
|
/**
|
|
4
|
-
* Build a CASL ability directly from
|
|
4
|
+
* Build a CASL ability directly from Role permissions and a user, without
|
|
5
5
|
* booting a Nest application. Intended for unit-testing permission definitions
|
|
6
|
-
* in projects that consume `@jperezmart/nest-casl`.
|
|
6
|
+
* in projects that consume `@jperezmart/nest-casl`. The role union, user and
|
|
7
|
+
* ability types are inferred from `permissions`; the user may carry roles the
|
|
8
|
+
* map does not declare.
|
|
7
9
|
*
|
|
8
10
|
* @example
|
|
9
11
|
* ```ts
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { AnyAbility, MongoAbility } from '@casl/ability';\nimport { AbilityBuilder, createMongoAbility } from '@casl/ability';\nimport type {\n AuthorizableUser,\n
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { AnyAbility, MongoAbility } from '@casl/ability';\nimport { AbilityBuilder, createMongoAbility } from '@casl/ability';\nimport type {\n AuthorizableUser,\n DefineRolePermissions,\n RolePermissions,\n} from '@jperezmart/nest-casl';\n\n/** Extra options accepted by {@link buildAbilityForTest}. */\nexport interface BuildAbilityForTestOptions {\n /**\n * Role that bypasses all checks (mirrors `CaslModule.forRoot`). Any string:\n * the superuser role usually has no entry in the Role permissions.\n */\n superuserRole?: string;\n\n /** Custom subject-type detection (mirrors `CaslModule.forRoot`). */\n detectSubjectType?: (subject: object) => string;\n}\n\n/**\n * Build a CASL ability directly from Role permissions and a user, without\n * booting a Nest application. Intended for unit-testing permission definitions\n * in projects that consume `@jperezmart/nest-casl`. The role union, user and\n * ability types are inferred from `permissions`; the user may carry roles the\n * map does not declare.\n *\n * @example\n * ```ts\n * const ability = buildAbilityForTest(permissions, { id: \"1\", roles: [\"author\"] });\n * expect(ability.can(\"update\", subject(\"Article\", { authorId: \"1\" }))).toBe(true);\n * ```\n */\nexport function buildAbilityForTest<\n Roles extends string = string,\n TUser extends AuthorizableUser = AuthorizableUser,\n TAbility extends AnyAbility = AnyAbility,\n>(\n permissions: RolePermissions<Roles, TUser, TAbility>,\n user: TUser,\n options: BuildAbilityForTestOptions = {},\n): TAbility {\n const builder = new AbilityBuilder<MongoAbility>(createMongoAbility);\n const buildOptions = options.detectSubjectType\n ? { detectSubjectType: options.detectSubjectType }\n : undefined;\n\n // Mirror AbilityFactory: a missing / non-array `roles` means \"no roles\".\n const roles: readonly string[] = Array.isArray(user.roles) ? user.roles : [];\n\n if (\n options.superuserRole !== undefined &&\n roles.includes(options.superuserRole)\n ) {\n builder.can('manage', 'all');\n return builder.build(buildOptions) as unknown as TAbility;\n }\n\n for (const role of roles) {\n const definition = (permissions as Record<string, unknown>)[role];\n if (definition === true) {\n builder.can('manage', 'all');\n } else if (typeof definition === 'function') {\n (definition as DefineRolePermissions<TUser, AnyAbility>)(\n user,\n builder as unknown as AbilityBuilder<AnyAbility>,\n );\n }\n }\n\n return builder.build(buildOptions) as unknown as TAbility;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiCA,SAAgB,oBAKd,aACA,MACA,UAAsC,CAAC,GAC7B;CACV,MAAM,UAAU,IAAI,eAA6B,kBAAkB;CACnE,MAAM,eAAe,QAAQ,oBACzB,EAAE,mBAAmB,QAAQ,kBAAkB,IAC/C,KAAA;CAGJ,MAAM,QAA2B,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC;CAE3E,IACE,QAAQ,kBAAkB,KAAA,KAC1B,MAAM,SAAS,QAAQ,aAAa,GACpC;EACA,QAAQ,IAAI,UAAU,KAAK;EAC3B,OAAO,QAAQ,MAAM,YAAY;CACnC;CAEA,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,aAAc,YAAwC;EAC5D,IAAI,eAAe,MACjB,QAAQ,IAAI,UAAU,KAAK;OACtB,IAAI,OAAO,eAAe,YAC/B,WACE,MACA,OACF;CAEJ;CAEA,OAAO,QAAQ,MAAM,YAAY;AACnC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jperezmart/nest-casl-testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Testing utilities for @jperezmart/nest-casl — build and assert abilities without booting Nest.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,17 +34,17 @@
|
|
|
34
34
|
],
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@casl/ability": "^7.0.0",
|
|
37
|
+
"@jperezmart/eslint-config": "^1.0.0",
|
|
38
|
+
"@jperezmart/nest-casl": "0.2.0",
|
|
39
|
+
"@jperezmart/typescript-config": "^1.0.0",
|
|
37
40
|
"eslint": "^10.3.0",
|
|
38
41
|
"tsdown": "^0.22.2",
|
|
39
42
|
"typescript": "^6.0.3",
|
|
40
|
-
"vitest": "^4.1.9"
|
|
41
|
-
"@jperezmart/eslint-config": "0.0.0",
|
|
42
|
-
"@jperezmart/typescript-config": "0.0.0",
|
|
43
|
-
"@jperezmart/nest-casl": "0.1.1"
|
|
43
|
+
"vitest": "^4.1.9"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@casl/ability": "^7.0.0",
|
|
47
|
-
"@jperezmart/nest-casl": "^0.
|
|
47
|
+
"@jperezmart/nest-casl": "^0.2.0"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"access": "public"
|