From a938c90364270c71ea64b9d18fa7fcee138fc441 Mon Sep 17 00:00:00 2001 From: Tim Cremer Date: Wed, 27 Nov 2024 11:41:22 +0100 Subject: [PATCH] added test cases for guard --- ...rd.service.ts => course-overview-guard.ts} | 21 ++- .../app/overview/courses-routing.module.ts | 55 +++--- .../overview/course-overview-guard.spec.ts | 170 ++++++++++++++++++ 3 files changed, 219 insertions(+), 27 deletions(-) rename src/main/webapp/app/overview/{course-overview-guard.service.ts => course-overview-guard.ts} (81%) create mode 100644 src/test/javascript/spec/component/overview/course-overview-guard.spec.ts diff --git a/src/main/webapp/app/overview/course-overview-guard.service.ts b/src/main/webapp/app/overview/course-overview-guard.ts similarity index 81% rename from src/main/webapp/app/overview/course-overview-guard.service.ts rename to src/main/webapp/app/overview/course-overview-guard.ts index bb45650d968c..4ea6e93ba2c9 100644 --- a/src/main/webapp/app/overview/course-overview-guard.service.ts +++ b/src/main/webapp/app/overview/course-overview-guard.ts @@ -6,11 +6,12 @@ import { CourseManagementService } from 'app/course/manage/course-management.ser import { Course } from 'app/entities/course.model'; import dayjs from 'dayjs/esm'; import { ArtemisServerDateService } from 'app/shared/server-date.service'; +import { CourseOverviewRoutePath } from 'app/overview/courses-routing.module'; @Injectable({ providedIn: 'root', }) -export class CourseOverviewGuardService implements CanActivate { +export class CourseOverviewGuard implements CanActivate { private courseStorageService = inject(CourseStorageService); private courseManagementService = inject(CourseManagementService); private router = inject(Router); @@ -49,22 +50,28 @@ export class CourseOverviewGuardService implements CanActivate { handleReturn = (course?: Course, type?: string): Observable => { let hasAccess: boolean; switch (type) { - case 'exams': + case CourseOverviewRoutePath.EXERCISES: + hasAccess = true; //default route, should always be accessible + break; + case CourseOverviewRoutePath.LECTURES: + hasAccess = !!course?.lectures; + break; + case CourseOverviewRoutePath.EXAMS: hasAccess = this.hasVisibleExams(course); break; - case 'competencies': + case CourseOverviewRoutePath.COMPETENCIES: hasAccess = !!(course?.numberOfCompetencies || course?.numberOfPrerequisites); break; - case 'tutorial-groups': + case CourseOverviewRoutePath.TUTORIAL_GROUPS: hasAccess = !!course?.numberOfTutorialGroups; break; - case 'dashboard': + case CourseOverviewRoutePath.DASHBOARD: hasAccess = course?.studentCourseAnalyticsDashboardEnabled ?? false; break; - case 'faq': + case CourseOverviewRoutePath.FAQ: hasAccess = course?.faqEnabled ?? false; break; - case 'learning-path': + case CourseOverviewRoutePath.LEARNING_PATH: hasAccess = course?.learningPathsEnabled ?? false; break; default: diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index a96428515616..e348d77c3a15 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -12,7 +12,22 @@ import { CourseTutorialGroupDetailComponent } from './tutorial-group-details/cou import { ExamParticipationComponent } from 'app/exam/participate/exam-participation.component'; import { PendingChangesGuard } from 'app/shared/guard/pending-changes.guard'; import { CourseArchiveComponent } from './course-archive/course-archive.component'; -import { CourseOverviewGuardService } from 'app/overview/course-overview-guard.service'; +import { CourseOverviewGuard } from 'app/overview/course-overview-guard'; + +export enum CourseOverviewRoutePath { + DASHBOARD = 'dashboard', + EXERCISES = 'exercises', + EXAMS = 'exams', + COMPETENCIES = 'competencies', + TUTORIAL_GROUPS = 'tutorial-groups', + FAQ = 'faq', + LEARNING_PATH = 'learning-path', + LECTURES = 'lectures', + ENROLL = 'enroll', + ARCHIVE = 'archive', + STATISTICS = 'statistics', + COMMUNICATION = 'communication', +} const routes: Routes = [ { @@ -25,11 +40,11 @@ const routes: Routes = [ canActivate: [UserRouteAccessService], }, { - path: 'enroll', + path: CourseOverviewRoutePath.ENROLL, loadChildren: () => import('./course-registration/course-registration.module').then((m) => m.CourseRegistrationModule), }, { - path: 'archive', + path: CourseOverviewRoutePath.ARCHIVE, component: CourseArchiveComponent, data: { authorities: [Authority.USER], @@ -54,7 +69,7 @@ const routes: Routes = [ canActivate: [UserRouteAccessService], children: [ { - path: 'exercises', + path: CourseOverviewRoutePath.EXERCISES, component: CourseExercisesComponent, data: { authorities: [Authority.USER], @@ -128,7 +143,7 @@ const routes: Routes = [ }, { - path: 'lectures', + path: CourseOverviewRoutePath.LECTURES, component: CourseLecturesComponent, data: { authorities: [Authority.USER], @@ -152,7 +167,7 @@ const routes: Routes = [ ], }, { - path: 'statistics', + path: CourseOverviewRoutePath.STATISTICS, loadChildren: () => import('./course-statistics/course-statistics.module').then((m) => m.CourseStatisticsModule), data: { authorities: [Authority.USER], @@ -161,13 +176,13 @@ const routes: Routes = [ }, }, { - path: 'competencies', + path: CourseOverviewRoutePath.COMPETENCIES, data: { authorities: [Authority.USER], pageTitle: 'overview.competencies', showRefreshButton: true, }, - canActivate: [CourseOverviewGuardService], + canActivate: [CourseOverviewGuard], children: [ { path: '', @@ -180,16 +195,16 @@ const routes: Routes = [ ], }, { - path: 'dashboard', + path: CourseOverviewRoutePath.DASHBOARD, loadChildren: () => import('./course-dashboard/course-dashboard.module').then((m) => m.CourseDashboardModule), data: { authorities: [Authority.USER], pageTitle: 'overview.dashboard', }, - canActivate: [UserRouteAccessService, CourseOverviewGuardService], + canActivate: [UserRouteAccessService, CourseOverviewGuard], }, { - path: 'learning-path', + path: CourseOverviewRoutePath.LEARNING_PATH, loadComponent: () => import('app/course/learning-paths/pages/learning-path-student-page/learning-path-student-page.component').then((c) => c.LearningPathStudentPageComponent), data: { @@ -197,10 +212,10 @@ const routes: Routes = [ pageTitle: 'overview.learningPath', showRefreshButton: true, }, - canActivate: [CourseOverviewGuardService], + canActivate: [CourseOverviewGuard], }, { - path: 'communication', + path: CourseOverviewRoutePath.COMMUNICATION, loadChildren: () => import('./course-conversations/course-conversations.module').then((m) => m.CourseConversationsModule), data: { authorities: [Authority.USER], @@ -210,7 +225,7 @@ const routes: Routes = [ }, }, { - path: 'tutorial-groups', + path: CourseOverviewRoutePath.TUTORIAL_GROUPS, component: CourseTutorialGroupsComponent, data: { authorities: [Authority.USER], @@ -218,7 +233,7 @@ const routes: Routes = [ hasSidebar: true, showRefreshButton: true, }, - canActivate: [UserRouteAccessService, CourseOverviewGuardService], + canActivate: [UserRouteAccessService, CourseOverviewGuard], children: [ { path: ':tutorialGroupId', @@ -235,7 +250,7 @@ const routes: Routes = [ ], }, { - path: 'exams', + path: CourseOverviewRoutePath.EXAMS, component: CourseExamsComponent, data: { authorities: [Authority.USER], @@ -243,7 +258,7 @@ const routes: Routes = [ hasSidebar: true, showRefreshButton: true, }, - canActivate: [UserRouteAccessService, CourseOverviewGuardService], + canActivate: [UserRouteAccessService, CourseOverviewGuard], children: [ { path: ':examId', @@ -269,7 +284,7 @@ const routes: Routes = [ }, }, { - path: 'faq', + path: CourseOverviewRoutePath.FAQ, loadComponent: () => import('../overview/course-faq/course-faq.component').then((m) => m.CourseFaqComponent), data: { authorities: [Authority.USER], @@ -277,11 +292,11 @@ const routes: Routes = [ hasSidebar: false, showRefreshButton: true, }, - canActivate: [CourseOverviewGuardService], + canActivate: [CourseOverviewGuard], }, { path: '', - redirectTo: 'dashboard', // dashboard will redirect to exercises if not enabled + redirectTo: CourseOverviewRoutePath.DASHBOARD, // dashboard will redirect to exercises if not enabled pathMatch: 'full', }, ], diff --git a/src/test/javascript/spec/component/overview/course-overview-guard.spec.ts b/src/test/javascript/spec/component/overview/course-overview-guard.spec.ts new file mode 100644 index 000000000000..e1201d3c9ca2 --- /dev/null +++ b/src/test/javascript/spec/component/overview/course-overview-guard.spec.ts @@ -0,0 +1,170 @@ +import { TestBed } from '@angular/core/testing'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { ArtemisTestModule } from '../../test.module'; +import { of } from 'rxjs'; +import { CourseStorageService } from 'app/course/manage/course-storage.service'; +import { CourseManagementService } from 'app/course/manage/course-management.service'; +import dayjs from 'dayjs/esm'; +import { Course } from 'app/entities/course.model'; +import { HttpResponse } from '@angular/common/http'; +import { CourseOverviewGuard } from 'app/overview/course-overview-guard'; +import { Exam } from 'app/entities/exam/exam.model'; +import { Lecture } from 'app/entities/lecture.model'; +import { CourseOverviewRoutePath } from 'app/overview/courses-routing.module'; + +describe('CourseOverviewGuard', () => { + let guard: CourseOverviewGuard; + let courseStorageService: CourseStorageService; + let courseManagementService: CourseManagementService; + let router: Router; + + const visibleRealExam = { + id: 1, + visibleDate: dayjs().subtract(1, 'days'), + startDate: dayjs().subtract(30, 'minutes'), + testExam: false, + } as Exam; + + const lecture = new Lecture(); + + const mockCourse: Course = { id: 1, lectures: [lecture], exams: [visibleRealExam], faqEnabled: true } as Course; + + const responseFakeCourse = { body: mockCourse } as HttpResponse; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + }); + guard = TestBed.inject(CourseOverviewGuard); + courseStorageService = TestBed.inject(CourseStorageService); + courseManagementService = TestBed.inject(CourseManagementService); + router = TestBed.inject(Router); + }); + + describe('canActivate', () => { + it('should return false if courseId is not present', () => { + const route = { parent: { paramMap: { get: () => undefined } }, routeConfig: { path: CourseOverviewRoutePath.EXERCISES } } as unknown as ActivatedRouteSnapshot; + let resultValue = true; + guard.canActivate(route).subscribe((result) => { + resultValue = result; + }); + expect(resultValue).toBeFalse(); + }); + + it('should return true if course is in cache', () => { + const route = { parent: { paramMap: { get: () => '1' } }, routeConfig: { path: CourseOverviewRoutePath.EXERCISES } } as unknown as ActivatedRouteSnapshot; + let resultValue = false; + jest.spyOn(courseStorageService, 'getCourse').mockReturnValue(mockCourse); + guard.canActivate(route).subscribe((result) => { + resultValue = result; + }); + expect(resultValue).toBeTrue(); + }); + + it('should return true if course is fetched from server', () => { + const route = { parent: { paramMap: { get: () => '1' } }, routeConfig: { path: CourseOverviewRoutePath.EXERCISES } } as unknown as ActivatedRouteSnapshot; + let resultValue = false; + jest.spyOn(courseStorageService, 'getCourse').mockReturnValue(undefined); + jest.spyOn(courseManagementService, 'find').mockReturnValue(of(responseFakeCourse)); + guard.canActivate(route).subscribe((result) => { + resultValue = result; + }); + expect(resultValue).toBeTrue(); + }); + }); + + describe('handleReturn', () => { + it('should return true if type is lectures and course has lectures', () => { + let resultValue = true; + const result = guard.handleReturn(mockCourse, 'lectures'); + result.subscribe((value) => { + resultValue = value; + }); + + expect(resultValue).toBeTrue(); + }); + + it('should return true if type is exams and course has visible exams', () => { + const result = guard.handleReturn(mockCourse, 'exams'); + let resultValue = true; + result.subscribe((value) => { + resultValue = value; + }); + expect(resultValue).toBeTrue(); + }); + + it('should return true if type is competencies and course has competencies', () => { + mockCourse.numberOfCompetencies = 1; + const result = guard.handleReturn(mockCourse, 'competencies'); + let resultValue = true; + result.subscribe((value) => { + resultValue = value; + }); + expect(resultValue).toBeTrue(); + }); + + it('should return true if type is competencies and course has prerequisits', () => { + mockCourse.numberOfPrerequisites = 1; + const result = guard.handleReturn(mockCourse, 'competencies'); + let resultValue = true; + result.subscribe((value) => { + resultValue = value; + }); + expect(resultValue).toBeTrue(); + }); + + it('should return true if type is tutorial-groups and course has tutorial groups', () => { + mockCourse.numberOfTutorialGroups = 1; + const result = guard.handleReturn(mockCourse, 'tutorial-groups'); + let resultValue = true; + result.subscribe((value) => { + resultValue = value; + }); + expect(resultValue).toBeTrue(); + }); + + it('should return true if type is dashboard and course has studentCourseAnalyticsDashboardEnabled', () => { + mockCourse.studentCourseAnalyticsDashboardEnabled = true; + const result = guard.handleReturn(mockCourse, 'dashboard'); + let resultValue = true; + result.subscribe((value) => { + resultValue = value; + }); + expect(resultValue).toBeTrue(); + }); + + it('should return true if type is faq and course has faqEnabled', () => { + const result = guard.handleReturn(mockCourse, 'faq'); + let resultValue = true; + result.subscribe((value) => { + resultValue = value; + }); + expect(resultValue).toBeTrue(); + }); + + it('should return true if type is learning-path and course has learningPathsEnabled', () => { + mockCourse.learningPathsEnabled = true; + const result = guard.handleReturn(mockCourse, 'learning-path'); + let resultValue = true; + result.subscribe((value) => { + resultValue = value; + }); + expect(resultValue).toBeTrue(); + }); + + it('should return false if type is unknown', () => { + const result = guard.handleReturn(mockCourse, 'unknown'); + let resultValue = true; + result.subscribe((value) => { + resultValue = value; + }); + expect(resultValue).toBeFalse(); + }); + + it('should navigate to exercises if type is unknown', () => { + const navigateSpy = jest.spyOn(router, 'navigate'); + guard.handleReturn(mockCourse, 'unknown'); + expect(navigateSpy).toHaveBeenCalledWith(['/courses/1/exercises']); + }); + }); +});