UNPKG

moleculer-iam

Version:

Centralized IAM module for moleculer. Including a certified OIDC provider and an Identity provider for user profile, credentials, and custom claims management. Custom claims could be defined/updated by declarative schema which contains claims validation a

272 lines 12.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.doCommonServiceTest = void 0; const uuid_1 = require("uuid"); const op_1 = require("../../op"); function doCommonServiceTest(broker, service) { beforeAll(async () => { await broker.start(); await broker.waitForServices("iam"); }); afterAll(() => broker.stop()); describe("iam.client.*", () => { it("iam.client.get", () => { return expect(broker.call("iam.client.get")).resolves.toEqual(expect.objectContaining({ entries: expect.any(Array), total: expect.any(Number), })); }); it("iam.client.count", () => { return expect(broker.call("iam.client.count")).resolves.toEqual(expect.any(Number)); }); it("iam.client.create/find/update/delete", async () => { const params = { client_id: "test-service-spec", client_name: "Test service spec", redirect_uris: ["https://test-service-spec.dummy.site.com/callback"] }; await broker.call("iam.client.delete", { id: params.client_id }).then(undefined, () => { }); await expect(broker.call("iam.client.create", params)).resolves.not.toThrow(); await expect(broker.call("iam.client.find", { id: params.client_id })).resolves.toEqual(expect.objectContaining(params)); await expect(broker.call("iam.client.update", { ...params, client_name: "updated" })).resolves.toEqual(expect.objectContaining({ ...params, client_name: "updated" })); await expect(broker.call("iam.client.delete", { id: params.client_id })).resolves.not.toThrow(); await new Promise(resolve => setTimeout(resolve, 1000)); await expect(broker.call("iam.client.find", { id: params.client_id })).resolves.toBeFalsy(); }); }); describe("iam.model.*", () => { for (const kind of op_1.OIDCProvider.volatileModelNames) { it(`iam.model.get/count/delete for ${kind}`, async () => { const where = { exp: { $neq: null, $and: { $lt: Math.floor(new Date().getTime() / 1000) } } }; const entries = await broker.call("iam.model.get", { kind, where }); if (entries.total > 0) { expect(entries).toEqual(expect.objectContaining({ entries: expect.arrayContaining([expect.objectContaining({ jti: expect.any(String), kind })]), total: expect.any(Number), })); await expect(broker.call("iam.model.count", { kind, where })).resolves.toBeGreaterThan(0); await expect(broker.call("iam.model.delete", { kind, where })).resolves.toBeGreaterThan(0); } else { expect(entries).toEqual(expect.objectContaining({ entries: expect.arrayContaining([]), total: 0, })); await expect(broker.call("iam.model.count", { kind, where })).resolves.toBe(0); await expect(broker.call("iam.model.delete", { kind, where })).resolves.toBe(0); } }); } }); describe("iam.schema.*", () => { it("iam.schema.get/find", async () => { await expect(broker.call("iam.schema.get").then(undefined, console.error)).resolves.toEqual(expect.arrayContaining([ expect.objectContaining({ scope: expect.any(String), key: expect.any(String), active: expect.any(Boolean), validation: expect.anything(), migration: expect.any(String), version: expect.any(String), }), ])); await expect(broker.call("iam.schema.get", { scope: ["profile"], active: true })).resolves.toEqual(expect.arrayContaining([ expect.objectContaining({ scope: "profile", key: expect.any(String), active: true, validation: expect.anything(), migration: expect.any(String), version: expect.any(String), }), ])); await expect(broker.call("iam.schema.find", { key: "email", active: true })).resolves.toEqual(expect.objectContaining({ scope: "email", key: "email", active: true, validation: expect.anything(), migration: expect.any(String), version: expect.any(String), })); }); it("iam.schema.define", async () => { await expect(broker.call("iam.schema.define", { key: "test-will-throw" })).rejects.toThrow(expect.objectContaining({ code: 422 })); await expect(broker.call("iam.schema.define", { key: "useless_claim", scope: "_test", validation: "string", })).resolves.toEqual(expect.objectContaining({ key: "useless_claim", scope: "_test", validation: "string", active: true, version: expect.any(String), migration: expect.any(String), })); }); }); describe("iam.identity.*", () => { it("iam.identity.validate/validateCredentials", async () => { // validate payload in pre-flight await expect(broker.call("iam.identity.validate", { scope: "email" })).rejects.toEqual(expect.objectContaining({ code: 422, data: expect.arrayContaining([ expect.objectContaining({ field: "claims.email" }), ]), })); // can validate credentials together await expect(broker.call("iam.identity.validate", { scope: "email", credentials: { password: "123" } })).rejects.toEqual(expect.objectContaining({ code: 422, data: expect.arrayContaining([ expect.objectContaining({ field: "claims.email" }), expect.objectContaining({ field: "credentials.password" }), ]), })); // can just validate credentials await expect(broker.call("iam.identity.validateCredentials", { password: "123" })).rejects.toEqual(expect.objectContaining({ code: 422, data: expect.arrayContaining([ expect.objectContaining({ field: "password" }), ]), })); }); it("iam.identity.create/update/delete/restore and find", async () => { // invalid payload await expect(broker.call("iam.identity.create", {})).rejects.toEqual(expect.objectContaining({ code: 422, data: expect.arrayContaining([ expect.objectContaining({ field: "claims.email" }), expect.objectContaining({ field: "claims.name" }), ]), })); // valid payload const email = `${uuid_1.v4().substr(0, 16)}@test-iam-service.com`; let identity; await expect(broker.call("iam.identity.create", { scope: "email openid profile", claims: { email, name: "Service Tester", }, credentials: { password: "12341234", }, }) .then(res => (identity = res))).resolves.toEqual(expect.objectContaining({ claims: expect.objectContaining({ email }), })); // update await expect(broker.call("iam.identity.update", { id: identity.id, scope: "profile", claims: { name: "updated" }, metadata: { whatever: "nothing" }, })).resolves.toEqual(expect.objectContaining({ id: identity.id, claims: expect.objectContaining({ name: "updated" }), metadata: expect.objectContaining({ whatever: "nothing" }), })); // delete (soft) await expect(broker.call("iam.identity.delete", { id: identity.id, permanently: false, })).resolves.toEqual(identity.id); // check soft delete await expect(broker.call("iam.identity.find", { id: identity.id, metadata: { softDeleted: false, }, })).resolves.toBeFalsy(); await expect(broker.call("iam.identity.find", { where: { id: identity.id, metadata: { softDeleted: true, }, }, })).resolves.toBeTruthy(); // restore await expect(broker.call("iam.identity.restore", { id: identity.id, })).resolves.toEqual(identity.id); // check restored await expect(broker.call("iam.identity.find", { id: identity.id, })).resolves.toBeTruthy(); // delete (hard) await expect(broker.call("iam.identity.delete", { id: identity.id, permanently: true, })).rejects.toThrow(); // requires soft delete first await expect(broker.call("iam.identity.delete", { id: identity.id, permanently: false, })).resolves.toEqual(identity.id); await expect(broker.call("iam.identity.delete", { id: identity.id, permanently: true, })).resolves.toEqual(identity.id); // check hard deleted await expect(broker.call("iam.identity.find", { where: { id: identity.id, metadata: { softDeleted: true, }, }, })).resolves.toBeFalsy(); }); it("iam.identity.get/count", async () => { const where = { claims: { email: { $like: "%@%" }, }, }; let result; await expect(broker.call("iam.identity.get", { where, limit: 5, }) .then(res => result = res)).resolves.not.toThrow(); expect(result).toEqual(expect.objectContaining({ entries: expect.arrayContaining(result && result.total > 0 ? [ expect.objectContaining({ id: expect.any(String), claims: expect.anything(), metadata: expect.anything(), }), ] : []), total: expect.any(Number), })); await expect(broker.call("iam.identity.count", { where })).resolves.toBe(result.total); }); // used to force refresh identity cache from external changes it("iam.identity.refresh", async () => { const idp = service.idp; const originalRefresher = idp.adapter.onClaimsUpdated; const mockedRefresher = jest.fn(); idp.adapter.onClaimsUpdated = mockedRefresher; const email = `${uuid_1.v4().substr(0, 16)}@test-iam-service.com`; let identity; await expect(idp.create({ scope: "email openid profile", claims: { email, name: "Service Tester2", }, credentials: { password: "12341234", }, metadata: {}, }).then(id => id.json()).then(i => identity = i)).resolves.toEqual(expect.objectContaining({ claims: expect.objectContaining({ email }) })); expect(mockedRefresher).toHaveBeenCalledWith(identity.id, expect.anything(), expect.anything()); // not all adapter requires/implements refreshing and cache mechanism // await expect(broker.call("iam.identity.find", {email})).resolves.toBeFalsy(); idp.adapter.onClaimsUpdated = originalRefresher; mockedRefresher.mockClear(); await expect(broker.call("iam.identity.refresh", { id: identity.id })).resolves.not.toThrow(); expect(mockedRefresher).not.toBeCalled(); await expect(broker.call("iam.identity.find", { email })).resolves.toBeTruthy(); }); }); } exports.doCommonServiceTest = doCommonServiceTest; //# sourceMappingURL=service.spec.common.js.map