From aced91f82cafcf410de4cd699a1e9d82380f51f0 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 24 Aug 2022 19:41:07 +0000 Subject: [PATCH 01/21] Update XService --- site/src/testHelpers/entities.ts | 8 ++++++-- site/src/xServices/entitlements/entitlementsXService.ts | 5 +---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 5c28f574704f5..aa620960f98cb 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -647,11 +647,15 @@ export const MockEntitlementsWithWarnings: TypesGen.Entitlements = { warnings: ["You are over your active user limit.", "And another thing."], has_license: true, features: { - activeUsers: { + user_limit: { enabled: true, - entitlement: "entitled", + entitlement: "grace_period", limit: 100, actual: 102, }, + audit_log: { + enabled: true, + entitlement: "entitled", + } }, } diff --git a/site/src/xServices/entitlements/entitlementsXService.ts b/site/src/xServices/entitlements/entitlementsXService.ts index 7458067172fa5..8fa9ec7213df7 100644 --- a/site/src/xServices/entitlements/entitlementsXService.ts +++ b/site/src/xServices/entitlements/entitlementsXService.ts @@ -47,7 +47,7 @@ export const entitlementsMachine = createMachine( on: { GET_ENTITLEMENTS: "gettingEntitlements", SHOW_MOCK_BANNER: { actions: "assignMockEntitlements" }, - HIDE_MOCK_BANNER: { actions: "clearMockEntitlements" }, + HIDE_MOCK_BANNER: "gettingEntitlements" }, }, gettingEntitlements: { @@ -81,9 +81,6 @@ export const entitlementsMachine = createMachine( assignMockEntitlements: assign({ entitlements: (_) => MockEntitlementsWithWarnings, }), - clearMockEntitlements: assign({ - entitlements: (_) => emptyEntitlements, - }), }, services: { getEntitlements: () => API.getEntitlements(), From 49f56529aa6096f5f30d0aaecfd9fb05df49c052 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 24 Aug 2022 19:41:27 +0000 Subject: [PATCH 02/21] Add simple wrapper --- .../RequirePermission/RequirePermission.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 site/src/components/RequirePermission/RequirePermission.tsx diff --git a/site/src/components/RequirePermission/RequirePermission.tsx b/site/src/components/RequirePermission/RequirePermission.tsx new file mode 100644 index 0000000000000..b17b56ad6f201 --- /dev/null +++ b/site/src/components/RequirePermission/RequirePermission.tsx @@ -0,0 +1,18 @@ +import { FC } from "react" +import { Navigate } from "react-router" + +export interface RequirePermissionProps { + children: JSX.Element + isFeatureVisible: boolean +} + +/** + * Wraps routes that are available based on RBAC or licensing. + */ +export const RequirePermission: FC = ({ children, isFeatureVisible }) => { + if (!isFeatureVisible) { + return + } else { + return children + } +} From a641881db7f144ba5626555a49f981695c9eb36e Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 24 Aug 2022 19:41:52 +0000 Subject: [PATCH 03/21] Add selector --- site/src/api/types.ts | 6 ++++ .../entitlementsSelectors.test.ts | 24 +++++++++++++++ .../entitlements/entitlementsSelectors.ts | 29 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 site/src/xServices/entitlements/entitlementsSelectors.test.ts create mode 100644 site/src/xServices/entitlements/entitlementsSelectors.ts diff --git a/site/src/api/types.ts b/site/src/api/types.ts index daf4e451ac5e8..d8fb01685906d 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -14,3 +14,9 @@ export interface ReconnectingPTYRequest { export type WorkspaceBuildTransition = "start" | "stop" | "delete" export type Message = { message: string } + +// Keep up to date with coder/codersdk/features.go +export enum FeatureNames { + AuditLog = "audit_log", + UserLimit = "user_limit" +} diff --git a/site/src/xServices/entitlements/entitlementsSelectors.test.ts b/site/src/xServices/entitlements/entitlementsSelectors.test.ts new file mode 100644 index 0000000000000..c3b3b000d71d7 --- /dev/null +++ b/site/src/xServices/entitlements/entitlementsSelectors.test.ts @@ -0,0 +1,24 @@ +import { getFeatureVisibility } from "./entitlementsSelectors" + +describe('getFeatureVisibility', () => { + it("returns empty object if there is no license", () => { + const result = getFeatureVisibility(false, { audit_log: { entitlement: "entitled", enabled: true } }) + expect(result).toEqual(expect.objectContaining({})) + }) + it('returns false for a feature that is not enabled', () => { + const result = getFeatureVisibility(true, { audit_log: { entitlement: "entitled", enabled: false } }) + expect(result).toEqual(expect.objectContaining({ audit_log: false })) + }) + it("returns false for a feature that is not entitled", () => { + const result = getFeatureVisibility(true, { audit_log: { entitlement: "not_entitled", enabled: true } }) + expect(result).toEqual(expect.objectContaining({ audit_log: false })) + }) + it("returns true for a feature that is in grace period", () => { + const result = getFeatureVisibility(true, { audit_log: { entitlement: "grace_period", enabled: true } }) + expect(result).toEqual(expect.objectContaining({ audit_log: true })) + }) + it("returns true for a feature that is in entitled", () => { + const result = getFeatureVisibility(true, { audit_log: { entitlement: "entitled", enabled: true } }) + expect(result).toEqual(expect.objectContaining({ audit_log: true })) + }) +}) diff --git a/site/src/xServices/entitlements/entitlementsSelectors.ts b/site/src/xServices/entitlements/entitlementsSelectors.ts new file mode 100644 index 0000000000000..cf966dc32d6e4 --- /dev/null +++ b/site/src/xServices/entitlements/entitlementsSelectors.ts @@ -0,0 +1,29 @@ +import { Feature } from "api/typesGenerated" +import { State } from "xstate" +import { EntitlementsContext, EntitlementsEvent } from "./entitlementsXService" + +type EntitlementState = State + +/** + * @param hasLicense true if Enterprise edition + * @param features record from feature name to feature object + * @returns record from feature name whether to show the feature + */ +export const getFeatureVisibility = (hasLicense: boolean, features: Record): Record => { + if (hasLicense) { + const permissionPairs = Object.keys(features).map((feature) => { + const { entitlement, limit, actual, enabled } = features[feature] + const entitled = ["entitled", "grace_period"].includes(entitlement) + const limitCompliant = (limit && actual) ? limit >= actual : true + return [feature, entitled && limitCompliant && enabled] + }) + console.log(permissionPairs, Object.fromEntries(permissionPairs)) + return Object.fromEntries(permissionPairs) + } else { + return {} + } +} + +export const selectFeatureVisibility = (state: EntitlementState): Record => { + return getFeatureVisibility(state.context.entitlements.has_license, state.context.entitlements.features) +} From 4e79b2df779427f29e24c83f509fa8aa0b1f9d27 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 24 Aug 2022 19:41:58 +0000 Subject: [PATCH 04/21] Condition page --- site/src/AppRouter.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 27b624e5b9d6c..a87b5418356b8 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -1,9 +1,12 @@ import { useSelector } from "@xstate/react" +import { FeatureNames } from "api/types" +import { RequirePermission } from "components/RequirePermission/RequirePermission" import { SetupPage } from "pages/SetupPage/SetupPage" import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateSettingsPage" import { FC, lazy, Suspense, useContext } from "react" import { Navigate, Route, Routes } from "react-router-dom" import { selectPermissions } from "xServices/auth/authSelectors" +import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" import { XServiceContext } from "xServices/StateContext" import { AuthAndFrame } from "./components/AuthAndFrame/AuthAndFrame" import { RequireAuth } from "./components/RequireAuth/RequireAuth" @@ -35,6 +38,8 @@ const AuditPage = lazy(() => import("./pages/AuditPage/AuditPage")) export const AppRouter: FC = () => { const xServices = useContext(XServiceContext) const permissions = useSelector(xServices.authXService, selectPermissions) + const featureVisibility = useSelector(xServices.entitlementsXService, selectFeatureVisibility) + return ( }> @@ -134,11 +139,13 @@ export const AppRouter: FC = () => { ) : ( - + + + ) } From 6dfe4df95423b9e44dcf3bc5c189eb29d64af3b9 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 24 Aug 2022 19:42:03 +0000 Subject: [PATCH 05/21] Condition link --- site/src/components/Navbar/Navbar.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/site/src/components/Navbar/Navbar.tsx b/site/src/components/Navbar/Navbar.tsx index cbfdfd949dd19..3c3a9a132e216 100644 --- a/site/src/components/Navbar/Navbar.tsx +++ b/site/src/components/Navbar/Navbar.tsx @@ -1,5 +1,7 @@ -import { useActor } from "@xstate/react" +import { useActor, useSelector } from "@xstate/react" +import { FeatureNames } from "api/types" import React, { useContext } from "react" +import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" import { XServiceContext } from "../../xServices/StateContext" import { NavbarView } from "../NavbarView/NavbarView" @@ -7,13 +9,15 @@ export const Navbar: React.FC = () => { const xServices = useContext(XServiceContext) const [authState, authSend] = useActor(xServices.authXService) const { me, permissions } = authState.context + const featureVisibility = useSelector(xServices.entitlementsXService, selectFeatureVisibility) + const canViewAuditLog = featureVisibility[FeatureNames.AuditLog] && !!permissions?.viewAuditLog const onSignOut = () => authSend("SIGN_OUT") return ( ) } From 17d8d1b5866cf26b92c6cf1dfba77d5a32e2ef14 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 24 Aug 2022 19:43:15 +0000 Subject: [PATCH 06/21] Format and lint --- site/src/AppRouter.tsx | 6 ++++- site/src/api/types.ts | 2 +- site/src/components/Navbar/Navbar.tsx | 8 +------ site/src/testHelpers/entities.ts | 2 +- .../entitlementsSelectors.test.ts | 24 +++++++++++++------ .../entitlements/entitlementsSelectors.ts | 19 +++++++++------ .../entitlements/entitlementsXService.ts | 2 +- 7 files changed, 38 insertions(+), 25 deletions(-) diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index a87b5418356b8..5f2955eb3a6fa 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -143,7 +143,11 @@ export const AppRouter: FC = () => { ) : ( - + diff --git a/site/src/api/types.ts b/site/src/api/types.ts index d8fb01685906d..1ac58f28cfccc 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -18,5 +18,5 @@ export type Message = { message: string } // Keep up to date with coder/codersdk/features.go export enum FeatureNames { AuditLog = "audit_log", - UserLimit = "user_limit" + UserLimit = "user_limit", } diff --git a/site/src/components/Navbar/Navbar.tsx b/site/src/components/Navbar/Navbar.tsx index 3c3a9a132e216..039990653a85f 100644 --- a/site/src/components/Navbar/Navbar.tsx +++ b/site/src/components/Navbar/Navbar.tsx @@ -13,11 +13,5 @@ export const Navbar: React.FC = () => { const canViewAuditLog = featureVisibility[FeatureNames.AuditLog] && !!permissions?.viewAuditLog const onSignOut = () => authSend("SIGN_OUT") - return ( - - ) + return } diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index aa620960f98cb..f1209cd47da55 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -656,6 +656,6 @@ export const MockEntitlementsWithWarnings: TypesGen.Entitlements = { audit_log: { enabled: true, entitlement: "entitled", - } + }, }, } diff --git a/site/src/xServices/entitlements/entitlementsSelectors.test.ts b/site/src/xServices/entitlements/entitlementsSelectors.test.ts index c3b3b000d71d7..9d179457a6e2e 100644 --- a/site/src/xServices/entitlements/entitlementsSelectors.test.ts +++ b/site/src/xServices/entitlements/entitlementsSelectors.test.ts @@ -1,24 +1,34 @@ import { getFeatureVisibility } from "./entitlementsSelectors" -describe('getFeatureVisibility', () => { +describe("getFeatureVisibility", () => { it("returns empty object if there is no license", () => { - const result = getFeatureVisibility(false, { audit_log: { entitlement: "entitled", enabled: true } }) + const result = getFeatureVisibility(false, { + audit_log: { entitlement: "entitled", enabled: true }, + }) expect(result).toEqual(expect.objectContaining({})) }) - it('returns false for a feature that is not enabled', () => { - const result = getFeatureVisibility(true, { audit_log: { entitlement: "entitled", enabled: false } }) + it("returns false for a feature that is not enabled", () => { + const result = getFeatureVisibility(true, { + audit_log: { entitlement: "entitled", enabled: false }, + }) expect(result).toEqual(expect.objectContaining({ audit_log: false })) }) it("returns false for a feature that is not entitled", () => { - const result = getFeatureVisibility(true, { audit_log: { entitlement: "not_entitled", enabled: true } }) + const result = getFeatureVisibility(true, { + audit_log: { entitlement: "not_entitled", enabled: true }, + }) expect(result).toEqual(expect.objectContaining({ audit_log: false })) }) it("returns true for a feature that is in grace period", () => { - const result = getFeatureVisibility(true, { audit_log: { entitlement: "grace_period", enabled: true } }) + const result = getFeatureVisibility(true, { + audit_log: { entitlement: "grace_period", enabled: true }, + }) expect(result).toEqual(expect.objectContaining({ audit_log: true })) }) it("returns true for a feature that is in entitled", () => { - const result = getFeatureVisibility(true, { audit_log: { entitlement: "entitled", enabled: true } }) + const result = getFeatureVisibility(true, { + audit_log: { entitlement: "entitled", enabled: true }, + }) expect(result).toEqual(expect.objectContaining({ audit_log: true })) }) }) diff --git a/site/src/xServices/entitlements/entitlementsSelectors.ts b/site/src/xServices/entitlements/entitlementsSelectors.ts index cf966dc32d6e4..62d7aae4b1e0b 100644 --- a/site/src/xServices/entitlements/entitlementsSelectors.ts +++ b/site/src/xServices/entitlements/entitlementsSelectors.ts @@ -9,15 +9,17 @@ type EntitlementState = State * @param features record from feature name to feature object * @returns record from feature name whether to show the feature */ -export const getFeatureVisibility = (hasLicense: boolean, features: Record): Record => { +export const getFeatureVisibility = ( + hasLicense: boolean, + features: Record, +): Record => { if (hasLicense) { const permissionPairs = Object.keys(features).map((feature) => { - const { entitlement, limit, actual, enabled } = features[feature] - const entitled = ["entitled", "grace_period"].includes(entitlement) - const limitCompliant = (limit && actual) ? limit >= actual : true - return [feature, entitled && limitCompliant && enabled] + const { entitlement, limit, actual, enabled } = features[feature] + const entitled = ["entitled", "grace_period"].includes(entitlement) + const limitCompliant = limit && actual ? limit >= actual : true + return [feature, entitled && limitCompliant && enabled] }) - console.log(permissionPairs, Object.fromEntries(permissionPairs)) return Object.fromEntries(permissionPairs) } else { return {} @@ -25,5 +27,8 @@ export const getFeatureVisibility = (hasLicense: boolean, features: Record => { - return getFeatureVisibility(state.context.entitlements.has_license, state.context.entitlements.features) + return getFeatureVisibility( + state.context.entitlements.has_license, + state.context.entitlements.features, + ) } diff --git a/site/src/xServices/entitlements/entitlementsXService.ts b/site/src/xServices/entitlements/entitlementsXService.ts index 8fa9ec7213df7..0da90acd79237 100644 --- a/site/src/xServices/entitlements/entitlementsXService.ts +++ b/site/src/xServices/entitlements/entitlementsXService.ts @@ -47,7 +47,7 @@ export const entitlementsMachine = createMachine( on: { GET_ENTITLEMENTS: "gettingEntitlements", SHOW_MOCK_BANNER: { actions: "assignMockEntitlements" }, - HIDE_MOCK_BANNER: "gettingEntitlements" + HIDE_MOCK_BANNER: "gettingEntitlements", }, }, gettingEntitlements: { From 8fdc83431cf7acd1be7dbc0defdb441ca2d99ef8 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 24 Aug 2022 20:08:22 +0000 Subject: [PATCH 07/21] Integration test --- site/src/components/Navbar/Navbar.test.tsx | 33 ++++++++++++++++++++++ site/src/testHelpers/entities.ts | 15 ++++++++++ 2 files changed, 48 insertions(+) create mode 100644 site/src/components/Navbar/Navbar.test.tsx diff --git a/site/src/components/Navbar/Navbar.test.tsx b/site/src/components/Navbar/Navbar.test.tsx new file mode 100644 index 0000000000000..f38f1a45d67fe --- /dev/null +++ b/site/src/components/Navbar/Navbar.test.tsx @@ -0,0 +1,33 @@ +import { render, MockEntitlementsWithAuditLog, MockMemberPermissions } from "testHelpers/renderHelpers" +import { server } from "testHelpers/server" +import { screen } from "@testing-library/react" +import { Navbar } from "./Navbar" +import { rest } from "msw" + +describe("Navbar", () => { + it("shows Audit Log link when permitted and entitled", () => { + server.use( + rest.get("/api/entitlements", (req, res, ctx) => { + return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog)) + }), + ) + render() + expect(screen.getByText("Audit Log")) + }) + + it("does not show Audit Log link when not entitled", () => { + server.use() + render() + expect(screen.queryByText("Audit Log")).not.toBeDefined() + }) + + it("does not show Audit Log link when not permitted via role", () => { + server.use( + rest.post("/api/v2/users/:userId/authorization", async (req, res, ctx) => { + return res(ctx.status(200), ctx.json(MockMemberPermissions)) + }), + ) + render() + expect(screen.queryByText("Audit Log")).not.toBeDefined() + }) +}) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index f1209cd47da55..48a5107851f1e 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -51,6 +51,10 @@ export function assignableRole(role: TypesGen.Role, assignable: boolean): TypesG } } +export const MockMemberPermissions = { + viewAuditLog: false +} + export const MockUser: TypesGen.User = { id: "test-user", username: "TestUser", @@ -659,3 +663,14 @@ export const MockEntitlementsWithWarnings: TypesGen.Entitlements = { }, }, } + +export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { + warnings: [], + has_license: true, + features: { + audit_log: { + enabled: true, + entitlement: "entitled", + }, + }, +} From 3627ef3260747fc220bfcd74acdf32dfdcfa8d94 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 24 Aug 2022 20:15:47 +0000 Subject: [PATCH 08/21] Add username to api call --- site/src/components/Navbar/Navbar.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/components/Navbar/Navbar.test.tsx b/site/src/components/Navbar/Navbar.test.tsx index f38f1a45d67fe..5c94210aeda4b 100644 --- a/site/src/components/Navbar/Navbar.test.tsx +++ b/site/src/components/Navbar/Navbar.test.tsx @@ -1,4 +1,4 @@ -import { render, MockEntitlementsWithAuditLog, MockMemberPermissions } from "testHelpers/renderHelpers" +import { render, MockEntitlementsWithAuditLog, MockMemberPermissions, MockUser } from "testHelpers/renderHelpers" import { server } from "testHelpers/server" import { screen } from "@testing-library/react" import { Navbar } from "./Navbar" @@ -23,7 +23,7 @@ describe("Navbar", () => { it("does not show Audit Log link when not permitted via role", () => { server.use( - rest.post("/api/v2/users/:userId/authorization", async (req, res, ctx) => { + rest.post(`/api/v2/users/${MockUser.id}/authorization`, async (req, res, ctx) => { return res(ctx.status(200), ctx.json(MockMemberPermissions)) }), ) From 8b88265d617c8a398e1a348168f82580d1c5bf4d Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 24 Aug 2022 20:52:18 +0000 Subject: [PATCH 09/21] Format --- site/src/components/Navbar/Navbar.test.tsx | 25 ++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/site/src/components/Navbar/Navbar.test.tsx b/site/src/components/Navbar/Navbar.test.tsx index 5c94210aeda4b..bd6f566c76d5c 100644 --- a/site/src/components/Navbar/Navbar.test.tsx +++ b/site/src/components/Navbar/Navbar.test.tsx @@ -1,33 +1,40 @@ -import { render, MockEntitlementsWithAuditLog, MockMemberPermissions, MockUser } from "testHelpers/renderHelpers" -import { server } from "testHelpers/server" import { screen } from "@testing-library/react" -import { Navbar } from "./Navbar" import { rest } from "msw" +import { + MockEntitlementsWithAuditLog, + MockMemberPermissions, + MockUser, + render, +} from "testHelpers/renderHelpers" +import { server } from "testHelpers/server" +import { Navbar } from "./Navbar" describe("Navbar", () => { - it("shows Audit Log link when permitted and entitled", () => { + it("shows Audit Log link when permitted and entitled", async () => { server.use( rest.get("/api/entitlements", (req, res, ctx) => { return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog)) }), ) render() - expect(screen.getByText("Audit Log")) + const link = await screen.findByText("Audit Log") + expect(link).toBeDefined() }) it("does not show Audit Log link when not entitled", () => { - server.use() render() - expect(screen.queryByText("Audit Log")).not.toBeDefined() + const link = screen.getByText("Audit Log") + expect(link).not.toBeDefined() }) it("does not show Audit Log link when not permitted via role", () => { - server.use( + server.use( rest.post(`/api/v2/users/${MockUser.id}/authorization`, async (req, res, ctx) => { return res(ctx.status(200), ctx.json(MockMemberPermissions)) }), ) render() - expect(screen.queryByText("Audit Log")).not.toBeDefined() + const link = screen.getByText("Audit Log") + expect(link).not.toBeDefined() }) }) From 4fea7850bc425b6b54f077d8dd192f1ec37f5999 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 24 Aug 2022 20:52:25 +0000 Subject: [PATCH 10/21] Format --- site/src/testHelpers/entities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 48a5107851f1e..e93975e4d15da 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -52,7 +52,7 @@ export function assignableRole(role: TypesGen.Role, assignable: boolean): TypesG } export const MockMemberPermissions = { - viewAuditLog: false + viewAuditLog: false, } export const MockUser: TypesGen.User = { From 733954b79a883efdb320eda0ae57e92c9d528221 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 25 Aug 2022 15:58:44 +0000 Subject: [PATCH 11/21] Fix link name --- site/src/components/Navbar/Navbar.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/components/Navbar/Navbar.test.tsx b/site/src/components/Navbar/Navbar.test.tsx index bd6f566c76d5c..f80d0f2466641 100644 --- a/site/src/components/Navbar/Navbar.test.tsx +++ b/site/src/components/Navbar/Navbar.test.tsx @@ -17,13 +17,13 @@ describe("Navbar", () => { }), ) render() - const link = await screen.findByText("Audit Log") + const link = await screen.findByText("Audit") expect(link).toBeDefined() }) it("does not show Audit Log link when not entitled", () => { render() - const link = screen.getByText("Audit Log") + const link = screen.getByText("Audit") expect(link).not.toBeDefined() }) @@ -34,7 +34,7 @@ describe("Navbar", () => { }), ) render() - const link = screen.getByText("Audit Log") + const link = screen.getByText("Audit") expect(link).not.toBeDefined() }) }) From fc237a0ebf2b2b446a777d377314003d6397c330 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 25 Aug 2022 18:37:20 +0000 Subject: [PATCH 12/21] Upgrade xstate/react to fix crashing tests --- site/package.json | 2 +- site/src/components/Navbar/Navbar.tsx | 4 ++-- site/yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/site/package.json b/site/package.json index 0d2422fb574e7..5ef59569e7790 100644 --- a/site/package.json +++ b/site/package.json @@ -33,7 +33,7 @@ "@material-ui/lab": "4.0.0-alpha.42", "@testing-library/react-hooks": "8.0.1", "@xstate/inspect": "0.6.5", - "@xstate/react": "3.0.0", + "@xstate/react": "3.0.1", "axios": "0.26.1", "can-ndjson-stream": "1.0.2", "cron-parser": "4.5.0", diff --git a/site/src/components/Navbar/Navbar.tsx b/site/src/components/Navbar/Navbar.tsx index 039990653a85f..28e08b3de89cb 100644 --- a/site/src/components/Navbar/Navbar.tsx +++ b/site/src/components/Navbar/Navbar.tsx @@ -1,4 +1,4 @@ -import { useActor, useSelector } from "@xstate/react" +import { shallowEqual, useActor, useSelector } from "@xstate/react" import { FeatureNames } from "api/types" import React, { useContext } from "react" import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" @@ -9,7 +9,7 @@ export const Navbar: React.FC = () => { const xServices = useContext(XServiceContext) const [authState, authSend] = useActor(xServices.authXService) const { me, permissions } = authState.context - const featureVisibility = useSelector(xServices.entitlementsXService, selectFeatureVisibility) + const featureVisibility = useSelector(xServices.entitlementsXService, selectFeatureVisibility, shallowEqual) const canViewAuditLog = featureVisibility[FeatureNames.AuditLog] && !!permissions?.viewAuditLog const onSignOut = () => authSend("SIGN_OUT") diff --git a/site/yarn.lock b/site/yarn.lock index 511aa73a00fa8..b9460c91fe599 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -3896,10 +3896,10 @@ resolved "https://registry.yarnpkg.com/@xstate/machine-extractor/-/machine-extractor-0.7.0.tgz#3f46a3686462f309ee208a97f6285e7d6a55cdb8" integrity sha512-dXHI/sWWWouN/yG687ZuRCP7Cm6XggFWSK1qWj3NohBTyhaYWSR7ojwP6OUK6e1cbiJqxmM9EDnE2Auf+Xlp+A== -"@xstate/react@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.0.0.tgz#888d9a6f128c70b632c18ad55f1f851f6ab092ba" - integrity sha512-KHSCfwtb8gZ7QH2luihvmKYI+0lcdHQOmGNRUxUEs4zVgaJCyd8csCEmwPsudpliLdUmyxX2pzUBojFkINpotw== +"@xstate/react@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.0.1.tgz#937eeb5d5d61734ab756ca40146f84a6fe977095" + integrity sha512-/tq/gg92P9ke8J+yDNDBv5/PAxBvXJf2cYyGDByzgtl5wKaxKxzDT82Gj3eWlCJXkrBg4J5/V47//gRJuVH2fA== dependencies: use-isomorphic-layout-effect "^1.0.0" use-sync-external-store "^1.0.0" From cb20ccd3297191021896741a8166547cad5aa743 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 25 Aug 2022 18:37:30 +0000 Subject: [PATCH 13/21] Fix tests --- site/src/components/Navbar/Navbar.test.tsx | 44 ++++++++++++++-------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/site/src/components/Navbar/Navbar.test.tsx b/site/src/components/Navbar/Navbar.test.tsx index f80d0f2466641..2c78da8bc28a4 100644 --- a/site/src/components/Navbar/Navbar.test.tsx +++ b/site/src/components/Navbar/Navbar.test.tsx @@ -1,40 +1,54 @@ -import { screen } from "@testing-library/react" +import { render, screen, waitFor } from "@testing-library/react" import { rest } from "msw" import { MockEntitlementsWithAuditLog, MockMemberPermissions, MockUser, - render, } from "testHelpers/renderHelpers" import { server } from "testHelpers/server" -import { Navbar } from "./Navbar" +import { App } from "app" +/** + * The LicenseBanner, mounted above the AppRouter, fetches entitlements. Thus, to test their + * effects, we must test at the App level and `waitFor` the fetch to be done. + */ describe("Navbar", () => { it("shows Audit Log link when permitted and entitled", async () => { server.use( - rest.get("/api/entitlements", (req, res, ctx) => { + rest.get("/api/v2/entitlements", (req, res, ctx) => { return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog)) }), ) - render() - const link = await screen.findByText("Audit") - expect(link).toBeDefined() + render() + await waitFor(() => { + const link = screen.getByText("Audit") + expect(link).toBeDefined() + }) }) - it("does not show Audit Log link when not entitled", () => { - render() - const link = screen.getByText("Audit") - expect(link).not.toBeDefined() + it("does not show Audit Log link when not entitled", async () => { + render() + await waitFor(() => { + const link = screen.queryByText("Audit") + expect(link).toBe(null) + }) }) - it("does not show Audit Log link when not permitted via role", () => { + it("does not show Audit Log link when not permitted via role", async () => { server.use( rest.post(`/api/v2/users/${MockUser.id}/authorization`, async (req, res, ctx) => { return res(ctx.status(200), ctx.json(MockMemberPermissions)) }), ) - render() - const link = screen.getByText("Audit") - expect(link).not.toBeDefined() + server.use( + rest.get("/api/v2/entitlements", (req, res, ctx) => { + return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog)) + }), + ) + render() + await waitFor(() => { + const link = screen.queryByText("Audit") + expect(link).toBe(null) + }) }) }) From 75f7f6e279a57be61e50ea407174f7c1db7ba201 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 25 Aug 2022 18:45:37 +0000 Subject: [PATCH 14/21] Format --- site/src/components/Navbar/Navbar.test.tsx | 2 +- site/src/components/Navbar/Navbar.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/site/src/components/Navbar/Navbar.test.tsx b/site/src/components/Navbar/Navbar.test.tsx index 2c78da8bc28a4..9ec298ed9f163 100644 --- a/site/src/components/Navbar/Navbar.test.tsx +++ b/site/src/components/Navbar/Navbar.test.tsx @@ -1,4 +1,5 @@ import { render, screen, waitFor } from "@testing-library/react" +import { App } from "app" import { rest } from "msw" import { MockEntitlementsWithAuditLog, @@ -6,7 +7,6 @@ import { MockUser, } from "testHelpers/renderHelpers" import { server } from "testHelpers/server" -import { App } from "app" /** * The LicenseBanner, mounted above the AppRouter, fetches entitlements. Thus, to test their diff --git a/site/src/components/Navbar/Navbar.tsx b/site/src/components/Navbar/Navbar.tsx index 28e08b3de89cb..608e8697e4f91 100644 --- a/site/src/components/Navbar/Navbar.tsx +++ b/site/src/components/Navbar/Navbar.tsx @@ -9,7 +9,11 @@ export const Navbar: React.FC = () => { const xServices = useContext(XServiceContext) const [authState, authSend] = useActor(xServices.authXService) const { me, permissions } = authState.context - const featureVisibility = useSelector(xServices.entitlementsXService, selectFeatureVisibility, shallowEqual) + const featureVisibility = useSelector( + xServices.entitlementsXService, + selectFeatureVisibility, + shallowEqual, + ) const canViewAuditLog = featureVisibility[FeatureNames.AuditLog] && !!permissions?.viewAuditLog const onSignOut = () => authSend("SIGN_OUT") From 2e942694ae44ef042cc6bfb45236b3cbf6fcc325 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 25 Aug 2022 18:59:17 +0000 Subject: [PATCH 15/21] Abstract strings --- site/src/components/Navbar/Navbar.test.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/site/src/components/Navbar/Navbar.test.tsx b/site/src/components/Navbar/Navbar.test.tsx index 9ec298ed9f163..968df171a0b62 100644 --- a/site/src/components/Navbar/Navbar.test.tsx +++ b/site/src/components/Navbar/Navbar.test.tsx @@ -1,5 +1,6 @@ import { render, screen, waitFor } from "@testing-library/react" import { App } from "app" +import { Language } from "components/NavbarView/NavbarView" import { rest } from "msw" import { MockEntitlementsWithAuditLog, @@ -21,7 +22,7 @@ describe("Navbar", () => { ) render() await waitFor(() => { - const link = screen.getByText("Audit") + const link = screen.getByText(Language.audit) expect(link).toBeDefined() }) }) @@ -29,7 +30,7 @@ describe("Navbar", () => { it("does not show Audit Log link when not entitled", async () => { render() await waitFor(() => { - const link = screen.queryByText("Audit") + const link = screen.queryByText(Language.audit) expect(link).toBe(null) }) }) @@ -47,7 +48,7 @@ describe("Navbar", () => { ) render() await waitFor(() => { - const link = screen.queryByText("Audit") + const link = screen.queryByText(Language.audit) expect(link).toBe(null) }) }) From 3c54e1ee6e1f42a8c70d59997f4a25c722f741fa Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 25 Aug 2022 19:33:54 +0000 Subject: [PATCH 16/21] Debug test --- site/src/components/Navbar/Navbar.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/Navbar/Navbar.test.tsx b/site/src/components/Navbar/Navbar.test.tsx index 968df171a0b62..108f00159002b 100644 --- a/site/src/components/Navbar/Navbar.test.tsx +++ b/site/src/components/Navbar/Navbar.test.tsx @@ -22,7 +22,7 @@ describe("Navbar", () => { ) render() await waitFor(() => { - const link = screen.getByText(Language.audit) + const link = screen.getByText(Language.users) // TODO change after debugging expect(link).toBeDefined() }) }) From 4d40c302212f3cda012d4a27a51c8dadbb10a41c Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 25 Aug 2022 21:39:27 +0000 Subject: [PATCH 17/21] Increase timeout --- site/src/components/Navbar/Navbar.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/Navbar/Navbar.test.tsx b/site/src/components/Navbar/Navbar.test.tsx index 108f00159002b..f8d73d7ff5be2 100644 --- a/site/src/components/Navbar/Navbar.test.tsx +++ b/site/src/components/Navbar/Navbar.test.tsx @@ -24,7 +24,7 @@ describe("Navbar", () => { await waitFor(() => { const link = screen.getByText(Language.users) // TODO change after debugging expect(link).toBeDefined() - }) + }, { timeout: 10000 }) }) it("does not show Audit Log link when not entitled", async () => { From a26786ed727f49f5beb11fe1221d54f9e0db5b0c Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 25 Aug 2022 21:50:27 +0000 Subject: [PATCH 18/21] Add comments and try shorter timeout --- site/src/components/Navbar/Navbar.test.tsx | 38 +++++++++++++++------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/site/src/components/Navbar/Navbar.test.tsx b/site/src/components/Navbar/Navbar.test.tsx index f8d73d7ff5be2..b1729df62b3cb 100644 --- a/site/src/components/Navbar/Navbar.test.tsx +++ b/site/src/components/Navbar/Navbar.test.tsx @@ -15,41 +15,55 @@ import { server } from "testHelpers/server" */ describe("Navbar", () => { it("shows Audit Log link when permitted and entitled", async () => { + // set entitlements to allow audit log server.use( rest.get("/api/v2/entitlements", (req, res, ctx) => { return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog)) }), ) render() - await waitFor(() => { - const link = screen.getByText(Language.users) // TODO change after debugging - expect(link).toBeDefined() - }, { timeout: 10000 }) + await waitFor( + () => { + const link = screen.getByText(Language.audit) + expect(link).toBeDefined() + }, + { timeout: 5000 }, + ) }) it("does not show Audit Log link when not entitled", async () => { + // by default, user is an Admin with permission to see the audit log, + // but is unlicensed so not entitled to see the audit log render() - await waitFor(() => { - const link = screen.queryByText(Language.audit) - expect(link).toBe(null) - }) + await waitFor( + () => { + const link = screen.queryByText(Language.audit) + expect(link).toBe(null) + }, + { timeout: 5000 }, + ) }) it("does not show Audit Log link when not permitted via role", async () => { + // set permissions to Member (can't audit) server.use( rest.post(`/api/v2/users/${MockUser.id}/authorization`, async (req, res, ctx) => { return res(ctx.status(200), ctx.json(MockMemberPermissions)) }), ) + // set entitlements to allow audit log server.use( rest.get("/api/v2/entitlements", (req, res, ctx) => { return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog)) }), ) render() - await waitFor(() => { - const link = screen.queryByText(Language.audit) - expect(link).toBe(null) - }) + await waitFor( + () => { + const link = screen.queryByText(Language.audit) + expect(link).toBe(null) + }, + { timeout: 5000 }, + ) }) }) From a7d836ba3a5e4b980092ce88aea546297f93d238 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 25 Aug 2022 21:53:58 +0000 Subject: [PATCH 19/21] Use PropsWithChildren --- site/src/components/RequirePermission/RequirePermission.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/components/RequirePermission/RequirePermission.tsx b/site/src/components/RequirePermission/RequirePermission.tsx index b17b56ad6f201..6e1b34f9e0d8f 100644 --- a/site/src/components/RequirePermission/RequirePermission.tsx +++ b/site/src/components/RequirePermission/RequirePermission.tsx @@ -1,10 +1,10 @@ -import { FC } from "react" +import { FC, PropsWithChildren } from "react" import { Navigate } from "react-router" -export interface RequirePermissionProps { +export type RequirePermissionProps = PropsWithChildren<{ children: JSX.Element isFeatureVisible: boolean -} +}> /** * Wraps routes that are available based on RBAC or licensing. From 4f975b86faf43340d4edfff3d9a42223c2a33220 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 25 Aug 2022 22:13:45 +0000 Subject: [PATCH 20/21] Undo PropsWithChildren, try lower timeout --- site/src/components/Navbar/Navbar.test.tsx | 6 +++--- site/src/components/RequirePermission/RequirePermission.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/site/src/components/Navbar/Navbar.test.tsx b/site/src/components/Navbar/Navbar.test.tsx index b1729df62b3cb..04496a6e71a73 100644 --- a/site/src/components/Navbar/Navbar.test.tsx +++ b/site/src/components/Navbar/Navbar.test.tsx @@ -27,7 +27,7 @@ describe("Navbar", () => { const link = screen.getByText(Language.audit) expect(link).toBeDefined() }, - { timeout: 5000 }, + { timeout: 2500 }, ) }) @@ -40,7 +40,7 @@ describe("Navbar", () => { const link = screen.queryByText(Language.audit) expect(link).toBe(null) }, - { timeout: 5000 }, + { timeout: 2500 }, ) }) @@ -63,7 +63,7 @@ describe("Navbar", () => { const link = screen.queryByText(Language.audit) expect(link).toBe(null) }, - { timeout: 5000 }, + { timeout: 2500 }, ) }) }) diff --git a/site/src/components/RequirePermission/RequirePermission.tsx b/site/src/components/RequirePermission/RequirePermission.tsx index 6e1b34f9e0d8f..cc5a3b20058c9 100644 --- a/site/src/components/RequirePermission/RequirePermission.tsx +++ b/site/src/components/RequirePermission/RequirePermission.tsx @@ -1,10 +1,10 @@ -import { FC, PropsWithChildren } from "react" +import { FC } from "react" import { Navigate } from "react-router" -export type RequirePermissionProps = PropsWithChildren<{ +export interface RequirePermissionProps { children: JSX.Element isFeatureVisible: boolean -}> +} /** * Wraps routes that are available based on RBAC or licensing. From 9505faa1f557d696c37a5e044f1418ada8beac7c Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 25 Aug 2022 22:21:42 +0000 Subject: [PATCH 21/21] Format, lower timeout --- site/src/components/Navbar/Navbar.test.tsx | 6 +++--- site/src/components/RequirePermission/RequirePermission.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/components/Navbar/Navbar.test.tsx b/site/src/components/Navbar/Navbar.test.tsx index 04496a6e71a73..7b2c65d12f4ca 100644 --- a/site/src/components/Navbar/Navbar.test.tsx +++ b/site/src/components/Navbar/Navbar.test.tsx @@ -27,7 +27,7 @@ describe("Navbar", () => { const link = screen.getByText(Language.audit) expect(link).toBeDefined() }, - { timeout: 2500 }, + { timeout: 2000 }, ) }) @@ -40,7 +40,7 @@ describe("Navbar", () => { const link = screen.queryByText(Language.audit) expect(link).toBe(null) }, - { timeout: 2500 }, + { timeout: 2000 }, ) }) @@ -63,7 +63,7 @@ describe("Navbar", () => { const link = screen.queryByText(Language.audit) expect(link).toBe(null) }, - { timeout: 2500 }, + { timeout: 2000 }, ) }) }) diff --git a/site/src/components/RequirePermission/RequirePermission.tsx b/site/src/components/RequirePermission/RequirePermission.tsx index cc5a3b20058c9..b17b56ad6f201 100644 --- a/site/src/components/RequirePermission/RequirePermission.tsx +++ b/site/src/components/RequirePermission/RequirePermission.tsx @@ -1,7 +1,7 @@ import { FC } from "react" import { Navigate } from "react-router" -export interface RequirePermissionProps { +export interface RequirePermissionProps { children: JSX.Element isFeatureVisible: boolean } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy