From 58df3201d4522d19ad47f380c248dcfa838acd9b Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Mon, 20 Feb 2023 16:39:40 +0100 Subject: [PATCH] Prevent normal users from navigating to the cockpit --- client/src/router/__tests__/guards.spec.ts | 39 ++++++++ client/src/router/guards.ts | 11 +++ client/src/router/index.ts | 7 +- .../stores/__tests__/courseSession.spec.ts | 91 +++++++++++++++++++ 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 client/src/router/__tests__/guards.spec.ts create mode 100644 client/src/stores/__tests__/courseSession.spec.ts diff --git a/client/src/router/__tests__/guards.spec.ts b/client/src/router/__tests__/guards.spec.ts new file mode 100644 index 00000000..c4384db1 --- /dev/null +++ b/client/src/router/__tests__/guards.spec.ts @@ -0,0 +1,39 @@ +import { createPinia, setActivePinia } from "pinia"; +import { beforeEach, describe, expect, vi } from "vitest"; +import * as courseSessions from "../../stores/courseSessions"; +import { expertRequired } from "../guards"; + +describe("Guards", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + beforeEach(() => { + // creates a fresh pinia and make it active so it's automatically picked + // up by any useStore() call without having to pass it to it: + // `useStore(pinia)` + setActivePinia(createPinia()); + }); + + it("cannot route to cockpit", () => { + vi.spyOn(courseSessions, "useCourseSessionsStore").mockReturnValue({ + hasCockpit: false, + }); + const slug = "test"; + expect(expertRequired({ params: { courseSlug: "test" } })).toEqual( + `/course/${slug}/learn` + ); + }); + + it("can route to cockpit", () => { + vi.spyOn(courseSessions, "useCourseSessionsStore").mockReturnValue({ + hasCockpit: true, + }); + const to = { + params: { + courseSlug: "test", + }, + }; + expect(expertRequired(to)).toEqual(to); + }); +}); diff --git a/client/src/router/guards.ts b/client/src/router/guards.ts index e4902f33..7304e6b9 100644 --- a/client/src/router/guards.ts +++ b/client/src/router/guards.ts @@ -1,3 +1,4 @@ +import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useUserStore } from "@/stores/user"; import type { NavigationGuardWithThis, RouteLocationNormalized } from "vue-router"; @@ -32,3 +33,13 @@ export const getCookieValue = (cookieName: string): string => { const loginRequired = (to: RouteLocationNormalized) => { return !to.meta?.public; }; + +export const expertRequired = (to: RouteLocationNormalized) => { + const courseSessionsStore = useCourseSessionsStore(); + if (courseSessionsStore.hasCockpit) { + return to; + } else { + const courseSlug = to.params.courseSlug as string; + return `/course/${courseSlug}/learn`; + } +}; diff --git a/client/src/router/index.ts b/client/src/router/index.ts index 0fa65d18..340f6b6e 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -1,6 +1,10 @@ import DashboardPage from "@/pages/DashboardPage.vue"; import LoginPage from "@/pages/LoginPage.vue"; -import { redirectToLoginIfRequired, updateLoggedIn } from "@/router/guards"; +import { + expertRequired, + redirectToLoginIfRequired, + updateLoggedIn, +} from "@/router/guards"; import { useAppStore } from "@/stores/app"; import { createRouter, createWebHistory } from "vue-router"; @@ -103,6 +107,7 @@ const router = createRouter({ path: "/course/:courseSlug/cockpit", props: true, component: () => import("@/pages/cockpit/CockpitParentPage.vue"), + beforeEnter: [expertRequired], children: [ { path: "", diff --git a/client/src/stores/__tests__/courseSession.spec.ts b/client/src/stores/__tests__/courseSession.spec.ts new file mode 100644 index 00000000..fd0a6121 --- /dev/null +++ b/client/src/stores/__tests__/courseSession.spec.ts @@ -0,0 +1,91 @@ +import { itGetCached, itPost } from "@/fetchHelpers"; +import type { CourseSession } from "@/types"; +import { createPinia, setActivePinia } from "pinia"; +import { beforeEach, describe, expect, vi } from "vitest"; +import { useRoute } from "vue-router"; +import { useCourseSessionsStore } from "../courseSessions"; +import { useUserStore } from "../user"; + +let user = {}; +let courseSessions: CourseSession[] = []; + +describe("CourseSession Store", () => { + vi.mock("vue-router", () => ({ + useRoute: () => ({ + path: "/course/test-course/learn/", + }), + })); + + vi.mock("@/fetchHelpers", () => { + const itGetCached = () => Promise.resolve([]); + const itPost = () => Promise.resolve([]); + + return { + itGetCached, + itPost, + }; + }); + + beforeEach(() => { + // creates a fresh pinia and make it active so it's automatically picked + // up by any useStore() call without having to pass it to it: + // `useStore(pinia)` + setActivePinia(createPinia()); + user = { + is_superuser: false, + course_session_expert: [], + }; + courseSessions = [ + { + id: 1, + created_at: "2021-05-11T10:00:00.000000Z", + updated_at: "2023-05-11T10:00:00.000000Z", + course: { + id: 1, + title: "Test Course", + category_name: "Test Category", + slug: "test-course", + }, + title: "Test Course Session", + start_date: "2022-05-11T10:00:00.000000Z", + end_date: "2023-05-11T10:00:00.000000Z", + learning_path_url: "/course/test-course/learn/", + competence_url: "/course/test-course/competence/", + course_url: "/course/test-course/", + media_library_url: "/course/test-course/media/", + additional_json_data: {}, + documents: [], + users: [], + }, + ]; + }); + + it("normal user has no cockpit", () => { + const userStore = useUserStore(); + const courseSessionsStore = useCourseSessionsStore(); + userStore.$patch(user); + courseSessionsStore.$patch({ courseSessions }); + + expect(courseSessionsStore.hasCockpit).toBeFalsy(); + }); + + it("superuser has cockpit", () => { + const userStore = useUserStore(); + const courseSessionsStore = useCourseSessionsStore(); + userStore.$patch(Object.assign(user, { is_superuser: true })); + courseSessionsStore.$patch({ courseSessions }); + + expect(courseSessionsStore.hasCockpit).toBeTruthy(); + }); + + it("expert has cockpit", () => { + const userStore = useUserStore(); + const courseSessionsStore = useCourseSessionsStore(); + userStore.$patch( + Object.assign(user, { course_session_experts: [courseSessions[0].id] }) + ); + courseSessionsStore.$patch({ courseSessions }); + + expect(courseSessionsStore.hasCockpit).toBeTruthy(); + }); +});