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 }