From fc533fe39f4c89bd27a7c0279500bd2ff12e672f Mon Sep 17 00:00:00 2001 From: Florian Gareis Date: Fri, 29 Sep 2023 07:52:23 +0200 Subject: [PATCH 01/19] Development: Revert cypress back to 12 (#7289) --- docker/cypress-E2E-tests-mysql.yml | 2 + docker/cypress-E2E-tests-postgres.yml | 2 + .../cypress/e2e/exam/ExamAssessment.cy.ts | 2 +- src/test/cypress/package-lock.json | 69 +++++++++++++------ src/test/cypress/package.json | 6 +- 5 files changed, 55 insertions(+), 26 deletions(-) diff --git a/docker/cypress-E2E-tests-mysql.yml b/docker/cypress-E2E-tests-mysql.yml index 99e96f8b7c87..a14c4c163ce7 100644 --- a/docker/cypress-E2E-tests-mysql.yml +++ b/docker/cypress-E2E-tests-mysql.yml @@ -46,6 +46,8 @@ services: CYPRESS_DB_TYPE: "MySQL" SORRY_CYPRESS_PROJECT_ID: "artemis-mysql" command: sh -c "cd /app/artemis/src/test/cypress && chmod 777 /root && npm ci && npm run cypress:setup && (npm run cypress:record:mysql & sleep 60 && npm run cypress:record:mysql & wait)" +# Old run method using plain cypress kept here as backup +# command: sh -c "cd /app/artemis/src/test/cypress && chmod 777 /root && npm ci && npm run cypress:run" networks: artemis: diff --git a/docker/cypress-E2E-tests-postgres.yml b/docker/cypress-E2E-tests-postgres.yml index 0c2ed641ee70..41fe8edf5faa 100644 --- a/docker/cypress-E2E-tests-postgres.yml +++ b/docker/cypress-E2E-tests-postgres.yml @@ -47,6 +47,8 @@ services: CYPRESS_DB_TYPE: "Postgres" SORRY_CYPRESS_PROJECT_ID: "artemis-postgres" command: sh -c "cd /app/artemis/src/test/cypress && chmod 777 /root && npm ci && npm run cypress:setup && (npm run cypress:record:postgres & sleep 60 && npm run cypress:record:postgres & wait)" +# Old run method using plain cypress kept here as backup +# command: sh -c "cd /app/artemis/src/test/cypress && chmod 777 /root && npm ci && npm run cypress:run" networks: artemis: diff --git a/src/test/cypress/e2e/exam/ExamAssessment.cy.ts b/src/test/cypress/e2e/exam/ExamAssessment.cy.ts index ca864efaac55..1c4aeb6f7ab1 100644 --- a/src/test/cypress/e2e/exam/ExamAssessment.cy.ts +++ b/src/test/cypress/e2e/exam/ExamAssessment.cy.ts @@ -74,7 +74,7 @@ describe('Exam assessment', () => { describe('Modeling exercise assessment', () => { before('Prepare exam', () => { - examEnd = dayjs().add(30, 'seconds'); + examEnd = dayjs().add(45, 'seconds'); prepareExam(course, examEnd, ExerciseType.MODELING); }); diff --git a/src/test/cypress/package-lock.json b/src/test/cypress/package-lock.json index 677ef5fe04c8..8fd1c857ccb4 100644 --- a/src/test/cypress/package-lock.json +++ b/src/test/cypress/package-lock.json @@ -8,9 +8,9 @@ "license": "MIT", "devDependencies": { "@4tw/cypress-drag-drop": "2.2.5", - "@types/node": "20.6.0", - "cypress": "13.2.0", - "cypress-cloud": "1.10.0-beta.4", + "@types/node": "20.7.1", + "cypress": "12.17.4", + "cypress-cloud": "2.0.0-beta.1", "cypress-file-upload": "5.0.8", "cypress-wait-until": "2.0.1", "typescript": "5.2.2", @@ -182,9 +182,9 @@ } }, "node_modules/@cypress/request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", - "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "version": "2.88.12", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz", + "integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==", "dev": true, "dependencies": { "aws-sign2": "~0.7.0", @@ -200,7 +200,7 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "6.10.4", + "qs": "~6.10.3", "safe-buffer": "^5.1.2", "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", @@ -301,9 +301,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz", - "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==", + "version": "20.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.7.1.tgz", + "integrity": "sha512-LT+OIXpp2kj4E2S/p91BMe+VgGX2+lfO+XTpfXhh+bCk2LkQtHZSub8ewFBMGP5ClysPjTDFa4sMI8Q3n4T0wg==", "dev": true }, "node_modules/@types/sinonjs__fake-timers": { @@ -904,15 +904,15 @@ } }, "node_modules/cypress": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.2.0.tgz", - "integrity": "sha512-AvDQxBydE771GTq0TR4ZUBvv9m9ffXuB/ueEtpDF/6gOcvFR96amgwSJP16Yhqw6VhmwqspT5nAGzoxxB+D89g==", + "version": "12.17.4", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.4.tgz", + "integrity": "sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@cypress/request": "^3.0.0", + "@cypress/request": "2.88.12", "@cypress/xvfb": "^1.2.4", - "@types/node": "^18.17.5", + "@types/node": "^16.18.39", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -958,13 +958,13 @@ "cypress": "bin/cypress" }, "engines": { - "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + "node": "^14.0.0 || ^16.0.0 || >=18.0.0" } }, "node_modules/cypress-cloud": { - "version": "1.10.0-beta.4", - "resolved": "https://registry.npmjs.org/cypress-cloud/-/cypress-cloud-1.10.0-beta.4.tgz", - "integrity": "sha512-8pe+ifmf8Uotx9lVL4Crq9LQokAa8U6/09p+wj9XEZoEiY/FJhbbvOygduqZmF6BaQaDAf5q1DagKKHmLXGErA==", + "version": "2.0.0-beta.1", + "resolved": "https://registry.npmjs.org/cypress-cloud/-/cypress-cloud-2.0.0-beta.1.tgz", + "integrity": "sha512-nMKf7077NaOK4AFHUwYGAnL3HtgTWsyQ+dSB4YxSH0GvQbtJo7Ljk0dlkzkUraiPb+0/Rr+XF0ozorSPz7ChJw==", "dev": true, "dependencies": { "@cypress/commit-info": "^2.2.0", @@ -985,6 +985,7 @@ "lil-http-terminator": "^1.2.3", "lodash": "^4.17.21", "nanoid": "^3.3.4", + "plur": "^4.0.0", "pretty-ms": "^7.0.1", "source-map-support": "^0.5.21", "table": "^6.8.1", @@ -1071,9 +1072,9 @@ "dev": true }, "node_modules/cypress/node_modules/@types/node": { - "version": "18.17.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.15.tgz", - "integrity": "sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==", + "version": "16.18.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.54.tgz", + "integrity": "sha512-oTmGy68gxZZ21FhTJVVvZBYpQHEBZxHKTsGshobMqm9qWpbqdZsA5jvsuPZcHu0KwpmLrOHWPdEfg7XDpNT9UA==", "dev": true }, "node_modules/dashdash": { @@ -1718,6 +1719,15 @@ "node": ">=10" } }, + "node_modules/irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", @@ -2425,6 +2435,21 @@ "node": ">=0.10.0" } }, + "node_modules/plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "dev": true, + "dependencies": { + "irregular-plurals": "^3.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", diff --git a/src/test/cypress/package.json b/src/test/cypress/package.json index 26ce69d2666e..f957ff8b4260 100644 --- a/src/test/cypress/package.json +++ b/src/test/cypress/package.json @@ -8,9 +8,9 @@ ], "devDependencies": { "@4tw/cypress-drag-drop": "2.2.5", - "@types/node": "20.6.0", - "cypress": "13.2.0", - "cypress-cloud": "1.10.0-beta.4", + "@types/node": "20.7.1", + "cypress": "12.17.4", + "cypress-cloud": "2.0.0-beta.1", "cypress-file-upload": "5.0.8", "cypress-wait-until": "2.0.1", "typescript": "5.2.2", From 84e351f0a3b4535f8b1f009f675d40029634afb0 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Fri, 29 Sep 2023 10:29:21 +0200 Subject: [PATCH 02/19] Adaptive learning: Add participation view for learning paths (#7052) --- .../web/rest/LearningPathResource.java | 37 ++- .../competency-node-details.component.html | 2 +- .../learning-paths/learning-path.service.ts | 4 + .../learning-paths/learning-paths.module.ts | 59 ++++- .../learning-path-container.component.html | 24 ++ .../learning-path-container.component.scss | 16 ++ .../learning-path-container.component.ts | 164 ++++++++++++++ ...learning-path-graph-sidebar.component.html | 36 +++ ...learning-path-graph-sidebar.component.scss | 77 +++++++ .../learning-path-graph-sidebar.component.ts | 54 +++++ .../learning-path-history-storage.service.ts | 86 +++++++ ...ning-path-lecture-unit-view.component.html | 17 ++ ...ning-path-lecture-unit-view.component.scss | 3 + ...arning-path-lecture-unit-view.component.ts | 59 +++++ .../learning-path-lecture-unit-view.module.ts | 33 +++ .../overview/course-overview.component.html | 12 + .../app/overview/course-overview.component.ts | 2 + .../app/overview/courses-routing.module.ts | 4 + .../discussion-section.component.ts | 6 +- .../course-exercise-details.component.ts | 10 +- .../shared/layouts/navbar/navbar.component.ts | 3 + src/main/webapp/i18n/de/competency.json | 16 ++ .../webapp/i18n/de/student-dashboard.json | 1 + src/main/webapp/i18n/en/competency.json | 16 ++ .../webapp/i18n/en/student-dashboard.json | 1 + .../lecture/LearningPathIntegrationTest.java | 22 ++ .../learning-path-container.component.spec.ts | 211 ++++++++++++++++++ ...rning-path-graph-sidebar.component.spec.ts | 49 ++++ ...g-path-lecture-unit-view.component.spec.ts | 112 ++++++++++ ...rning-path-history-storage.service.spec.ts | 127 +++++++++++ .../service/learning-path.service.spec.ts | 6 + 31 files changed, 1260 insertions(+), 9 deletions(-) create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.scss create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts create mode 100644 src/main/webapp/app/course/learning-paths/participate/learning-path-history-storage.service.ts create mode 100644 src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.html create mode 100644 src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.scss create mode 100644 src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts create mode 100644 src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts create mode 100644 src/test/javascript/spec/component/learning-paths/participate/learning-path-container.component.spec.ts create mode 100644 src/test/javascript/spec/component/learning-paths/participate/learning-path-graph-sidebar.component.spec.ts create mode 100644 src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts create mode 100644 src/test/javascript/spec/service/learning-path-history-storage.service.spec.ts diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java index be11819bc2aa..e2f39f1ce557 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LearningPathResource.java @@ -138,7 +138,7 @@ public ResponseEntity getHealthStatusForCourse(@PathVaria } /** - * GET /learning-path/:learningPathId/graph : Gets the ngx representation of the learning path. + * GET /learning-path/:learningPathId/graph : Gets the ngx representation of the learning path as a graph. * * @param learningPathId the id of the learning path that should be fetched * @return the ResponseEntity with status 200 (OK) and with body the ngx representation of the learning path @@ -147,7 +147,7 @@ public ResponseEntity getHealthStatusForCourse(@PathVaria @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getLearningPathNgxGraph(@PathVariable Long learningPathId) { - log.debug("REST request to get ngx representation of learning path with id: {}", learningPathId); + log.debug("REST request to get ngx graph representation of learning path with id: {}", learningPathId); LearningPath learningPath = learningPathRepository.findWithEagerCompetenciesAndLearningObjectsAndCompletedUsersByIdElseThrow(learningPathId); Course course = courseRepository.findByIdElseThrow(learningPath.getCourse().getId()); if (!course.getLearningPathsEnabled()) { @@ -165,4 +165,37 @@ else if (!authorizationCheckService.isAtLeastInstructorInCourse(course, user) && NgxLearningPathDTO graph = learningPathService.generateNgxGraphRepresentation(learningPath); return ResponseEntity.ok(graph); } + + /** + * GET /courses/:courseId/learning-path-id : Gets the id of the learning path. + * If the learning path has not been generated although the course has learning paths enabled, the corresponding learning path will be created. + * + * @param courseId the id of the course from which the learning path id should be fetched + * @return the ResponseEntity with status 200 (OK) and with body the id of the learning path + */ + @GetMapping("/courses/{courseId}/learning-path-id") + @EnforceAtLeastStudent + public ResponseEntity getLearningPathId(@PathVariable Long courseId) { + log.debug("REST request to get learning path id for course with id: {}", courseId); + Course course = courseRepository.findByIdElseThrow(courseId); + if (!authorizationCheckService.isStudentInCourse(course, null)) { + throw new BadRequestException("You are not a student in this course."); + } + if (!course.getLearningPathsEnabled()) { + throw new BadRequestException("Learning paths are not enabled for this course."); + } + + // generate learning path if missing + User user = userRepository.getUser(); + final var learningPathOptional = learningPathRepository.findByCourseIdAndUserId(course.getId(), user.getId()); + LearningPath learningPath; + if (learningPathOptional.isEmpty()) { + course = courseRepository.findWithEagerCompetenciesByIdElseThrow(courseId); + learningPath = learningPathService.generateLearningPathForUser(course, user); + } + else { + learningPath = learningPathOptional.get(); + } + return ResponseEntity.ok(learningPath.getId()); + } } diff --git a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.html b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.html index b31670b4d330..b4a52421a5fc 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component.html @@ -12,7 +12,7 @@

Mastered Optional

-
{{ competency.description }}
+
diff --git a/src/main/webapp/app/course/learning-paths/learning-path.service.ts b/src/main/webapp/app/course/learning-paths/learning-path.service.ts index b201a57f84d3..f0ea55d45eb2 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path.service.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path.service.ts @@ -36,4 +36,8 @@ export class LearningPathService { }), ); } + + getLearningPathId(courseId: number) { + return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/learning-path-id`, { observe: 'response' }); + } } diff --git a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts index 8c37905813e9..7a125407fa01 100644 --- a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts +++ b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts @@ -13,9 +13,62 @@ import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learni import { CompetencyNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component'; import { LectureUnitNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component'; import { ExerciseNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component'; +import { LearningPathContainerComponent } from 'app/course/learning-paths/participate/learning-path-container.component'; +import { Authority } from 'app/shared/constants/authority.constants'; +import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; +import { RouterModule, Routes } from '@angular/router'; +import { LearningPathGraphSidebarComponent } from 'app/course/learning-paths/participate/learning-path-graph-sidebar.component'; + +const routes: Routes = [ + { + path: '', + component: LearningPathContainerComponent, + data: { + authorities: [Authority.USER], + pageTitle: 'overview.learningPath', + }, + canActivate: [UserRouteAccessService], + children: [ + { + path: 'lecture-unit', + pathMatch: 'full', + children: [ + { + path: '', + pathMatch: 'full', + loadChildren: () => + import('app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module').then( + (m) => m.ArtemisLearningPathLectureUnitViewModule, + ), + }, + ], + }, + { + path: 'exercise', + pathMatch: 'full', + children: [ + { + path: '', + pathMatch: 'full', + loadChildren: () => import('app/overview/exercise-details/course-exercise-details.module').then((m) => m.CourseExerciseDetailsModule), + }, + ], + }, + ], + }, +]; @NgModule({ - imports: [ArtemisSharedModule, FormsModule, ReactiveFormsModule, ArtemisSharedComponentModule, NgxGraphModule, ArtemisLectureUnitsModule, ArtemisCompetenciesModule], + imports: [ + ArtemisSharedModule, + FormsModule, + ReactiveFormsModule, + ArtemisSharedComponentModule, + NgxGraphModule, + ArtemisLectureUnitsModule, + ArtemisCompetenciesModule, + RouterModule.forChild(routes), + ], declarations: [ LearningPathManagementComponent, LearningPathProgressModalComponent, @@ -25,7 +78,9 @@ import { ExerciseNodeDetailsComponent } from 'app/course/learning-paths/learning CompetencyNodeDetailsComponent, LectureUnitNodeDetailsComponent, ExerciseNodeDetailsComponent, + LearningPathContainerComponent, + LearningPathGraphSidebarComponent, ], - exports: [], + exports: [LearningPathContainerComponent], }) export class ArtemisLearningPathsModule {} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html new file mode 100644 index 000000000000..a0b7ee135370 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.html @@ -0,0 +1,24 @@ +
+
+ +
+
+
+
+ No task selected +
+ +
+
+
+ + diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.scss b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.scss new file mode 100644 index 000000000000..f1d6cecd0efc --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.scss @@ -0,0 +1,16 @@ +.lp-participation-view-container { + width: 100%; + margin-left: 0; +} + +.graph-wrapper { + max-width: min-content; +} + +.learning-object-wrapper { + border-style: none; +} + +.exercise-wrapper { + padding: 1rem; +} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts new file mode 100644 index 000000000000..0f85e88d14ae --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-container.component.ts @@ -0,0 +1,164 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons'; +import { Exercise } from 'app/entities/exercise.model'; +import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; +import { Lecture } from 'app/entities/lecture.model'; +import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; +import { NgxLearningPathNode, NodeType } from 'app/entities/competency/learning-path.model'; +import { LectureService } from 'app/lecture/lecture.service'; +import { onError } from 'app/shared/util/global.utils'; +import { HttpErrorResponse } from '@angular/common/http'; +import { AlertService } from 'app/core/util/alert.service'; +import { LearningPathLectureUnitViewComponent } from 'app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component'; +import { CourseExerciseDetailsComponent } from 'app/overview/exercise-details/course-exercise-details.component'; +import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { ExerciseEntry, LearningPathHistoryStorageService, LectureUnitEntry } from 'app/course/learning-paths/participate/learning-path-history-storage.service'; + +@Component({ + selector: 'jhi-learning-path-container', + styleUrls: ['./learning-path-container.component.scss'], + templateUrl: './learning-path-container.component.html', +}) +export class LearningPathContainerComponent implements OnInit { + @Input() courseId: number; + learningPathId: number; + + learningObjectId: number; + lectureId?: number; + lecture?: Lecture; + lectureUnit?: LectureUnit; + exercise?: Exercise; + + // icons + faChevronLeft = faChevronLeft; + faChevronRight = faChevronRight; + + constructor( + private router: Router, + private activatedRoute: ActivatedRoute, + private alertService: AlertService, + private learningPathService: LearningPathService, + private lectureService: LectureService, + private exerciseService: ExerciseService, + public learningPathHistoryStorageService: LearningPathHistoryStorageService, + ) {} + + ngOnInit() { + if (!this.courseId) { + this.activatedRoute.parent!.parent!.params.subscribe((params) => { + this.courseId = params['courseId']; + }); + } + this.learningPathService.getLearningPathId(this.courseId).subscribe((learningPathIdResponse) => { + this.learningPathId = learningPathIdResponse.body!; + + // load latest lecture unit or exercise that was accessed + this.onPrevTask(); + }); + } + + onNextTask() { + if (this.lectureUnit?.id) { + this.learningPathHistoryStorageService.storeLectureUnit(this.learningPathId, this.lectureId!, this.lectureUnit.id); + } else if (this.exercise?.id) { + this.learningPathHistoryStorageService.storeExercise(this.learningPathId, this.exercise.id); + } + // reset state to avoid invalid states + this.undefineAll(); + // todo: load recommendation, part of next pr + } + + undefineAll() { + this.lecture = undefined; + this.lectureUnit = undefined; + this.exercise = undefined; + } + + onPrevTask() { + // reset state to avoid invalid states + this.undefineAll(); + if (this.learningPathHistoryStorageService.hasPrevious(this.learningPathId)) { + const entry = this.learningPathHistoryStorageService.getPrevious(this.learningPathId); + if (entry instanceof LectureUnitEntry) { + this.learningObjectId = entry.lectureUnitId; + this.lectureId = entry.lectureId; + this.loadLectureUnit(); + } else if (entry instanceof ExerciseEntry) { + this.learningObjectId = entry.exerciseId; + this.loadExercise(); + } + } + } + + loadLectureUnit() { + this.lectureService.findWithDetails(this.lectureId!).subscribe({ + next: (findLectureResult) => { + this.lecture = findLectureResult.body!; + if (this.lecture?.lectureUnits) { + this.lectureUnit = this.lecture.lectureUnits.find((lectureUnit) => lectureUnit.id === this.learningObjectId); + } + }, + error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), + }); + this.router.navigate(['lecture-unit'], { relativeTo: this.activatedRoute }); + } + + loadExercise() { + this.exerciseService.getExerciseDetails(this.learningObjectId).subscribe({ + next: (exerciseResponse) => { + this.exercise = exerciseResponse.body!; + }, + error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), + }); + this.router.navigate(['exercise'], { relativeTo: this.activatedRoute }); + } + + /** + * This function gets called if the router outlet gets activated. This is + * used only for the LearningPathLectureUnitViewComponent + * @param instance The component instance + */ + onChildActivate(instance: LearningPathLectureUnitViewComponent | CourseExerciseDetailsComponent) { + if (instance instanceof LearningPathLectureUnitViewComponent) { + this.setupLectureUnitView(instance); + } else { + this.setupExerciseView(instance); + } + } + + setupLectureUnitView(instance: LearningPathLectureUnitViewComponent) { + if (this.lecture) { + instance.lecture = this.lecture; + instance.lectureUnit = this.lectureUnit!; + } + } + + setupExerciseView(instance: CourseExerciseDetailsComponent) { + if (this.exercise) { + instance.learningPathMode = true; + instance.courseId = this.courseId; + instance.exerciseId = this.learningObjectId; + } + } + + onNodeClicked(node: NgxLearningPathNode) { + if (node.type !== NodeType.LECTURE_UNIT && node.type !== NodeType.EXERCISE) { + return; + } + if (this.lectureUnit?.id) { + this.learningPathHistoryStorageService.storeLectureUnit(this.learningPathId, this.lectureId!, this.lectureUnit.id); + } else if (this.exercise?.id) { + this.learningPathHistoryStorageService.storeExercise(this.learningPathId, this.exercise.id); + } + // reset state to avoid invalid states + this.undefineAll(); + this.learningObjectId = node.linkedResource!; + this.lectureId = node.linkedResourceParent; + if (node.type === NodeType.LECTURE_UNIT) { + this.loadLectureUnit(); + } else if (node.type === NodeType.EXERCISE) { + this.loadExercise(); + } + } +} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html new file mode 100644 index 000000000000..0c64d339f083 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.html @@ -0,0 +1,36 @@ +
+
+
+ +
+

+ + {{ 'artemisApp.learningPath.sideBar.header' | artemisTranslate }} +

+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ + Learning Path + + +
+
diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss new file mode 100644 index 000000000000..ee0763fe5218 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.scss @@ -0,0 +1,77 @@ +@import 'src/main/webapp/content/scss/artemis-variables'; + +$draggable-width: 15px; +$graph-min-width: 215px; + +.learning-path-sidebar { + .expanded-graph { + display: flex; + width: calc(#{$draggable-width} + #{$graph-min-width}); + min-height: 500px; + margin-left: auto; + + .draggable-right { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: $draggable-width; + } + + .card { + width: inherit; + min-width: $graph-min-width; + + .card-header { + display: inline-flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + + .card-title { + display: flex; + } + + .row > .col-auto:last-child { + display: flex; + flex-direction: column; + justify-content: center; + } + } + + .card-body { + padding: 0; + } + } + } + + .collapsed-graph { + display: flex; + width: 38px; + justify-content: space-between; + flex-flow: column; + cursor: pointer; + + span { + writing-mode: vertical-lr; + transform: rotate(180deg); + margin: auto; + } + + .expand-graph-icon { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + place-self: center; + } + } + + @media screen and (max-width: 992px) { + .expanded-graph { + width: 94vw; + + .draggable-right { + display: none; + } + } + } +} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts new file mode 100644 index 000000000000..c21da727e371 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-graph-sidebar.component.ts @@ -0,0 +1,54 @@ +import { AfterViewInit, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import interact from 'interactjs'; +import { faChevronLeft, faChevronRight, faGripLinesVertical, faNetworkWired } from '@fortawesome/free-solid-svg-icons'; +import { LearningPathGraphComponent } from 'app/course/learning-paths/learning-path-graph/learning-path-graph.component'; +import { NgxLearningPathNode } from 'app/entities/competency/learning-path.model'; + +@Component({ + selector: 'jhi-learning-path-graph-sidebar', + styleUrls: ['./learning-path-graph-sidebar.component.scss'], + templateUrl: './learning-path-graph-sidebar.component.html', +}) +export class LearningPathGraphSidebarComponent implements AfterViewInit { + @Input() courseId: number; + @Input() learningPathId: number; + collapsed: boolean; + + // Icons + faChevronLeft = faChevronLeft; + faChevronRight = faChevronRight; + faGripLinesVertical = faGripLinesVertical; + faNetworkWired = faNetworkWired; + + @ViewChild('learningPathGraphComponent', { static: false }) + learningPathGraphComponent: LearningPathGraphComponent; + + @Output() nodeClicked: EventEmitter = new EventEmitter(); + + ngAfterViewInit(): void { + // allows the sidebar to be resized towards the right-hand side + interact('.expanded-graph') + .resizable({ + edges: { left: false, right: '.draggable-right', bottom: false, top: false }, + modifiers: [ + // Set maximum width of the sidebar + interact.modifiers!.restrictSize({ + min: { width: 230, height: 0 }, + max: { width: 500, height: 4000 }, + }), + ], + inertia: true, + }) + .on('resizestart', (event: any) => { + event.target.classList.add('card-resizable'); + }) + .on('resizeend', (event: any) => { + event.target.classList.remove('card-resizable'); + this.learningPathGraphComponent.onResize(); + }) + .on('resizemove', (event: any) => { + const target = event.target; + target.style.width = event.rect.width + 'px'; + }); + } +} diff --git a/src/main/webapp/app/course/learning-paths/participate/learning-path-history-storage.service.ts b/src/main/webapp/app/course/learning-paths/participate/learning-path-history-storage.service.ts new file mode 100644 index 000000000000..47b75bdd4ccb --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/learning-path-history-storage.service.ts @@ -0,0 +1,86 @@ +import { Injectable } from '@angular/core'; + +/** + * This service is used to store the histories of learning path participation for the currently logged-in user. + */ +@Injectable({ providedIn: 'root' }) +export class LearningPathHistoryStorageService { + private readonly learningPathHistories: Map = new Map(); + + /** + * Stores the lecture unit in the learning path's history. + * + * @param learningPathId the id of the learning path to which the new entry should be added + * @param lectureId the id of the lecture, the lecture unit belongs to + * @param lectureUnitId the id of the lecture unit + */ + storeLectureUnit(learningPathId: number, lectureId: number, lectureUnitId: number) { + this.store(learningPathId, new LectureUnitEntry(lectureId, lectureUnitId)); + } + + /** + * Stores the exercise in the learning path's history. + * + * @param learningPathId the id of the learning path to which the new entry should be added + * @param exerciseId the id of the exercise + */ + storeExercise(learningPathId: number, exerciseId: number) { + this.store(learningPathId, new ExerciseEntry(exerciseId)); + } + + private store(learningPathId: number, entry: HistoryEntry) { + if (!entry) { + return; + } + if (!this.learningPathHistories.has(learningPathId)) { + this.learningPathHistories.set(learningPathId, []); + } + this.learningPathHistories.get(learningPathId)!.push(entry); + } + + /** + * Returns if the learning path's history stores at least one entry. + * + * @param learningPathId the id of the learning path for which the history should be checked + */ + hasPrevious(learningPathId: number): boolean { + if (this.learningPathHistories.has(learningPathId)) { + return this.learningPathHistories.get(learningPathId)!.length !== 0; + } + return false; + } + + /** + * Gets and removes the latest stored entry from the learning path's history. + * + * @param learningPathId + */ + getPrevious(learningPathId: number) { + if (!this.hasPrevious(learningPathId)) { + return undefined; + } + return this.learningPathHistories.get(learningPathId)!.pop(); + } +} + +export abstract class HistoryEntry {} + +export class LectureUnitEntry extends HistoryEntry { + lectureUnitId: number; + lectureId: number; + + constructor(lectureId: number, lectureUnitId: number) { + super(); + this.lectureId = lectureId; + this.lectureUnitId = lectureUnitId; + } +} + +export class ExerciseEntry extends HistoryEntry { + readonly exerciseId: number; + + constructor(exerciseId: number) { + super(); + this.exerciseId = exerciseId; + } +} diff --git a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.html b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.html new file mode 100644 index 000000000000..2bf7446b1d04 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.html @@ -0,0 +1,17 @@ +
+
+
+ + + + +
+
+
+ +
+
diff --git a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.scss b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.scss new file mode 100644 index 000000000000..886feddbee00 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.scss @@ -0,0 +1,3 @@ +.communication-wrapper { + max-width: min-content; +} diff --git a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts new file mode 100644 index 000000000000..1941ce7abfb3 --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts @@ -0,0 +1,59 @@ +import { Component, Input } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { LectureUnit, LectureUnitType } from 'app/entities/lecture-unit/lectureUnit.model'; +import { onError } from 'app/shared/util/global.utils'; +import { Lecture } from 'app/entities/lecture.model'; +import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; +import { AlertService } from 'app/core/util/alert.service'; +import { isCommunicationEnabled, isMessagingEnabled } from 'app/entities/course.model'; +import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; + +export interface LectureUnitCompletionEvent { + lectureUnit: LectureUnit; + completed: boolean; +} + +@Component({ + selector: 'jhi-learning-path-lecture-unit-view', + styleUrls: ['./learning-path-lecture-unit-view.component.scss'], + templateUrl: './learning-path-lecture-unit-view.component.html', +}) +export class LearningPathLectureUnitViewComponent { + @Input() lecture: Lecture; + @Input() lectureUnit: LectureUnit; + readonly LectureUnitType = LectureUnitType; + + discussionComponent?: DiscussionSectionComponent; + + protected readonly isMessagingEnabled = isMessagingEnabled; + protected readonly isCommunicationEnabled = isCommunicationEnabled; + + constructor( + private lectureUnitService: LectureUnitService, + private alertService: AlertService, + ) {} + + completeLectureUnit(event: LectureUnitCompletionEvent): void { + if (this.lecture && event.lectureUnit.visibleToStudents && event.lectureUnit.completed !== event.completed) { + this.lectureUnitService.setCompletion(event.lectureUnit.id!, this.lecture.id!, event.completed).subscribe({ + next: () => { + event.lectureUnit.completed = event.completed; + }, + error: (res: HttpErrorResponse) => onError(this.alertService, res), + }); + } + } + + /** + * This function gets called if the router outlet gets activated. This is + * used only for the DiscussionComponent + * @param instance The component instance + */ + onChildActivate(instance: DiscussionSectionComponent) { + this.discussionComponent = instance; // save the reference to the component instance + if (this.lecture) { + instance.lecture = this.lecture; + instance.isCommunicationPage = false; + } + } +} diff --git a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts new file mode 100644 index 000000000000..33dc55941bda --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { LearningPathLectureUnitViewComponent } from 'app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component'; +import { ArtemisLectureUnitsModule } from 'app/overview/course-lectures/lecture-units.module'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { Authority } from 'app/shared/constants/authority.constants'; +import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; + +const routes: Routes = [ + { + path: '', + component: LearningPathLectureUnitViewComponent, + data: { + authorities: [Authority.USER], + pageTitle: 'overview.learningPath', + }, + canActivate: [UserRouteAccessService], + children: [ + { + path: '', + pathMatch: 'full', + loadChildren: () => import('app/overview/discussion-section/discussion-section.module').then((m) => m.DiscussionSectionModule), + }, + ], + }, +]; + +@NgModule({ + imports: [ArtemisSharedModule, RouterModule.forChild(routes), ArtemisLectureUnitsModule], + declarations: [LearningPathLectureUnitViewComponent], + exports: [LearningPathLectureUnitViewComponent], +}) +export class ArtemisLearningPathLectureUnitViewModule {} diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index 937df274a472..6a54cc3c3f15 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -50,6 +50,18 @@ Competencies + + + Learning Path +
+
+ diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.ts new file mode 100644 index 000000000000..0084bf57ed9c --- /dev/null +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component.ts @@ -0,0 +1,16 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { HealthStatus, getWarningAction, getWarningBody, getWarningHint, getWarningTitle } from 'app/entities/competency/learning-path-health.model'; + +@Component({ + selector: 'jhi-learning-path-health-status-warning', + templateUrl: './learning-path-health-status-warning.component.html', +}) +export class LearningPathHealthStatusWarningComponent { + @Input() status: HealthStatus; + @Output() onButtonClicked: EventEmitter = new EventEmitter(); + + readonly getWarningTitle = getWarningTitle; + readonly getWarningBody = getWarningBody; + readonly getWarningHint = getWarningHint; + readonly getWarningAction = getWarningAction; +} diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html index c6e89d1e7a8c..5542ff44ebf6 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.html @@ -6,7 +6,7 @@

Learning Path Management

-
+
Disabled
-
-
-
{{ 'artemisApp.learningPath.manageLearningPaths.health.missing.title' | artemisTranslate }}
-

{{ 'artemisApp.learningPath.manageLearningPaths.health.missing.body' | artemisTranslate }}

- -
-
-
+ + + + + +
Search for Learning Path: diff --git a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts index 60d0b168c6ec..e5c9d0c19160 100644 --- a/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts +++ b/src/main/webapp/app/course/learning-paths/learning-path-management/learning-path-management.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { Subject } from 'rxjs'; import { LearningPathService } from 'app/course/learning-paths/learning-path.service'; import { debounceTime, finalize, switchMap, tap } from 'rxjs/operators'; @@ -11,7 +11,7 @@ import { LearningPathPagingService } from 'app/course/learning-paths/learning-pa import { SortService } from 'app/shared/service/sort.service'; import { LearningPathPageableSearchDTO } from 'app/entities/competency/learning-path.model'; import { faSort, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; -import { HealthStatus, LearningPathHealthDTO } from 'app/entities/competency/learning-path-health.model'; +import { HealthStatus, LearningPathHealthDTO, getWarningAction, getWarningBody, getWarningHint, getWarningTitle } from 'app/entities/competency/learning-path-health.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { LearningPathProgressModalComponent } from 'app/course/learning-paths/learning-path-management/learning-path-progress-modal.component'; @@ -53,6 +53,7 @@ export class LearningPathManagementComponent implements OnInit { constructor( private activatedRoute: ActivatedRoute, + private router: Router, private learningPathService: LearningPathService, private alertService: AlertService, private pagingService: LearningPathPagingService, @@ -134,7 +135,7 @@ export class LearningPathManagementComponent implements OnInit { .subscribe({ next: (res) => { this.health = res.body!; - if (this.health.status !== HealthStatus.DISABLED) { + if (!this.health.status?.includes(HealthStatus.DISABLED)) { this.performSearch(this.sort, 0); this.performSearch(this.search, 300); } @@ -163,6 +164,10 @@ export class LearningPathManagementComponent implements OnInit { }); } + routeToCompetencyManagement() { + this.router.navigate(['../competency-management'], { relativeTo: this.activatedRoute }); + } + /** * Method to perform the search based on a search subject * @@ -214,4 +219,8 @@ export class LearningPathManagementComponent implements OnInit { } protected readonly HealthStatus = HealthStatus; + protected readonly getWarningTitle = getWarningTitle; + protected readonly getWarningBody = getWarningBody; + protected readonly getWarningAction = getWarningAction; + protected readonly getWarningHint = getWarningHint; } diff --git a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts index 7a125407fa01..862086b32d3e 100644 --- a/src/main/webapp/app/course/learning-paths/learning-paths.module.ts +++ b/src/main/webapp/app/course/learning-paths/learning-paths.module.ts @@ -13,6 +13,7 @@ import { LearningPathGraphNodeComponent } from 'app/course/learning-paths/learni import { CompetencyNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/competency-node-details.component'; import { LectureUnitNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/lecture-unit-node-details.component'; import { ExerciseNodeDetailsComponent } from 'app/course/learning-paths/learning-path-graph/node-details/exercise-node-details.component'; +import { LearningPathHealthStatusWarningComponent } from 'app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component'; import { LearningPathContainerComponent } from 'app/course/learning-paths/participate/learning-path-container.component'; import { Authority } from 'app/shared/constants/authority.constants'; import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; @@ -71,6 +72,7 @@ const routes: Routes = [ ], declarations: [ LearningPathManagementComponent, + LearningPathHealthStatusWarningComponent, LearningPathProgressModalComponent, LearningPathProgressNavComponent, LearningPathGraphComponent, diff --git a/src/main/webapp/app/entities/competency/learning-path-health.model.ts b/src/main/webapp/app/entities/competency/learning-path-health.model.ts index bf3a9794d178..1cbcb13ba367 100644 --- a/src/main/webapp/app/entities/competency/learning-path-health.model.ts +++ b/src/main/webapp/app/entities/competency/learning-path-health.model.ts @@ -1,8 +1,8 @@ export class LearningPathHealthDTO { - public status?: HealthStatus; + public status?: HealthStatus[]; public missingLearningPaths?: number; - constructor(status: HealthStatus) { + constructor(status: HealthStatus[]) { this.status = status; } } @@ -11,4 +11,51 @@ export enum HealthStatus { OK = 'OK', DISABLED = 'DISABLED', MISSING = 'MISSING', + NO_COMPETENCIES = 'NO_COMPETENCIES', + NO_RELATIONS = 'NO_RELATIONS', +} + +function getWarningTranslation(status: HealthStatus, element: string) { + if (!status || status === HealthStatus.OK || status === HealthStatus.DISABLED) { + return ''; + } + + const translation = { + [HealthStatus.MISSING]: 'missing', + [HealthStatus.NO_COMPETENCIES]: 'noCompetencies', + [HealthStatus.NO_RELATIONS]: 'noRelations', + }; + return `artemisApp.learningPath.manageLearningPaths.health.${translation[status]}.${element}`; +} + +export function getWarningTitle(status: HealthStatus) { + if (!status || status === HealthStatus.OK || status === HealthStatus.DISABLED) { + return ''; + } + + return getWarningTranslation(status, 'title'); +} + +export function getWarningBody(status: HealthStatus) { + if (!status || status === HealthStatus.OK || status === HealthStatus.DISABLED) { + return ''; + } + + return getWarningTranslation(status, 'body'); +} + +export function getWarningAction(status: HealthStatus) { + if (!status || status === HealthStatus.OK || status === HealthStatus.DISABLED) { + return ''; + } + + return getWarningTranslation(status, 'action'); +} + +export function getWarningHint(status: HealthStatus) { + if (!status || status === HealthStatus.OK || status === HealthStatus.DISABLED) { + return ''; + } + + return getWarningTranslation(status, 'hint'); } diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index d5ec0d5c3ecc..2b52dae6d2d2 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -163,6 +163,18 @@ "body": "Für einige Studierende wurde noch kein Lernpfad erstellt. Dies ist nicht kritisch. Ihre Lernpfade werden generiert, wenn sie ihren Lernpfad das erste mal anfragen.", "action": "Erstellen", "hint": "Erstellen der fehlenden Lernpfade" + }, + "noCompetencies": { + "title": "Keine Kompetenzen", + "body": "Es wurden noch keine Kompetenzen erstellt. Lernpfade setzen sich aus den Kompetenzen zusammen, die Studierende erreichen müssen. Gehe zum Kompetenzmanagement um neue Kompetenzen zu erstellen oder bestehende zu importieren.", + "action": "Kompetenzmanagement", + "hint": "Gehe zum Kompetenzmanagement" + }, + "noRelations": { + "title": "Keine Beziehungen", + "body": "Es wurden noch keine Beziehungen zwischen Kompetenzen konfiguriert. Lernpfade nutzen die Informationen über die Struktur der Lerninhalte um zuverlässig qualitiv hochwertige Empfehlungen für Studierende zu generieren. Gehe zum Kompetenzmanagement um die Beziehungen zwischen Kompetenzen zu konfigurieren.", + "action": "Kompetenzmanagement", + "hint": "Gehe zum Kompetenzmanagement" } }, "isDisabled": "Lernpfade sind für diesen Kurs nicht aktiviert.", diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index b4d3e89115a8..3ec6dc3de02a 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -162,6 +162,18 @@ "body": "Some students have not generated their learning paths yet. This is not critical. Their learning paths will be created once they request their learning path for the first time.", "action": "Generate", "hint": "Generate missing Learning Paths" + }, + "noCompetencies": { + "title": "No Competencies", + "body": "You have not created competencies yet. Learning paths are composed of the competencies students have to fulfill. Go to the competency management tab to create new competencies or import existing ones.", + "action": "Competency Management", + "hint": "Go to competency management" + }, + "noRelations": { + "title": "No Relations", + "body": "You have not configured relations between competencies. Learning paths use this information about the structure of the learning materials to reliably provide high quality recommendations to students. Go to the competency management tab to configure competency relations.", + "action": "Competency Management", + "hint": "Go to competency management" } }, "isDisabled": "Learning Paths are currently disabled for this course.", diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java index 3607539ed02f..50813bc3a4c8 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LearningPathIntegrationTest.java @@ -198,18 +198,21 @@ private void deleteCompetencyRESTCall(Competency competency) throws Exception { @WithMockUser(username = STUDENT_OF_COURSE, roles = "USER") void testAll_asStudent() throws Exception { this.testAllPreAuthorize(); + request.get("/api/courses/" + course.getId() + "/learning-path-id", HttpStatus.CONFLICT, Long.class); } @Test @WithMockUser(username = TUTOR_OF_COURSE, roles = "TA") void testAll_asTutor() throws Exception { this.testAllPreAuthorize(); + request.get("/api/courses/" + course.getId() + "/learning-path-id", HttpStatus.FORBIDDEN, Long.class); } @Test @WithMockUser(username = EDITOR_OF_COURSE, roles = "EDITOR") void testAll_asEditor() throws Exception { this.testAllPreAuthorize(); + request.get("/api/courses/" + course.getId() + "/learning-path-id", HttpStatus.FORBIDDEN, Long.class); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 0783a9e28b63..8be16f8ce24d 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -87,24 +87,50 @@ void setup() { @Test void testHealthStatusDisabled() { var healthStatus = learningPathService.getHealthStatusForCourse(course); - assertThat(healthStatus.status()).isEqualTo(LearningPathHealthDTO.HealthStatus.DISABLED); + assertThat(healthStatus.status()).containsExactly(LearningPathHealthDTO.HealthStatus.DISABLED); + assertThat(healthStatus.missingLearningPaths()).isNull(); } @Test void testHealthStatusOK() { + final var competency1 = competencyUtilService.createCompetency(course); + final var competency2 = competencyUtilService.createCompetency(course); + competencyUtilService.addRelation(competency1, CompetencyRelation.RelationType.MATCHES, competency2); course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); var healthStatus = learningPathService.getHealthStatusForCourse(course); - assertThat(healthStatus.status()).isEqualTo(LearningPathHealthDTO.HealthStatus.OK); + assertThat(healthStatus.status()).containsExactly(LearningPathHealthDTO.HealthStatus.OK); + assertThat(healthStatus.missingLearningPaths()).isNull(); } @Test void testHealthStatusMissing() { + final var competency1 = competencyUtilService.createCompetency(course); + final var competency2 = competencyUtilService.createCompetency(course); + competencyUtilService.addRelation(competency1, CompetencyRelation.RelationType.MATCHES, competency2); course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); userUtilService.addStudent(TEST_PREFIX + "tumuser", TEST_PREFIX + "student1337"); var healthStatus = learningPathService.getHealthStatusForCourse(course); - assertThat(healthStatus.status()).isEqualTo(LearningPathHealthDTO.HealthStatus.MISSING); + assertThat(healthStatus.status()).containsExactly(LearningPathHealthDTO.HealthStatus.MISSING); assertThat(healthStatus.missingLearningPaths()).isEqualTo(1); } + + @Test + void testHealthStatusNoCompetencies() { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + var healthStatus = learningPathService.getHealthStatusForCourse(course); + assertThat(healthStatus.status()).containsExactlyInAnyOrder(LearningPathHealthDTO.HealthStatus.NO_COMPETENCIES, LearningPathHealthDTO.HealthStatus.NO_RELATIONS); + assertThat(healthStatus.missingLearningPaths()).isNull(); + } + + @Test + void testHealthStatusNoRelations() { + competencyUtilService.createCompetency(course); + competencyUtilService.createCompetency(course); + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + var healthStatus = learningPathService.getHealthStatusForCourse(course); + assertThat(healthStatus.status()).containsExactly(LearningPathHealthDTO.HealthStatus.NO_RELATIONS); + assertThat(healthStatus.missingLearningPaths()).isNull(); + } } @Nested diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-health-status-warning.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-health-status-warning.component.spec.ts new file mode 100644 index 000000000000..f5396e32157d --- /dev/null +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-health-status-warning.component.spec.ts @@ -0,0 +1,68 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../test.module'; +import { MockPipe } from 'ng-mocks'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { NgbTooltipMocksModule } from '../../../helpers/mocks/directive/ngbTooltipMocks.module'; +import { LearningPathHealthStatusWarningComponent } from 'app/course/learning-paths/learning-path-management/learning-path-health-status-warning.component'; +import { HealthStatus } from 'app/entities/competency/learning-path-health.model'; +import { MockHasAnyAuthorityDirective } from '../../../helpers/mocks/directive/mock-has-any-authority.directive'; + +describe('LearningPathHealthStatusWarningComponent', () => { + let fixture: ComponentFixture; + let comp: LearningPathHealthStatusWarningComponent; + let getWarningTitleStub: jest.SpyInstance; + let getWarningBodyStub: jest.SpyInstance; + let getWarningActionStub: jest.SpyInstance; + let getWarningHintStub: jest.SpyInstance; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, NgbTooltipMocksModule], + declarations: [LearningPathHealthStatusWarningComponent, MockPipe(ArtemisTranslatePipe), MockHasAnyAuthorityDirective], + providers: [], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(LearningPathHealthStatusWarningComponent); + comp = fixture.componentInstance; + getWarningTitleStub = jest.spyOn(comp, 'getWarningTitle'); + getWarningBodyStub = jest.spyOn(comp, 'getWarningBody'); + getWarningActionStub = jest.spyOn(comp, 'getWarningAction'); + getWarningHintStub = jest.spyOn(comp, 'getWarningHint'); + fixture.detectChanges(); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should create', () => { + expect(fixture).toBeTruthy(); + expect(comp).toBeTruthy(); + }); + + it.each([HealthStatus.MISSING, HealthStatus.NO_COMPETENCIES, HealthStatus.NO_RELATIONS])('should load title', (status: HealthStatus) => { + comp.status = status; + fixture.detectChanges(); + expect(getWarningTitleStub).toHaveBeenCalledWith(status); + }); + + it.each([HealthStatus.MISSING, HealthStatus.NO_COMPETENCIES, HealthStatus.NO_RELATIONS])('should load body', (status: HealthStatus) => { + comp.status = status; + fixture.detectChanges(); + expect(getWarningBodyStub).toHaveBeenCalledWith(status); + }); + + it.each([HealthStatus.MISSING, HealthStatus.NO_COMPETENCIES, HealthStatus.NO_RELATIONS])('should load action', (status: HealthStatus) => { + comp.status = status; + fixture.detectChanges(); + expect(getWarningActionStub).toHaveBeenCalledWith(status); + }); + + it.each([HealthStatus.MISSING, HealthStatus.NO_COMPETENCIES, HealthStatus.NO_RELATIONS])('should load hint', (status: HealthStatus) => { + comp.status = status; + fixture.detectChanges(); + expect(getWarningHintStub).toHaveBeenCalledWith(status); + }); +}); diff --git a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts index bf89f3f55e26..9da130b9851a 100644 --- a/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/management/learning-path-management.component.spec.ts @@ -90,7 +90,7 @@ describe('LearningPathManagementComponent', () => { searchForLearningPathsStub.mockReturnValue(of(searchResult)); enableLearningPathsStub.mockReturnValue(of(new HttpResponse())); generateMissingLearningPathsForCourseStub.mockReturnValue(of(new HttpResponse())); - health = new LearningPathHealthDTO(HealthStatus.OK); + health = new LearningPathHealthDTO([HealthStatus.OK]); getHealthStatusForCourseStub.mockReturnValue(of(new HttpResponse({ body: health }))); }); @@ -122,7 +122,7 @@ describe('LearningPathManagementComponent', () => { })); it('should enable learning paths and load data', fakeAsync(() => { - const healthDisabled = new LearningPathHealthDTO(HealthStatus.DISABLED); + const healthDisabled = new LearningPathHealthDTO([HealthStatus.DISABLED]); getHealthStatusForCourseStub.mockReturnValueOnce(of(new HttpResponse({ body: healthDisabled }))).mockReturnValueOnce(of(new HttpResponse({ body: health }))); fixture.detectChanges(); comp.ngOnInit(); @@ -145,7 +145,7 @@ describe('LearningPathManagementComponent', () => { })); it('should generate missing learning paths and load data', fakeAsync(() => { - const healthMissing = new LearningPathHealthDTO(HealthStatus.MISSING); + const healthMissing = new LearningPathHealthDTO([HealthStatus.MISSING]); getHealthStatusForCourseStub.mockReturnValueOnce(of(new HttpResponse({ body: healthMissing }))).mockReturnValueOnce(of(new HttpResponse({ body: health }))); fixture.detectChanges(); comp.ngOnInit(); From 8cf42cfac127334bf6c8e4b917f04dd678f8e319 Mon Sep 17 00:00:00 2001 From: Jakub Riegel Date: Fri, 29 Sep 2023 14:57:59 +0200 Subject: [PATCH 04/19] Quiz exercises: Do not include ungraded results on scores page (#7252) --- .../StudentParticipationRepository.java | 26 ++++++++++++++----- .../web/rest/ParticipationResource.java | 13 +++++++--- .../ParticipationIntegrationTest.java | 25 ++++++++++++++++++ .../ParticipationUtilService.java | 2 +- 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index ba0402ff8808..4a7d7bdace73 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -184,17 +184,31 @@ Optional findByExerciseIdAndStudentIdAndTestRunWithEagerSu * @return participations for exercise. */ @Query(""" - SELECT DISTINCT p FROM StudentParticipation p - LEFT JOIN FETCH p.results r - LEFT JOIN FETCH r.submission s - LEFT JOIN FETCH p.submissions - WHERE p.exercise.id = :#{#exerciseId} - AND (r.id = (SELECT max(id) FROM p.results) + SELECT DISTINCT p + FROM StudentParticipation p + LEFT JOIN FETCH p.results r + LEFT JOIN FETCH r.submission s + LEFT JOIN FETCH p.submissions + WHERE p.exercise.id = :exerciseId + AND (r.id = (SELECT max(p_r.id) FROM p.results p_r) OR r.assessmentType <> 'AUTOMATIC' OR r IS NULL) """) Set findByExerciseIdWithLatestAndManualResults(@Param("exerciseId") Long exerciseId); + @Query(""" + SELECT DISTINCT p + FROM StudentParticipation p + LEFT JOIN FETCH p.results r + LEFT JOIN FETCH r.submission s + LEFT JOIN FETCH p.submissions + WHERE p.exercise.id = :exerciseId + AND (r.id = (SELECT max(p_r.id) FROM p.results p_r WHERE p_r.rated = true) + OR r.assessmentType <> 'AUTOMATIC' + OR r IS NULL) + """) + Set findByExerciseIdWithLatestAndManualRatedResults(@Param("exerciseId") Long exerciseId); + @Query(""" SELECT DISTINCT p FROM StudentParticipation p LEFT JOIN FETCH p.results r diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java index bef5f75f0b42..d0f41c78f81c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java @@ -26,9 +26,7 @@ import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.config.GuidedTourConfiguration; import de.tum.in.www1.artemis.domain.*; -import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; -import de.tum.in.www1.artemis.domain.enumeration.InitializationState; -import de.tum.in.www1.artemis.domain.enumeration.SubmissionType; +import de.tum.in.www1.artemis.domain.enumeration.*; import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.participation.*; import de.tum.in.www1.artemis.domain.quiz.AbstractQuizSubmission; @@ -516,6 +514,13 @@ public ResponseEntity> updateParticipationDueDates(@P return ResponseEntity.ok().body(updatedParticipations); } + private Set findParticipationWithLatestResults(Exercise exercise) { + if (exercise.getExerciseType() == ExerciseType.QUIZ) { + return studentParticipationRepository.findByExerciseIdWithLatestAndManualRatedResults(exercise.getId()); + } + return studentParticipationRepository.findByExerciseIdWithLatestAndManualResults(exercise.getId()); + } + /** * GET /exercises/:exerciseId/participations : get all the participations for an exercise * @@ -532,7 +537,7 @@ public ResponseEntity> getAllParticipationsForExercise authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.TEACHING_ASSISTANT, exercise, null); Set participations; if (withLatestResults) { - participations = studentParticipationRepository.findByExerciseIdWithLatestAndManualResults(exerciseId); + participations = findParticipationWithLatestResults(exercise); participations.forEach(participation -> { participation.setSubmissionCount(participation.getSubmissions().size()); if (participation.getResults() != null && !participation.getResults().isEmpty()) { diff --git a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java index 4a3a054f64cc..e58780beaa22 100644 --- a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java @@ -644,6 +644,31 @@ void getAllParticipationsForExercise_withLatestResults() throws Exception { assertThat(receivedTestParticipation.getSubmissionCount()).isZero(); } + @Test + @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") + void getAllParticipationsForExercise_withLatestResults_forQuizExercise() throws Exception { + var quizExercise = QuizExerciseFactory.generateQuizExercise(ZonedDateTime.now().minusDays(1), ZonedDateTime.now().plusDays(1), QuizMode.INDIVIDUAL, course); + course.addExercises(quizExercise); + courseRepo.save(course); + exerciseRepo.save(quizExercise); + + var participation = participationUtilService.createAndSaveParticipationForExercise(quizExercise, TEST_PREFIX + "student1"); + var result1 = participationUtilService.createSubmissionAndResult(participation, 42, true); + var notGradedResult = participationUtilService.addResultToParticipation(participation, result1.getSubmission()); + notGradedResult.setRated(false); + resultRepository.save(notGradedResult); + + final var params = new LinkedMultiValueMap(); + params.add("withLatestResults", "true"); + var participations = request.getList("/api/exercises/" + quizExercise.getId() + "/participations", HttpStatus.OK, StudentParticipation.class, params); + + var receivedParticipationWithResult = participations.stream().filter(p -> ((User) p.getParticipant()).getLogin().equals(TEST_PREFIX + "student1")).findFirst() + .orElseThrow(); + assertThat(receivedParticipationWithResult.getResults()).containsOnly(result1); + assertThat(receivedParticipationWithResult.getSubmissions()).isEmpty(); + assertThat(receivedParticipationWithResult.getSubmissionCount()).isEqualTo(1); + } + @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void getAllParticipationsForExercise_withLatestResults_multipleAssessments() throws Exception { diff --git a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationUtilService.java b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationUtilService.java index 9a8f1e9efa4e..9dce0a6ecb22 100644 --- a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationUtilService.java @@ -305,7 +305,7 @@ public Result addResultToParticipation(AssessmentType assessmentType, ZonedDateT } public Result addResultToParticipation(Participation participation, Submission submission) { - Result result = new Result().participation(participation).successful(true).score(100D); + Result result = new Result().participation(participation).successful(true).score(100D).rated(true); result = resultRepo.save(result); result.setSubmission(submission); submission.addResult(result); From bb7b39ff1cc524db6657775e27e16a226f06b44b Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Fri, 29 Sep 2023 17:06:41 +0200 Subject: [PATCH 05/19] Development: Add LTI profile (#7279) --- .../artemis/config/SecurityConfiguration.java | 18 ++++--- .../config/lti/CustomLti13Configurer.java | 2 + .../security/lti/Lti13LaunchFilter.java | 2 + .../security/lti/Lti13TokenRetriever.java | 2 + .../artemis/service/AssessmentService.java | 12 +++-- .../www1/artemis/service/ResultService.java | 15 +++--- .../service/TextAssessmentService.java | 3 +- .../service/connectors/lti/Lti10Service.java | 2 + .../service/connectors/lti/Lti13Service.java | 2 + .../lti/LtiDynamicRegistrationService.java | 2 + .../connectors/lti/LtiNewResultService.java | 2 + .../service/connectors/lti/LtiService.java | 14 ++--- .../ProgrammingAssessmentService.java | 4 +- .../ProgrammingMessagingService.java | 10 ++-- .../www1/artemis/web/rest/LegacyResource.java | 31 +---------- .../in/www1/artemis/web/rest/LtiResource.java | 2 + .../rest/ProgrammingAssessmentResource.java | 8 +-- .../www1/artemis/web/rest/UserResource.java | 7 +-- .../web/rest/open/PublicLtiResource.java | 2 + .../finish/password-reset-finish.component.ts | 2 - .../manage/course-management.component.ts | 52 +++++++++---------- .../manage/course-update.component.html | 2 +- .../course/manage/course-update.component.ts | 16 ++---- .../detail/course-detail.component.html | 14 ++--- .../manage/detail/course-detail.component.ts | 8 +++ ...ingIntegrationBambooBitbucketJiraTest.java | 2 +- ...ringIntegrationGitlabCIGitlabSamlTest.java | 2 +- ...tractSpringIntegrationIndependentTest.java | 2 +- ...actSpringIntegrationJenkinsGitlabTest.java | 2 +- ...ctSpringIntegrationLocalCILocalVCTest.java | 2 +- .../in/www1/artemis/LtiIntegrationTest.java | 8 +-- ...InternalAuthenticationIntegrationTest.java | 29 ----------- .../JiraAuthenticationIntegrationTest.java | 23 +------- .../artemis/connectors/LtiServiceTest.java | 8 ++- .../course/course-update.component.spec.ts | 5 +- 35 files changed, 130 insertions(+), 187 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/config/SecurityConfiguration.java b/src/main/java/de/tum/in/www1/artemis/config/SecurityConfiguration.java index 2ebb088e3689..a1d323f04cdf 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/SecurityConfiguration.java +++ b/src/main/java/de/tum/in/www1/artemis/config/SecurityConfiguration.java @@ -1,9 +1,6 @@ package de.tum.in.www1.artemis.config; -import static de.tum.in.www1.artemis.config.Constants.*; - -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import javax.annotation.PostConstruct; @@ -12,6 +9,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.core.env.Environment; import org.springframework.http.HttpMethod; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; @@ -62,8 +60,11 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Value("#{'${spring.prometheus.monitoringIp:127.0.0.1}'.split(',')}") private List monitoringIpAddresses; + private final Environment env; + public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, UserDetailsService userDetailsService, TokenProvider tokenProvider, - CorsFilter corsFilter, SecurityProblemSupport problemSupport, PasswordService passwordService, Optional remoteUserAuthenticationProvider) { + CorsFilter corsFilter, SecurityProblemSupport problemSupport, PasswordService passwordService, Optional remoteUserAuthenticationProvider, + Environment env) { this.authenticationManagerBuilder = authenticationManagerBuilder; this.userDetailsService = userDetailsService; this.tokenProvider = tokenProvider; @@ -71,6 +72,7 @@ public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerB this.problemSupport = problemSupport; this.passwordService = passwordService; this.remoteUserAuthenticationProvider = remoteUserAuthenticationProvider; + this.env = env; } /** @@ -170,7 +172,6 @@ protected void configure(HttpSecurity http) throws Exception { .antMatchers(HttpMethod.POST, "/api/programming-exercises/new-result").permitAll() .antMatchers(HttpMethod.POST, "/api/programming-submissions/*").permitAll() .antMatchers(HttpMethod.POST, "/api/programming-exercises/test-cases-changed/*").permitAll() - .antMatchers(HttpMethod.POST, "/api/lti/launch/*").permitAll() .antMatchers("/websocket/**").permitAll() .antMatchers("/.well-known/jwks.json").permitAll() .antMatchers("/management/prometheus/**").access(getMonitoringAccessDefinition()) @@ -178,7 +179,10 @@ protected void configure(HttpSecurity http) throws Exception { .and() .apply(securityConfigurerAdapter()); - http.apply(new CustomLti13Configurer()); + Collection activeProfiles = Arrays.asList(env.getActiveProfiles()); + if (activeProfiles.contains("lti")) { + http.apply(new CustomLti13Configurer()); + } // @formatter:on } diff --git a/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java b/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java index 6dfa3b6b57a1..dd4f1de01542 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java +++ b/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java @@ -1,6 +1,7 @@ package de.tum.in.www1.artemis.config.lti; import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Profile; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; @@ -18,6 +19,7 @@ /** * Configures and registers Security Filters to handle LTI 1.3 Resource Link Launches */ +@Profile("lti") public class CustomLti13Configurer extends Lti13Configurer { private static final String LOGIN_PATH = "/auth-login"; diff --git a/src/main/java/de/tum/in/www1/artemis/security/lti/Lti13LaunchFilter.java b/src/main/java/de/tum/in/www1/artemis/security/lti/Lti13LaunchFilter.java index 7a172a5ae87c..c29b4584e56a 100644 --- a/src/main/java/de/tum/in/www1/artemis/security/lti/Lti13LaunchFilter.java +++ b/src/main/java/de/tum/in/www1/artemis/security/lti/Lti13LaunchFilter.java @@ -10,6 +10,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.user.OidcUser; @@ -29,6 +30,7 @@ * Step 3. of OpenID Connect Third Party Initiated Login is handled solely by spring-security-lti13 * OAuth2LoginAuthenticationFilter. */ +@Profile("lti") public class Lti13LaunchFilter extends OncePerRequestFilter { private final OAuth2LoginAuthenticationFilter defaultFilter; diff --git a/src/main/java/de/tum/in/www1/artemis/security/lti/Lti13TokenRetriever.java b/src/main/java/de/tum/in/www1/artemis/security/lti/Lti13TokenRetriever.java index 877705ec36c4..0d015b913ea1 100644 --- a/src/main/java/de/tum/in/www1/artemis/security/lti/Lti13TokenRetriever.java +++ b/src/main/java/de/tum/in/www1/artemis/security/lti/Lti13TokenRetriever.java @@ -7,6 +7,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; import org.springframework.http.*; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.stereotype.Component; @@ -31,6 +32,7 @@ * This class is responsible to retrieve access tokens from an LTI 1.3 platform of a specific ClientRegistration. */ @Component +@Profile("lti") public class Lti13TokenRetriever { private final OAuth2JWKSService oAuth2JWKSService; diff --git a/src/main/java/de/tum/in/www1/artemis/service/AssessmentService.java b/src/main/java/de/tum/in/www1/artemis/service/AssessmentService.java index 39e347927409..6b48589ea2c8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/AssessmentService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/AssessmentService.java @@ -42,12 +42,12 @@ public class AssessmentService { private final SubmissionService submissionService; - private final LtiNewResultService ltiNewResultService; + private final Optional ltiNewResultService; public AssessmentService(ComplaintResponseService complaintResponseService, ComplaintRepository complaintRepository, FeedbackRepository feedbackRepository, ResultRepository resultRepository, StudentParticipationRepository studentParticipationRepository, ResultService resultService, SubmissionService submissionService, SubmissionRepository submissionRepository, ExamDateService examDateService, GradingCriterionRepository gradingCriterionRepository, UserRepository userRepository, - LtiNewResultService ltiNewResultService) { + Optional ltiNewResultService) { this.complaintResponseService = complaintResponseService; this.complaintRepository = complaintRepository; this.feedbackRepository = feedbackRepository; @@ -216,8 +216,12 @@ public Result submitManualAssessment(long resultId, Exercise exercise) { result.setRatedIfNotAfterDueDate(); result.setCompletionDate(ZonedDateTime.now()); result = resultRepository.submitResult(result, exercise, ExerciseDateService.getDueDate(result.getParticipation())); - // Note: we always need to report the result (independent of the assessment due date) over LTI, otherwise it might never become visible in the external system - ltiNewResultService.onNewResult((StudentParticipation) result.getParticipation()); + + if (ltiNewResultService.isPresent()) { + // Note: we always need to report the result (independent of the assessment due date) over LTI, if LTI is configured. + // Otherwise, it might never become visible in the external system + ltiNewResultService.get().onNewResult((StudentParticipation) result.getParticipation()); + } return result; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java index 92a8ecb3f772..e2b5af5545fa 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java @@ -34,7 +34,7 @@ public class ResultService { private final ResultRepository resultRepository; - private final LtiNewResultService ltiNewResultService; + private final Optional ltiNewResultService; private final ResultWebsocketService resultWebsocketService; @@ -60,10 +60,11 @@ public class ResultService { private final StudentExamRepository studentExamRepository; - public ResultService(UserRepository userRepository, ResultRepository resultRepository, LtiNewResultService ltiNewResultService, ResultWebsocketService resultWebsocketService, - ComplaintResponseRepository complaintResponseRepository, RatingRepository ratingRepository, FeedbackRepository feedbackRepository, - ComplaintRepository complaintRepository, ParticipantScoreRepository participantScoreRepository, AuthorizationCheckService authCheckService, - ExerciseDateService exerciseDateService, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository, + public ResultService(UserRepository userRepository, ResultRepository resultRepository, Optional ltiNewResultService, + ResultWebsocketService resultWebsocketService, ComplaintResponseRepository complaintResponseRepository, RatingRepository ratingRepository, + FeedbackRepository feedbackRepository, ComplaintRepository complaintRepository, ParticipantScoreRepository participantScoreRepository, + AuthorizationCheckService authCheckService, ExerciseDateService exerciseDateService, + TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository, SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, StudentExamRepository studentExamRepository) { this.userRepository = userRepository; @@ -110,8 +111,8 @@ public Result createNewManualResult(Result result, boolean ratedResult) { // if it is an example result we do not have any participation (isExampleResult can be also null) if (Boolean.FALSE.equals(savedResult.isExampleResult()) || savedResult.isExampleResult() == null) { - if (savedResult.getParticipation() instanceof ProgrammingExerciseStudentParticipation) { - ltiNewResultService.onNewResult((StudentParticipation) savedResult.getParticipation()); + if (savedResult.getParticipation() instanceof ProgrammingExerciseStudentParticipation && ltiNewResultService.isPresent()) { + ltiNewResultService.get().onNewResult((StudentParticipation) savedResult.getParticipation()); } resultWebsocketService.broadcastNewResult(savedResult.getParticipation(), savedResult); diff --git a/src/main/java/de/tum/in/www1/artemis/service/TextAssessmentService.java b/src/main/java/de/tum/in/www1/artemis/service/TextAssessmentService.java index e544c2eac462..32ecc49d75e0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/TextAssessmentService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/TextAssessmentService.java @@ -3,6 +3,7 @@ import static org.hibernate.Hibernate.isInitialized; import java.util.List; +import java.util.Optional; import javax.annotation.Nullable; @@ -22,7 +23,7 @@ public class TextAssessmentService extends AssessmentService { public TextAssessmentService(UserRepository userRepository, ComplaintResponseService complaintResponseService, ComplaintRepository complaintRepository, FeedbackRepository feedbackRepository, ResultRepository resultRepository, StudentParticipationRepository studentParticipationRepository, ResultService resultService, SubmissionRepository submissionRepository, TextBlockService textBlockService, ExamDateService examDateService, GradingCriterionRepository gradingCriterionRepository, - SubmissionService submissionService, LtiNewResultService ltiNewResultService) { + SubmissionService submissionService, Optional ltiNewResultService) { super(complaintResponseService, complaintRepository, feedbackRepository, resultRepository, studentParticipationRepository, resultService, submissionService, submissionRepository, examDateService, gradingCriterionRepository, userRepository, ltiNewResultService); this.textBlockService = textBlockService; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti10Service.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti10Service.java index 0ec104be151d..6c027df4b19a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti10Service.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti10Service.java @@ -21,6 +21,7 @@ import org.imsglobal.pox.IMSPOXRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.client.HttpClientErrorException; @@ -36,6 +37,7 @@ import oauth.signpost.exception.OAuthException; @Service +@Profile("lti") public class Lti10Service { private final Logger log = LoggerFactory.getLogger(Lti10Service.class); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti13Service.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti13Service.java index 8f3acbc36502..7697abf862d6 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti13Service.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti13Service.java @@ -13,6 +13,7 @@ import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -35,6 +36,7 @@ import net.minidev.json.JSONObject; @Service +@Profile("lti") public class Lti13Service { private static final String EXERCISE_PATH_PATTERN = "/courses/{courseId}/exercises/{exerciseId}"; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDynamicRegistrationService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDynamicRegistrationService.java index 16b3f1bc6d30..e09f5f1357af 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDynamicRegistrationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDynamicRegistrationService.java @@ -5,6 +5,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -22,6 +23,7 @@ import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @Service +@Profile("lti") public class LtiDynamicRegistrationService { @Value("${server.url}") diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiNewResultService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiNewResultService.java index b9cfd6a2aa33..1568c07b084f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiNewResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiNewResultService.java @@ -1,10 +1,12 @@ package de.tum.in.www1.artemis.service.connectors.lti; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; @Service +@Profile("lti") public class LtiNewResultService { private final Lti10Service lti10Service; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiService.java index 8605d20766d6..32ac14e5fcea 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiService.java @@ -11,6 +11,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.security.authentication.InternalAuthenticationServiceException; @@ -38,6 +39,7 @@ import tech.jhipster.security.RandomUtil; @Service +@Profile("lti") public class LtiService { public static final String LTI_GROUP_NAME = "lti"; @@ -95,11 +97,11 @@ public void authenticateLtiUser(String email, String username, String firstName, } } - // 2. Case: Lookup user with the LTI email address and sign in as this user + // 2. Case: Lookup user with the LTI email address and make sure it's not in use final var usernameLookupByEmail = artemisAuthenticationProvider.getUsernameForEmail(email); if (usernameLookupByEmail.isPresent()) { - SecurityContextHolder.getContext().setAuthentication(loginUserByEmail(usernameLookupByEmail.get(), email)); - return; + throw new InternalAuthenticationServiceException( + "Email address is already in use by Artemis. Please use a different address with your service or contact your instructor to gain direct access."); } // 3. Case: Create new user if an existing user is not required @@ -111,12 +113,6 @@ public void authenticateLtiUser(String email, String username, String firstName, throw new InternalAuthenticationServiceException("Could not find existing user or create new LTI user."); // If user couldn't be authenticated, throw an error } - private Authentication loginUserByEmail(String username, String email) { - log.info("Signing in as {}", username); - final var user = artemisAuthenticationProvider.getOrCreateUser(new UsernamePasswordAuthenticationToken(username, ""), null, null, email, true); - return new UsernamePasswordAuthenticationToken(user.getLogin(), user.getPassword(), SIMPLE_USER_LIST_AUTHORITY); - } - @NotNull private Authentication createNewUserFromLaunchRequest(String email, String username, String firstName, String lastName) { final var user = userRepository.findOneByLogin(username).orElseGet(() -> { diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java index 010db24151dd..eb9f1be27c74 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.service.programming; +import java.util.Optional; + import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.*; @@ -15,7 +17,7 @@ public class ProgrammingAssessmentService extends AssessmentService { public ProgrammingAssessmentService(ComplaintResponseService complaintResponseService, ComplaintRepository complaintRepository, FeedbackRepository feedbackRepository, ResultRepository resultRepository, StudentParticipationRepository studentParticipationRepository, ResultService resultService, SubmissionService submissionService, SubmissionRepository submissionRepository, ExamDateService examDateService, UserRepository userRepository, GradingCriterionRepository gradingCriterionRepository, - LtiNewResultService ltiNewResultService) { + Optional ltiNewResultService) { super(complaintResponseService, complaintRepository, feedbackRepository, resultRepository, studentParticipationRepository, resultService, submissionService, submissionRepository, examDateService, gradingCriterionRepository, userRepository, ltiNewResultService); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingMessagingService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingMessagingService.java index 98ff77a0bb21..9e6ed460d85a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingMessagingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingMessagingService.java @@ -2,6 +2,8 @@ import static de.tum.in.www1.artemis.config.Constants.*; +import java.util.Optional; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -26,10 +28,10 @@ public class ProgrammingMessagingService { private final ResultWebsocketService resultWebsocketService; - private final LtiNewResultService ltiNewResultService; + private final Optional ltiNewResultService; public ProgrammingMessagingService(GroupNotificationService groupNotificationService, WebsocketMessagingService websocketMessagingService, - ResultWebsocketService resultWebsocketService, LtiNewResultService ltiNewResultService) { + ResultWebsocketService resultWebsocketService, Optional ltiNewResultService) { this.groupNotificationService = groupNotificationService; this.websocketMessagingService = websocketMessagingService; this.resultWebsocketService = resultWebsocketService; @@ -138,9 +140,9 @@ public void notifyUserAboutNewResult(Result result, ProgrammingExerciseParticipa // notify user via websocket resultWebsocketService.broadcastNewResult((Participation) participation, result); - if (participation instanceof ProgrammingExerciseStudentParticipation studentParticipation) { + if (participation instanceof ProgrammingExerciseStudentParticipation studentParticipation && ltiNewResultService.isPresent()) { // do not try to report results for template or solution participations - ltiNewResultService.onNewResult(studentParticipation); + ltiNewResultService.get().onNewResult(studentParticipation); } } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LegacyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LegacyResource.java index 9659a44855d0..713a55c31bf1 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LegacyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LegacyResource.java @@ -1,10 +1,5 @@ package de.tum.in.www1.artemis.web.rest; -import java.io.IOException; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.springframework.http.ResponseEntity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.web.bind.annotation.*; @@ -12,8 +7,6 @@ import de.tum.in.www1.artemis.config.SecurityConfiguration; import de.tum.in.www1.artemis.security.annotations.EnforceNothing; import de.tum.in.www1.artemis.security.annotations.ManualConfig; -import de.tum.in.www1.artemis.web.rest.dto.LtiLaunchRequestDTO; -import de.tum.in.www1.artemis.web.rest.open.PublicLtiResource; import de.tum.in.www1.artemis.web.rest.open.PublicProgrammingSubmissionResource; import de.tum.in.www1.artemis.web.rest.open.PublicResultResource; @@ -26,37 +19,15 @@ @Deprecated(forRemoval = true) public class LegacyResource { - private final PublicLtiResource publicLtiResource; - private final PublicProgrammingSubmissionResource publicProgrammingSubmissionResource; private final PublicResultResource publicResultResource; - public LegacyResource(PublicLtiResource publicLtiResource, PublicProgrammingSubmissionResource publicProgrammingSubmissionResource, PublicResultResource publicResultResource) { - this.publicLtiResource = publicLtiResource; + public LegacyResource(PublicProgrammingSubmissionResource publicProgrammingSubmissionResource, PublicResultResource publicResultResource) { this.publicProgrammingSubmissionResource = publicProgrammingSubmissionResource; this.publicResultResource = publicResultResource; } - /** - * POST lti/launch/:exerciseId : Launch the exercise app using request by an LTI consumer. Redirects the user to - * the exercise on success. - * - * @param launchRequest the LTI launch request (ExerciseLtiConfigurationDTO) - * @param exerciseId the id of the exercise the user wants to open - * @param request the request - * @param response the response - * @deprecated use {@link PublicLtiResource#launch(LtiLaunchRequestDTO, Long, HttpServletRequest, HttpServletResponse)} instead - */ - @PostMapping("lti/launch/{exerciseId}") - @EnforceNothing - @ManualConfig - @Deprecated(forRemoval = true) - public void legacyLtiLaunch(@ModelAttribute LtiLaunchRequestDTO launchRequest, @PathVariable("exerciseId") Long exerciseId, HttpServletRequest request, - HttpServletResponse response) throws IOException { - publicLtiResource.launch(launchRequest, exerciseId, request, response); - } - /** * Receive a new push notification from the VCS server and save a submission in the database * diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LtiResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LtiResource.java index 99f661058c31..c2d644b36085 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LtiResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LtiResource.java @@ -1,5 +1,6 @@ package de.tum.in.www1.artemis.web.rest; +import org.springframework.context.annotation.Profile; import org.springframework.web.bind.annotation.*; import de.tum.in.www1.artemis.domain.Course; @@ -14,6 +15,7 @@ */ @RestController @RequestMapping("/api") +@Profile("lti") public class LtiResource { private final LtiDynamicRegistrationService ltiDynamicRegistrationService; diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java index 4b34fa6951b8..b6174e2948e2 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java @@ -45,7 +45,7 @@ public class ProgrammingAssessmentResource extends AssessmentResource { private final ProgrammingSubmissionRepository programmingSubmissionRepository; - private final LtiNewResultService ltiNewResultService; + private final Optional ltiNewResultService; private final StudentParticipationRepository studentParticipationRepository; @@ -53,7 +53,7 @@ public class ProgrammingAssessmentResource extends AssessmentResource { public ProgrammingAssessmentResource(AuthorizationCheckService authCheckService, UserRepository userRepository, ProgrammingAssessmentService programmingAssessmentService, ProgrammingSubmissionRepository programmingSubmissionRepository, ExerciseRepository exerciseRepository, ResultRepository resultRepository, ExamService examService, - ResultWebsocketService resultWebsocketService, LtiNewResultService ltiNewResultService, StudentParticipationRepository studentParticipationRepository, + ResultWebsocketService resultWebsocketService, Optional ltiNewResultService, StudentParticipationRepository studentParticipationRepository, ExampleSubmissionRepository exampleSubmissionRepository, SubmissionRepository submissionRepository, SingleUserNotificationService singleUserNotificationService, ProgrammingExerciseParticipationService programmingExerciseParticipationService) { super(authCheckService, userRepository, exerciseRepository, programmingAssessmentService, resultRepository, examService, resultWebsocketService, @@ -202,7 +202,9 @@ public ResponseEntity saveProgrammingAssessment(@PathVariable Long parti newManualResult.getParticipation().filterSensitiveInformation(); } // Note: we always need to report the result over LTI, otherwise it might never become visible in the external system - ltiNewResultService.onNewResult((StudentParticipation) newManualResult.getParticipation()); + if (ltiNewResultService.isPresent()) { + ltiNewResultService.get().onNewResult((StudentParticipation) newManualResult.getParticipation()); + } if (submit && ExerciseDateService.isAfterAssessmentDueDate(programmingExercise)) { resultWebsocketService.broadcastNewResult(newManualResult.getParticipation(), newManualResult); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/UserResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/UserResource.java index ae58d67b2022..72c1be292734 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/UserResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/UserResource.java @@ -2,6 +2,7 @@ import java.time.ZonedDateTime; import java.util.List; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,11 +59,11 @@ public class UserResource { private final UserCreationService userCreationService; - private final LtiService ltiService; + private final Optional ltiService; private final UserRepository userRepository; - public UserResource(UserRepository userRepository, UserService userService, UserCreationService userCreationService, LtiService ltiService) { + public UserResource(UserRepository userRepository, UserService userService, UserCreationService userCreationService, Optional ltiService) { this.userRepository = userRepository; this.userService = userService; this.ltiService = ltiService; @@ -152,7 +153,7 @@ public ResponseEntity initializeUser() { if (user.getActivated()) { return ResponseEntity.ok().body(new UserInitializationDTO()); } - if (!ltiService.isLtiCreatedUser(user) || !user.isInternal()) { + if ((ltiService.isPresent() && !ltiService.get().isLtiCreatedUser(user)) || !user.isInternal()) { user.setActivated(true); userRepository.save(user); return ResponseEntity.ok().body(new UserInitializationDTO()); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicLtiResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicLtiResource.java index aa546ee0de94..0cd72162c321 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicLtiResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicLtiResource.java @@ -9,6 +9,7 @@ import org.glassfish.jersey.uri.UriComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.web.bind.annotation.ModelAttribute; @@ -36,6 +37,7 @@ */ @RestController @RequestMapping("api/public/") +@Profile("lti") public class PublicLtiResource { private final Logger log = LoggerFactory.getLogger(PublicLtiResource.class); diff --git a/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.ts b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.ts index c2220bf609c4..2ff807085f83 100644 --- a/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.ts +++ b/src/main/webapp/app/account/password-reset/finish/password-reset-finish.component.ts @@ -2,7 +2,6 @@ import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angula import { ActivatedRoute } from '@angular/router'; import { PasswordResetFinishService } from './password-reset-finish.service'; -import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH } from 'app/app.constants'; @@ -28,7 +27,6 @@ export class PasswordResetFinishComponent implements OnInit, AfterViewInit { constructor( private passwordResetFinishService: PasswordResetFinishService, private route: ActivatedRoute, - private profileService: ProfileService, private fb: FormBuilder, ) {} diff --git a/src/main/webapp/app/course/manage/course-management.component.ts b/src/main/webapp/app/course/manage/course-management.component.ts index 37b6dfe29abc..d09576e30598 100644 --- a/src/main/webapp/app/course/manage/course-management.component.ts +++ b/src/main/webapp/app/course/manage/course-management.component.ts @@ -7,8 +7,6 @@ import { onError } from 'app/shared/util/global.utils'; import { GuidedTourService } from 'app/guided-tour/guided-tour.service'; import { tutorAssessmentTour } from 'app/guided-tour/tours/tutor-assessment-tour'; import { AlertService } from 'app/core/util/alert.service'; -import { ExamManagementService } from 'app/exam/manage/exam-management.service'; -import { LectureService } from 'app/lecture/lecture.service'; import { CourseManagementOverviewStatisticsDto } from 'app/course/manage/overview/course-management-overview-statistics-dto.model'; import { EventManager } from 'app/core/util/event-manager.service'; import { faAngleDown, faAngleUp, faPlus } from '@fortawesome/free-solid-svg-icons'; @@ -44,14 +42,36 @@ export class CourseManagementComponent implements OnInit, OnDestroy, AfterViewIn faAngleUp = faAngleUp; constructor( - private examService: ExamManagementService, - private lectureService: LectureService, private courseManagementService: CourseManagementService, private alertService: AlertService, private eventManager: EventManager, private guidedTourService: GuidedTourService, ) {} + /** + * loads all courses and subscribes to courseListModification + */ + ngOnInit() { + this.loadAll(); + this.registerChangeInCourses(); + } + + /** + * notifies the guided-tour service that the current component has + * been fully loaded + */ + ngAfterViewInit(): void { + this.guidedTourService.componentPageLoaded(); + } + + /** + * unsubscribe on component destruction + */ + ngOnDestroy() { + this.eventManager.destroy(this.eventSubscriber); + this.dialogErrorSource.unsubscribe(); + } + /** * loads all courses from courseService */ @@ -188,30 +208,6 @@ export class CourseManagementComponent implements OnInit, OnDestroy, AfterViewIn }); } - /** - * loads all courses and subscribes to courseListModification - */ - ngOnInit() { - this.loadAll(); - this.registerChangeInCourses(); - } - - /** - * notifies the guided-tour service that the current component has - * been fully loaded - */ - ngAfterViewInit(): void { - this.guidedTourService.componentPageLoaded(); - } - - /** - * unsubscribe on component destruction - */ - ngOnDestroy() { - this.eventManager.destroy(this.eventSubscriber); - this.dialogErrorSource.unsubscribe(); - } - /** * subscribes to courseListModification event */ diff --git a/src/main/webapp/app/course/manage/course-update.component.html b/src/main/webapp/app/course/manage/course-update.component.html index b943661e4fa1..040fc4f8768b 100644 --- a/src/main/webapp/app/course/manage/course-update.component.html +++ b/src/main/webapp/app/course/manage/course-update.component.html @@ -363,7 +363,7 @@
-
+
Course Details:{{ 'global.generic.yes' | artemisTranslate }} {{ 'global.generic.no' | artemisTranslate }} -
Online Course
-
- {{ 'global.generic.yes' | artemisTranslate }} - {{ 'global.generic.no' | artemisTranslate }} -
- + +
Online Course
+
+ {{ 'global.generic.yes' | artemisTranslate }} + {{ 'global.generic.no' | artemisTranslate }} +
+
+
LTI Configuration
diff --git a/src/main/webapp/app/course/manage/detail/course-detail.component.ts b/src/main/webapp/app/course/manage/detail/course-detail.component.ts index 56c3114f5763..a7f81434d675 100644 --- a/src/main/webapp/app/course/manage/detail/course-detail.component.ts +++ b/src/main/webapp/app/course/manage/detail/course-detail.component.ts @@ -1,6 +1,8 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { PROFILE_LOCALVC } from 'app/app.constants'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { Subscription } from 'rxjs'; import { Course } from 'app/entities/course.model'; import { CourseManagementService } from '../course-management.service'; @@ -34,6 +36,8 @@ export class CourseDetailComponent implements OnInit, OnDestroy { activeStudents?: number[]; course: Course; + ltiEnabled = false; + private eventSubscriber: Subscription; paramSub: Subscription; @@ -52,12 +56,16 @@ export class CourseDetailComponent implements OnInit, OnDestroy { private courseManagementService: CourseManagementService, private route: ActivatedRoute, private alertService: AlertService, + private profileService: ProfileService, ) {} /** * On init load the course information and subscribe to listen for changes in courses. */ ngOnInit() { + this.profileService.getProfileInfo().subscribe((profileInfo) => { + this.ltiEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); + }); this.route.data.subscribe(({ course }) => { if (course) { this.course = course; diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationBambooBitbucketJiraTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationBambooBitbucketJiraTest.java index b5d0a0e03fcf..f41ce4242b01 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationBambooBitbucketJiraTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationBambooBitbucketJiraTest.java @@ -64,7 +64,7 @@ @ResourceLock("AbstractSpringIntegrationBambooBitbucketJiraTest") @AutoConfigureEmbeddedDatabase // NOTE: we use a common set of active profiles to reduce the number of application launches during testing. This significantly saves time and memory! -@ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "bamboo", "bitbucket", "jira", "ldap", "scheduling", "athena", "apollon", "iris" }) +@ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "bamboo", "bitbucket", "jira", "ldap", "scheduling", "athena", "apollon", "iris", "lti" }) public abstract class AbstractSpringIntegrationBambooBitbucketJiraTest extends AbstractArtemisIntegrationTest { @SpyBean diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationGitlabCIGitlabSamlTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationGitlabCIGitlabSamlTest.java index 9203c5e0ac25..8a4fcb55bc15 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationGitlabCIGitlabSamlTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationGitlabCIGitlabSamlTest.java @@ -49,7 +49,7 @@ @ResourceLock("AbstractSpringIntegrationGitlabCIGitlabSamlTest") @AutoConfigureEmbeddedDatabase // NOTE: we use a common set of active profiles to reduce the number of application launches during testing. This significantly saves time and memory! -@ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "gitlabci", "gitlab", "saml2", "scheduling" }) +@ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "gitlabci", "gitlab", "saml2", "scheduling", "lti" }) @TestPropertySource(properties = { "artemis.user-management.use-external=false" }) public abstract class AbstractSpringIntegrationGitlabCIGitlabSamlTest extends AbstractArtemisIntegrationTest { diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationIndependentTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationIndependentTest.java index f1e0b8343697..a24c014dafcb 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationIndependentTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationIndependentTest.java @@ -35,7 +35,7 @@ @ResourceLock("AbstractSpringIntegrationIndependentTest") @AutoConfigureEmbeddedDatabase // NOTE: we use a common set of active profiles to reduce the number of application launches during testing. This significantly saves time and memory! -@ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "scheduling" }) +@ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "scheduling", "lti" }) @TestPropertySource(properties = { "artemis.user-management.use-external=false" }) public abstract class AbstractSpringIntegrationIndependentTest extends AbstractArtemisIntegrationTest { diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationJenkinsGitlabTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationJenkinsGitlabTest.java index e1b6e7a24cf5..7bf2730901a2 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationJenkinsGitlabTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationJenkinsGitlabTest.java @@ -47,7 +47,7 @@ @ResourceLock("AbstractSpringIntegrationJenkinsGitlabTest") @AutoConfigureEmbeddedDatabase // NOTE: we use a common set of active profiles to reduce the number of application launches during testing. This significantly saves time and memory! -@ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "gitlab", "jenkins", "athena", "scheduling" }) +@ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "gitlab", "jenkins", "athena", "scheduling", "lti" }) @TestPropertySource(properties = { "info.guided-tour.course-group-tutors=artemis-artemistutorial-tutors", "info.guided-tour.course-group-students=artemis-artemistutorial-students", "info.guided-tour.course-group-editors=artemis-artemistutorial-editors", "info.guided-tour.course-group-instructors=artemis-artemistutorial-instructors", "artemis.user-management.use-external=false", "artemis.user-management.course-enrollment.allowed-username-pattern=^(?!authorizationservicestudent2).*$" }) diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java index b46c528daa67..cc81cb138b88 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java @@ -62,7 +62,7 @@ @ResourceLock("AbstractSpringIntegrationLocalCILocalVCTest") @AutoConfigureEmbeddedDatabase // NOTE: we use a common set of active profiles to reduce the number of application launches during testing. This significantly saves time and memory! -@ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "localci", "localvc", "scheduling", "ldap-only" }) +@ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "localci", "localvc", "scheduling", "ldap-only", "lti" }) // Note: the server.port property must correspond to the port used in the artemis.version-control.url property. @TestPropertySource(properties = { "server.port=49152", "artemis.version-control.url=http://localhost:49152", "artemis.version-control.local-vcs-repo-path=${java.io.tmpdir}", "artemis.continuous-integration.thread-pool-size=1", "artemis.continuous-integration.asynchronous=false", diff --git a/src/test/java/de/tum/in/www1/artemis/LtiIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/LtiIntegrationTest.java index 83f74b5025da..d15b84a2b3c3 100644 --- a/src/test/java/de/tum/in/www1/artemis/LtiIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/LtiIntegrationTest.java @@ -230,12 +230,8 @@ void launchAsAnonymousUser_WithExistingEmail(String requestBody) throws Exceptio addJiraMocks(email, TEST_PREFIX + "student1"); Long exerciseId = programmingExercise.getId(); - Long courseId = programmingExercise.getCourseViaExerciseGroupOrCourseMember().getId(); - URI header = request.post("/api/public/lti/launch/" + exerciseId, requestBody, HttpStatus.FOUND, MediaType.APPLICATION_FORM_URLENCODED, false); - - var uriComponents = UriComponentsBuilder.fromUri(header).build(); - assertParametersExistingStudent(UriComponentsBuilder.fromUri(header).build().getQueryParams()); - assertThat(uriComponents.getPathSegments()).containsSequence("courses", courseId.toString(), "exercises", exerciseId.toString()); + request.postWithoutLocation("/api/public/lti/launch/" + exerciseId, requestBody.getBytes(), HttpStatus.BAD_REQUEST, new HttpHeaders(), + MediaType.APPLICATION_FORM_URLENCODED_VALUE); } @ParameterizedTest diff --git a/src/test/java/de/tum/in/www1/artemis/authentication/InternalAuthenticationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/authentication/InternalAuthenticationIntegrationTest.java index cb5ca02e6cbc..415bdd0a9041 100644 --- a/src/test/java/de/tum/in/www1/artemis/authentication/InternalAuthenticationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/authentication/InternalAuthenticationIntegrationTest.java @@ -22,7 +22,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; @@ -151,34 +150,6 @@ void teardown() { } } - @Test - void launchLtiRequest_authViaEmail_success() throws Exception { - request.postForm("/api/public/lti/launch/" + programmingExercise.getId(), ltiLaunchRequest, HttpStatus.FOUND); - - final var user = userUtilService.getUserByLogin(USERNAME); - final var ltiOutcome = ltiOutcomeUrlRepository.findByUserAndExercise(user, programmingExercise).orElseThrow(); - - assertThat(ltiOutcome.getUser()).isEqualTo(user); - assertThat(ltiOutcome.getExercise()).isEqualTo(programmingExercise); - assertThat(ltiOutcome.getUrl()).isEqualTo(ltiLaunchRequest.getLis_outcome_service_url()); - assertThat(ltiOutcome.getSourcedId()).isEqualTo(ltiLaunchRequest.getLis_result_sourcedid()); - - final var updatedStudent = userRepository.findOneWithGroupsAndAuthoritiesByLogin(USERNAME).orElseThrow(); - assertThat(student).isEqualTo(updatedStudent); - } - - @Test - @WithAnonymousUser - void authenticateAfterLtiRequest_success() throws Exception { - launchLtiRequest_authViaEmail_success(); - - final var auth = new TestingAuthenticationToken(student.getLogin(), USER_PASSWORD); - final var authResponse = artemisInternalAuthenticationProvider.authenticate(auth); - - assertThat(authResponse.getCredentials()).hasToString(student.getPassword()); - assertThat(authResponse.getPrincipal()).isEqualTo(student.getLogin()); - } - @Test @WithMockUser(username = "ab12cde") void registerForCourse_internalAuth_success() throws Exception { diff --git a/src/test/java/de/tum/in/www1/artemis/authentication/JiraAuthenticationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/authentication/JiraAuthenticationIntegrationTest.java index 63aba99f091c..6de559dfb495 100644 --- a/src/test/java/de/tum/in/www1/artemis/authentication/JiraAuthenticationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/authentication/JiraAuthenticationIntegrationTest.java @@ -22,7 +22,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.test.context.support.WithAnonymousUser; import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; @@ -135,27 +134,7 @@ void launchLtiRequest_authViaEmail_success(String launchEmail) throws Exception jiraRequestMockProvider.mockAddUserToGroupForMultipleGroups(Set.of(course.getStudentGroupName())); jiraRequestMockProvider.mockGetOrCreateUserLti(username, "", username, email, firstName, groups); - request.postForm("/api/public/lti/launch/" + programmingExercise.getId(), ltiLaunchRequest, HttpStatus.FOUND); - final var user = userRepository.findOneByLogin(username).orElseThrow(); - final var ltiOutcome = ltiOutcomeUrlRepository.findByUserAndExercise(user, programmingExercise).orElseThrow(); - - assertThat(ltiOutcome.getUser()).isEqualTo(user); - assertThat(ltiOutcome.getExercise()).isEqualTo(programmingExercise); - assertThat(ltiOutcome.getUrl()).isEqualTo(ltiLaunchRequest.getLis_outcome_service_url()); - assertThat(ltiOutcome.getSourcedId()).isEqualTo(ltiLaunchRequest.getLis_result_sourcedid()); - - final var mrrobotUser = userRepository.findOneWithGroupsAndAuthoritiesByLogin(username).orElseThrow(); - assertThat(mrrobotUser.getEmail()).isEqualTo(email); - assertThat(mrrobotUser.getFirstName()).isEqualTo(firstName); - assertThat(mrrobotUser.getGroups()).containsAll(groups); - assertThat(mrrobotUser.getGroups()).contains(course.getStudentGroupName()); - assertThat(mrrobotUser.getAuthorities()).containsAll(authorityRepository.findAll()); - - final var auth = new TestingAuthenticationToken(username, ""); - final var responseAuth = jiraAuthenticationProvider.authenticate(auth); - - assertThat(responseAuth.getPrincipal()).isEqualTo(username); - assertThat(responseAuth.getCredentials()).isEqualTo(mrrobotUser.getPassword()); + request.postFormWithoutLocation("/api/public/lti/launch/" + programmingExercise.getId(), ltiLaunchRequest, HttpStatus.BAD_REQUEST); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java b/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java index dd5d80795a2d..9fb564a7cee6 100644 --- a/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java @@ -204,11 +204,9 @@ void authenticateLtiUser_newUser() { when(artemisAuthenticationProvider.getUsernameForEmail("email")).thenReturn(Optional.of("username")); when(artemisAuthenticationProvider.getOrCreateUser(any(), any(), any(), any(), anyBoolean())).thenReturn(user); - ltiService.authenticateLtiUser("email", "username", "firstname", "lastname", onlineCourseConfiguration.isRequireExistingUser()); - - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - assertThat(auth.getPrincipal()).isEqualTo(user.getLogin()); - assertThat(user.getGroups()).contains(LtiService.LTI_GROUP_NAME); + assertThatExceptionOfType(InternalAuthenticationServiceException.class) + .isThrownBy(() -> ltiService.authenticateLtiUser("email", "username", "firstname", "lastname", onlineCourseConfiguration.isRequireExistingUser())) + .withMessageContaining("already in use"); } @Test diff --git a/src/test/javascript/spec/component/course/course-update.component.spec.ts b/src/test/javascript/spec/component/course/course-update.component.spec.ts index cee5192b8d4b..885ceaaa8b05 100644 --- a/src/test/javascript/spec/component/course/course-update.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-update.component.spec.ts @@ -1,5 +1,5 @@ import { HttpResponse } from '@angular/common/http'; -import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, flush, tick } from '@angular/core/testing'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { NgbTooltipModule, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; @@ -142,7 +142,7 @@ describe('Course Management Update Component', () => { describe('ngOnInit', () => { it('should get course, profile and fill the form', fakeAsync(() => { - const profileInfo = { inProduction: false } as ProfileInfo; + const profileInfo = { inProduction: false, activeProfiles: ['lti'] } as ProfileInfo; const profileInfoSubject = new BehaviorSubject(profileInfo).asObservable(); const getProfileStub = jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(profileInfoSubject); const organization = new Organization(); @@ -188,6 +188,7 @@ describe('Course Management Update Component', () => { expect(comp.courseForm.get(['color'])?.value).toBe(course.color); expect(comp.courseForm.get(['courseIcon'])?.value).toBe(course.courseIcon); expect(comp.courseForm.get(['learningPathsEnabled'])?.value).toBe(course.learningPathsEnabled); + flush(); })); }); From 65399c3b30db2141af404b2d59514d5d7cd37e21 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Sat, 30 Sep 2023 09:08:42 +0200 Subject: [PATCH 06/19] Development: Remove online course from e2e tests --- src/test/cypress/e2e/course/CourseManagement.cy.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/test/cypress/e2e/course/CourseManagement.cy.ts b/src/test/cypress/e2e/course/CourseManagement.cy.ts index e1ec4060a55b..7ab1665dbf9a 100644 --- a/src/test/cypress/e2e/course/CourseManagement.cy.ts +++ b/src/test/cypress/e2e/course/CourseManagement.cy.ts @@ -29,7 +29,6 @@ const courseData = { maxComplaintTimeDays: 6, enableMoreFeedback: true, maxRequestMoreFeedbackTimeDays: 4, - onlineCourse: true, presentationScoreEnabled: true, presentationScore: 10, }; @@ -109,7 +108,6 @@ describe('Course management', () => { courseCreation.setMaxComplaintsTimeDays(courseData.maxComplaintTimeDays); courseCreation.setEnableMoreFeedback(courseData.enableMoreFeedback); courseCreation.setMaxRequestMoreFeedbackTimeDays(courseData.maxRequestMoreFeedbackTimeDays); - courseCreation.setOnlineCourse(courseData.onlineCourse); courseCreation.setCustomizeGroupNames(courseData.customizeGroupNames); courseCreation.submit().then((request: Interception) => { const courseBody = request.response!.body; @@ -128,7 +126,6 @@ describe('Course management', () => { expect(courseBody.maxTeamComplaints).to.eq(courseData.maxTeamComplaints); expect(courseBody.maxComplaintTimeDays).to.eq(courseData.maxComplaintTimeDays); expect(courseBody.requestMoreFeedbackEnabled).to.eq(courseData.enableMoreFeedback); - expect(courseBody.onlineCourse).to.eq(courseData.onlineCourse); expect(courseBody.studentGroupName).to.eq(`artemis-${courseData.shortName}-students`); expect(courseBody.editorGroupName).to.eq(`artemis-${courseData.shortName}-editors`); expect(courseBody.instructorGroupName).to.eq(`artemis-${courseData.shortName}-instructors`); @@ -147,7 +144,6 @@ describe('Course management', () => { courseManagement.getCourseSemester().contains(courseData.semester); courseManagement.getCourseProgrammingLanguage().contains(courseData.programmingLanguage); courseManagement.getCourseTestCourse().contains(convertBooleanToYesNo(courseData.testCourse)); - courseManagement.getCourseOnlineCourse().contains(convertBooleanToYesNo(courseData.onlineCourse)); courseManagement.getCourseMaxComplaints().contains(courseData.maxComplaints); courseManagement.getCourseMaxTeamComplaints().contains(courseData.maxTeamComplaints); courseManagement.getMaxComplaintTimeDays().contains(courseData.maxComplaintTimeDays); From 7fa6f3284ee761c30ccf106127e47135ecb921d3 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Sat, 30 Sep 2023 09:10:40 +0200 Subject: [PATCH 07/19] Development: Remove old and unused tests --- package.json | 1 - src/test/gatling/conf/gatling.conf | 132 ------ src/test/gatling/conf/logback.xml | 24 - .../simulations/GetCoursesSimulation.scala | 83 ---- .../LtiOutcomeUrlGatlingTest.scala | 90 ---- .../QuizParticipationSimulation.scala | 212 --------- .../resources/modeling_submission_1.txt | 1 - .../AnswerCounterGatlingTest.scala | 96 ---- .../simulations/AnswerOptionGatlingTest.scala | 101 ---- .../ApollonDiagramGatlingTest.scala | 98 ---- .../simulations/ConflictSimulation.scala | 128 ----- .../simulations/CourseGatlingTest.scala | 103 ---- .../DragAndDropMappingGatlingTest.scala | 99 ---- .../DragAndDropQuestionGatlingTest.scala | 97 ---- ...gAndDropQuestionStatisticGatlingTest.scala | 96 ---- ...ragAndDropSubmittedAnswerGatlingTest.scala | 96 ---- .../simulations/DragItemGatlingTest.scala | 99 ---- .../DropLocationCounterGatlingTest.scala | 96 ---- .../simulations/DropLocationGatlingTest.scala | 101 ---- .../simulations/ExerciseGatlingTest.scala | 104 ----- .../simulations/ExerciseHintGatlingTest.scala | 98 ---- .../ExerciseResultGatlingTest.scala | 102 ---- .../simulations/FeedbackGatlingTest.scala | 100 ---- .../FileUploadExerciseGatlingTest.scala | 97 ---- .../FileUploadSubmissionGatlingTest.scala | 97 ---- .../LtiOutcomeUrlGatlingTest.scala | 98 ---- .../ModelingExerciseGatlingTest.scala | 99 ---- .../ModelingSubmissionGatlingTest.scala | 98 ---- .../MultipleChoiceQuestionGatlingTest.scala | 96 ---- ...leChoiceQuestionStatisticGatlingTest.scala | 96 ---- ...ipleChoiceSubmittedAnswerGatlingTest.scala | 96 ---- .../ParticipationGatlingTest.scala | 101 ---- .../simulations/PointCounterGatlingTest.scala | 97 ---- .../ProgrammingExerciseGatlingTest.scala | 101 ---- ...ogrammingExerciseTestCaseGatlingTest.scala | 99 ---- .../ProgrammingSubmissionGatlingTest.scala | 97 ---- .../simulations/QuestionGatlingTest.scala | 104 ----- .../QuestionStatisticGatlingTest.scala | 98 ---- .../simulations/QuizExerciseGatlingTest.scala | 104 ----- .../QuizPointStatisticGatlingTest.scala | 96 ---- .../QuizSubmissionGatlingTest.scala | 97 ---- .../ShortAnswerMappingGatlingTest.scala | 99 ---- .../ShortAnswerQuestionGatlingTest.scala | 96 ---- ...rtAnswerQuestionStatisticGatlingTest.scala | 96 ---- .../ShortAnswerSolutionGatlingTest.scala | 98 ---- .../ShortAnswerSpotCounterGatlingTest.scala | 96 ---- .../ShortAnswerSpotGatlingTest.scala | 98 ---- ...hortAnswerSubmittedAnswerGatlingTest.scala | 96 ---- .../ShortAnswerSubmittedTextGatlingTest.scala | 97 ---- .../StatisticCounterGatlingTest.scala | 98 ---- .../simulations/StatisticGatlingTest.scala | 99 ---- .../simulations/SubmissionGatlingTest.scala | 99 ---- .../SubmittedAnswerGatlingTest.scala | 97 ---- .../simulations/TextExerciseGatlingTest.scala | 97 ---- .../TextSubmissionGatlingTest.scala | 97 ---- src/test/k6/CodeEditor.js | 79 ---- src/test/k6/ExamAPIs.js | 263 ----------- src/test/k6/ModelingExerciseAPIs.js | 164 ------- src/test/k6/ProgrammingExerciseAPIs.js | 164 ------- src/test/k6/QuizExerciseAPIs.js | 110 ----- src/test/k6/TextExerciseAPIs.js | 154 ------ src/test/k6/api_tests.sh | 135 ------ src/test/k6/requests/course.js | 96 ---- src/test/k6/requests/endpoints.js | 46 -- src/test/k6/requests/exam.js | 122 ----- src/test/k6/requests/exercises.js | 61 --- src/test/k6/requests/modeling.js | 101 ---- src/test/k6/requests/programmingExercise.js | 236 ---------- src/test/k6/requests/quiz.js | 267 ----------- src/test/k6/requests/requests.js | 168 ------- src/test/k6/requests/text.js | 126 ----- src/test/k6/requests/user.js | 106 ----- src/test/k6/resource/constants_c.js | 63 --- src/test/k6/resource/constants_java.js | 441 ------------------ src/test/k6/resource/constants_python.js | 296 ------------ src/test/k6/util/utils.js | 41 -- 76 files changed, 8530 deletions(-) delete mode 100644 src/test/gatling/conf/gatling.conf delete mode 100644 src/test/gatling/conf/logback.xml delete mode 100644 src/test/gatling/simulations/GetCoursesSimulation.scala delete mode 100644 src/test/gatling/simulations/LtiOutcomeUrlGatlingTest.scala delete mode 100644 src/test/gatling/simulations/QuizParticipationSimulation.scala delete mode 100644 src/test/gatling/user-files/resources/modeling_submission_1.txt delete mode 100644 src/test/gatling/user-files/simulations/AnswerCounterGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/AnswerOptionGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ApollonDiagramGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ConflictSimulation.scala delete mode 100644 src/test/gatling/user-files/simulations/CourseGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/DragAndDropMappingGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/DragAndDropQuestionGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/DragAndDropQuestionStatisticGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/DragAndDropSubmittedAnswerGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/DragItemGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/DropLocationCounterGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/DropLocationGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ExerciseGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ExerciseHintGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ExerciseResultGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/FeedbackGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/FileUploadExerciseGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/FileUploadSubmissionGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/LtiOutcomeUrlGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ModelingExerciseGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ModelingSubmissionGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/MultipleChoiceQuestionGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/MultipleChoiceQuestionStatisticGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/MultipleChoiceSubmittedAnswerGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ParticipationGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/PointCounterGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ProgrammingExerciseGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ProgrammingExerciseTestCaseGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ProgrammingSubmissionGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/QuestionGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/QuestionStatisticGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/QuizExerciseGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/QuizPointStatisticGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/QuizSubmissionGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ShortAnswerMappingGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ShortAnswerQuestionGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ShortAnswerQuestionStatisticGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ShortAnswerSolutionGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ShortAnswerSpotCounterGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ShortAnswerSpotGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ShortAnswerSubmittedAnswerGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/ShortAnswerSubmittedTextGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/StatisticCounterGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/StatisticGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/SubmissionGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/SubmittedAnswerGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/TextExerciseGatlingTest.scala delete mode 100644 src/test/gatling/user-files/simulations/TextSubmissionGatlingTest.scala delete mode 100644 src/test/k6/CodeEditor.js delete mode 100644 src/test/k6/ExamAPIs.js delete mode 100644 src/test/k6/ModelingExerciseAPIs.js delete mode 100644 src/test/k6/ProgrammingExerciseAPIs.js delete mode 100644 src/test/k6/QuizExerciseAPIs.js delete mode 100644 src/test/k6/TextExerciseAPIs.js delete mode 100755 src/test/k6/api_tests.sh delete mode 100644 src/test/k6/requests/course.js delete mode 100644 src/test/k6/requests/endpoints.js delete mode 100644 src/test/k6/requests/exam.js delete mode 100644 src/test/k6/requests/exercises.js delete mode 100644 src/test/k6/requests/modeling.js delete mode 100644 src/test/k6/requests/programmingExercise.js delete mode 100644 src/test/k6/requests/quiz.js delete mode 100644 src/test/k6/requests/requests.js delete mode 100644 src/test/k6/requests/text.js delete mode 100644 src/test/k6/requests/user.js delete mode 100644 src/test/k6/resource/constants_c.js delete mode 100644 src/test/k6/resource/constants_java.js delete mode 100644 src/test/k6/resource/constants_python.js delete mode 100644 src/test/k6/util/utils.js diff --git a/package.json b/package.json index 00573d87d6c0..486e88a2608a 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,6 @@ "test-diff:ci": "git fetch origin develop && npm run prebuild && ng test --log-heap-usage -w=4 --ci --reporters=default --reporters=jest-junit --pass-with-no-tests --changed-since=origin/develop", "test:leaks": "npm run prebuild && ng test --log-heap-usage --detect-leaks", "test:open-handles": "npm run prebuild && ng test --detect-open-handles", - "test:server-api-tests": "`pwd`/src/test/k6/api_tests.sh", "test:watch": "npm run prebuild && npm run test -- --watch", "webapp:build": "npm run clean-www && npm run webapp:build:dev", "webapp:build:dev": "npm run prebuild -- --develop && ng build --configuration development", diff --git a/src/test/gatling/conf/gatling.conf b/src/test/gatling/conf/gatling.conf deleted file mode 100644 index 6916e8b7ee02..000000000000 --- a/src/test/gatling/conf/gatling.conf +++ /dev/null @@ -1,132 +0,0 @@ -######################### -# Gatling Configuration # -######################### - -# This file contains all the settings configurable for Gatling with their default values - -gatling { - core { - #outputDirectoryBaseName = "" # The prefix for each simulation result folder (then suffixed by the report generation timestamp) - #runDescription = "" # The description for this simulation run, displayed in each report - #encoding = "utf-8" # Encoding to use throughout Gatling for file and string manipulation - #simulationClass = "" # The FQCN of the simulation to run (when used in conjunction with noReports, the simulation for which assertions will be validated) - #mute = false # When set to true, don't ask for simulation name nor run description (currently only used by Gatling SBT plugin) - #elFileBodiesCacheMaxCapacity = 200 # Cache size for request body EL templates, set to 0 to disable - #rawFileBodiesCacheMaxCapacity = 200 # Cache size for request body Raw templates, set to 0 to disable - #rawFileBodiesInMemoryMaxSize = 1000 # Below this limit, raw file bodies will be cached in memory - - extract { - regex { - #cacheMaxCapacity = 200 # Cache size for the compiled regexes, set to 0 to disable caching - } - xpath { - #cacheMaxCapacity = 200 # Cache size for the compiled XPath queries, set to 0 to disable caching - } - jsonPath { - #cacheMaxCapacity = 200 # Cache size for the compiled jsonPath queries, set to 0 to disable caching - #preferJackson = false # When set to true, prefer Jackson over Boon for JSON-related operations - } - css { - #cacheMaxCapacity = 200 # Cache size for the compiled CSS selectors queries, set to 0 to disable caching - } - } - - directory { - #data = user-files/data # Folder where user's data (e.g. files used by Feeders) is located - #bodies = user-files/bodies # Folder where bodies are located - #simulations = user-files/simulations # Folder where the bundle's simulations are located - #reportsOnly = "" # If set, name of report folder to look for in order to generate its report - #binaries = "" # If set, name of the folder where compiles classes are located: Defaults to GATLING_HOME/target. - #results = results # Name of the folder where all reports folder are located - } - } - charting { - #noReports = false # When set to true, don't generate HTML reports - #maxPlotPerSeries = 1000 # Number of points per graph in Gatling reports - #useGroupDurationMetric = false # Switch group timings from cumulated response time to group duration. - indicators { - #lowerBound = 800 # Lower bound for the requests' response time to track in the reports and the console summary - #higherBound = 1200 # Higher bound for the requests' response time to track in the reports and the console summary - #percentile1 = 50 # Value for the 1st percentile to track in the reports, the console summary and Graphite - #percentile2 = 75 # Value for the 2nd percentile to track in the reports, the console summary and Graphite - #percentile3 = 95 # Value for the 3rd percentile to track in the reports, the console summary and Graphite - #percentile4 = 99 # Value for the 4th percentile to track in the reports, the console summary and Graphite - } - } - http { - #fetchedCssCacheMaxCapacity = 200 # Cache size for CSS parsed content, set to 0 to disable - #fetchedHtmlCacheMaxCapacity = 200 # Cache size for HTML parsed content, set to 0 to disable - #perUserCacheMaxCapacity = 200 # Per virtual user cache size, set to 0 to disable - #warmUpUrl = "http://gatling.io" # The URL to use to warm-up the HTTP stack (blank means disabled) - #enableGA = true # Very light Google Analytics, please support - ssl { - keyStore { - #type = "" # Type of SSLContext's KeyManagers store - #file = "" # Location of SSLContext's KeyManagers store - #password = "" # Password for SSLContext's KeyManagers store - #algorithm = "" # Algorithm used SSLContext's KeyManagers store - } - trustStore { - #type = "" # Type of SSLContext's TrustManagers store - #file = "" # Location of SSLContext's TrustManagers store - #password = "" # Password for SSLContext's TrustManagers store - #algorithm = "" # Algorithm used by SSLContext's TrustManagers store - } - } - ahc { - #keepAlive = true # Allow pooling HTTP connections (keep-alive header automatically added) - #connectTimeout = 10000 # Timeout when establishing a connection - #handshakeTimeout = 10000 # Timeout when performing TLS hashshake - #pooledConnectionIdleTimeout = 60000 # Timeout when a connection stays unused in the pool - #readTimeout = 60000 # Timeout when a used connection stays idle - #maxRetry = 2 # Number of times that a request should be tried again - #requestTimeout = 60000 # Timeout of the requests - #acceptAnyCertificate = true # When set to true, doesn't validate SSL certificates - #httpClientCodecMaxInitialLineLength = 4096 # Maximum length of the initial line of the response (e.g. "HTTP/1.0 200 OK") - #httpClientCodecMaxHeaderSize = 8192 # Maximum size, in bytes, of each request's headers - #httpClientCodecMaxChunkSize = 8192 # Maximum length of the content or each chunk - #webSocketMaxFrameSize = 10240000 # Maximum frame payload size - #sslEnabledProtocols = [TLSv1.2, TLSv1.1, TLSv1] # Array of enabled protocols for HTTPS, if empty use the JDK defaults - #sslEnabledCipherSuites = [] # Array of enabled cipher suites for HTTPS, if empty use the AHC defaults - #sslSessionCacheSize = 0 # SSLSession cache size, set to 0 to use JDK's default - #sslSessionTimeout = 0 # SSLSession timeout in seconds, set to 0 to use JDK's default (24h) - #useOpenSsl = false # if OpenSSL should be used instead of JSSE (requires tcnative jar) - #useNativeTransport = false # if native transport should be used instead of Java NIO (requires netty-transport-native-epoll, currently Linux only) - #tcpNoDelay = true - #soReuseAddress = false - #soLinger = -1 - #soSndBuf = -1 - #soRcvBuf = -1 - #allocator = "pooled" # switch to unpooled for unpooled ByteBufAllocator - #maxThreadLocalCharBufferSize = 200000 # Netty's default is 16k - } - dns { - #queryTimeout = 5000 # Timeout of each DNS query in millis - #maxQueriesPerResolve = 6 # Maximum allowed number of DNS queries for a given name resolution - } - } - jms { - #acknowledgedMessagesBufferSize = 5000 # size of the buffer used to tracked acknowledged messages and protect against duplicate receives - } - data { - #writers = [console, file] # The list of DataWriters to which Gatling write simulation data (currently supported : console, file, graphite, jdbc) - console { - #light = false # When set to true, displays a light version without detailed request stats - } - file { - #bufferSize = 8192 # FileDataWriter's internal data buffer size, in bytes - } - leak { - #noActivityTimeout = 30 # Period, in seconds, for which Gatling may have no activity before considering a leak may be happening - } - graphite { - #light = false # only send the all* stats - #host = "localhost" # The host where the Carbon server is located - #port = 2003 # The port to which the Carbon server listens to (2003 is default for plaintext, 2004 is default for pickle) - #protocol = "tcp" # The protocol used to send data to Carbon (currently supported : "tcp", "udp") - #rootPathPrefix = "gatling" # The common prefix of all metrics sent to Graphite - #bufferSize = 8192 # GraphiteDataWriter's internal data buffer size, in bytes - #writeInterval = 1 # GraphiteDataWriter's write interval, in seconds - } - } -} diff --git a/src/test/gatling/conf/logback.xml b/src/test/gatling/conf/logback.xml deleted file mode 100644 index 189bcdab8125..000000000000 --- a/src/test/gatling/conf/logback.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - %d{HH:mm:ss.SSS} [%-5level] %logger{15} - %msg%n%rEx - false - - - - - - - - - - - - - - - diff --git a/src/test/gatling/simulations/GetCoursesSimulation.scala b/src/test/gatling/simulations/GetCoursesSimulation.scala deleted file mode 100644 index 51dc40ab84bc..000000000000 --- a/src/test/gatling/simulations/GetCoursesSimulation.scala +++ /dev/null @@ -1,83 +0,0 @@ -package simulations - -import _root_.io.gatling.core.scenario.Simulation -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ - -class GetCoursesSimulation extends Simulation { - val username = "" // NOTE: replace with actual username - val password = "" // NOTE: replace with actual password - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "X-XSRF-TOKEN" -> "${xsrf_token}" - ) - - val scn = scenario("Test Getting all Course entities, creating and deleting a course") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - .check(headerRegex("Set-Cookie", "XSRF-TOKEN=(.*);[\\s]").saveAs("xsrf_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authentication") - .post("/api/authentication") - .headers(headers_http_authenticated) - .formParam("j_username", username) - .formParam("j_password", password) - .formParam("remember-me", "true") - .formParam("submit", "Login") - .check(headerRegex("Set-Cookie", "XSRF-TOKEN=(.*);[\\s]").saveAs("xsrf_token"))).exitHereIfFailed - .pause(1) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(2) - .repeat(2) { - exec(http("Get all courses") - .get("/api/courses") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(2, 5) - .exec(http("Create new course") - .post("/api/courses") - .headers(headers_http_authenticated) - .body(StringBody("""{"id":null, "title":"SAMPLE_TEXT", "studentGroupName":"SAMPLE_TEXT", "teachingAssistantGroupName":"SAMPLE_TEXT"}""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_course_url"))).exitHereIfFailed - .pause(2) - .repeat(5) { - exec(http("Get created course") - .get("${new_course_url}") - .headers(headers_http_authenticated)) - .pause(2) - } - .exec(http("Delete created course") - .delete("${new_course_url}") - .headers(headers_http_authenticated)) - .pause(2) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 500)) during(Integer.getInteger("ramp", 30) seconds)) - ).protocols(httpConf) - -} diff --git a/src/test/gatling/simulations/LtiOutcomeUrlGatlingTest.scala b/src/test/gatling/simulations/LtiOutcomeUrlGatlingTest.scala deleted file mode 100644 index 1174e43238d1..000000000000 --- a/src/test/gatling/simulations/LtiOutcomeUrlGatlingTest.scala +++ /dev/null @@ -1,90 +0,0 @@ -package simulations - -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.LoggerContext -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the LtiOutcomeUrl entity. - */ -class LtiOutcomeUrlGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://127.0.0.1:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - ) - - val scn = scenario("Test the LtiOutcomeUrl entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - .pause(10) - .exec(http("Authentication") - .post("/api/authentication") - .headers(headers_http_authenticated) - .formParam("j_username", "admin") - .formParam("j_password", "admin") - .formParam("remember-me", "true") - .formParam("submit", "Login")).exitHereIfFailed - .pause(1) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200)) - .pause(10) - .repeat(2) { - exec(http("Get all ltiOutcomeUrls") - .get("/api/lti-outcome-urls") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new ltiOutcomeUrl") - .post("/api/lti-outcome-urls") - .headers(headers_http_authenticated) - .body(StringBody("""{"id":null, "url":"SAMPLE_TEXT", "sourcedId":"SAMPLE_TEXT"}""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_ltiOutcomeUrl_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created ltiOutcomeUrl") - .get("${new_ltiOutcomeUrl_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created ltiOutcomeUrl") - .delete("${new_ltiOutcomeUrl_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(100) during(1 minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/simulations/QuizParticipationSimulation.scala b/src/test/gatling/simulations/QuizParticipationSimulation.scala deleted file mode 100644 index fe53a8834d13..000000000000 --- a/src/test/gatling/simulations/QuizParticipationSimulation.scala +++ /dev/null @@ -1,212 +0,0 @@ -package simulations - -import _root_.io.gatling.core.scenario.Simulation -import io.gatling.core.Predef._ -import io.gatling.core.json._ -import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder} -import io.gatling.http.Predef._ -import io.gatling.http.protocol.HttpProtocolBuilder - -import scala.concurrent.duration._ -import scala.language.existentials -import scala.util.parsing.json._ - -class QuizParticipationSimulation extends Simulation { - - // NOTE: update these values to fit the tested exercise - val exerciseId = 187 - val backgroundPicturePath = "/api/files/drag-and-drop/backgrounds/98/DragAndDropBackground_2018-02-16T11-45-42-684_7f0aa8e4.png" - - // NOTE: Adjust these numbers for desired load - val numUsersSubmit = 200 - val numUsersNoSubmit = 200 - - val feeder: Iterator[Map[String, String]] = Iterator.tabulate(500)(i => Map( - "username" -> (""), // NOTE: generate real username for each value of i (removed for security) - "password" -> ("") // NOTE: generate real password for each value of i (removed for security) - )) - - val baseURL = "http://localhost:8080" - val wsBaseURL = "ws://localhost:8080" - - val httpConf: HttpProtocolBuilder = http - .baseUrl(baseURL) - .wsBaseUrl(wsBaseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "X-XSRF-TOKEN" -> "${xsrf_token}" - ) - - /** - * Selects random answers and adds submittedAnswers to the submission - * - * @param questionsString the questions of the exercise as a JSON string - * @return the submission including the submittedAnswers as a JSON string - */ - def selectRandomAnswers(questionsString: String, submit: Boolean): String = { - // parse json strings into objects (Map or List) - val questions = JSON.parseFull(questionsString).get.asInstanceOf[List[Any]] - - // save submitted answers in a List - var submittedAnswers = List[Map[String, Any]]() - - // iterate through all questions to select answers - questions.foreach((questionP) => { -// val question = questionP.get.asInstanceOf[Map[String, Any]] -// val questionType = question("type").get.asInstanceOf[String] - - // create a submitted answer for this question -// var submittedAnswer = Map( -// "question" -> question, -// "type" -> questionType -// ) -// -// if (questionType.equals("multiple-choice")) { -// // save selected options in a List -// var selectedOptions = List[Map[String, Any]]() -// -// // iterate through all answer options of this question -// val answerOptions = question("answerOptions").get.asInstanceOf[List[Any]] -// answerOptions.foreach((answerOptionP) => { -// val answerOption = answerOptionP.get.asInstanceOf[Map[String, Any]] -// -// // select each answer option with a 50/50 chance -// if (math.random < 0.5) { -// selectedOptions = answerOption +: selectedOptions -// } -// }) -// -// // add selected options to submitted answer -// submittedAnswer = submittedAnswer + ("selectedOptions" -> selectedOptions) -// } else if (questionType.equals("drag-and-drop")) { -// // save mappings in a List -// var mappings = List[Map[String, Any]]() -// -// // extract drag items and drop locations -// var dragItems = question("dragItems").get.asInstanceOf[List[Any]] -// var dropLocations = question("dropLocations").get.asInstanceOf[List[Any]] -// -// while (dragItems.nonEmpty && dropLocations.nonEmpty) { -// // create a random mapping -// val dragItemIndex = (math.random * dragItems.size).floor.toInt -// val dropLocationIndex = (math.random * dropLocations.size).floor.toInt -// -// val mapping = Map( -// "dragItem" -> dragItems.get(dragItemIndex).get.asInstanceOf[Map[String, Any]], -// "dropLocation" -> dropLocations.get(dropLocationIndex).get.asInstanceOf[Map[String, Any]] -// ) -// -// // remove selected elements from lists -// dragItems = dragItems.take(dragItemIndex) ++ dragItems.drop(dragItemIndex + 1) -// dropLocations = dropLocations.take(dropLocationIndex) ++ dropLocations.drop(dropLocationIndex + 1) -// -// // add mapping to mappings -// mappings = mapping +: mappings -// } -// -// // add mappings to submitted answer -// submittedAnswer = submittedAnswer + ("mappings" -> mappings) -// } -// -// // add submitted answer to the List -// submittedAnswers = submittedAnswer +: submittedAnswers - }) - - // add submitted answers to submission - var result: Map[String, Any] = Map("submittedAnswers" -> submittedAnswers) - - if (submit) { - result = result + ("submitted" -> true) - } - - // convert back into json string - Json.stringify(result) - } - - val login: ChainBuilder = exec( - http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - .check(headerRegex("Set-Cookie", "XSRF-TOKEN=([^;]*);[\\s]").saveAs("xsrf_token"))).exitHereIfFailed - .feed(feeder) - .pause(2 seconds) - .exec(http("Authentication") - .post("/api/authentication") - .headers(headers_http_authenticated) - .formParam("j_username", "${username}") - .formParam("j_password", "${password}") - .formParam("remember-me", "true") - .formParam("submit", "Login") - .check(status.is(200)) - .check(headerRegex("Set-Cookie", "XSRF-TOKEN=([^;]*);[\\s]").saveAs("xsrf_token"))).exitHereIfFailed - .pause(10 seconds) - - val loadDashboard: ChainBuilder = rendezVous(Math.min(numUsersSubmit, numUsersNoSubmit)) - .exec(http("Get dashboard") - .get("/api/courses/for-dashboard") - .headers(headers_http_authenticated) - .check(status.is(200))).exitHereIfFailed - .pause(5 seconds) - - val startQuiz: ChainBuilder = rendezVous(Math.min(numUsersSubmit, numUsersNoSubmit)) - .exec(http("Get Participation with Quiz") - .get("/api/courses/1/exercises/" + exerciseId + "/participation") - .headers(headers_http_authenticated) - .check(status.is(200)) - .check(jsonPath("$.exercise.questions").saveAs("questions")) - .resources( - http("Load Picture").get(backgroundPicturePath).headers(headers_http_authenticated) - ) - ).exitHereIfFailed - .pause(5 seconds, 15 seconds) - - val workOnQuiz: ChainBuilder = exec( - ws("Connect WebSocket") - .connect("/websocket/websocket")).exitHereIfFailed - .pause(5 seconds) - .exec(ws("Connect STOMP") - .sendText("CONNECT\nX-XSRF-TOKEN:${xsrf_token}\naccept-version:1.2\nheart-beat:10000,10000\n\n\u0000") - .await(10 seconds)()) - .exec(ws("Subscribe Submission") - .sendText("SUBSCRIBE\nid:sub-1\ndestination:/user/topic/quizExercise/" + exerciseId + "/submission\n\n\u0000")) - .pause(5 seconds) - .repeat(20) { - exec(ws("Send Answers") - .sendText(session => "SEND\ndestination:/topic/quizExercise/" + exerciseId + "/submission\n\n" + selectRandomAnswers(session("questions").as[String], false) + "\u0000") - .await(10 seconds)()) - .pause(5 seconds) - } - - val submitQuiz: ChainBuilder = rendezVous(numUsersSubmit) - .exec(ws("Submit Answers") - .sendText(session => "SEND\ndestination:/topic/quizExercise/" + exerciseId + "/submission\n\n" + selectRandomAnswers(session("questions").as[String], true) + "\u0000") - .await(10 seconds)()) - .pause(5 seconds) - - val waitForResult: ChainBuilder = pause(10 seconds) - .exec(ws("Subscribe Participation") - .sendText("SUBSCRIBE\nid:sub-1\ndestination:/user/topic/quizExercise/" + exerciseId + "/participation\n\n\u0000") - .await(600 seconds)()) - - - val usersNoSubmit: ScenarioBuilder = scenario("Users without submit").exec(login, loadDashboard, startQuiz, workOnQuiz, waitForResult) - val usersSubmit: ScenarioBuilder = scenario("Users with submit").exec(login, loadDashboard, startQuiz, workOnQuiz, submitQuiz, waitForResult) - - setUp( - usersNoSubmit.inject(rampUsers(numUsersNoSubmit) during (20 seconds)), - usersSubmit.inject(rampUsers(numUsersSubmit) during (20 seconds)) - ).protocols(httpConf) - -} diff --git a/src/test/gatling/user-files/resources/modeling_submission_1.txt b/src/test/gatling/user-files/resources/modeling_submission_1.txt deleted file mode 100644 index 0c99bb83f7ac..000000000000 --- a/src/test/gatling/user-files/resources/modeling_submission_1.txt +++ /dev/null @@ -1 +0,0 @@ -{\"version\":\"2.0.0\",\"size\":{\"width\":910,\"height\":370},\"type\":\"ClassDiagram\",\"interactive\":{\"elements\":[],\"relationships\":[]},\"elements\":[{\"id\":\"62c4675c-dd53-4e5c-bc18-0042081b9ac7\",\"name\":\"Tea\",\"owner\":null,\"type\":\"Class\",\"bounds\":{\"x\":0,\"y\":270,\"width\":200,\"height\":100},\"attributes\":[\"e0133088-a2d2-4913-bfab-550182fbf77d\"],\"methods\":[\"dfe1c9e9-d4aa-4707-aedd-19b0406c2e20\"]},{\"id\":\"e0133088-a2d2-4913-bfab-550182fbf77d\",\"name\":\"+ temp: Type\",\"owner\":\"62c4675c-dd53-4e5c-bc18-0042081b9ac7\",\"type\":\"ClassAttribute\",\"bounds\":{\"x\":0,\"y\":310,\"width\":200,\"height\":30}},{\"id\":\"dfe1c9e9-d4aa-4707-aedd-19b0406c2e20\",\"name\":\"+ drink()\",\"owner\":\"62c4675c-dd53-4e5c-bc18-0042081b9ac7\",\"type\":\"ClassMethod\",\"bounds\":{\"x\":0,\"y\":340,\"width\":200,\"height\":30}},{\"id\":\"2ac69a3b-7e75-4010-8af5-0785fe3aaa4d\",\"name\":\"Class\",\"owner\":null,\"type\":\"Class\",\"bounds\":{\"x\":710,\"y\":270,\"width\":200,\"height\":100},\"attributes\":[\"b94f0549-cfcc-4069-9058-f66413b18cde\"],\"methods\":[\"5181da8e-6f29-4b46-97c3-2eca7e1ee1e3\"]},{\"id\":\"b94f0549-cfcc-4069-9058-f66413b18cde\",\"name\":\"+ attribute: Type\",\"owner\":\"2ac69a3b-7e75-4010-8af5-0785fe3aaa4d\",\"type\":\"ClassAttribute\",\"bounds\":{\"x\":710,\"y\":310,\"width\":200,\"height\":30}},{\"id\":\"5181da8e-6f29-4b46-97c3-2eca7e1ee1e3\",\"name\":\"+ method()\",\"owner\":\"2ac69a3b-7e75-4010-8af5-0785fe3aaa4d\",\"type\":\"ClassMethod\",\"bounds\":{\"x\":710,\"y\":340,\"width\":200,\"height\":30}},{\"id\":\"c430c7a2-046f-45d7-925f-a754c7a3190f\",\"name\":\"Drink\",\"owner\":null,\"type\":\"AbstractClass\",\"bounds\":{\"x\":360,\"y\":0,\"width\":200,\"height\":70},\"attributes\":[],\"methods\":[\"6819b6c6-12ed-4f07-98a4-94d76d7409bc\"]},{\"id\":\"6819b6c6-12ed-4f07-98a4-94d76d7409bc\",\"name\":\"+ drink()\",\"owner\":\"c430c7a2-046f-45d7-925f-a754c7a3190f\",\"type\":\"ClassMethod\",\"bounds\":{\"x\":360,\"y\":40,\"width\":200,\"height\":30}}],\"relationships\":[],\"assessments\":[]} diff --git a/src/test/gatling/user-files/simulations/AnswerCounterGatlingTest.scala b/src/test/gatling/user-files/simulations/AnswerCounterGatlingTest.scala deleted file mode 100644 index 47be82adc65f..000000000000 --- a/src/test/gatling/user-files/simulations/AnswerCounterGatlingTest.scala +++ /dev/null @@ -1,96 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the AnswerCounter entity. - */ -class AnswerCounterGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the AnswerCounter entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all answerCounters") - .get("/api/answer-counters") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new answerCounter") - .post("/api/answer-counters") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_answerCounter_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created answerCounter") - .get("${new_answerCounter_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created answerCounter") - .delete("${new_answerCounter_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/AnswerOptionGatlingTest.scala b/src/test/gatling/user-files/simulations/AnswerOptionGatlingTest.scala deleted file mode 100644 index 92ef8af2be91..000000000000 --- a/src/test/gatling/user-files/simulations/AnswerOptionGatlingTest.scala +++ /dev/null @@ -1,101 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the AnswerOption entity. - */ -class AnswerOptionGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the AnswerOption entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all answerOptions") - .get("/api/answer-options") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new answerOption") - .post("/api/answer-options") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "text":"SAMPLE_TEXT" - , "hint":"SAMPLE_TEXT" - , "explanation":"SAMPLE_TEXT" - , "isCorrect":null - , "invalid":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_answerOption_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created answerOption") - .get("${new_answerOption_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created answerOption") - .delete("${new_answerOption_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ApollonDiagramGatlingTest.scala b/src/test/gatling/user-files/simulations/ApollonDiagramGatlingTest.scala deleted file mode 100644 index 79720729638e..000000000000 --- a/src/test/gatling/user-files/simulations/ApollonDiagramGatlingTest.scala +++ /dev/null @@ -1,98 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ApollonDiagram entity. - */ -class ApollonDiagramGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ApollonDiagram entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all apollonDiagrams") - .get("/api/apollon-diagrams") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new apollonDiagram") - .post("/api/apollon-diagrams") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "title":"SAMPLE_TEXT" - , "jsonRepresentation":"SAMPLE_TEXT" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_apollonDiagram_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created apollonDiagram") - .get("${new_apollonDiagram_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created apollonDiagram") - .delete("${new_apollonDiagram_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ConflictSimulation.scala b/src/test/gatling/user-files/simulations/ConflictSimulation.scala deleted file mode 100644 index 90b6fca2848c..000000000000 --- a/src/test/gatling/user-files/simulations/ConflictSimulation.scala +++ /dev/null @@ -1,128 +0,0 @@ -package simulations - -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import scala.concurrent.duration._ -import util.Properties - -class ConflictSimulation extends Simulation { - - val httpProtocol = http - .baseUrl("http://localhost:9000") - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("en-US,en;q=0.5") - .userAgentHeader("Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0") - .silentResources - - val userCredentials: Array[(String, String)] = Array(("", ""), ("", "")) - val model1: String = """{\"version\":\"2.0.0\",\"size\":{\"width\":910,\"height\":370},\"type\":\"ClassDiagram\",\"interactive\":{\"elements\":[],\"relationships\":[]},\"elements\":[{\"id\":\"62c4675c-dd53-4e5c-bc18-0042081b9ac7\",\"name\":\"Tea\",\"owner\":null,\"type\":\"Class\",\"bounds\":{\"x\":0,\"y\":270,\"width\":200,\"height\":100},\"attributes\":[\"e0133088-a2d2-4913-bfab-550182fbf77d\"],\"methods\":[\"dfe1c9e9-d4aa-4707-aedd-19b0406c2e20\"]},{\"id\":\"e0133088-a2d2-4913-bfab-550182fbf77d\",\"name\":\"+ temp: Type\",\"owner\":\"62c4675c-dd53-4e5c-bc18-0042081b9ac7\",\"type\":\"ClassAttribute\",\"bounds\":{\"x\":0,\"y\":310,\"width\":200,\"height\":30}},{\"id\":\"dfe1c9e9-d4aa-4707-aedd-19b0406c2e20\",\"name\":\"+ drink()\",\"owner\":\"62c4675c-dd53-4e5c-bc18-0042081b9ac7\",\"type\":\"ClassMethod\",\"bounds\":{\"x\":0,\"y\":340,\"width\":200,\"height\":30}},{\"id\":\"2ac69a3b-7e75-4010-8af5-0785fe3aaa4d\",\"name\":\"Class\",\"owner\":null,\"type\":\"Class\",\"bounds\":{\"x\":710,\"y\":270,\"width\":200,\"height\":100},\"attributes\":[\"b94f0549-cfcc-4069-9058-f66413b18cde\"],\"methods\":[\"5181da8e-6f29-4b46-97c3-2eca7e1ee1e3\"]},{\"id\":\"b94f0549-cfcc-4069-9058-f66413b18cde\",\"name\":\"+ attribute: Type\",\"owner\":\"2ac69a3b-7e75-4010-8af5-0785fe3aaa4d\",\"type\":\"ClassAttribute\",\"bounds\":{\"x\":710,\"y\":310,\"width\":200,\"height\":30}},{\"id\":\"5181da8e-6f29-4b46-97c3-2eca7e1ee1e3\",\"name\":\"+ method()\",\"owner\":\"2ac69a3b-7e75-4010-8af5-0785fe3aaa4d\",\"type\":\"ClassMethod\",\"bounds\":{\"x\":710,\"y\":340,\"width\":200,\"height\":30}},{\"id\":\"c430c7a2-046f-45d7-925f-a754c7a3190f\",\"name\":\"Drink\",\"owner\":null,\"type\":\"AbstractClass\",\"bounds\":{\"x\":360,\"y\":0,\"width\":200,\"height\":70},\"attributes\":[],\"methods\":[\"6819b6c6-12ed-4f07-98a4-94d76d7409bc\"]},{\"id\":\"6819b6c6-12ed-4f07-98a4-94d76d7409bc\",\"name\":\"+ drink()\",\"owner\":\"c430c7a2-046f-45d7-925f-a754c7a3190f\",\"type\":\"ClassMethod\",\"bounds\":{\"x\":360,\"y\":40,\"width\":200,\"height\":30}}],\"relationships\":[],\"assessments\":[]}""" - val assessmentModel1:String ="""[{"referenceId":"c430c7a2-046f-45d7-925f-a754c7a3190f","referenceType":"AbstractClass","reference":"AbstractClass:c430c7a2-046f-45d7-925f-a754c7a3190f","credits":0.5},{"referenceId":"6819b6c6-12ed-4f07-98a4-94d76d7409bc","referenceType":"ClassMethod","reference":"ClassMethod:6819b6c6-12ed-4f07-98a4-94d76d7409bc","credits":0.5},{"referenceId":"2ac69a3b-7e75-4010-8af5-0785fe3aaa4d","referenceType":"Class","reference":"Class:2ac69a3b-7e75-4010-8af5-0785fe3aaa4d","credits":0.5},{"referenceId":"b94f0549-cfcc-4069-9058-f66413b18cde","referenceType":"ClassAttribute","reference":"ClassAttribute:b94f0549-cfcc-4069-9058-f66413b18cde","credits":0.5},{"referenceId":"5181da8e-6f29-4b46-97c3-2eca7e1ee1e3","referenceType":"ClassMethod","reference":"ClassMethod:5181da8e-6f29-4b46-97c3-2eca7e1ee1e3","credits":0.5},{"referenceId":"62c4675c-dd53-4e5c-bc18-0042081b9ac7","referenceType":"Class","reference":"Class:62c4675c-dd53-4e5c-bc18-0042081b9ac7","credits":0.5},{"referenceId":"e0133088-a2d2-4913-bfab-550182fbf77d","referenceType":"ClassAttribute","reference":"ClassAttribute:e0133088-a2d2-4913-bfab-550182fbf77d","credits":0.5},{"referenceId":"dfe1c9e9-d4aa-4707-aedd-19b0406c2e20","referenceType":"ClassMethod","reference":"ClassMethod:dfe1c9e9-d4aa-4707-aedd-19b0406c2e20","credits":0.5}]""" - val model2: String = """{\"version\":\"2.0.0\",\"size\":{\"width\":1410,\"height\":530},\"type\":\"ClassDiagram\",\"interactive\":{\"elements\":[],\"relationships\":[]},\"elements\":[{\"id\":\"753f3d0d-73ac-433a-8318-76ab56adc846\",\"name\":\"Drink\",\"owner\":null,\"type\":\"AbstractClass\",\"bounds\":{\"x\":0,\"y\":50,\"width\":200,\"height\":100},\"attributes\":[\"54f52912-5c7e-4401-9788-3f3de239e715\"],\"methods\":[\"e15ca6cb-10c0-42fc-827e-b77f268c4f4c\"]},{\"id\":\"54f52912-5c7e-4401-9788-3f3de239e715\",\"name\":\"+ nothing\",\"owner\":\"753f3d0d-73ac-433a-8318-76ab56adc846\",\"type\":\"ClassAttribute\",\"bounds\":{\"x\":0,\"y\":90,\"width\":200,\"height\":30}},{\"id\":\"e15ca6cb-10c0-42fc-827e-b77f268c4f4c\",\"name\":\"+ drink()\",\"owner\":\"753f3d0d-73ac-433a-8318-76ab56adc846\",\"type\":\"ClassMethod\",\"bounds\":{\"x\":0,\"y\":120,\"width\":200,\"height\":30}},{\"id\":\"01f98b8b-3139-44ad-a783-fa43d51dcdba\",\"name\":\"Tea\",\"owner\":null,\"type\":\"Class\",\"bounds\":{\"x\":1210,\"y\":0,\"width\":200,\"height\":100},\"attributes\":[\"7fd3f82d-1a8f-4d1b-82ab-84e3df93be84\"],\"methods\":[\"0d44e404-1dde-4c40-aedb-ae60af5244a2\"]},{\"id\":\"7fd3f82d-1a8f-4d1b-82ab-84e3df93be84\",\"name\":\"+ temp: Type\",\"owner\":\"01f98b8b-3139-44ad-a783-fa43d51dcdba\",\"type\":\"ClassAttribute\",\"bounds\":{\"x\":1210,\"y\":40,\"width\":200,\"height\":30}},{\"id\":\"0d44e404-1dde-4c40-aedb-ae60af5244a2\",\"name\":\"+ drink()\",\"owner\":\"01f98b8b-3139-44ad-a783-fa43d51dcdba\",\"type\":\"ClassMethod\",\"bounds\":{\"x\":1210,\"y\":70,\"width\":200,\"height\":30}},{\"id\":\"0b396708-3561-4fdb-b6d6-b04020194d07\",\"name\":\"Coffee\",\"owner\":null,\"type\":\"Class\",\"bounds\":{\"x\":870,\"y\":430,\"width\":200,\"height\":100},\"attributes\":[\"c1a04810-a52f-42d5-b100-872b85f0cc89\"],\"methods\":[\"7eb695c9-5b67-4b74-abf2-aad998be850c\"]},{\"id\":\"c1a04810-a52f-42d5-b100-872b85f0cc89\",\"name\":\"+ temp: Type\",\"owner\":\"0b396708-3561-4fdb-b6d6-b04020194d07\",\"type\":\"ClassAttribute\",\"bounds\":{\"x\":870,\"y\":470,\"width\":200,\"height\":30}},{\"id\":\"7eb695c9-5b67-4b74-abf2-aad998be850c\",\"name\":\"+ drink()\",\"owner\":\"0b396708-3561-4fdb-b6d6-b04020194d07\",\"type\":\"ClassMethod\",\"bounds\":{\"x\":870,\"y\":500,\"width\":200,\"height\":30}}],\"relationships\":[{\"id\":\"40f12b68-b42f-42a1-8bae-623dc58897ee\",\"name\":\"\",\"type\":\"ClassBidirectional\",\"source\":{\"element\":\"0b396708-3561-4fdb-b6d6-b04020194d07\",\"direction\":\"Left\",\"multiplicity\":\"\",\"role\":\"\"},\"target\":{\"element\":\"753f3d0d-73ac-433a-8318-76ab56adc846\",\"direction\":\"Right\",\"multiplicity\":\"\",\"role\":\"\"},\"path\":[{\"x\":672,\"y\":382},{\"x\":337,\"y\":382},{\"x\":337,\"y\":2},{\"x\":2,\"y\":2}],\"bounds\":{\"x\":198,\"y\":98,\"width\":674,\"height\":384}},{\"id\":\"155256b5-0cf7-4dbf-8e2a-cab00ef97a67\",\"name\":\"\",\"type\":\"ClassBidirectional\",\"source\":{\"element\":\"01f98b8b-3139-44ad-a783-fa43d51dcdba\",\"direction\":\"Left\",\"multiplicity\":\"\",\"role\":\"\"},\"target\":{\"element\":\"753f3d0d-73ac-433a-8318-76ab56adc846\",\"direction\":\"Right\",\"multiplicity\":\"\",\"role\":\"\"},\"path\":[{\"x\":1012,\"y\":2},{\"x\":2,\"y\":2}],\"bounds\":{\"x\":198,\"y\":73,\"width\":1014,\"height\":4}}],\"assessments\":[]}""" - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Accept" -> """application/json""", - "Content-Type" -> """application/json""", - "X-XSRF-TOKEN" -> "${xsrf_token}" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "X-XSRF-TOKEN" -> "${xsrf_token}", - "Authorization" -> "${access_token}" - ) - - val headers_http_authenticated_JSON = Map( - "Accept" -> """application/json""", - "Content-Type" -> """application/json""", - "X-XSRF-TOKEN" -> "${xsrf_token}", - "Authorization" -> "${access_token}" - ) - - val firstModel = scenario("Submit and assess 1. Model") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - .check(headerRegex("set-cookie", "XSRF-TOKEN=(.*);[\\s]").saveAs("xsrf_token"))).exitHereIfFailed - .exec(http("Authentication of 1. User") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"""" + userCredentials(0)._1 + """", "password":"""" + userCredentials(0)._2 + """"}""")).asJson - .check(status.is(200)) - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .exec((http("Create Course")) - .post("/api/courses") - .headers(headers_http_authenticated_JSON) - .body(StringBody("""{"id":null,"title":"GatlingGeneratedCourse","shortName":"TTTXY","studentGroupName":"tumuser","teachingAssistantGroupName":null,"instructorGroupName":"tumuser","description":null,"startDate":null,"endDate":null,"onlineCourse":false,"registrationEnabled":false,"color":null,"courseIcon":null}""")).asJson - .check(status.is(201)) - .check(jsonPath("$.id").saveAs("course_id")) - .check(headerRegex("set-cookie", "XSRF-TOKEN=(.*);[\\s]").saveAs("xsrf_token"))).exitHereIfFailed - .exec((http("Create ModelingExercise")) - .post("/api/modeling-exercises") - .headers(headers_http_authenticated_JSON) - .body(StringBody("""{"isAtLeastTutor":false,"isAtLeastInstructor":false,"type":"modeling","course":{"id":""" + "${course_id}" + ""","title":"CourseXY","shortName":"TTTXY","studentGroupName":"tumuser","instructorGroupName":"tumuser","onlineCourse":false,"registrationEnabled":false,"startDate":null,"endDate":null,"exercises":[]},"diagramType":"ClassDiagram","title":"Exercise 1","maxScore":10,"problemStatement":"","releaseDate":null,"dueDate":null,"assessmentDueDate":null}""")).asJson - .check(status.is(201)) - .check(jsonPath("$.id").saveAs("exercise_id")) - .check(headerRegex("set-cookie", "XSRF-TOKEN=(.*);[\\s]").saveAs("xsrf_token"))).exitHereIfFailed - .exec((http("Participate in Modeling Exercise")) - .post("/api/courses/${course_id}/exercises/${exercise_id}/participations") - .headers(headers_http_authenticated_JSON) - .check(status.is(201)) - .check(jsonPath("$.id").saveAs("participation_id")) - .check(bodyString.saveAs("participation")) - .check(headerRegex("set-cookie", "XSRF-TOKEN=(.*);[\\s]").saveAs("xsrf_token"))).exitHereIfFailed - .exec((http("Submit Model1")) - .put("/api/exercises/${exercise_id}/modeling-submissions") - .headers(headers_http_authenticated_JSON) - .body(StringBody("""{"submissionExerciseType":"modeling","participation":${participation},"submitted":true,"model":"""" + model1 + """"}""")).asJson - .check(status.is(200)) - .check(jsonPath("$.id").saveAs("submission_id")) - .check(headerRegex("set-cookie", "XSRF-TOKEN=(.*);[\\s]").saveAs("xsrf_token"))).exitHereIfFailed - .exec((http("Assess Model")) - .put("/api/modeling-submissions/${submission_id}/assessment") - .queryParam("submit","true") - .headers(headers_http_authenticated_JSON) - .body(StringBody(assessmentModel1)).asJson - .check(status.is(200))) - - - val secondModel = scenario("Submit 2nd. Model") - .exec(flushSessionCookies) - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - .check(headerRegex("set-cookie", "XSRF-TOKEN=(.*);[\\s]").saveAs("xsrf_token"))).exitHereIfFailed - .exec(http("Authentication of 2nd User") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"""" + userCredentials(1)._1 + """", "password":"""" + userCredentials(1)._2 + """"}""")).asJson - .check(status.is(200)) - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .exec((http("Participate in Modeling Exercise")) - .post("/api/courses/${course_id}/exercises/${exercise_id}/participations") - .headers(headers_http_authenticated_JSON) - .check(status.is(201)) - .check(jsonPath("$.id").saveAs("participation_id")) - .check(bodyString.saveAs("participation")) - .check(headerRegex("set-cookie", "XSRF-TOKEN=(.*);[\\s]").saveAs("xsrf_token"))).exitHereIfFailed - .exec((http("Submit Model2")) - .put("/api/exercises/${exercise_id}/modeling-submissions") - .headers(headers_http_authenticated_JSON) - .body(StringBody("""{"submissionExerciseType":"modeling","submitted":true,"model":"""" + model2 + """"}""")).asJson - .check(status.is(200))) - - val submitModels = scenario("Submitting both Models") - .exec(firstModel) - .exec(secondModel) - - setUp( - submitModels.inject(atOnceUsers(1)) - ).protocols(httpProtocol) -} diff --git a/src/test/gatling/user-files/simulations/CourseGatlingTest.scala b/src/test/gatling/user-files/simulations/CourseGatlingTest.scala deleted file mode 100644 index 9f334e2525cd..000000000000 --- a/src/test/gatling/user-files/simulations/CourseGatlingTest.scala +++ /dev/null @@ -1,103 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the Course entity. - */ -class CourseGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the Course entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all courses") - .get("/api/courses") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new course") - .post("/api/courses") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "title":"SAMPLE_TEXT" - , "studentGroupName":"SAMPLE_TEXT" - , "teachingAssistantGroupName":"SAMPLE_TEXT" - , "instructorGroupName":"SAMPLE_TEXT" - , "startDate":"2020-01-01T00:00:00.000Z" - , "endDate":"2020-01-01T00:00:00.000Z" - , "onlineCourse":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_course_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created course") - .get("${new_course_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created course") - .delete("${new_course_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/DragAndDropMappingGatlingTest.scala b/src/test/gatling/user-files/simulations/DragAndDropMappingGatlingTest.scala deleted file mode 100644 index 3677cbc9b013..000000000000 --- a/src/test/gatling/user-files/simulations/DragAndDropMappingGatlingTest.scala +++ /dev/null @@ -1,99 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the DragAndDropMapping entity. - */ -class DragAndDropMappingGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the DragAndDropMapping entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all dragAndDropMappings") - .get("/api/drag-and-drop-mappings") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new dragAndDropMapping") - .post("/api/drag-and-drop-mappings") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "dragItemIndex":"0" - , "dropLocationIndex":"0" - , "invalid":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_dragAndDropMapping_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created dragAndDropMapping") - .get("${new_dragAndDropMapping_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created dragAndDropMapping") - .delete("${new_dragAndDropMapping_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/DragAndDropQuestionGatlingTest.scala b/src/test/gatling/user-files/simulations/DragAndDropQuestionGatlingTest.scala deleted file mode 100644 index 5b58a050e251..000000000000 --- a/src/test/gatling/user-files/simulations/DragAndDropQuestionGatlingTest.scala +++ /dev/null @@ -1,97 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the DragAndDropQuestion entity. - */ -class DragAndDropQuestionGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the DragAndDropQuestion entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all dragAndDropQuestions") - .get("/api/drag-and-drop-questions") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new dragAndDropQuestion") - .post("/api/drag-and-drop-questions") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "backgroundFilePath":"SAMPLE_TEXT" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_dragAndDropQuestion_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created dragAndDropQuestion") - .get("${new_dragAndDropQuestion_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created dragAndDropQuestion") - .delete("${new_dragAndDropQuestion_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/DragAndDropQuestionStatisticGatlingTest.scala b/src/test/gatling/user-files/simulations/DragAndDropQuestionStatisticGatlingTest.scala deleted file mode 100644 index 2a234abf2232..000000000000 --- a/src/test/gatling/user-files/simulations/DragAndDropQuestionStatisticGatlingTest.scala +++ /dev/null @@ -1,96 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the DragAndDropQuestionStatistic entity. - */ -class DragAndDropQuestionStatisticGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the DragAndDropQuestionStatistic entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all dragAndDropQuestionStatistics") - .get("/api/drag-and-drop-question-statistics") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new dragAndDropQuestionStatistic") - .post("/api/drag-and-drop-question-statistics") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_dragAndDropQuestionStatistic_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created dragAndDropQuestionStatistic") - .get("${new_dragAndDropQuestionStatistic_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created dragAndDropQuestionStatistic") - .delete("${new_dragAndDropQuestionStatistic_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/DragAndDropSubmittedAnswerGatlingTest.scala b/src/test/gatling/user-files/simulations/DragAndDropSubmittedAnswerGatlingTest.scala deleted file mode 100644 index e23a8bdc9f09..000000000000 --- a/src/test/gatling/user-files/simulations/DragAndDropSubmittedAnswerGatlingTest.scala +++ /dev/null @@ -1,96 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the DragAndDropSubmittedAnswer entity. - */ -class DragAndDropSubmittedAnswerGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the DragAndDropSubmittedAnswer entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all dragAndDropSubmittedAnswers") - .get("/api/drag-and-drop-submitted-answers") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new dragAndDropSubmittedAnswer") - .post("/api/drag-and-drop-submitted-answers") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_dragAndDropSubmittedAnswer_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created dragAndDropSubmittedAnswer") - .get("${new_dragAndDropSubmittedAnswer_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created dragAndDropSubmittedAnswer") - .delete("${new_dragAndDropSubmittedAnswer_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/DragItemGatlingTest.scala b/src/test/gatling/user-files/simulations/DragItemGatlingTest.scala deleted file mode 100644 index 668fd6eb89d2..000000000000 --- a/src/test/gatling/user-files/simulations/DragItemGatlingTest.scala +++ /dev/null @@ -1,99 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the DragItem entity. - */ -class DragItemGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the DragItem entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all dragItems") - .get("/api/drag-items") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new dragItem") - .post("/api/drag-items") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "pictureFilePath":"SAMPLE_TEXT" - , "text":"SAMPLE_TEXT" - , "invalid":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_dragItem_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created dragItem") - .get("${new_dragItem_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created dragItem") - .delete("${new_dragItem_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/DropLocationCounterGatlingTest.scala b/src/test/gatling/user-files/simulations/DropLocationCounterGatlingTest.scala deleted file mode 100644 index 8f9b530fc1a7..000000000000 --- a/src/test/gatling/user-files/simulations/DropLocationCounterGatlingTest.scala +++ /dev/null @@ -1,96 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the DropLocationCounter entity. - */ -class DropLocationCounterGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the DropLocationCounter entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all dropLocationCounters") - .get("/api/drop-location-counters") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new dropLocationCounter") - .post("/api/drop-location-counters") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_dropLocationCounter_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created dropLocationCounter") - .get("${new_dropLocationCounter_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created dropLocationCounter") - .delete("${new_dropLocationCounter_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/DropLocationGatlingTest.scala b/src/test/gatling/user-files/simulations/DropLocationGatlingTest.scala deleted file mode 100644 index 52bb8d0f1d13..000000000000 --- a/src/test/gatling/user-files/simulations/DropLocationGatlingTest.scala +++ /dev/null @@ -1,101 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the DropLocation entity. - */ -class DropLocationGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the DropLocation entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all dropLocations") - .get("/api/drop-locations") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new dropLocation") - .post("/api/drop-locations") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "posX":"0" - , "posY":"0" - , "width":"0" - , "height":"0" - , "invalid":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_dropLocation_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created dropLocation") - .get("${new_dropLocation_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created dropLocation") - .delete("${new_dropLocation_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ExerciseGatlingTest.scala b/src/test/gatling/user-files/simulations/ExerciseGatlingTest.scala deleted file mode 100644 index ab5d5621b5a0..000000000000 --- a/src/test/gatling/user-files/simulations/ExerciseGatlingTest.scala +++ /dev/null @@ -1,104 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the Exercise entity. - */ -class ExerciseGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the Exercise entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all exercises") - .get("/api/exercises") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new exercise") - .post("/api/exercises") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "problemStatement":"SAMPLE_TEXT" - , "gradingInstructions":"SAMPLE_TEXT" - , "title":"SAMPLE_TEXT" - , "releaseDate":"2020-01-01T00:00:00.000Z" - , "dueDate":"2020-01-01T00:00:00.000Z" - , "maxScore":null - , "difficulty":"EASY" - , "categories":"SAMPLE_TEXT" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_exercise_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created exercise") - .get("${new_exercise_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created exercise") - .delete("${new_exercise_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ExerciseHintGatlingTest.scala b/src/test/gatling/user-files/simulations/ExerciseHintGatlingTest.scala deleted file mode 100644 index 51821590cd1f..000000000000 --- a/src/test/gatling/user-files/simulations/ExerciseHintGatlingTest.scala +++ /dev/null @@ -1,98 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ExerciseHint entity. - */ -class ExerciseHintGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ExerciseHint entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all exerciseHints") - .get("/api/exercise-hints") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new exerciseHint") - .post("/api/exercise-hints") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "title":"SAMPLE_TEXT" - , "content":"SAMPLE_TEXT" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_exerciseHint_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created exerciseHint") - .get("${new_exerciseHint_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created exerciseHint") - .delete("${new_exerciseHint_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during (Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ExerciseResultGatlingTest.scala b/src/test/gatling/user-files/simulations/ExerciseResultGatlingTest.scala deleted file mode 100644 index 2c5f5ae3d71f..000000000000 --- a/src/test/gatling/user-files/simulations/ExerciseResultGatlingTest.scala +++ /dev/null @@ -1,102 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ExerciseResult entity. - */ -class ExerciseResultGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ExerciseResult entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all exerciseResults") - .get("/api/exercise-results") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new exerciseResult") - .post("/api/exercise-results") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "completionDate":"2020-01-01T00:00:00.000Z" - , "successful":null - , "buildArtifact":null - , "score":null - , "rated":null - , "assessmentType":"AUTOMATIC" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_exerciseResult_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created exerciseResult") - .get("${new_exerciseResult_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created exerciseResult") - .delete("${new_exerciseResult_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/FeedbackGatlingTest.scala b/src/test/gatling/user-files/simulations/FeedbackGatlingTest.scala deleted file mode 100644 index a67fec80e5fb..000000000000 --- a/src/test/gatling/user-files/simulations/FeedbackGatlingTest.scala +++ /dev/null @@ -1,100 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the Feedback entity. - */ -class FeedbackGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the Feedback entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all feedbacks") - .get("/api/feedbacks") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new feedback") - .post("/api/feedbacks") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "text":"SAMPLE_TEXT" - , "detailText":"SAMPLE_TEXT" - , "positive":null - , "type":"AUTOMATIC" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_feedback_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created feedback") - .get("${new_feedback_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created feedback") - .delete("${new_feedback_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/FileUploadExerciseGatlingTest.scala b/src/test/gatling/user-files/simulations/FileUploadExerciseGatlingTest.scala deleted file mode 100644 index e2ca1cb899d2..000000000000 --- a/src/test/gatling/user-files/simulations/FileUploadExerciseGatlingTest.scala +++ /dev/null @@ -1,97 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the FileUploadExercise entity. - */ -class FileUploadExerciseGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the FileUploadExercise entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all fileUploadExercises") - .get("/api/file-upload-exercises") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new fileUploadExercise") - .post("/api/file-upload-exercises") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "filePattern":"SAMPLE_TEXT" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_fileUploadExercise_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created fileUploadExercise") - .get("${new_fileUploadExercise_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created fileUploadExercise") - .delete("${new_fileUploadExercise_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/FileUploadSubmissionGatlingTest.scala b/src/test/gatling/user-files/simulations/FileUploadSubmissionGatlingTest.scala deleted file mode 100644 index 2f75294a4dd4..000000000000 --- a/src/test/gatling/user-files/simulations/FileUploadSubmissionGatlingTest.scala +++ /dev/null @@ -1,97 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the FileUploadSubmission entity. - */ -class FileUploadSubmissionGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the FileUploadSubmission entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all fileUploadSubmissions") - .get("/api/file-upload-submissions") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new fileUploadSubmission") - .post("/api/file-upload-submissions") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "filePath":"SAMPLE_TEXT" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_fileUploadSubmission_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created fileUploadSubmission") - .get("${new_fileUploadSubmission_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created fileUploadSubmission") - .delete("${new_fileUploadSubmission_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/LtiOutcomeUrlGatlingTest.scala b/src/test/gatling/user-files/simulations/LtiOutcomeUrlGatlingTest.scala deleted file mode 100644 index 12bd7bf9d699..000000000000 --- a/src/test/gatling/user-files/simulations/LtiOutcomeUrlGatlingTest.scala +++ /dev/null @@ -1,98 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the LtiOutcomeUrl entity. - */ -class LtiOutcomeUrlGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the LtiOutcomeUrl entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all ltiOutcomeUrls") - .get("/api/lti-outcome-urls") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new ltiOutcomeUrl") - .post("/api/lti-outcome-urls") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "url":"SAMPLE_TEXT" - , "sourcedId":"SAMPLE_TEXT" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_ltiOutcomeUrl_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created ltiOutcomeUrl") - .get("${new_ltiOutcomeUrl_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created ltiOutcomeUrl") - .delete("${new_ltiOutcomeUrl_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ModelingExerciseGatlingTest.scala b/src/test/gatling/user-files/simulations/ModelingExerciseGatlingTest.scala deleted file mode 100644 index 37da6ae2e133..000000000000 --- a/src/test/gatling/user-files/simulations/ModelingExerciseGatlingTest.scala +++ /dev/null @@ -1,99 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ModelingExercise entity. - */ -class ModelingExerciseGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ModelingExercise entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all modelingExercises") - .get("/api/modeling-exercises") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new modelingExercise") - .post("/api/modeling-exercises") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "diagramType":"CLASS" - , "exampleSolutionModel":"SAMPLE_TEXT" - , "exampleSolutionExplanation":"SAMPLE_TEXT" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_modelingExercise_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created modelingExercise") - .get("${new_modelingExercise_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created modelingExercise") - .delete("${new_modelingExercise_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ModelingSubmissionGatlingTest.scala b/src/test/gatling/user-files/simulations/ModelingSubmissionGatlingTest.scala deleted file mode 100644 index 442822aa2170..000000000000 --- a/src/test/gatling/user-files/simulations/ModelingSubmissionGatlingTest.scala +++ /dev/null @@ -1,98 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ModelingSubmission entity. - */ -class ModelingSubmissionGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ModelingSubmission entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all modelingSubmissions") - .get("/api/modeling-submissions") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new modelingSubmission") - .post("/api/modeling-submissions") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "model":"SAMPLE_TEXT" - , "explanationText":"SAMPLE_TEXT" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_modelingSubmission_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created modelingSubmission") - .get("${new_modelingSubmission_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created modelingSubmission") - .delete("${new_modelingSubmission_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/MultipleChoiceQuestionGatlingTest.scala b/src/test/gatling/user-files/simulations/MultipleChoiceQuestionGatlingTest.scala deleted file mode 100644 index 2c0af8cf3a0b..000000000000 --- a/src/test/gatling/user-files/simulations/MultipleChoiceQuestionGatlingTest.scala +++ /dev/null @@ -1,96 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the MultipleChoiceQuestion entity. - */ -class MultipleChoiceQuestionGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the MultipleChoiceQuestion entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all multipleChoiceQuestions") - .get("/api/multiple-choice-questions") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new multipleChoiceQuestion") - .post("/api/multiple-choice-questions") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_multipleChoiceQuestion_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created multipleChoiceQuestion") - .get("${new_multipleChoiceQuestion_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created multipleChoiceQuestion") - .delete("${new_multipleChoiceQuestion_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/MultipleChoiceQuestionStatisticGatlingTest.scala b/src/test/gatling/user-files/simulations/MultipleChoiceQuestionStatisticGatlingTest.scala deleted file mode 100644 index 405ae76de041..000000000000 --- a/src/test/gatling/user-files/simulations/MultipleChoiceQuestionStatisticGatlingTest.scala +++ /dev/null @@ -1,96 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the MultipleChoiceQuestionStatistic entity. - */ -class MultipleChoiceQuestionStatisticGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the MultipleChoiceQuestionStatistic entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all multipleChoiceQuestionStatistics") - .get("/api/multiple-choice-question-statistics") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new multipleChoiceQuestionStatistic") - .post("/api/multiple-choice-question-statistics") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_multipleChoiceQuestionStatistic_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created multipleChoiceQuestionStatistic") - .get("${new_multipleChoiceQuestionStatistic_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created multipleChoiceQuestionStatistic") - .delete("${new_multipleChoiceQuestionStatistic_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/MultipleChoiceSubmittedAnswerGatlingTest.scala b/src/test/gatling/user-files/simulations/MultipleChoiceSubmittedAnswerGatlingTest.scala deleted file mode 100644 index d4875c725880..000000000000 --- a/src/test/gatling/user-files/simulations/MultipleChoiceSubmittedAnswerGatlingTest.scala +++ /dev/null @@ -1,96 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the MultipleChoiceSubmittedAnswer entity. - */ -class MultipleChoiceSubmittedAnswerGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the MultipleChoiceSubmittedAnswer entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all multipleChoiceSubmittedAnswers") - .get("/api/multiple-choice-submitted-answers") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new multipleChoiceSubmittedAnswer") - .post("/api/multiple-choice-submitted-answers") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_multipleChoiceSubmittedAnswer_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created multipleChoiceSubmittedAnswer") - .get("${new_multipleChoiceSubmittedAnswer_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created multipleChoiceSubmittedAnswer") - .delete("${new_multipleChoiceSubmittedAnswer_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ParticipationGatlingTest.scala b/src/test/gatling/user-files/simulations/ParticipationGatlingTest.scala deleted file mode 100644 index d68481ba5ad0..000000000000 --- a/src/test/gatling/user-files/simulations/ParticipationGatlingTest.scala +++ /dev/null @@ -1,101 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the Participation entity. - */ -class ParticipationGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the Participation entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all participations") - .get("/api/participations") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new participation") - .post("/api/participations") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "repositoryUrl":"SAMPLE_TEXT" - , "buildPlanId":"SAMPLE_TEXT" - , "initializationState":"UNINITIALIZED" - , "initializationDate":"2020-01-01T00:00:00.000Z" - , "presentationScore":"0" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_participation_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created participation") - .get("${new_participation_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created participation") - .delete("${new_participation_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/PointCounterGatlingTest.scala b/src/test/gatling/user-files/simulations/PointCounterGatlingTest.scala deleted file mode 100644 index 21d5ce970818..000000000000 --- a/src/test/gatling/user-files/simulations/PointCounterGatlingTest.scala +++ /dev/null @@ -1,97 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the PointCounter entity. - */ -class PointCounterGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the PointCounter entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all pointCounters") - .get("/api/point-counters") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new pointCounter") - .post("/api/point-counters") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "points":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_pointCounter_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created pointCounter") - .get("${new_pointCounter_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created pointCounter") - .delete("${new_pointCounter_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ProgrammingExerciseGatlingTest.scala b/src/test/gatling/user-files/simulations/ProgrammingExerciseGatlingTest.scala deleted file mode 100644 index c11dfa691431..000000000000 --- a/src/test/gatling/user-files/simulations/ProgrammingExerciseGatlingTest.scala +++ /dev/null @@ -1,101 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ProgrammingExercise entity. - */ -class ProgrammingExerciseGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ProgrammingExercise entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all programmingExercises") - .get("/api/programming-exercises") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new programmingExercise") - .post("/api/programming-exercises") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "templateRepositoryUrl":"SAMPLE_TEXT" - , "solutionRepositoryUrl":"SAMPLE_TEXT" - , "templateBuildPlanId":"SAMPLE_TEXT" - , "publishBuildPlanUrl":null - , "allowOnlineEditor":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_programmingExercise_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created programmingExercise") - .get("${new_programmingExercise_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created programmingExercise") - .delete("${new_programmingExercise_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ProgrammingExerciseTestCaseGatlingTest.scala b/src/test/gatling/user-files/simulations/ProgrammingExerciseTestCaseGatlingTest.scala deleted file mode 100644 index cf52be534615..000000000000 --- a/src/test/gatling/user-files/simulations/ProgrammingExerciseTestCaseGatlingTest.scala +++ /dev/null @@ -1,99 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ProgrammingExerciseTestCase entity. - */ -class ProgrammingExerciseTestCaseGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseURL(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ProgrammingExerciseTestCase entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJSON - .check(header.get("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all programmingExerciseTestCases") - .get("/api/programming-exercise-test-cases") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new programmingExerciseTestCase") - .post("/api/programming-exercise-test-cases") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "testName":"SAMPLE_TEXT" - , "weight":"0" - , "active":null - }""")).asJSON - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_programmingExerciseTestCase_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created programmingExerciseTestCase") - .get("${new_programmingExerciseTestCase_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created programmingExerciseTestCase") - .delete("${new_programmingExerciseTestCase_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) over (Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ProgrammingSubmissionGatlingTest.scala b/src/test/gatling/user-files/simulations/ProgrammingSubmissionGatlingTest.scala deleted file mode 100644 index 7cafcb860269..000000000000 --- a/src/test/gatling/user-files/simulations/ProgrammingSubmissionGatlingTest.scala +++ /dev/null @@ -1,97 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ProgrammingSubmission entity. - */ -class ProgrammingSubmissionGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ProgrammingSubmission entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all programmingSubmissions") - .get("/api/programming-submissions") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new programmingSubmission") - .post("/api/programming-submissions") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "commitHash":"SAMPLE_TEXT" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_programmingSubmission_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created programmingSubmission") - .get("${new_programmingSubmission_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created programmingSubmission") - .delete("${new_programmingSubmission_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/QuestionGatlingTest.scala b/src/test/gatling/user-files/simulations/QuestionGatlingTest.scala deleted file mode 100644 index 93e10d9aa475..000000000000 --- a/src/test/gatling/user-files/simulations/QuestionGatlingTest.scala +++ /dev/null @@ -1,104 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the Question entity. - */ -class QuestionGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the Question entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all questions") - .get("/api/questions") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new question") - .post("/api/questions") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "title":"SAMPLE_TEXT" - , "text":"SAMPLE_TEXT" - , "hint":"SAMPLE_TEXT" - , "explanation":"SAMPLE_TEXT" - , "score":"0" - , "scoringType":"ALL_OR_NOTHING" - , "randomizeOrder":null - , "invalid":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_question_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created question") - .get("${new_question_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created question") - .delete("${new_question_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/QuestionStatisticGatlingTest.scala b/src/test/gatling/user-files/simulations/QuestionStatisticGatlingTest.scala deleted file mode 100644 index 21710d99073d..000000000000 --- a/src/test/gatling/user-files/simulations/QuestionStatisticGatlingTest.scala +++ /dev/null @@ -1,98 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the QuizQuestionStatistic entity. - */ -class QuestionStatisticGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the QuizQuestionStatistic entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all questionStatistics") - .get("/api/question-statistics") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new questionStatistic") - .post("/api/question-statistics") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "ratedCorrectCounter":"0" - , "unRatedCorrectCounter":"0" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_questionStatistic_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created questionStatistic") - .get("${new_questionStatistic_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created questionStatistic") - .delete("${new_questionStatistic_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/QuizExerciseGatlingTest.scala b/src/test/gatling/user-files/simulations/QuizExerciseGatlingTest.scala deleted file mode 100644 index 50b51d19e2fa..000000000000 --- a/src/test/gatling/user-files/simulations/QuizExerciseGatlingTest.scala +++ /dev/null @@ -1,104 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the QuizExercise entity. - */ -class QuizExerciseGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the QuizExercise entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all quizExercises") - .get("/api/quiz-exercises") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new quizExercise") - .post("/api/quiz-exercises") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "description":"SAMPLE_TEXT" - , "explanation":"SAMPLE_TEXT" - , "randomizeQuestionOrder":null - , "allowedNumberOfAttempts":"0" - , "isVisibleBeforeStart":null - , "isOpenForPractice":null - , "isPlannedToStart":null - , "duration":"0" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_quizExercise_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created quizExercise") - .get("${new_quizExercise_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created quizExercise") - .delete("${new_quizExercise_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/QuizPointStatisticGatlingTest.scala b/src/test/gatling/user-files/simulations/QuizPointStatisticGatlingTest.scala deleted file mode 100644 index f58daa55ae7b..000000000000 --- a/src/test/gatling/user-files/simulations/QuizPointStatisticGatlingTest.scala +++ /dev/null @@ -1,96 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the QuizPointStatistic entity. - */ -class QuizPointStatisticGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the QuizPointStatistic entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all quizPointStatistics") - .get("/api/quiz-point-statistics") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new quizPointStatistic") - .post("/api/quiz-point-statistics") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_quizPointStatistic_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created quizPointStatistic") - .get("${new_quizPointStatistic_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created quizPointStatistic") - .delete("${new_quizPointStatistic_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/QuizSubmissionGatlingTest.scala b/src/test/gatling/user-files/simulations/QuizSubmissionGatlingTest.scala deleted file mode 100644 index 0d26fc0d9db7..000000000000 --- a/src/test/gatling/user-files/simulations/QuizSubmissionGatlingTest.scala +++ /dev/null @@ -1,97 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the QuizSubmission entity. - */ -class QuizSubmissionGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the QuizSubmission entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all quizSubmissions") - .get("/api/quiz-submissions") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new quizSubmission") - .post("/api/quiz-submissions") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "scoreInPoints":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_quizSubmission_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created quizSubmission") - .get("${new_quizSubmission_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created quizSubmission") - .delete("${new_quizSubmission_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ShortAnswerMappingGatlingTest.scala b/src/test/gatling/user-files/simulations/ShortAnswerMappingGatlingTest.scala deleted file mode 100644 index ce11c7ae295b..000000000000 --- a/src/test/gatling/user-files/simulations/ShortAnswerMappingGatlingTest.scala +++ /dev/null @@ -1,99 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ShortAnswerMapping entity. - */ -class ShortAnswerMappingGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ShortAnswerMapping entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all shortAnswerMappings") - .get("/api/short-answer-mappings") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new shortAnswerMapping") - .post("/api/short-answer-mappings") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "shortAnswerSpotIndex":"0" - , "shortAnswerSolutionIndex":"0" - , "invalid":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_shortAnswerMapping_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created shortAnswerMapping") - .get("${new_shortAnswerMapping_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created shortAnswerMapping") - .delete("${new_shortAnswerMapping_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ShortAnswerQuestionGatlingTest.scala b/src/test/gatling/user-files/simulations/ShortAnswerQuestionGatlingTest.scala deleted file mode 100644 index 1d5820c9c289..000000000000 --- a/src/test/gatling/user-files/simulations/ShortAnswerQuestionGatlingTest.scala +++ /dev/null @@ -1,96 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ShortAnswerQuestion entity. - */ -class ShortAnswerQuestionGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ShortAnswerQuestion entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all shortAnswerQuestions") - .get("/api/short-answer-questions") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new shortAnswerQuestion") - .post("/api/short-answer-questions") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_shortAnswerQuestion_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created shortAnswerQuestion") - .get("${new_shortAnswerQuestion_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created shortAnswerQuestion") - .delete("${new_shortAnswerQuestion_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ShortAnswerQuestionStatisticGatlingTest.scala b/src/test/gatling/user-files/simulations/ShortAnswerQuestionStatisticGatlingTest.scala deleted file mode 100644 index 921b2d7de247..000000000000 --- a/src/test/gatling/user-files/simulations/ShortAnswerQuestionStatisticGatlingTest.scala +++ /dev/null @@ -1,96 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ShortAnswerQuestionStatistic entity. - */ -class ShortAnswerQuestionStatisticGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ShortAnswerQuestionStatistic entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all shortAnswerQuestionStatistics") - .get("/api/short-answer-question-statistics") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new shortAnswerQuestionStatistic") - .post("/api/short-answer-question-statistics") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_shortAnswerQuestionStatistic_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created shortAnswerQuestionStatistic") - .get("${new_shortAnswerQuestionStatistic_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created shortAnswerQuestionStatistic") - .delete("${new_shortAnswerQuestionStatistic_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ShortAnswerSolutionGatlingTest.scala b/src/test/gatling/user-files/simulations/ShortAnswerSolutionGatlingTest.scala deleted file mode 100644 index 1de14f1023f7..000000000000 --- a/src/test/gatling/user-files/simulations/ShortAnswerSolutionGatlingTest.scala +++ /dev/null @@ -1,98 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ShortAnswerSolution entity. - */ -class ShortAnswerSolutionGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ShortAnswerSolution entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all shortAnswerSolutions") - .get("/api/short-answer-solutions") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new shortAnswerSolution") - .post("/api/short-answer-solutions") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "text":"SAMPLE_TEXT" - , "invalid":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_shortAnswerSolution_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created shortAnswerSolution") - .get("${new_shortAnswerSolution_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created shortAnswerSolution") - .delete("${new_shortAnswerSolution_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ShortAnswerSpotCounterGatlingTest.scala b/src/test/gatling/user-files/simulations/ShortAnswerSpotCounterGatlingTest.scala deleted file mode 100644 index 545dbf71f6d8..000000000000 --- a/src/test/gatling/user-files/simulations/ShortAnswerSpotCounterGatlingTest.scala +++ /dev/null @@ -1,96 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ShortAnswerSpotCounter entity. - */ -class ShortAnswerSpotCounterGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ShortAnswerSpotCounter entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all shortAnswerSpotCounters") - .get("/api/short-answer-spot-counters") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new shortAnswerSpotCounter") - .post("/api/short-answer-spot-counters") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_shortAnswerSpotCounter_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created shortAnswerSpotCounter") - .get("${new_shortAnswerSpotCounter_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created shortAnswerSpotCounter") - .delete("${new_shortAnswerSpotCounter_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ShortAnswerSpotGatlingTest.scala b/src/test/gatling/user-files/simulations/ShortAnswerSpotGatlingTest.scala deleted file mode 100644 index c67a0fe32d78..000000000000 --- a/src/test/gatling/user-files/simulations/ShortAnswerSpotGatlingTest.scala +++ /dev/null @@ -1,98 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ShortAnswerSpot entity. - */ -class ShortAnswerSpotGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ShortAnswerSpot entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all shortAnswerSpots") - .get("/api/short-answer-spots") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new shortAnswerSpot") - .post("/api/short-answer-spots") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "width":"0" - , "invalid":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_shortAnswerSpot_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created shortAnswerSpot") - .get("${new_shortAnswerSpot_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created shortAnswerSpot") - .delete("${new_shortAnswerSpot_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ShortAnswerSubmittedAnswerGatlingTest.scala b/src/test/gatling/user-files/simulations/ShortAnswerSubmittedAnswerGatlingTest.scala deleted file mode 100644 index a83a614d2579..000000000000 --- a/src/test/gatling/user-files/simulations/ShortAnswerSubmittedAnswerGatlingTest.scala +++ /dev/null @@ -1,96 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ShortAnswerSubmittedAnswer entity. - */ -class ShortAnswerSubmittedAnswerGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ShortAnswerSubmittedAnswer entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all shortAnswerSubmittedAnswers") - .get("/api/short-answer-submitted-answers") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new shortAnswerSubmittedAnswer") - .post("/api/short-answer-submitted-answers") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_shortAnswerSubmittedAnswer_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created shortAnswerSubmittedAnswer") - .get("${new_shortAnswerSubmittedAnswer_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created shortAnswerSubmittedAnswer") - .delete("${new_shortAnswerSubmittedAnswer_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/ShortAnswerSubmittedTextGatlingTest.scala b/src/test/gatling/user-files/simulations/ShortAnswerSubmittedTextGatlingTest.scala deleted file mode 100644 index d8e0f33ac7ec..000000000000 --- a/src/test/gatling/user-files/simulations/ShortAnswerSubmittedTextGatlingTest.scala +++ /dev/null @@ -1,97 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the ShortAnswerSubmittedText entity. - */ -class ShortAnswerSubmittedTextGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the ShortAnswerSubmittedText entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all shortAnswerSubmittedTexts") - .get("/api/short-answer-submitted-texts") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new shortAnswerSubmittedText") - .post("/api/short-answer-submitted-texts") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "text":"SAMPLE_TEXT" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_shortAnswerSubmittedText_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created shortAnswerSubmittedText") - .get("${new_shortAnswerSubmittedText_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created shortAnswerSubmittedText") - .delete("${new_shortAnswerSubmittedText_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/StatisticCounterGatlingTest.scala b/src/test/gatling/user-files/simulations/StatisticCounterGatlingTest.scala deleted file mode 100644 index 0f166231756e..000000000000 --- a/src/test/gatling/user-files/simulations/StatisticCounterGatlingTest.scala +++ /dev/null @@ -1,98 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the QuizStatisticCounter entity. - */ -class StatisticCounterGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the QuizStatisticCounter entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all statisticCounters") - .get("/api/statistic-counters") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new statisticCounter") - .post("/api/statistic-counters") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "ratedCounter":"0" - , "unRatedCounter":"0" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_statisticCounter_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created statisticCounter") - .get("${new_statisticCounter_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created statisticCounter") - .delete("${new_statisticCounter_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/StatisticGatlingTest.scala b/src/test/gatling/user-files/simulations/StatisticGatlingTest.scala deleted file mode 100644 index 3efaeed2c5e7..000000000000 --- a/src/test/gatling/user-files/simulations/StatisticGatlingTest.scala +++ /dev/null @@ -1,99 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the QuizStatistic entity. - */ -class StatisticGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the QuizStatistic entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all statistics") - .get("/api/statistics") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new statistic") - .post("/api/statistics") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "released":null - , "participantsRated":"0" - , "participantsUnrated":"0" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_statistic_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created statistic") - .get("${new_statistic_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created statistic") - .delete("${new_statistic_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/SubmissionGatlingTest.scala b/src/test/gatling/user-files/simulations/SubmissionGatlingTest.scala deleted file mode 100644 index f3d1ed3d528f..000000000000 --- a/src/test/gatling/user-files/simulations/SubmissionGatlingTest.scala +++ /dev/null @@ -1,99 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the Submission entity. - */ -class SubmissionGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the Submission entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all submissions") - .get("/api/submissions") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new submission") - .post("/api/submissions") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "submitted":null - , "submissionDate":"2020-01-01T00:00:00.000Z" - , "type":"MANUAL" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_submission_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created submission") - .get("${new_submission_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created submission") - .delete("${new_submission_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/SubmittedAnswerGatlingTest.scala b/src/test/gatling/user-files/simulations/SubmittedAnswerGatlingTest.scala deleted file mode 100644 index 6cf4b6cbaaa3..000000000000 --- a/src/test/gatling/user-files/simulations/SubmittedAnswerGatlingTest.scala +++ /dev/null @@ -1,97 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the SubmittedAnswer entity. - */ -class SubmittedAnswerGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the SubmittedAnswer entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all submittedAnswers") - .get("/api/submitted-answers") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new submittedAnswer") - .post("/api/submitted-answers") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "scoreInPoints":null - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_submittedAnswer_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created submittedAnswer") - .get("${new_submittedAnswer_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created submittedAnswer") - .delete("${new_submittedAnswer_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/TextExerciseGatlingTest.scala b/src/test/gatling/user-files/simulations/TextExerciseGatlingTest.scala deleted file mode 100644 index 285e53902f56..000000000000 --- a/src/test/gatling/user-files/simulations/TextExerciseGatlingTest.scala +++ /dev/null @@ -1,97 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the TextExercise entity. - */ -class TextExerciseGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the TextExercise entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all textExercises") - .get("/api/text-exercises") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new textExercise") - .post("/api/text-exercises") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "exampleSolution":"SAMPLE_TEXT" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_textExercise_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created textExercise") - .get("${new_textExercise_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created textExercise") - .delete("${new_textExercise_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/gatling/user-files/simulations/TextSubmissionGatlingTest.scala b/src/test/gatling/user-files/simulations/TextSubmissionGatlingTest.scala deleted file mode 100644 index 53d244177332..000000000000 --- a/src/test/gatling/user-files/simulations/TextSubmissionGatlingTest.scala +++ /dev/null @@ -1,97 +0,0 @@ -import _root_.io.gatling.core.scenario.Simulation -import ch.qos.logback.classic.{Level, LoggerContext} -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ -import org.slf4j.LoggerFactory - -import scala.concurrent.duration._ - -/** - * Performance test for the TextSubmission entity. - */ -class TextSubmissionGatlingTest extends Simulation { - - val context: LoggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext] - // Log all HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("TRACE")) - // Log failed HTTP requests - //context.getLogger("io.gatling.http").setLevel(Level.valueOf("DEBUG")) - - val baseURL = Option(System.getProperty("baseURL")) getOrElse """http://localhost:8080""" - - val httpConf = http - .baseUrl(baseURL) - .inferHtmlResources() - .acceptHeader("*/*") - .acceptEncodingHeader("gzip, deflate") - .acceptLanguageHeader("fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3") - .connectionHeader("keep-alive") - .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:33.0) Gecko/20100101 Firefox/33.0") - .silentResources // Silence all resources like css or css so they don't clutter the results - - val headers_http = Map( - "Accept" -> """application/json""" - ) - - val headers_http_authentication = Map( - "Content-Type" -> """application/json""", - "Accept" -> """application/json""" - ) - - val headers_http_authenticated = Map( - "Accept" -> """application/json""", - "Authorization" -> "${access_token}" - ) - - val scn = scenario("Test the TextSubmission entity") - .exec(http("First unauthenticated request") - .get("/api/public/account") - .headers(headers_http) - .check(status.is(401)) - ).exitHereIfFailed - .pause(10) - .exec(http("Authentication") - .post("/api/public/authenticate") - .headers(headers_http_authentication) - .body(StringBody("""{"username":"admin", "password":"admin"}""")).asJson - .check(header("Authorization").saveAs("access_token"))).exitHereIfFailed - .pause(2) - .exec(http("Authenticated request") - .get("/api/public/account") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10) - .repeat(2) { - exec(http("Get all textSubmissions") - .get("/api/text-submissions") - .headers(headers_http_authenticated) - .check(status.is(200))) - .pause(10 seconds, 20 seconds) - .exec(http("Create new textSubmission") - .post("/api/text-submissions") - .headers(headers_http_authenticated) - .body(StringBody("""{ - "id":null - , "text":"SAMPLE_TEXT" - }""")).asJson - .check(status.is(201)) - .check(headerRegex("Location", "(.*)").saveAs("new_textSubmission_url"))).exitHereIfFailed - .pause(10) - .repeat(5) { - exec(http("Get created textSubmission") - .get("${new_textSubmission_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - .exec(http("Delete created textSubmission") - .delete("${new_textSubmission_url}") - .headers(headers_http_authenticated)) - .pause(10) - } - - val users = scenario("Users").exec(scn) - - setUp( - users.inject(rampUsers(Integer.getInteger("users", 100)) during(Integer.getInteger("ramp", 1) minutes)) - ).protocols(httpConf) -} diff --git a/src/test/k6/CodeEditor.js b/src/test/k6/CodeEditor.js deleted file mode 100644 index b80b972b7dd1..000000000000 --- a/src/test/k6/CodeEditor.js +++ /dev/null @@ -1,79 +0,0 @@ -import { login } from './requests/requests.js'; -import { group, sleep } from 'k6'; -import { deleteCourse, newCourse } from './requests/course.js'; -import { createExercise, ParticipationSimulation, simulateSubmission, TestResult } from './requests/programmingExercise.js'; -import { buildErrorContentJava } from './resource/constants_java.js'; -import { startExercise, deleteExercise } from './requests/exercises.js'; -// Version: 1.1 -// Creator: Firefox -// Browser: Firefox - -export let options = { - maxRedirects: 0, - iterations: __ENV.ITERATIONS, - vus: __ENV.ITERATIONS, - rps: 5, -}; - -const adminUsername = __ENV.ADMIN_USERNAME; -const adminPassword = __ENV.ADMIN_PASSWORD; -let baseUsername = __ENV.BASE_USERNAME; -let basePassword = __ENV.BASE_PASSWORD; -// Use users with ID >= 100 to avoid manual testers entering the wrong password too many times interfering with tests -const userIdOffset = 99; - -export function setup() { - // Create course as admin - const artemisAdmin = login(adminUsername, adminPassword); - const courseId = newCourse(artemisAdmin).id; - - const instructorUsername = baseUsername.replace('USERID', '101'); - const instructorPassword = basePassword.replace('USERID', '101'); - - // Login to Artemis - const artemisInstructor = login(instructorUsername, instructorPassword); - - // Create new exercise - const exerciseId = createExercise(artemisInstructor, courseId); - - return { exerciseId: exerciseId, courseId: courseId }; -} - -export default function (data) { - const websocketConnectionTime = parseFloat(__ENV.TIMEOUT_PARTICIPATION); // Time in seconds the websocket is kept open, if set to 0 no websocket connection is estahblished - - // Delay so that not all users start at the same time, batches of 3 users per second - const delay = Math.floor(__VU / 3); - sleep(delay); - - group('Artemis Programming Exercise Participation Loadtest', function () { - // The user is randomly selected - const userId = __VU + userIdOffset; - const currentUsername = baseUsername.replace('USERID', userId); - const currentPassword = basePassword.replace('USERID', userId); - const artemis = login(currentUsername, currentPassword); - - // Start exercise - const participationId = startExercise(artemis, data.exerciseId); - - // Initiate websocket connection if connection time is set to value greater than 0 - if (websocketConnectionTime > 0) { - if (participationId) { - const simulation = new ParticipationSimulation(websocketConnectionTime, data.exerciseId, participationId, buildErrorContentJava); - simulateSubmission(artemis, simulation, TestResult.BUILD_ERROR); - } - sleep(websocketConnectionTime - delay); - } - }); - - return data; -} - -export function teardown(data) { - const artemis = login(adminUsername, adminPassword); - const courseId = data.courseId; - const exerciseId = data.exerciseId; - - deleteExercise(artemis, exerciseId); - deleteCourse(artemis, courseId); -} diff --git a/src/test/k6/ExamAPIs.js b/src/test/k6/ExamAPIs.js deleted file mode 100644 index 0dc08a3f2bda..000000000000 --- a/src/test/k6/ExamAPIs.js +++ /dev/null @@ -1,263 +0,0 @@ -import { login } from './requests/requests.js'; -import { group, sleep } from 'k6'; -import { newCourse, deleteCourse, addUserToInstructorsInCourse } from './requests/course.js'; -import { createUsersIfNeeded } from './requests/user.js'; -import { createQuizExercise, submitRandomAnswerRESTExam } from './requests/quiz.js'; -import { - newExam, - newExerciseGroup, - addUserToStudentsInExam, - generateExams, - startExercises, - getExamForUser, - getStudentExams, - updateWorkingTime, - evaluateQuizzes, - startStudentExamForUser, - submitExam, -} from './requests/exam.js'; -import { submitRandomTextAnswerExam, newTextExercise } from './requests/text.js'; -import { newModelingExercise, submitRandomModelingAnswerExam } from './requests/modeling.js'; -import { createProgrammingExercise, ParticipationSimulation, simulateSubmission, TestResult } from './requests/programmingExercise.js'; -import { someSuccessfulErrorContentJava, allSuccessfulContentJava, buildErrorContentJava } from './resource/constants_java.js'; - -// Version: 1.1 -// Creator: Firefox -// Browser: Firefox - -export let options = { - maxRedirects: 0, - iterations: __ENV.ITERATIONS, - vus: __ENV.ITERATIONS, - rps: 5, - setupTimeout: '480s', - teardownTimeout: '240s', -}; - -const adminUsername = __ENV.ADMIN_USERNAME; -const adminPassword = __ENV.ADMIN_PASSWORD; -let baseUsername = __ENV.BASE_USERNAME; -let basePassword = __ENV.BASE_PASSWORD; -let userOffset = parseInt(__ENV.USER_OFFSET); -const onlyPrepare = __ENV.ONLY_PREPARE === true || __ENV.ONLY_PREPARE === 'true'; -// Use users with ID >= 100 to avoid manual testers entering the wrong password too many times interfering with tests -const userIdOffset = 99; - -export function setup() { - console.log('__ENV.CREATE_USERS: ' + __ENV.CREATE_USERS); - console.log('__ENV.TIMEOUT_PARTICIPATION: ' + __ENV.TIMEOUT_PARTICIPATION); - console.log('__ENV.TIMEOUT_EXERCISE: ' + __ENV.TIMEOUT_EXERCISE); - console.log('__ENV.ITERATIONS: ' + __ENV.ITERATIONS); - console.log('__ENV.USER_OFFSET: ' + __ENV.USER_OFFSET); - console.log('__ENV.ONLY_PREPARE: ' + onlyPrepare); - - const iterations = parseInt(__ENV.ITERATIONS); - - if (parseInt(__ENV.COURSE_ID) === 0 || parseInt(__ENV.EXERCISE_ID) === 0) { - console.log('Creating new course and exercise as no parameters are given'); - - // Create course - const artemisAdmin = login(adminUsername, adminPassword); - - const course = newCourse(artemisAdmin); - - createUsersIfNeeded(artemisAdmin, baseUsername, basePassword, adminUsername, adminPassword, course, userOffset); - - const instructorUsername = baseUsername.replace('USERID', '101'); - const instructorPassword = basePassword.replace('USERID', '101'); - - addUserToInstructorsInCourse(artemisAdmin, instructorUsername, course.id); - - // Login to Artemis - const artemis = login(instructorUsername, instructorPassword); - - // it might be necessary that the newly created groups or accounts are synced with the version control and continuous integration servers, so we wait for 1 minute - const timeoutExercise = parseFloat(__ENV.TIMEOUT_EXERCISE); - if (timeoutExercise > 0) { - console.log('Wait ' + timeoutExercise + 's before creating the exam so that the setup can finish properly'); - sleep(timeoutExercise); - } - - // Create new exam - const exam = newExam(artemis, course); - - const exerciseGroup1 = newExerciseGroup(artemis, exam); - const exerciseGroup2 = newExerciseGroup(artemis, exam); - const exerciseGroup3 = newExerciseGroup(artemis, exam); - const exerciseGroup4 = newExerciseGroup(artemis, exam); - - newTextExercise(artemis, exerciseGroup1); - - createQuizExercise(artemis, undefined, exerciseGroup2, false, false); - - newModelingExercise(artemis, exerciseGroup3); - - createProgrammingExercise(artemis, undefined, exerciseGroup4, 'JAVA', false); - - for (let i = 1 + userIdOffset; i <= iterations + userIdOffset; i++) { - addUserToStudentsInExam(artemis, baseUsername.replace('USERID', i + userOffset), exam); - } - - generateExams(artemis, exam); - - const studentExams = getStudentExams(artemis, exam); - - for (let index in studentExams) { - if (index % 10 === 0) { - const studentExam = studentExams[index]; - updateWorkingTime(artemis, exam, studentExam, 180); - } - } - - if (onlyPrepare) { - return; - } - - startExercises(artemis, exam); - - sleep(2); - - return { courseId: exam.course.id, examId: exam.id }; - } else { - console.log('Using existing course and exercise'); - return { examId: parseInt(__ENV.EXERCISE_ID), courseId: parseInt(__ENV.COURSE_ID) }; - } -} - -export default function (data) { - if (onlyPrepare) { - return; - } - const websocketConnectionTime = parseFloat(__ENV.TIMEOUT_PARTICIPATION); // Time in seconds the websocket is kept open, if set to 0 no websocket connection is estahblished - - // Delay so that not all users start at the same time, batches of 50 users per second - const delay = Math.floor(__VU / 50); - sleep(delay); - - group('Artemis Exam Stresstest', function () { - const userId = parseInt(__VU) + userOffset + userIdOffset; - const currentUsername = baseUsername.replace('USERID', userId); - const currentPassword = basePassword.replace('USERID', userId); - const artemis = login(currentUsername, currentPassword); - - sleep(30); - - const studentExamId = startStudentExamForUser(artemis, data.courseId, data.examId).id; - - const studentExam = getExamForUser(artemis, data.courseId, data.examId, studentExamId); - - console.log(studentExam.exam.startDate); - const parsedStartDate = new Date(Date.parse(studentExam.exam.startDate)); - - console.log(parsedStartDate); - - const currentTime = Date.now(); - const differenceInMilliSeconds = parsedStartDate - currentTime; - const timeUntilExamStart = differenceInMilliSeconds / 1000; - - console.log(`Waiting ${timeUntilExamStart}s for exam start`); - - const workingTime = studentExam.workingTime; - const individualEndDate = new Date(parsedStartDate.getTime() + workingTime * 1000); - - artemis.websocket(function (socket) { - socket.setInterval(function timeout() { - socket.close(); - }, individualEndDate.getTime() - Date.now()); - - socket.setTimeout(function timeout() { - console.log('Sleeping now'); - - sleep(1 + timeUntilExamStart); - - console.log('Individual end date: ' + individualEndDate.toISOString()); - - console.log('Remaining: ' + (individualEndDate.getTime() - Date.now())); - - let programmingSubmissionCounter = 0; - endDateLoop: while (true) { - let submissions, submissionId; - for (const exercise of studentExam.exercises) { - if (individualEndDate.getTime() - Date.now() < 5000) { - console.log(`End date is reached`); - break endDateLoop; - } - console.log(`Exercise is of type ${exercise.type}`); - let studentParticipations = exercise.studentParticipations; - - switch (exercise.type) { - case 'quiz': - submissions = studentParticipations[0].submissions; - submissionId = submissions[0].id; - submissions[0] = submitRandomAnswerRESTExam(artemis, exercise, 10, submissionId); - break; - - case 'text': - submissions = studentParticipations[0].submissions; - submissionId = submissions[0].id; - submissions[0] = submitRandomTextAnswerExam(artemis, exercise, submissionId); - break; - - case 'modeling': - submissions = studentParticipations[0].submissions; - submissionId = submissions[0].id; - submissions[0] = submitRandomModelingAnswerExam(artemis, exercise, submissionId); - break; - - case 'programming': - console.log('Programming submission counter is ' + programmingSubmissionCounter); - if (programmingSubmissionCounter === 0) { - let simulation = new ParticipationSimulation( - websocketConnectionTime, - exercise.id, - studentParticipations[0].id, - someSuccessfulErrorContentJava(false), - ); - simulateSubmission(artemis, simulation, TestResult.FAIL, '2 of 13 passed'); - } else if (programmingSubmissionCounter === 1) { - let simulation = new ParticipationSimulation(websocketConnectionTime, exercise.id, studentParticipations[0].id, allSuccessfulContentJava); - simulateSubmission(artemis, simulation, TestResult.SUCCESS); - } else if (programmingSubmissionCounter >= 2) { - let simulation = new ParticipationSimulation(websocketConnectionTime, exercise.id, studentParticipations[0].id, buildErrorContentJava); - simulateSubmission(artemis, simulation, TestResult.BUILD_ERROR); - } - programmingSubmissionCounter++; - sleep(40); - break; - } - sleep(10); - } - } - - studentExam.started = true; - console.log('Submitting exam: ' + JSON.stringify(studentExam)); - submitExam(artemis, data.courseId, data.examId, studentExam); - }, 1000); - }); - - // console.log('Received EXAM ' + JSON.stringify(studentExam) + ' for user ' + baseUsername.replace('USERID', userId)); - }); - - return data; -} - -export function teardown(data) { - if (onlyPrepare) { - return; - } - const instructorUsername = baseUsername.replace('USERID', '101'); - const instructorPassword = basePassword.replace('USERID', '101'); - - const artemis = login(instructorUsername, instructorPassword); - - sleep(5); - evaluateQuizzes(artemis, data.courseId, data.examId); - - const shouldCleanup = __ENV.CLEANUP === true || __ENV.CLEANUP === 'true'; - if (shouldCleanup) { - const artemis = login(adminUsername, adminPassword); - const courseId = data.courseId; - - deleteCourse(artemis, courseId); - } -} diff --git a/src/test/k6/ModelingExerciseAPIs.js b/src/test/k6/ModelingExerciseAPIs.js deleted file mode 100644 index ed84b545d2a7..000000000000 --- a/src/test/k6/ModelingExerciseAPIs.js +++ /dev/null @@ -1,164 +0,0 @@ -import { group, sleep } from 'k6'; -import { addUserToInstructorsInCourse, deleteCourse, newCourse } from './requests/course.js'; -import { assessModelingSubmission, newModelingExercise, submitRandomModelingAnswerExam, updateModelingExerciseDueDate } from './requests/modeling.js'; -import { startExercise, getExercise, startTutorParticipation, deleteExercise, getAndLockSubmission } from './requests/exercises.js'; -import { login } from './requests/requests.js'; -import { createUsersIfNeeded } from './requests/user.js'; -import { MODELING_EXERCISE, MODELING_SUBMISSION_WITHOUT_ASSESSMENT } from './requests/endpoints.js'; - -// Version: 1.1 -// Creator: Firefox -// Browser: Firefox - -export let options = { - maxRedirects: 0, - iterations: __ENV.ITERATIONS, - vus: __ENV.ITERATIONS, - rps: 5, - setupTimeout: '480s', - teardownTimeout: '240s', -}; - -const adminUsername = __ENV.ADMIN_USERNAME; -const adminPassword = __ENV.ADMIN_PASSWORD; -let baseUsername = __ENV.BASE_USERNAME; -let basePassword = __ENV.BASE_PASSWORD; -let userOffset = parseInt(__ENV.USER_OFFSET); -const onlyPrepare = __ENV.ONLY_PREPARE === true || __ENV.ONLY_PREPARE === 'true'; -// Use users with ID >= 100 to avoid manual testers entering the wrong password too many times interfering with tests -const userIdOffset = 99; - -export function setup() { - console.log('__ENV.CREATE_USERS: ' + __ENV.CREATE_USERS); - console.log('__ENV.TIMEOUT_PARTICIPATION: ' + __ENV.TIMEOUT_PARTICIPATION); - console.log('__ENV.TIMEOUT_EXERCISE: ' + __ENV.TIMEOUT_EXERCISE); - console.log('__ENV.ITERATIONS: ' + __ENV.ITERATIONS); - console.log('__ENV.USER_OFFSET: ' + __ENV.USER_OFFSET); - console.log('__ENV.ONLY_PREPARE: ' + onlyPrepare); - - let courseId; - let exerciseId; - let artemis; - let exercise; - const iterations = parseInt(__ENV.ITERATIONS); - // Create course - const instructorUsername = baseUsername.replace('USERID', '101'); - const instructorPassword = basePassword.replace('USERID', '101'); - if (parseInt(__ENV.COURSE_ID) === 0 || parseInt(__ENV.EXERCISE_ID) === 0) { - console.log('Creating new exercise as no parameters are given'); - - // Create course - const artemisAdmin = login(adminUsername, adminPassword); - - const course = newCourse(artemisAdmin); - courseId = course.id; - console.log('Create users with ids starting from ' + userOffset + ' and up to ' + (userOffset + iterations)); - createUsersIfNeeded(artemisAdmin, baseUsername, basePassword, adminUsername, adminPassword, course, userOffset); - console.log('Create users with ids starting from ' + (userOffset + iterations) + ' and up to ' + (userOffset + iterations + iterations)); - createUsersIfNeeded(artemisAdmin, baseUsername, basePassword, adminUsername, adminPassword, course, userOffset + iterations, true); - - console.log('Assigning ' + instructorUsername + 'to course ' + course.id + ' as the instructor'); - addUserToInstructorsInCourse(artemisAdmin, instructorUsername, course.id); - - // Login to Artemis - artemis = login(instructorUsername, instructorPassword); - - // it might be necessary that the newly created groups or accounts are synced with the version control and continuous integration servers, so we wait for 1 minute - const timeoutExercise = parseFloat(__ENV.TIMEOUT_EXERCISE); - if (timeoutExercise > 0) { - console.log('Wait ' + timeoutExercise + 's before creating the exam so that the setup can finish properly'); - sleep(timeoutExercise); - } - - // Create new exercise - exercise = newModelingExercise(artemis, undefined, course.id); - exerciseId = exercise.id; - console.log('Created exercise with id ' + exerciseId); - - sleep(2); - } else { - exerciseId = parseInt(__ENV.EXERCISE_ID); - courseId = parseInt(__ENV.COURSE_ID); - artemis = login(adminUsername, adminPassword); - - console.log('Getting exercise'); - exercise = getExercise(artemis, exerciseId, MODELING_EXERCISE(exerciseId)); - } - - for (let i = 1 + userIdOffset; i <= iterations + userIdOffset; i++) { - console.log(userOffset); - const userId = parseInt(__VU) + userOffset + i; - const currentUsername = baseUsername.replace('USERID', userId); - const currentPassword = basePassword.replace('USERID', userId); - console.log('Logging in as user ' + currentUsername); - artemis = login(currentUsername, currentPassword); - // Delay so that not all users start at the same time, batches of 3 users per second - const delay = Math.floor(__VU / 3); - sleep(delay * 3); - - console.log('Starting exercise ' + exerciseId); - let participation = startExercise(artemis, exerciseId); - if (participation) { - const submissionId = participation.submissions[0].id; - console.log('Submitting submission ' + submissionId); - submitRandomModelingAnswerExam(artemis, exercise, submissionId); - } - sleep(1); - } - - sleep(2); - - // Login to Artemis - artemis = login(instructorUsername, instructorPassword); - - updateModelingExerciseDueDate(artemis, exercise); - - sleep(30); - - console.log('Using existing course ' + courseId + ' and exercise ' + exerciseId); - return { exerciseId, courseId }; -} - -export default function (data) { - // The user id (1, 2, 3) is stored in __VU - const iterations = parseInt(__ENV.ITERATIONS); - const userId = parseInt(__VU) + userOffset + iterations + userIdOffset; - const currentUsername = baseUsername.replace('USERID', userId); - const currentPassword = basePassword.replace('USERID', userId); - - console.log('Logging in as user ' + currentUsername); - const artemis = login(currentUsername, currentPassword); - const exerciseId = data.exerciseId; - - // Delay so that not all users start at the same time, batches of 3 users per second - const delay = Math.floor(__VU / 3); - sleep(delay * 3); - - group('Assess modeling submissions', function () { - console.log('Start participation for tutor ' + currentUsername); - let participation = startTutorParticipation(artemis, exerciseId); - if (participation) { - console.log('Get and lock modeling submission for tutor ' + userId + ' and exercise'); - const submission = getAndLockSubmission(artemis, exerciseId, MODELING_SUBMISSION_WITHOUT_ASSESSMENT(exerciseId)); - const submissionId = submission.id; - console.log('Assess modeling submission ' + submissionId); - console.log('Result before manual assessment ' + JSON.stringify(submission.results[0])); - assessModelingSubmission(artemis, submissionId, submission.results[0].id); - } - sleep(1); - }); - - return data; -} - -export function teardown(data) { - const shouldCleanup = __ENV.CLEANUP === true || __ENV.CLEANUP === 'true'; - if (shouldCleanup) { - const artemis = login(adminUsername, adminPassword); - const courseId = data.courseId; - const exerciseId = data.exerciseId; - - deleteExercise(artemis, exerciseId, MODELING_EXERCISE(exerciseId)); - deleteCourse(artemis, courseId); - } -} diff --git a/src/test/k6/ProgrammingExerciseAPIs.js b/src/test/k6/ProgrammingExerciseAPIs.js deleted file mode 100644 index 162d280b8c9b..000000000000 --- a/src/test/k6/ProgrammingExerciseAPIs.js +++ /dev/null @@ -1,164 +0,0 @@ -import { group, sleep } from 'k6'; -import { login } from './requests/requests.js'; -import { - createProgrammingExercise, - configureScaCategories, - getScaCategories, - simulateSubmission, - ParticipationSimulation, - TestResult, - deleteProgrammingExercise, -} from './requests/programmingExercise.js'; -import { startExercise } from './requests/exercises.js'; -import { addUserToInstructorsInCourse, deleteCourse, newCourse } from './requests/course.js'; -import { createUsersIfNeeded } from './requests/user.js'; -import { allSuccessfulContentJava, buildErrorContentJava, someSuccessfulErrorContentJava } from './resource/constants_java.js'; -import { allSuccessfulContentPython, buildErrorContentPython, someSuccessfulErrorContentPython } from './resource/constants_python.js'; -import { allSuccessfulContentC, buildErrorContentC, someSuccessfulErrorContentC } from './resource/constants_c.js'; - -export const options = { - maxRedirects: 0, - iterations: __ENV.ITERATIONS, - vus: __ENV.ITERATIONS, - rps: 4, - setupTimeout: '240s', - teardownTimeout: '240s', -}; - -const adminUsername = __ENV.ADMIN_USERNAME; -const adminPassword = __ENV.ADMIN_PASSWORD; -const baseUsername = __ENV.BASE_USERNAME; -const basePassword = __ENV.BASE_PASSWORD; -const userOffset = parseInt(__ENV.USER_OFFSET); -const programmingLanguage = __ENV.PROGRAMMING_LANGUAGE; -const enableSCA = __ENV.ENABLE_SCA === 'true'; -// Use users with ID >= 100 to avoid manual testers entering the wrong password too many times interfering with tests -const userIdOffset = 99; - -export function setup() { - console.log('__ENV.CREATE_USERS: ' + __ENV.CREATE_USERS); - console.log('__ENV.TIMEOUT_PARTICIPATION: ' + __ENV.TIMEOUT_PARTICIPATION); - console.log('__ENV.TIMEOUT_EXERCISE: ' + __ENV.TIMEOUT_EXERCISE); - console.log('__ENV.ITERATIONS: ' + __ENV.ITERATIONS); - - let artemis, exerciseId, course; - - if (parseInt(__ENV.COURSE_ID) === 0 || parseInt(__ENV.EXERCISE_ID) === 0) { - console.log('Creating new course and exercise as no parameters are given'); - - // Create course - artemis = login(adminUsername, adminPassword); - - course = newCourse(artemis); - - createUsersIfNeeded(artemis, baseUsername, basePassword, adminUsername, adminPassword, course, userOffset); - - const instructorUsername = baseUsername.replace('USERID', '101'); - const instructorPassword = basePassword.replace('USERID', '101'); - - addUserToInstructorsInCourse(artemis, instructorUsername, course.id); - - // Login to Artemis - artemis = login(instructorUsername, instructorPassword); - - // it might be necessary that the newly created groups or accounts are synced with the version control and continuous integration servers, so we wait for 1 minute - const timeoutExercise = parseFloat(__ENV.TIMEOUT_EXERCISE); - if (timeoutExercise > 0) { - console.log('Wait ' + timeoutExercise + 's before creating the programming exercise so that the setup can finish properly'); - sleep(timeoutExercise); - } - - // Create new exercise - exerciseId = createProgrammingExercise(artemis, course.id, undefined, programmingLanguage, enableSCA); - - // Wait some time for builds to finish and test results to come in - sleep(20); - - if (enableSCA) { - // Get SCA categories - const scaCategories = getScaCategories(artemis, exerciseId, programmingLanguage); - - // Configure SCA categories - configureScaCategories(artemis, exerciseId, scaCategories, programmingLanguage); - sleep(2); - } - - return { exerciseId: exerciseId, courseId: course.id }; - } else { - console.log('Using existing course and exercise'); - return { exerciseId: parseInt(__ENV.EXERCISE_ID), courseId: parseInt(__ENV.COURSE_ID) }; - } -} - -export default function (data) { - // The user id (1, 2, 3) is stored in __VU - const userId = parseInt(__VU) + userOffset + userIdOffset; - const currentUsername = baseUsername.replace('USERID', userId); - const currentPassword = basePassword.replace('USERID', userId); - const artemis = login(currentUsername, currentPassword); - const exerciseId = data.exerciseId; - const courseId = data.courseId; - const timeoutParticipation = parseFloat(__ENV.TIMEOUT_PARTICIPATION); - - // Delay so that not all users start at the same time, batches of 3 users per second - const startTime = new Date().getTime(); - const delay = Math.floor(__VU / 3); - sleep(delay * 3); - - let someSuccessfulErrorContent, allSuccessfulContent, buildErrorContent, somePassedString; - switch (programmingLanguage) { - case 'JAVA': - someSuccessfulErrorContent = someSuccessfulErrorContentJava(enableSCA); - allSuccessfulContent = allSuccessfulContentJava; - buildErrorContent = buildErrorContentJava; - somePassedString = enableSCA ? '3 of 13 passed, 4 issues' : '2 of 13 passed'; - break; - case 'PYTHON': - someSuccessfulErrorContent = someSuccessfulErrorContentPython; - allSuccessfulContent = allSuccessfulContentPython; - buildErrorContent = buildErrorContentPython; - somePassedString = '2 of 13 passed'; - break; - case 'C': - someSuccessfulErrorContent = someSuccessfulErrorContentC; - allSuccessfulContent = allSuccessfulContentC; - buildErrorContent = buildErrorContentC; - somePassedString = '5 of 22 passed'; - break; - } - - group('Participate in Programming Exercise', function () { - let participationId = startExercise(artemis, exerciseId).id; - if (participationId) { - // partial success, then 100%, then build error -- wait some time between submissions in order to the build server time for the result - let simulation = new ParticipationSimulation(timeoutParticipation, exerciseId, participationId, someSuccessfulErrorContent); - simulateSubmission(artemis, simulation, TestResult.FAIL, somePassedString); - simulation = new ParticipationSimulation(timeoutParticipation, exerciseId, participationId, allSuccessfulContent); - simulateSubmission(artemis, simulation, TestResult.SUCCESS); - simulation = new ParticipationSimulation(timeoutParticipation, exerciseId, participationId, buildErrorContent); - if (programmingLanguage === 'C') { - // C builds do never fail - they will only show 0/21 passed - simulateSubmission(artemis, simulation, TestResult.FAIL, '0 of 21 passed'); - } else { - simulateSubmission(artemis, simulation, TestResult.BUILD_ERROR); - } - } - - const delta = (new Date().getTime() - startTime) / 1000; - sleep(timeoutParticipation - delta); - }); - - return data; -} - -export function teardown(data) { - const shouldCleanup = __ENV.CLEANUP === true || __ENV.CLEANUP === 'true'; - if (shouldCleanup) { - const artemis = login(adminUsername, adminPassword); - const courseId = data.courseId; - const exerciseId = data.exerciseId; - - deleteProgrammingExercise(artemis, exerciseId); - deleteCourse(artemis, courseId); - } -} diff --git a/src/test/k6/QuizExerciseAPIs.js b/src/test/k6/QuizExerciseAPIs.js deleted file mode 100644 index 2478e70ae33e..000000000000 --- a/src/test/k6/QuizExerciseAPIs.js +++ /dev/null @@ -1,110 +0,0 @@ -import { login } from './requests/requests.js'; -import { group, sleep } from 'k6'; -import { getQuizQuestions, simulateQuizWork } from './requests/quiz.js'; -import { newCourse, deleteCourse } from './requests/course.js'; -import { createUsersIfNeeded } from './requests/user.js'; -import { createQuizExercise, deleteQuizExercise, waitForQuizStartAndStart } from './requests/quiz.js'; - -// Version: 1.1 -// Creator: Firefox -// Browser: Firefox - -export let options = { - maxRedirects: 0, - iterations: __ENV.ITERATIONS, - vus: __ENV.ITERATIONS, - rps: 5, - setupTimeout: '480s', - teardownTimeout: '240s', -}; - -const adminUsername = __ENV.ADMIN_USERNAME; -const adminPassword = __ENV.ADMIN_PASSWORD; -let baseUsername = __ENV.BASE_USERNAME; -let basePassword = __ENV.BASE_PASSWORD; -let userOffset = parseInt(__ENV.USER_OFFSET); -let waitQuizStart = __ENV.WAIT_QUIZ_START === 'true'; -// Use users with ID >= 100 to avoid manual testers entering the wrong password too many times interfering with tests -const userIdOffset = 99; - -export function setup() { - console.log('__ENV.CREATE_USERS: ' + __ENV.CREATE_USERS); - console.log('__ENV.TIMEOUT_PARTICIPATION: ' + __ENV.TIMEOUT_PARTICIPATION); - console.log('__ENV.TIMEOUT_EXERCISE: ' + __ENV.TIMEOUT_EXERCISE); - console.log('__ENV.ITERATIONS: ' + __ENV.ITERATIONS); - console.log('__ENV.USER_OFFSET: ' + __ENV.USER_OFFSET); - - let artemis, exerciseId, course, userId; - - if (parseInt(__ENV.COURSE_ID) === 0 || parseInt(__ENV.EXERCISE_ID) === 0) { - console.log('Creating new course and exercise as no parameters are given'); - - // Create course - artemis = login(adminUsername, adminPassword); - - course = newCourse(artemis); - - createUsersIfNeeded(artemis, baseUsername, basePassword, adminUsername, adminPassword, course, userOffset); - - const instructorUsername = baseUsername.replace('USERID', '101'); - const instructorPassword = basePassword.replace('USERID', '101'); - - // Login to Artemis - artemis = login(instructorUsername, instructorPassword); - - // it might be necessary that the newly created groups or accounts are synced with the version control and continuous integration servers, so we wait for 1 minute - const timeoutExercise = parseFloat(__ENV.TIMEOUT_EXERCISE); - if (timeoutExercise > 0) { - console.log('Wait ' + timeoutExercise + 's before creating the quiz exercise so that the setup can finish properly'); - sleep(timeoutExercise); - } - - // Create new exercise - exerciseId = createQuizExercise(artemis, course); - - sleep(2); - - return { exerciseId: exerciseId, courseId: course.id }; - } else { - console.log('Using existing course and exercise'); - return { exerciseId: parseInt(__ENV.EXERCISE_ID), courseId: parseInt(__ENV.COURSE_ID) }; - } -} - -export default function (data) { - const websocketConnectionTime = parseFloat(__ENV.TIMEOUT_PARTICIPATION); // Time in seconds the websocket is kept open, if set to 0 no websocket connection is estahblished - - // Delay so that not all users start at the same time, batches of 50 users per second - const delay = Math.floor(__VU / 50); - sleep(delay); - - group('Artemis Quiz Exercise Participation Websocket Stresstest', function () { - const userId = parseInt(__VU) + userOffset + userIdOffset; - const currentUsername = baseUsername.replace('USERID', userId); - const currentPassword = basePassword.replace('USERID', userId); - const artemis = login(currentUsername, currentPassword); - - const remainingTime = websocketConnectionTime - delay; - const startTime = new Date().getTime(); - if (waitQuizStart) { - waitForQuizStartAndStart(artemis, data.exerciseId, parseInt(__ENV.TIMEOUT_PARTICIPATION), currentUsername, data.courseId); - } else { - const questions = getQuizQuestions(artemis, data.courseId, data.exerciseId); - simulateQuizWork(artemis, data.exerciseId, questions, parseInt(__ENV.TIMEOUT_PARTICIPATION), currentUsername); - } - }); - - return data; -} - -export function teardown(data) { - const shouldCleanup = __ENV.CLEANUP === true || __ENV.CLEANUP === 'true'; - if (shouldCleanup) { - const artemis = login(adminUsername, adminPassword); - const courseId = data.courseId; - const exerciseId = data.exerciseId; - - deleteQuizExercise(artemis, exerciseId); - deleteCourse(artemis, courseId); - } -} diff --git a/src/test/k6/TextExerciseAPIs.js b/src/test/k6/TextExerciseAPIs.js deleted file mode 100644 index ddb2bcbaeca2..000000000000 --- a/src/test/k6/TextExerciseAPIs.js +++ /dev/null @@ -1,154 +0,0 @@ -import { group, sleep } from 'k6'; -import { addUserToInstructorsInCourse, deleteCourse, newCourse } from './requests/course.js'; -import { startExercise, getExercise, deleteExercise, startTutorParticipation, getAndLockSubmission } from './requests/exercises.js'; -import { assessTextSubmission, newTextExercise, submitRandomTextAnswerExam } from './requests/text.js'; -import { login } from './requests/requests.js'; -import { createUsersIfNeeded } from './requests/user.js'; -import { TEXT_SUBMISSION_WITHOUT_ASSESSMENT, TEXT_EXERCISE } from './requests/endpoints.js'; - -export let options = { - maxRedirects: 0, - iterations: __ENV.ITERATIONS, - vus: __ENV.ITERATIONS, - rps: 5, - setupTimeout: '480s', - teardownTimeout: '240s', -}; - -const adminUsername = __ENV.ADMIN_USERNAME; -const adminPassword = __ENV.ADMIN_PASSWORD; -let baseUsername = __ENV.BASE_USERNAME; -let basePassword = __ENV.BASE_PASSWORD; -let userOffset = parseInt(__ENV.USER_OFFSET); -const onlyPrepare = __ENV.ONLY_PREPARE === true || __ENV.ONLY_PREPARE === 'true'; -// Use users with ID >= 100 to avoid manual testers entering the wrong password too many times interfering with tests -const userIdOffset = 99; - -export function setup() { - console.log('__ENV.CREATE_USERS: ' + __ENV.CREATE_USERS); - console.log('__ENV.TIMEOUT_PARTICIPATION: ' + __ENV.TIMEOUT_PARTICIPATION); - console.log('__ENV.TIMEOUT_EXERCISE: ' + __ENV.TIMEOUT_EXERCISE); - console.log('__ENV.ITERATIONS: ' + __ENV.ITERATIONS); - console.log('__ENV.USER_OFFSET: ' + __ENV.USER_OFFSET); - console.log('__ENV.ONLY_PREPARE: ' + onlyPrepare); - - let courseId; - let exerciseId; - let artemis; - let exercise; - const iterations = parseInt(__ENV.ITERATIONS); - - if (parseInt(__ENV.COURSE_ID) === 0 || parseInt(__ENV.EXERCISE_ID) === 0) { - console.log('Creating new exercise as no parameters are given'); - - // Create course - const artemisAdmin = login(adminUsername, adminPassword); - - const course = newCourse(artemisAdmin); - courseId = course.id; - console.log('Create users with ids starting from ' + userOffset + ' and up to ' + (userOffset + iterations)); - createUsersIfNeeded(artemisAdmin, baseUsername, basePassword, adminUsername, adminPassword, course, userOffset); - console.log('Create users with ids starting from ' + (userOffset + iterations) + ' and up to ' + (userOffset + iterations + iterations)); - createUsersIfNeeded(artemisAdmin, baseUsername, basePassword, adminUsername, adminPassword, course, userOffset + iterations, true); - - // Create course instructor - const instructorUsername = baseUsername.replace('USERID', '101'); - const instructorPassword = basePassword.replace('USERID', '101'); - - console.log('Assigning ' + instructorUsername + 'to course ' + course.id + ' as the instructor'); - addUserToInstructorsInCourse(artemisAdmin, instructorUsername, course.id); - - // Login to Artemis - artemis = login(instructorUsername, instructorPassword); - - // it might be necessary that the newly created groups or accounts are synced with the version control and continuous integration servers, so we wait for 1 minute - const timeoutExercise = parseFloat(__ENV.TIMEOUT_EXERCISE); - if (timeoutExercise > 0) { - console.log('Wait ' + timeoutExercise + 's before creating the exercise so that the setup can finish properly'); - sleep(timeoutExercise); - } - - // Create new exercise - exercise = newTextExercise(artemis, undefined, course.id); - exerciseId = exercise.id; - console.log('Created text exercise with id ' + exerciseId); - - sleep(2); - } else { - exerciseId = parseInt(__ENV.EXERCISE_ID); - courseId = parseInt(__ENV.COURSE_ID); - artemis = login(adminUsername, adminPassword); - console.log('Getting text exercise'); - exercise = getExercise(artemis, exerciseId, TEXT_EXERCISE(exerciseId)); - } - - for (let i = 1 + userIdOffset; i <= iterations + userIdOffset; i++) { - console.log(userOffset); - const userId = parseInt(__VU) + userOffset + i; - const currentUsername = baseUsername.replace('USERID', userId); - const currentPassword = basePassword.replace('USERID', userId); - console.log('Logging in as user ' + currentUsername); - artemis = login(currentUsername, currentPassword); - // Delay so that not all users start at the same time, batches of 3 users per second - const delay = Math.floor(__VU / 3); - sleep(delay * 3); - - console.log('Starting exercise ' + exerciseId); - let participation = startExercise(artemis, exerciseId); - if (participation) { - const submissionId = participation.submissions[0].id; - console.log('Submitting submission ' + submissionId); - submitRandomTextAnswerExam(artemis, exercise, submissionId); - } - sleep(1); - } - - sleep(2); - - console.log('Using existing course ' + courseId + ' and exercise ' + exerciseId); - return { exerciseId, courseId }; -} - -export default function (data) { - // The user id (1, 2, 3) is stored in __VU - const iterations = parseInt(__ENV.ITERATIONS); - const userId = parseInt(__VU) + userOffset + iterations + userIdOffset; - const currentUsername = baseUsername.replace('USERID', userId); - const currentPassword = basePassword.replace('USERID', userId); - - console.log('Logging in as user ' + currentUsername); - const artemis = login(currentUsername, currentPassword); - const exerciseId = data.exerciseId; - - // Delay so that not all users start at the same time, batches of 3 users per second - const delay = Math.floor(__VU / 3); - sleep(delay * 3); - - group('Assess text submissions', function () { - console.log('Start participation for tutor ' + currentUsername); - let participation = startTutorParticipation(artemis, exerciseId); - if (participation) { - console.log('Get and lock text submission for tutor ' + userId + ' and exercise'); - const submission = getAndLockSubmission(artemis, exerciseId, TEXT_SUBMISSION_WITHOUT_ASSESSMENT(exerciseId)); - const submissionId = submission.id; - console.log('Assess text submission ' + submissionId); - console.log('Result before manual assessment ' + JSON.stringify(submission.results[0])); - assessTextSubmission(artemis, exerciseId, submission.results[0].id); - } - sleep(1); - }); - - return data; -} - -export function teardown(data) { - const shouldCleanup = __ENV.CLEANUP === true || __ENV.CLEANUP === 'true'; - if (shouldCleanup) { - const artemis = login(adminUsername, adminPassword); - const courseId = data.courseId; - const exerciseId = data.exerciseId; - - deleteExercise(artemis, exerciseId, TEXT_EXERCISE(exerciseId)); - deleteCourse(artemis, courseId); - } -} diff --git a/src/test/k6/api_tests.sh b/src/test/k6/api_tests.sh deleted file mode 100755 index eb89d175aa23..000000000000 --- a/src/test/k6/api_tests.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash - -currentDir=$(pwd) -baseDir=$currentDir/src/test/k6 - -# param parsing -PARAMS="" -while (( "$#" )); do - case "$1" in - -bu|--baseUrl) # URL of the sut - baseUrl=$2 - shift 2 - ;; - -i|--iterations) # How many students should try to participate? - iterations=$2 - shift 2 - ;; - -tp|--timeoutParticipation) # Timeout for participations in seconds - timeoutParticipation=$2 - shift 2 - ;; - -te|--timeoutExercise) # Timeout before creating the exercise in seconds - timeoutExercise=$2 - shift 2 - ;; - -p|--password) # Base password for all test users - basePassword=$2 - shift 2 - ;; - -u|--username) # Base username for all test users - baseUsername=$2 - shift 2 - ;; - -au|--admin-username) - adminUsername=$2 - shift 2 - ;; - -ap|--admin-password) - adminPassword=$2 - shift 2 - ;; - -cu|--createUsers) - createUsers=true - shift 1 - ;; - -cl|--cleanup) - cleanup=true - shift 1 - ;; - --tests) - tests=$2 - shift 2 - ;; - -pl|--programming-language) - programmingLanguage=$2 - shift 2 - ;; - -sca|--staticCodeAnalysis) - enableStaticCodeAnalysis=$2 - shift 2 - ;; - -uo|--user-offset) - userOffset=$2 - shift 2 - ;; - -ci|--course-id) - courseId=$2 - shift 2 - ;; - -ei|--exercise-id) - exerciseId=$2 - shift 2 - ;; - -wqs|--wait-quiz-start) - waitQuizStart=true - shift 1 - ;; - -op|--only-prepare) - onlyPrepare=true - shift 1 - ;; - --) # end argument parsing - shift - break - ;; - -*) # unsupported flags - echo "Error: Unsupported flag $1" >&2 - exit 1 - ;; - *) # preserve positional arguments - PARAMS="$PARAMS $1" - shift - ;; - esac -done -# set positional arguments in their proper place -eval set -- "$PARAMS" - -# Exceptions and defaults -baseUrl=${baseUrl:?You have to specify the base URL} -basePassword=${basePassword:?"You have to specify the test user's base password"} -baseUsername=${baseUsername:?"You have to specify the test user's base username"} -adminUsername=${adminUsername:?"You have to specify the username of one admin"} -adminPassword=${adminPassword:?"You have to specify the password of one admin"} -createUsers=${createUsers:-false} -cleanup=${cleanup:-false} -tests=${tests:?"You have to specify which tests to run"} -iterations=${iterations:-10} -timeoutParticipation=${timeoutParticipation:-60} -timeoutExercise=${timeoutExercise:-10} -programmingLanguage=${programmingLanguage:-"JAVA"} -enableStaticCodeAnalysis=${enableStaticCodeAnalysis:-false} -userOffset=${userOffset:-0} -courseId=${courseId:-0} -exerciseId=${exerciseId:-0} -waitQuizStart=${waitQuizStart:-false} -onlyPrepare=${onlyPrepare:-false} - -echo "################### STARTING API Tests ###################" -result=$(docker run -i --rm --network=host --name api-tests-"$tests"-"$programmingLanguage" -v "$baseDir":/src -e BASE_USERNAME="$baseUsername" -e BASE_URL="$baseUrl" \ - -e BASE_PASSWORD="$basePassword" -e ITERATIONS="$iterations" -e TIMEOUT_PARTICIPATION="$timeoutParticipation" -e CLEANUP="$cleanup" \ - -e ADMIN_USERNAME="$adminUsername" -e ADMIN_PASSWORD="$adminPassword" -e CREATE_USERS="$createUsers" -e TIMEOUT_EXERCISE="$timeoutExercise" \ - -e PROGRAMMING_LANGUAGE="$programmingLanguage" -e ENABLE_SCA="$enableStaticCodeAnalysis" -e USER_OFFSET="$userOffset" -e COURSE_ID="$courseId" -e EXERCISE_ID="$exerciseId" \ - -e WAIT_QUIZ_START="$waitQuizStart" -e ONLY_PREPARE="$onlyPrepare" \ - grafana/k6 run --address localhost:0 /src/"$tests".js 2>&1) - -echo "########## FINISHED testing - evaluating result ##########" -echo "$result" -if echo "$result" | grep -iqF FAILTEST; then - echo "################### ERROR Server API tests failed ###################" - exit 1 -fi - -echo "######### SUCCESS Server API tests finished #########" -exit 0 diff --git a/src/test/k6/requests/course.js b/src/test/k6/requests/course.js deleted file mode 100644 index 9706b871f1b9..000000000000 --- a/src/test/k6/requests/course.js +++ /dev/null @@ -1,96 +0,0 @@ -import { COURSE, COURSES, COURSE_STUDENTS, COURSE_INSTRUCTORS, COURSE_TUTORS, ADMIN_COURSES, ADMIN_COURSE } from './endpoints.js'; -import { nextAlphanumeric } from '../util/utils.js'; -import { fail } from 'k6'; -import http from 'k6/http'; -import { FormData } from 'https://jslib.k6.io/formdata/0.0.2/index.js'; - -export function getCourse(artemis, courseId) { - const res = JSON.parse(artemis.get(COURSE(courseId), null)); - if (res[0].status !== 200) { - console.log('ERROR when getting existing course. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not get course (status: ' + res[0].status + ')! response: ' + res[0].body); - } - console.log('SUCCESS: Get existing course'); - - return JSON.parse(res[0].body); -} - -export function newCourseShortName(artemis, courseId) { - const course = JSON.parse(artemis.get(COURSE(courseId), null)[0].body); - course.shortName = 'TEST' + nextAlphanumeric(5); - artemis.put(COURSES, course); -} - -export function newCourse(artemis) { - const currentDate = new Date(); - const startDate = new Date(currentDate.getTime() - 2 * 60 * 60 * 1000); // - 2h - const endDate = new Date(currentDate.getTime() + 2 * 60 * 60 * 1000); // + 2h - const course = { - title: 'K6 Test Course', - description: 'K6 performance tests generated course', - shortName: 'testk6' + nextAlphanumeric(5), - registrationEnabled: false, - maxComplaints: 3, - maxComplaintTimeDays: 7, - accuracyOfScores: 1, - startDate: startDate, - endDate: endDate, - }; - - const formData = new FormData(); - formData.append('course', http.file(JSON.stringify(course), 'course', 'application/json')); - - const res = artemis.post(ADMIN_COURSES, undefined, undefined, formData); - if (res[0].status !== 201) { - console.log('ERROR when creating a new course. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not create course (status: ' + res[0].status + ')! response: ' + res[0].body); - } - console.log('SUCCESS: Generated new course'); - - return JSON.parse(res[0].body); -} - -export function addUserToStudentsInCourse(artemis, username, courseId) { - const res = artemis.post(COURSE_STUDENTS(courseId, username)); - console.log('Add user ' + username + ' to students in course ' + courseId + ' status: ' + res[0].status); -} - -export function removeUserFromStudentsInCourse(artemis, username, courseId) { - const res = artemis.delete(COURSE_STUDENTS(courseId, username)); - console.log('Remove user ' + username + ' from students in course ' + courseId + ' status: ' + res[0].status); -} - -export function addUserToTutorsInCourse(artemis, username, courseId) { - const res = artemis.post(COURSE_TUTORS(courseId, username)); - console.log('Add user ' + username + ' to tutors in course ' + courseId + ' status: ' + res[0].status); -} - -export function removeUserFromTutorsInCourse(artemis, username, courseId) { - const res = artemis.delete(COURSE_TUTORS(courseId, username)); - console.log('Remove user ' + username + ' from tutors in course ' + courseId + ' status: ' + res[0].status); -} - -export function addUserToInstructorsInCourse(artemis, username, courseId) { - const res = artemis.post(COURSE_INSTRUCTORS(courseId, username)); - console.log('Add user ' + username + ' to instructors in course ' + courseId + ' status: ' + res[0].status); -} - -export function removeUserFromInstructorsInCourse(artemis, username, courseId) { - const res = artemis.delete(COURSE_INSTRUCTORS(courseId, username)); - console.log('Remove user ' + username + ' from instructors in course ' + courseId + ' status: ' + res[0].status); -} - -export function deleteCourse(artemis, courseId) { - const res = artemis.delete(ADMIN_COURSE(courseId)); - - if (res[0].status !== 200) { - fail('FAILTEST: Unable to delete course ' + courseId); - } - console.log('SUCCESS: Deleted course ' + courseId); -} diff --git a/src/test/k6/requests/endpoints.js b/src/test/k6/requests/endpoints.js deleted file mode 100644 index 442286a7d3cb..000000000000 --- a/src/test/k6/requests/endpoints.js +++ /dev/null @@ -1,46 +0,0 @@ -export const PROGRAMMING_EXERCISES_SETUP = '/programming-exercises/setup'; -export const PROGRAMMING_EXERCISES = '/programming-exercises'; -export const PROGRAMMING_EXERCISE = (exerciseId) => `${PROGRAMMING_EXERCISES}/${exerciseId}`; -export const SCA_CATEGORIES = (exerciseId) => `/programming-exercises/${exerciseId}/static-code-analysis-categories`; -export const QUIZ_EXERCISES = '/quiz-exercises'; -export const QUIZ_EXERCISE = (exerciseId) => `${QUIZ_EXERCISES}/${exerciseId}`; -export const ADMIN_COURSES = '/admin/courses'; -export const COURSES = '/courses'; -export const USERS = '/users'; -export const COURSE = (courseId) => `${COURSES}/${courseId}`; -export const ADMIN_COURSE = (courseId) => `${ADMIN_COURSES}/${courseId}`; -export const COURSE_STUDENTS = (courseId, username) => `${COURSES}/${courseId}/students/${username}`; -export const COURSE_TUTORS = (courseId, username) => `${COURSES}/${courseId}/tutors/${username}`; -export const COURSE_INSTRUCTORS = (courseId, username) => `${COURSES}/${courseId}/instructors/${username}`; -export const EXERCISES = (courseId) => `${COURSE(courseId)}/exercises`; -export const PARTICIPATION = (exerciseId) => `/exercises/${exerciseId}/participation`; -export const PARTICIPATIONS = (exerciseId) => `/exercises/${exerciseId}/participations`; -export const FILES = (participationId) => `/repository/${participationId}/files`; -export const COMMIT = (participationId) => `/repository/${participationId}/commit`; -export const NEW_FILE = (participationId) => `/repository/${participationId}/file`; -export const PARTICIPATION_WITH_RESULT = (participationId) => `/participations/${participationId}/withLatestResult`; -export const SUBMIT_QUIZ_LIVE = (exerciseId) => `/exercises/${exerciseId}/submissions/live`; -export const SUBMIT_QUIZ_EXAM = (exerciseId) => `/exercises/${exerciseId}/submissions/exam`; -export const EXAMS = (courseId) => `${COURSE(courseId)}/exams`; -export const EXAM = (courseId, examId) => EXAMS(courseId) + `/${examId}`; -export const EXERCISE_GROUPS = (courseId, examId) => `${EXAM(courseId, examId)}/exerciseGroups`; -export const TEXT_EXERCISES = '/text-exercises'; -export const TEXT_EXERCISE = (exerciseId) => `/text-exercises/${exerciseId}`; -export const SUBMIT_TEXT_EXAM = (exerciseId) => `/exercises/${exerciseId}/text-submissions`; -export const TEXT_SUBMISSION_WITHOUT_ASSESSMENT = (exerciseId) => `/exercises/${exerciseId}/text-submission-without-assessment?lock=true`; -export const ASSESS_TEXT_SUBMISSION = (exerciseId, resultId) => `/exercise/${exerciseId}/result/${resultId}`; -export const EXAM_STUDENTS = (courseId, examId, username) => `${EXAM(courseId, examId)}/students/${username}`; -export const GENERATE_STUDENT_EXAMS = (courseId, examId) => `${EXAM(courseId, examId)}/generate-student-exams`; -export const STUDENT_EXAMS = (courseId, examId) => `${EXAM(courseId, examId)}/student-exams`; -export const STUDENT_EXAM_WORKINGTIME = (courseId, examId, studentExamId) => `${EXAM(courseId, examId)}/student-exams/${studentExamId}/working-time`; -export const START_EXERCISES = (courseId, examId) => `${EXAM(courseId, examId)}/student-exams/start-exercises`; -export const EVALUATE_QUIZ_EXAM = (courseId, examId) => `${EXAM(courseId, examId)}/student-exams/evaluate-quiz-exercises`; -export const SUBMIT_EXAM = (courseId, examId) => `${EXAM(courseId, examId)}/student-exams/submit`; -export const EXAM_START = (courseId, examId) => `${EXAM(courseId, examId)}/start`; -export const EXAM_CONDUCTION = (courseId, examId, studentExamId) => `${EXAM(courseId, examId)}/student-exams/${studentExamId}/conduction`; -export const MODELING_EXERCISES = '/modeling-exercises'; -export const MODELING_EXERCISE = (exerciseId) => `/modeling-exercises/${exerciseId}`; -export const SUBMIT_MODELING_EXAM = (exerciseId) => `/exercises/${exerciseId}/modeling-submissions`; -export const TUTOR_PARTICIPATIONS = (exerciseId) => `/exercises/${exerciseId}/tutor-participations`; -export const MODELING_SUBMISSION_WITHOUT_ASSESSMENT = (exerciseId) => `/exercises/${exerciseId}/modeling-submission-without-assessment?lock=true`; -export const ASSESS_MODELING_SUBMISSION = (submissionId, resultId) => `/modeling-submissions/${submissionId}/result/${resultId}/assessment?submit=true`; diff --git a/src/test/k6/requests/exam.js b/src/test/k6/requests/exam.js deleted file mode 100644 index e78b9fbb7606..000000000000 --- a/src/test/k6/requests/exam.js +++ /dev/null @@ -1,122 +0,0 @@ -import { - EVALUATE_QUIZ_EXAM, - EXAM_CONDUCTION, - EXAM_START, - EXAM_STUDENTS, - EXAMS, - EXERCISE_GROUPS, - GENERATE_STUDENT_EXAMS, - START_EXERCISES, - STUDENT_EXAM_WORKINGTIME, - STUDENT_EXAMS, - SUBMIT_EXAM, -} from './endpoints.js'; -import { nextAlphanumeric } from '../util/utils.js'; -import { fail } from 'k6'; - -export function newExam(artemis, course) { - const currentDate = new Date(); - const visibleDate = new Date(currentDate.getTime() + 30000); // Visible in 30 secs - const startDate = new Date(currentDate.getTime() + 60000); // Starting in 60 secs - const endDate = new Date(currentDate.getTime() + 600000); // Ending in 600 secs - - const examName = nextAlphanumeric(5); - - const exam = { - course: course, - visibleDate: visibleDate, - startDate: startDate, - endDate: endDate, - examMaxPoints: 54, - numberOfExercisesInExam: 4, - randomizeExerciseOrder: false, - started: false, - title: 'Exam K6 ' + examName, - visible: false, - gracePeriod: 180, - channelName: 'exam-' + examName, - }; - - const res = artemis.post(EXAMS(course.id), exam); - if (res[0].status !== 201) { - console.log('ERROR when creating a new exam. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not create exam (status: ' + res[0].status + ')! response: ' + res[0].body); - } - console.log('SUCCESS: Generated new exam'); - - return JSON.parse(res[0].body); -} - -export function newExerciseGroup(artemis, exam, mandatory = true) { - const exerciseGroup = { - exam: exam, - isMandatory: mandatory, - title: 'Group K6 ' + nextAlphanumeric(5), - }; - - const res = artemis.post(EXERCISE_GROUPS(exam.course.id, exam.id), exerciseGroup); - if (res[0].status !== 201) { - console.log('ERROR when creating a new exercise group. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not create exercise group (status: ' + res[0].status + ')! response: ' + res[0].body); - } - console.log('SUCCESS: Generated new exercise group'); - - return JSON.parse(res[0].body); -} - -export function addUserToStudentsInExam(artemis, username, exam) { - const res = artemis.post(EXAM_STUDENTS(exam.course.id, exam.id, username)); - console.log('Add user ' + username + ' to students in exam ' + exam.id + ' status: ' + res[0].status); -} - -export function generateExams(artemis, exam) { - const res = artemis.post(GENERATE_STUDENT_EXAMS(exam.course.id, exam.id)); - console.log('Generated student exams in exam ' + exam.id + ' status: ' + res[0].status); -} - -export function startExercises(artemis, exam) { - const res = artemis.post(START_EXERCISES(exam.course.id, exam.id)); - console.log('Start exercises for exam ' + exam.id + ' status: ' + res[0].status); -} - -export function startStudentExamForUser(artemis, courseId, examId) { - const res = artemis.get(EXAM_START(courseId, examId)); - console.log('Started student exam for exam ' + examId + ' status: ' + res[0].status); - - return JSON.parse(res[0].body); -} - -export function getExamForUser(artemis, courseId, examId, studentExamId) { - const res = artemis.get(EXAM_CONDUCTION(courseId, examId, studentExamId)); - console.log('Retrieved student exam for exam ' + examId + ' status: ' + res[0].status); - - return JSON.parse(res[0].body); -} - -export function getStudentExams(artemis, exam) { - const res = artemis.get(STUDENT_EXAMS(exam.course.id, exam.id)); - console.log('Retrieved student exams for exam ' + exam.id + ' status: ' + res[0].status); - - return JSON.parse(res[0].body); -} - -export function updateWorkingTime(artemis, exam, studentExam, workingTime) { - const res = artemis.patch(STUDENT_EXAM_WORKINGTIME(exam.course.id, exam.id, studentExam.id), workingTime); - console.log('Updated student Exam for exam ' + exam.id + ' status: ' + res[0].status); -} - -export function evaluateQuizzes(artemis, courseId, examId) { - const res = artemis.post(EVALUATE_QUIZ_EXAM(courseId, examId)); - console.log('Evaluated quiz exercises in exam ' + examId + ' status: ' + res[0].status); -} - -export function submitExam(artemis, courseId, examId, studentExam) { - const res = artemis.post(SUBMIT_EXAM(courseId, examId), studentExam); - console.log('Evaluated quiz exercises in exam ' + examId + ' status: ' + res[0].status); -} diff --git a/src/test/k6/requests/exercises.js b/src/test/k6/requests/exercises.js deleted file mode 100644 index 7714de3e3b00..000000000000 --- a/src/test/k6/requests/exercises.js +++ /dev/null @@ -1,61 +0,0 @@ -import { PARTICIPATIONS, TUTOR_PARTICIPATIONS } from './endpoints.js'; - -export function startExercise(artemis, exerciseId) { - console.log('Try to start exercise for test user ' + __VU); - const res = artemis.post(PARTICIPATIONS(exerciseId), undefined, undefined); - - if (res[0].status === 400) { - sleep(3000); - return; - } - - if (res[0].status !== 201) { - fail('FAILTEST: error trying to start exercise for test user ' + __VU + ':\n #####ERROR (' + res[0].status + ')##### ' + res[0].body); - } else { - console.log('SUCCESSFULLY started exercise for test user ' + __VU); - } - - return JSON.parse(res[0].body); -} - -export function getExercise(artemis, exerciseId, endpoint) { - const res = artemis.get(endpoint); - console.log('Server response is ' + JSON.stringify(res)); - if (res[0].status !== 200) { - console.log('ERROR when getting existing exercise. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not get exercise (status: ' + res[0].status + ')! response: ' + res[0].body); - } - console.log('SUCCESS: Get existing exercise'); - - return JSON.parse(res[0].body); -} - -export function deleteExercise(artemis, exerciseId, endpoint) { - const res = artemis.delete(endpoint); - if (res[0].status !== 200) { - fail('FAILTEST: Could not delete exercise (' + res[0].status + ')! Response was + ' + res[0].body); - } - console.log('DELETED modeling exercise, ID=' + exerciseId); -} - -export function startTutorParticipation(artemis, exerciseId) { - const res = artemis.post(TUTOR_PARTICIPATIONS(exerciseId), { status: 'NOT_PARTICIPATED' }); - if (res[0].status !== 201) { - fail('FAILTEST: error trying to start tutor participation for test user ' + __VU + ':\n #####ERROR (' + res[0].status + ')##### ' + res[0].body); - } else { - console.log('SUCCESSFULLY started tutor participation for test user ' + __VU); - } - - return JSON.parse(res[0].body); -} - -export function getAndLockSubmission(artemis, exerciseId, endpoint) { - const res = artemis.get(endpoint); - if (res[0].status !== 200) { - fail('FAILTEST: Could not get submission without assessment (' + res[0].status + ')! Response was + ' + res[0].body); - } - return JSON.parse(res[0].body); -} diff --git a/src/test/k6/requests/modeling.js b/src/test/k6/requests/modeling.js deleted file mode 100644 index 63b7aded3f22..000000000000 --- a/src/test/k6/requests/modeling.js +++ /dev/null @@ -1,101 +0,0 @@ -import { fail } from 'k6'; -import { nextAlphanumeric } from '../util/utils.js'; -import { ASSESS_MODELING_SUBMISSION, SUBMIT_MODELING_EXAM, MODELING_EXERCISES } from './endpoints.js'; - -export function submitRandomModelingAnswerExam(artemis, exercise, submissionId, participation) { - const answer = { - id: submissionId, - isSynced: false, - submissionExerciseType: 'modeling', - submitted: true, - model: '{"version":"2.0.0","type":"ClassDiagram","size":{"width":626,"height":578},"interactive":{"elements":[],"relationships":[]},"elements":[{"id":"35f037f7-0606-4798-b43a-9bc76d741421","name":"Package","type":"Package","owner":null,"bounds":{"x":348,"y":40,"width":200,"height":100}},{"id":"15ac99ee-292b-4a43-8e7f-47820843d132","name":"Abstract","type":"AbstractClass","owner":null,"bounds":{"x":342,"y":271,"width":200,"height":110},"attributes":["d29384ec-ffaa-4deb-812d-d18784074dbf"],"methods":["eb8ae77d-ee16-4f67-ba16-272caa3d56e1"]},{"id":"d29384ec-ffaa-4deb-812d-d18784074dbf","name":"+ attribute: Type","type":"ClassAttribute","owner":"15ac99ee-292b-4a43-8e7f-47820843d132","bounds":{"x":342,"y":321,"width":200,"height":30}},{"id":"eb8ae77d-ee16-4f67-ba16-272caa3d56e1","name":"+ method()","type":"ClassMethod","owner":"15ac99ee-292b-4a43-8e7f-47820843d132","bounds":{"x":342,"y":351,"width":200,"height":30}},{"id":"ac3a15e7-582b-4366-a350-3c4adc529e69","name":"Package","type":"Package","owner":null,"bounds":{"x":0,"y":66,"width":200,"height":100}}],"relationships":[{"id":"5fef0380-c219-413f-a6f9-e5fbac190840","name":"","type":"ClassBidirectional","owner":null,"bounds":{"x":442,"y":0,"width":146,"height":271},"path":[{"x":0,"y":271},{"x":0,"y":231},{"x":146,"y":231},{"x":146,"y":0},{"x":6,"y":0},{"x":6,"y":40}],"source":{"direction":"Up","element":"15ac99ee-292b-4a43-8e7f-47820843d132","multiplicity":"","role":""},"target":{"direction":"Up","element":"35f037f7-0606-4798-b43a-9bc76d741421","multiplicity":"","role":""}},{"id":"a3416bb8-f2c1-43ed-8e09-6f15270425ba","name":"","type":"ClassBidirectional","owner":null,"bounds":{"x":200,"y":0,"width":248,"height":116},"path":[{"x":0,"y":116},{"x":40,"y":116},{"x":40,"y":0},{"x":248,"y":0},{"x":248,"y":40}],"source":{"direction":"Right","element":"ac3a15e7-582b-4366-a350-3c4adc529e69","multiplicity":"","role":""},"target":{"direction":"Up","element":"35f037f7-0606-4798-b43a-9bc76d741421","multiplicity":"","role":""}}],"assessments":[]}', - }; - - if (participation) { - answer.participation = participation; - } - - let res = artemis.put(SUBMIT_MODELING_EXAM(exercise.id), answer); - if (res[0].status !== 200) { - console.log('ERROR when submitting modeling (Exam) via REST. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not submit modeling (Exam) via REST (status: ' + res[0].status + ')! response: ' + res[0].body); - } - return answer; -} - -export function newModelingExercise(artemis, exerciseGroup, courseId) { - const exerciseName = nextAlphanumeric(5); - const exercise = { - maxPoints: 1, - title: 'Modeling K6 ' + exerciseName, - type: 'modeling', - mode: 'INDIVIDUAL', - assessmentType: 'SEMI_AUTOMATIC', - diagramType: 'ClassDiagram', - channelName: 'exercise-' + exerciseName, - }; - - if (courseId) { - exercise.course = { id: courseId }; - } - - if (exerciseGroup) { - exercise.exerciseGroup = exerciseGroup; - } - - const res = artemis.post(MODELING_EXERCISES, exercise); - if (res[0].status !== 201) { - console.log('ERROR when creating a new modeling exercise. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not create modeling exercise (status: ' + res[0].status + ')! response: ' + res[0].body); - } - console.log('SUCCESS: Generated new modeling exercise'); - - return JSON.parse(res[0].body); -} - -export function updateModelingExerciseDueDate(artemis, exercise) { - const currentDate = new Date(); - - const updateExercise = Object.assign({}, exercise); - updateExercise.dueDate = new Date(currentDate.getTime() + 10000); // Visible in 1 minutes - - const res = artemis.put(MODELING_EXERCISES, updateExercise); - console.log(res); - if (res[0].status !== 200) { - console.log('ERROR when updating the modeling exercise. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not create modeling exercise (status: ' + res[0].status + ')! response: ' + res[0].body); - } - console.log('SUCCESS: Generated new modeling exercise'); - - return JSON.parse(res[0].body); -} - -export function assessModelingSubmission(artemis, submissionId, resultId) { - const assessment = [ - { - credits: 4, - reference: 'Package:35f037f7-0606-4798-b43a-9bc76d741421', - referenceId: '35f037f7-0606-4798-b43a-9bc76d741421', - referenceType: 'Package', - text: 'AssessmentText', - }, - ]; - let res = artemis.put(ASSESS_MODELING_SUBMISSION(submissionId, resultId), assessment); - if (res[0].status !== 200) { - console.log('ERROR when assessing modeling (Exercise) via REST. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not assess modeling (Exercise) via REST (status: ' + res[0].status + ')! response: ' + res[0].body); - } - return assessment; -} diff --git a/src/test/k6/requests/programmingExercise.js b/src/test/k6/requests/programmingExercise.js deleted file mode 100644 index 6d12db499e47..000000000000 --- a/src/test/k6/requests/programmingExercise.js +++ /dev/null @@ -1,236 +0,0 @@ -import { extractDestination, nextAlphanumeric, nextWSSubscriptionId } from '../util/utils.js'; -import { COMMIT, FILES, NEW_FILE, PARTICIPATION_WITH_RESULT, PROGRAMMING_EXERCISE, PROGRAMMING_EXERCISES_SETUP, SCA_CATEGORIES } from './endpoints.js'; -import { fail } from 'k6'; -import { programmingExerciseProblemStatementJava } from '../resource/constants_java.js'; -import { programmingExerciseProblemStatementPython } from '../resource/constants_python.js'; -import { programmingExerciseProblemStatementC } from '../resource/constants_c.js'; - -export function ParticipationSimulation(timeout, exerciseId, participationId, content) { - this.timeout = timeout; - this.exerciseId = exerciseId; - this.participationId = participationId; - this.newFiles = content.newFiles; - this.content = content.content; - - this.returnsExpectedResult = function (result, expectedResult) { - console.log('Received test result ' + result.successful); - - switch (expectedResult) { - case TestResult.SUCCESS: - { - if (!result.successful) fail('FAILTEST: The result for participation ' + participationId + ' was not successful!'); - } - break; - case TestResult.FAIL: - { - if (result.successful) fail('FAILTEST: The result for participation ' + participationId + ' did not fail!'); - } - break; - default: { - if (result.successful) fail('FAILTEST: The result for participation ' + participationId + ' contained no build errors!'); - } - } - }; - - this.extractResultFromWebSocketMessage = function (message) { - const resReg = /(.*\n\n)([^\u0000]*)(\u0000)/g; - const match = resReg.exec(message); - return JSON.parse(match[2]); - }; -} - -export function getLatestResult(artemis, participationId) { - const res = artemis.get(PARTICIPATION_WITH_RESULT(participationId)); - if (res[0].status !== 200) { - fail('FAILTEST: Could not get participation information (' + res[0].status + ')! response was + ' + res[0].body); - } - - const results = JSON.parse(res[0].body).results; - if (!results || results.length === 0) { - fail('FAILTEST: Did not receive result for test user ' + __VU); - } - const lastResult = results[results.length - 1]; - console.log(JSON.stringify(lastResult)); - - return lastResult; -} - -export const TestResult = { - SUCCESS: 'success', - FAIL: 'failure', - BUILD_ERROR: 'error', -}; - -export function createProgrammingExercise(artemis, courseId, exerciseGroup = undefined, programmingLanguage, enableSCA = false) { - let res; - - let programmingExerciseProblemStatement; - switch (programmingLanguage) { - case 'JAVA': - programmingExerciseProblemStatement = programmingExerciseProblemStatementJava; - break; - case 'PYTHON': - programmingExerciseProblemStatement = programmingExerciseProblemStatementPython; - break; - case 'C': - programmingExerciseProblemStatement = programmingExerciseProblemStatementC; - break; - } - - const exerciseName = nextAlphanumeric(10); - // The actual exercise - const exercise = { - title: 'TEST K6' + exerciseName, - shortName: 'TESTK6' + nextAlphanumeric(5).toUpperCase(), - maxPoints: 42, - assessmentType: 'AUTOMATIC', - type: 'programming', - programmingLanguage: programmingLanguage, - allowOnlineEditor: true, - packageName: 'de.test', - problemStatement: programmingExerciseProblemStatement, - presentationScoreEnabled: false, - staticCodeAnalysisEnabled: enableSCA, - sequentialTestRuns: false, - mode: 'INDIVIDUAL', - projectType: programmingLanguage === 'JAVA' ? 'PLAIN_MAVEN' : undefined, - channelName: 'exercise-' + exerciseName, - }; - - if (courseId) { - exercise.course = { id: courseId }; - } - - if (exerciseGroup) { - exercise.exerciseGroup = exerciseGroup; - } - - res = artemis.post(PROGRAMMING_EXERCISES_SETUP, exercise); - if (res[0].status !== 201) { - console.log('ERROR when creating a new programming exercise. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not create exercise (status: ' + res[0].status + ')! response: ' + res[0].body); - } - const exerciseId = JSON.parse(res[0].body).id; - console.log('CREATED new programming exercise, ID=' + exerciseId); - - return exerciseId; -} - -export function getScaCategories(artemis, exerciseId) { - const res = artemis.get(SCA_CATEGORIES(exerciseId)); - if (res[0].status !== 200) { - fail('FAILTEST: Could not get SCA categories (' + res[0].status + ')! Response was + ' + res[0].body); - } - console.log('GET SCA categories for programming exercise with id=' + exerciseId); - return JSON.parse(res[0].body); -} - -export function configureScaCategories(artemis, exerciseId, scaCategories, programmingLanguage) { - // Find and prepare categories for the configuration update - let patchedCategories; - switch (programmingLanguage) { - case 'JAVA': - let badPracticeCategory = scaCategories.find((category) => category.name === 'Bad Practice'); - if (!badPracticeCategory) { - fail(`FAILTEST: Could not find SCA category "Bad Practice" for exercise: ${exerciseId}`); - } - patchedCategories = [ - { - id: badPracticeCategory.id, - penalty: 1, - maxPenalty: 3, - state: 'GRADED', - }, - ]; - } - - const res = artemis.patch(SCA_CATEGORIES(exerciseId), patchedCategories); - if (res[0].status !== 200) { - fail('FAILTEST: Could not patch SCA categories (' + res[0].status + ')! Response was + ' + res[0].body); - } - console.log('PATCHED SCA categories for programming exercise with id=' + exerciseId); -} - -export function deleteProgrammingExercise(artemis, exerciseId) { - const res = artemis.delete(PROGRAMMING_EXERCISE(exerciseId), { - deleteStudentReposBuildPlans: true, - deleteBaseReposBuildPlans: true, - }); - if (res[0].status !== 200) { - fail('FAILTEST: Could not delete exercise (' + res[0].status + ')! Response was + ' + res[0].body); - } - console.log('DELETED programming exercise, ID=' + exerciseId); -} - -export function createNewFile(artemis, participationId, filename) { - const res = artemis.post(NEW_FILE(participationId), undefined, { file: filename }); - - if (res[0].status !== 200) { - fail('FAILTEST: Unable to create new file ' + filename); - } -} - -function subscribe(socket, exerciseId) { - socket.send('SUBSCRIBE\nid:sub-' + nextWSSubscriptionId() + '\ndestination:/user/topic/newResults\n\n\u0000'); - socket.send('SUBSCRIBE\nid:sub-' + nextWSSubscriptionId() + '\ndestination:/user/topic/exercise/' + exerciseId + '/participation\n\n\u0000'); -} - -function updateFileContent(artemis, participationId, content) { - const res = artemis.put(FILES(participationId), content); - - if (res[0].status !== 200) { - fail('FAILTEST: Unable to update file content for participation' + participationId); - } -} - -export function simulateSubmission(artemis, participationSimulation, expectedResult) { - // First, we have to create all new files - if (participationSimulation.newFiles) { - participationSimulation.newFiles.forEach((file) => createNewFile(artemis, participationSimulation.participationId, file)); - } - - artemis.websocket(function (socket) { - // Subscribe to new results and participations - socket.setTimeout(function () { - subscribe(socket, participationSimulation.exerciseId); - }, 5 * 1000); - - socket.setTimeout(function () { - // submitChange(participationSimulation.content); - updateFileContent(artemis, participationSimulation.participationId, participationSimulation.content); - console.log('SEND file data for test user ' + __VU); - }, 10 * 1000); - - // Commit changes - socket.setTimeout(function () { - artemis.post(COMMIT(participationSimulation.participationId)); - console.log('COMMIT changes for test user ' + __VU); - }, 15 * 1000); - - // Wait for new result - socket.on('message', function (message) { - if (message.startsWith('MESSAGE\n') && extractDestination(message) === '/user/topic/newResults') { - socket.close(); - const result = participationSimulation.extractResultFromWebSocketMessage(message); - participationSimulation.returnsExpectedResult(result, expectedResult); - console.log(`RECEIVE new result for test user ` + __VU); - } - }); - - // Fail after timeout - socket.setTimeout(function () { - socket.close(); - // Try to GET latest result - console.log('Websocket timed out, trying to GET now'); - const result = getLatestResult(artemis, participationSimulation.participationId); - if (result !== undefined) { - participationSimulation.returnsExpectedResult(result, expectedResult); - } else { - fail('FAILTEST: Did not receive result for test user ' + __VU); - } - }, participationSimulation.timeout * 1000); - }); -} diff --git a/src/test/k6/requests/quiz.js b/src/test/k6/requests/quiz.js deleted file mode 100644 index 23a0d662c2e7..000000000000 --- a/src/test/k6/requests/quiz.js +++ /dev/null @@ -1,267 +0,0 @@ -import { PARTICIPATION, QUIZ_EXERCISES } from './endpoints.js'; -import { fail, sleep } from 'k6'; -import { nextAlphanumeric, nextWSSubscriptionId, randomArrayValue, extractDestination, extractMessageContent } from '../util/utils.js'; -import { QUIZ_EXERCISE, SUBMIT_QUIZ_LIVE, SUBMIT_QUIZ_EXAM } from './endpoints.js'; - -export function createQuizExercise(artemis, course, exerciseGroup = null, startQuiz = true, setReleaseDate = true) { - let res; - - const currentDate = new Date(); - const releaseDate = new Date(currentDate.getTime() + 7 * 24 * 60 * 60 * 1000); // Automatic release in one week -> Will be set to 'NOW' once set-visible is called - - const exerciseName = nextAlphanumeric(10); - // The actual exercise - const exercise = { - title: 'Quiz K6' + exerciseName, - type: 'quiz', - teamMode: false, - releaseDate: setReleaseDate ? releaseDate : null, - randomizeQuestionOrder: true, - presentationScoreEnabled: false, - duration: 120, - isActiveQuiz: false, - isAtLeastInstructor: false, - isAtLeastTutor: false, - isOpenForPractice: false, - isPlannedToStart: false, - isPracticeModeAvailable: true, - isVisibleBeforeStart: false, - mode: 'INDIVIDUAL', - quizMode: 'SYNCHRONIZED', - course: course, - exerciseGroup: exerciseGroup, - quizQuestions: generateQuizQuestions(10), - channelName: 'exercise-' + exerciseName, - }; - - res = artemis.post(QUIZ_EXERCISES, exercise); - if (res[0].status !== 201) { - console.log('ERROR when creating a new quiz exercise. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not create exercise (status: ' + res[0].status + ')! response: ' + res[0].body); - } - const exerciseId = JSON.parse(res[0].body).id; - console.log('CREATED new quiz exercise, ID=' + exerciseId); - - if (startQuiz) { - console.log('Setting quiz to visible'); - res = artemis.put(QUIZ_EXERCISE(exerciseId) + '/set-visible'); - if (res[0].status !== 200) { - fail('FAILTEST: Could not set quiz to visible (' + res[0].status + ')! Response was + ' + res[0].body); - } - - console.log('Starting quiz'); - res = artemis.put(QUIZ_EXERCISE(exerciseId) + '/start-now'); - if (res[0].status !== 200) { - fail('FAILTEST: Could not start quiz (' + res[0].status + ')! Response was + ' + res[0].body); - } - } - - return exerciseId; -} - -export function generateQuizQuestions(amount) { - let questions = []; - for (let i = 0; i < amount; i++) { - let question = { - type: 'multiple-choice', - title: 'question' + i, - text: 'Some question', - scoringType: 'ALL_OR_NOTHING', - points: 1, - randomizeOrder: true, - invalid: false, - hint: 'Some question hint', - exportQuiz: 'false', - answerOptions: generateAnswerOptions(), - }; - questions.push(question); - } - - return questions; - - function generateAnswerOptions() { - let answerOptions = []; - let correctAnswerOption = { - explanation: 'Correct answer explanation', - hint: 'Correct answer hint', - invalid: false, - isCorrect: true, - text: 'Correct answer option', - }; - let wrongAnswerOption = { - explanation: 'Wrong answer explanation', - hint: 'Wrong answer hint', - invalid: false, - isCorrect: false, - text: 'Wrong answer option', - }; - - answerOptions.push(correctAnswerOption); - answerOptions.push(wrongAnswerOption); - - return answerOptions; - } -} - -export function deleteQuizExercise(artemis, exerciseId) { - const res = artemis.delete(QUIZ_EXERCISE(exerciseId)); - if (res[0].status !== 200) { - fail('FAILTEST: Could not delete exercise (' + res[0].status + ')! Response was + ' + res[0].body); - } - console.log('DELETED quiz exercise, ID=' + exerciseId); -} - -export function getQuizQuestions(artemis, courseId, exerciseId) { - const res = artemis.get(PARTICIPATION(exerciseId)); - if (res[0].status !== 200) { - fail('FAILTEST: Could not get quiz information (' + res[0].status + ')! response was + ' + res[0].body); - } - - return JSON.parse(res[0].body).exercise.quizQuestions; -} - -export function submitRandomAnswerRESTExam(artemis, exercise, numberOfQuestions, submissionId) { - const answer = { - id: submissionId, - isSynced: false, - submissionExerciseType: 'quiz', - submitted: true, - submittedAnswers: exercise.quizQuestions.slice(0, numberOfQuestions).map((q) => generateAnswer(q)), - }; - - let res = artemis.put(SUBMIT_QUIZ_EXAM(exercise.id), answer); - if (res[0].status !== 200) { - console.log('ERROR when submitting quiz (Exam) via REST. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not submit quiz (Exam) via REST (status: ' + res[0].status + ')! response: ' + res[0].body); - } - return answer; -} - -export function generateAnswer(question) { - const randAnswer = randomArrayValue(question.answerOptions); - return { - type: question.type, - quizQuestion: question, - selectedOptions: [randAnswer], - }; -} - -export function simulateQuizWork(artemis, exerciseId, questions, timeout, currentUsername) { - artemis.websocket(function (socket) { - function subscribe() { - socket.send('SUBSCRIBE\nid:sub-' + nextWSSubscriptionId() + '\ndestination:/user/topic/exercise/' + exerciseId + '/participation\n\n\u0000'); - } - - function submitRandomAnswer(numberOfQuestions) { - const answer = { - submissionExerciseType: 'quiz', - submitted: false, - submittedAnswers: questions.slice(0, numberOfQuestions).map((q) => generateAnswer(q)), - }; - const answerString = JSON.stringify(answer); - const wsMessage = `SEND\ndestination:/topic/quizExercise/${exerciseId}/submission\ncontent-length:${answerString.length}\n\n${answerString}\u0000`; - - socket.send(wsMessage); - } - - function submitRandomAnswerREST(numberOfQuestions) { - const answer = { - submissionExerciseType: 'quiz', - submitted: false, - submittedAnswers: questions.slice(0, numberOfQuestions).map((q) => generateAnswer(q)), - }; - - let res = artemis.post(SUBMIT_QUIZ_LIVE(exerciseId), answer); - if (res[0].status !== 200) { - console.log('ERROR when submitting quiz via REST. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not submit quiz via REST (status: ' + res[0].status + ')! response: ' + res[0].body); - } - } - - // Subscribe to callback response from server (response after submitted answer) - socket.setTimeout(function () { - subscribe(); - }, 5 * 1000); - - // Wait for new result - socket.on('message', function (message) { - if (message.startsWith('MESSAGE\n') && extractDestination(message) === '/user/topic/exercise/' + exerciseId + '/participation') { - console.log(`RECEIVED callback from server for ${currentUsername}`); - sleep(5); - socket.close(); - } else if (message !== '\n') { - console.log(`Unexpected message ${message} for user ${currentUsername}`); - } - }); - - for (let questionCount = 1; questionCount <= 50; questionCount++) { - // submit new quiz answer - socket.setTimeout( - function () { - if (questionCount === 50) { - console.log('Submitting via REST for ' + currentUsername); - submitRandomAnswerREST(10); - } else { - console.log('Submitting via WS for ' + currentUsername); - submitRandomAnswer(10); - } - }, - (questionCount - 1) * 500 + 1000, - ); - } - - // Stop after timeout - socket.setTimeout(function () { - console.log('Connection timed out for user ' + currentUsername); - socket.close(); - }, timeout * 1000); - }); -} - -export function waitForQuizStartAndStart(artemis, exerciseId, timeout, currentUsername, courseId) { - artemis.websocket(function (socket) { - function subscribe() { - socket.send('SUBSCRIBE\nid:sub-' + nextWSSubscriptionId() + '\ndestination:/topic/courses/' + courseId + '/quizExercises\n\n\u0000'); - } - - socket.setTimeout(function () { - subscribe(); - }, 1000); - - // Wait for new result - socket.on('message', function (message) { - if (message.startsWith('MESSAGE\n') && extractDestination(message) === '/topic/courses/' + courseId + '/quizExercises') { - // console.log(`RECEIVED quiz start for user ${currentUsername}: ${message}`); - //console.log(message.match('MESSAGE\ndestination:\/topic\/courses\/(?:\d)*\/quizExercises\nsubscription:(?:\\w|-)*\nmessage-id:(?:\w|\d|-)*\ncontent-length:(?:\d)*\n\n(.*)')); - let receivedPayload = extractMessageContent(message); - let parsedQuiz = JSON.parse(receivedPayload); - - if (parsedQuiz.quizMode !== 'SYNCHRONIZED') { - fail('The k6 tests currently only support SYNCHRONIZED quizzes'); - return; - } - - const defaultBatch = parsedQuiz.quizBatches[0]; - if (defaultBatch.started) { - // Quiz started - console.log(`Quiz start received for user ${currentUsername}, will send answers`); - const questions = parsedQuiz.quizQuestions; - socket.close(); - simulateQuizWork(artemis, exerciseId, questions, timeout, currentUsername); - } else { - // Quiz is now visible, but not started - console.log(`Quiz visible received for user ${currentUsername}, will wait for quiz start`); - } - } - }); - }); -} diff --git a/src/test/k6/requests/requests.js b/src/test/k6/requests/requests.js deleted file mode 100644 index c4f4574b36f9..000000000000 --- a/src/test/k6/requests/requests.js +++ /dev/null @@ -1,168 +0,0 @@ -import http from 'k6/http'; -import ws from 'k6/ws'; -import { fail } from 'k6'; - -const protocol = 'https'; // https or http -const websocketProtocol = 'wss'; // wss if https is used; ws if http is used -const host = __ENV.BASE_URL; // host including port if differing from 80 (http) or 443 (https) -const baseUrl = protocol + '://' + host; - -const userAgent = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0'; -const acceptLanguage = 'en-CA,en-US;q=0.7,en;q=0.3'; -const acceptEncoding = 'gzip, deflate, br'; - -const request = function (method, endpoint, authToken, body, params, formData) { - let paramString; - if (params) { - paramString = Object.keys(params) - .map((key) => key + '=' + params[key]) - .join('&'); - } - - let bodyParameter = null; - if (body) { - bodyParameter = JSON.stringify(body); - } else if (formData) { - bodyParameter = formData.body(); - } - - let url = baseUrl + '/api' + endpoint + (paramString ? '?' + paramString : ''); - let req = [ - { - method: method, - url: url, - body: bodyParameter, - params: { - headers: { - Host: host, - 'User-Agent': userAgent, - Accept: 'application/json, text/plain, */*', - 'Accept-Language': acceptLanguage, - 'Accept-Encoding': acceptEncoding, - Referer: baseUrl + '/', - 'Content-Type': formData ? 'multipart/form-data; boundary=' + formData.boundary : 'application/json', - 'X-Artemis-Client-Fingerprint': 'b832814fcce0cab9fc5f717d5b93fa07', - 'X-Artemis-Client-Instance-ID': '9e0b78ec-e43e-43da-a767-89b3f80df63a', - Connection: 'keep-alive', - TE: 'Trailers', - }, - tags: { name: url }, - cookies: { - jwt: authToken, - }, - }, - }, - ]; - - return http.batch(req); -}; - -export function login(username, password) { - let req, res; - - console.log('Try to login with ' + username + ':' + password); - - // The user logs in; the authToken gets saved as we need it later - req = [ - { - method: 'post', - url: baseUrl + '/api/public/authenticate', - body: '{"username":"' + username + '","password":"' + password + '","rememberMe":true}', - params: { - headers: { - Host: host, - 'User-Agent': userAgent, - Accept: 'application/json, text/plain, */*', - 'Accept-Language': acceptLanguage, - 'Accept-Encoding': acceptEncoding, - Referer: baseUrl + '/', - 'Content-Type': 'application/json', - Connection: 'keep-alive', - TE: 'Trailers', - }, - tags: { name: baseUrl + '/api/public/authenticate' }, - }, - }, - ]; - res = http.batch(req); - if (res[0].status !== 200) { - fail('FAILTEST: failed to login as user ' + username + ' (' + res[0].status + ')! Response was + ' + res[0].body); - } - const authToken = res[0].cookies.jwt[0].value; - // console.log('GOT authToken ' + authToken + ' for user ' + username); - - // The user requests it own information of the account - req = [ - { - method: 'get', - url: baseUrl + '/api/public/account', - params: { - headers: { - Host: host, - 'User-Agent': userAgent, - Accept: 'application/json, text/plain, */*', - 'Accept-Language': acceptLanguage, - 'Accept-Encoding': acceptEncoding, - Referer: baseUrl + '/', - Connection: 'keep-alive', - TE: 'Trailers', - }, - tags: { name: baseUrl + '/api/public/account' }, - cookies: { - jwt: authToken, - }, - }, - }, - ]; - res = http.batch(req); - - return new Artemis(authToken); -} - -export function Artemis(authToken) { - this.get = function (endpoint, params) { - return request('get', endpoint, authToken, null, params); - }; - this.post = function (endpoint, body, params, formData) { - return request('post', endpoint, authToken, body, params, formData); - }; - this.put = function (endpoint, body, params) { - return request('put', endpoint, authToken, body, params); - }; - this.patch = function (endpoint, body, params) { - return request('patch', endpoint, authToken, body, params); - }; - this.delete = function (endpoint, params) { - return request('delete', endpoint, authToken, null, params); - }; - this.websocket = function (doOnSocket) { - const websocketEndpoint = websocketProtocol + '://' + host + '/websocket/websocket'; - - const jar = new http.CookieJar(); - jar.set(baseUrl, 'jwt', authToken); - - ws.connect(websocketEndpoint, { tags: { name: websocketEndpoint }, jar }, function (socket) { - socket.on('open', function open() { - socket.send('CONNECT\naccept-version:1.2\nheart-beat:10000,10000\n\n\u0000'); - socket.setInterval(function timeout() { - socket.ping(); - // Pinging every 10sec (setInterval) - }, 10000); - // TODO: is ping not the same as the heartbeat? - // Send heartbeat to server so session is kept alive - socket.setInterval(function timeout() { - socket.send('\n'); - }, 10000); - }); - - socket.on('error', function (e) { - if (e.error() !== 'websocket: close sent') { - console.log('Websocket connection closed due to: ', e.error()); - } - // TODO: try to reconnect - }); - - doOnSocket(socket); - }); - }; -} diff --git a/src/test/k6/requests/text.js b/src/test/k6/requests/text.js deleted file mode 100644 index cf615dc0fbbb..000000000000 --- a/src/test/k6/requests/text.js +++ /dev/null @@ -1,126 +0,0 @@ -import { fail } from 'k6'; -import { nextAlphanumeric } from '../util/utils.js'; -import { ASSESS_TEXT_SUBMISSION, SUBMIT_TEXT_EXAM, TEXT_EXERCISES } from './endpoints.js'; - -export function submitRandomTextAnswerExam(artemis, exercise, submissionId) { - const answer = { - id: submissionId, - isSynced: false, - submissionExerciseType: 'text', - submitted: true, - text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum', - }; - - let res = artemis.put(SUBMIT_TEXT_EXAM(exercise.id), answer); - if (res[0].status !== 200) { - console.log('ERROR when submitting text (Exam) via REST. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not submit text (Exam) via REST (status: ' + res[0].status + ')! response: ' + res[0].body); - } - return answer; -} - -export function newTextExercise(artemis, exerciseGroup, courseID) { - const exerciseName = nextAlphanumeric(5); - const textExercise = { - maxPoints: 1, - title: 'Text K6 ' + exerciseName, - type: 'text', - mode: 'INDIVIDUAL', - channelName: 'exercise-' + exerciseName, - }; - - if (courseID) { - textExercise.course = { id: courseId }; - } - - if (exerciseGroup) { - textExercise.exerciseGroup = exerciseGroup; - } - - const res = artemis.post(TEXT_EXERCISES, textExercise); - if (res[0].status !== 201) { - console.log('ERROR when creating a new text exercise. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not create text exercise (status: ' + res[0].status + ')! response: ' + res[0].body); - } - console.log('SUCCESS: Generated new text exercise'); - - return JSON.parse(res[0].body); -} - -export function assessTextSubmission(artemis, exerciseId, resultId) { - const assessment = [ - { - feedbacks: [ - { - credits: 1, - reference: '674aa1b614606278282b1b0d80e34f609c641972', - detailText: 'Good', - type: 'MANUAL', - }, - { - credits: 2, - reference: '314a5bbaeb4755e0e29ee69ec4fb0f9f3aa3d2d9', - detailText: 'Good', - type: 'MANUAL', - }, - { - credits: 0, - reference: 'bb7b48643ec0fa3b8194e196bf97692ec6e4821f', - detailText: 'Neutral', - type: 'MANUAL', - }, - { - credits: -0.5, - reference: '21e56278b55f2dfa5c1a9d646823d177b2ac2ef0', - detailText: 'Negative', - type: 'MANUAL', - }, - ], - textBlocks: [ - { - id: '674aa1b614606278282b1b0d80e34f609c641972', - text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', - startIndex: 0, - endIndex: 123, - type: 'AUTOMATIC', - }, - { - id: '314a5bbaeb4755e0e29ee69ec4fb0f9f3aa3d2d9', - text: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', - startIndex: 124, - endIndex: 231, - type: 'AUTOMATIC', - }, - { - id: 'bb7b48643ec0fa3b8194e196bf97692ec6e4821f', - text: 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.', - startIndex: 232, - endIndex: 334, - type: 'AUTOMATIC', - }, - { - id: '21e56278b55f2dfa5c1a9d646823d177b2ac2ef0', - text: 'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', - startIndex: 335, - endIndex: 445, - type: 'AUTOMATIC', - }, - ], - }, - ]; - let res = artemis.put(ASSESS_TEXT_SUBMISSION(exerciseId, resultId), assessment); - if (res[0].status !== 200) { - console.log('ERROR when assessing modeling (Exercise) via REST. Response headers:'); - for (let [key, value] of Object.entries(res[0].headers)) { - console.log(`${key}: ${value}`); - } - fail('FAILTEST: Could not assess modeling (Exercise) via REST (status: ' + res[0].status + ')! response: ' + res[0].body); - } - return assessment; -} diff --git a/src/test/k6/requests/user.js b/src/test/k6/requests/user.js deleted file mode 100644 index ec617d56289a..000000000000 --- a/src/test/k6/requests/user.js +++ /dev/null @@ -1,106 +0,0 @@ -import { USERS } from './endpoints.js'; -import { addUserToInstructorsInCourse, addUserToStudentsInCourse, addUserToTutorsInCourse } from './course.js'; -import { login } from './requests.js'; - -export function getUser(artemis, i, baseUsername) { - const username = baseUsername.replace('USERID', i); - const res = artemis.get(USERS + '/' + username); - if (res[0].status !== 200) { - console.info('Unable to get user ' + username + ' (status: ' + res[0].status + ')!'); - } - return res[0].body; -} - -export function updateUser(artemis, user) { - const res = artemis.put(USERS, user); - if (res[0].status !== 200) { - console.info('Unable to update user ' + user.login + ' (status: ' + res[0].status + ')!'); - } -} - -export function newUser(artemis, i, baseUsername, basePassword, studentGroupName, instructorGroupName, asTutor) { - const username = baseUsername.replace('USERID', i); - const password = basePassword.replace('USERID', i); - let authorities = ['ROLE_USER']; - if (i === 1) { - authorities = ['ROLE_USER', 'ROLE_INSTRUCTOR']; - } - if (asTutor) { - authorities = ['ROLE_USER', 'ROLE_TA']; - } - - let groups = [studentGroupName]; - if (i === 1) { - groups = [studentGroupName, instructorGroupName]; - } - - const user = { - login: username, - password: password, - firstName: 'Artemis Test ' + i, - lastName: 'Artemis Test ' + i, - email: username + '_testuser_' + i + '@tum.invalid', - activated: true, - langKey: 'en', - authorities: authorities, - groups: groups, - createdBy: 'test-case', - }; - - console.log('Try to create new user ' + username); - const res = artemis.post(USERS, user); - if (res[0].status !== 201) { - console.info('Unable to generate new user ' + username + ' (status: ' + res[0].status + ')!'); - return -1; - } else { - console.log('SUCCESS: Created new user ' + username + ' with groups ' + res[0].body.groups); - } - - return JSON.parse(res[0].body).id; -} - -export function updateUserWithGroup(artemis, i, baseUsername, course, asTutor) { - const username = baseUsername.replace('USERID', i); - if (asTutor) { - addUserToTutorsInCourse(artemis, username, course.id); - } else { - addUserToStudentsInCourse(artemis, username, course.id); - } - - if (i === 101) { - addUserToInstructorsInCourse(artemis, username, course.id); - } -} - -export function createUsersIfNeeded(artemis, baseUsername, basePassword, adminUsername, adminPassword, course, userOffset, asTutor) { - const shouldCreateUsers = __ENV.CREATE_USERS === true || __ENV.CREATE_USERS === 'true'; - const iterations = parseInt(__ENV.ITERATIONS); - // Use users with ID >= 100 to avoid manual testers entering the wrong password too many times interfering with tests - const userIdOffset = 99; - - if (shouldCreateUsers) { - console.log('Try to create ' + iterations + ' users'); - for (let i = 1 + userIdOffset; i <= iterations + userIdOffset; i++) { - let userId; - if (asTutor) { - userId = newUser(artemis, i + userOffset, baseUsername, basePassword, course.teachingAssistantGroupName, course.instructorGroupName, asTutor); - } else { - userId = newUser(artemis, i + userOffset, baseUsername, basePassword, course.studentGroupName, course.instructorGroupName); - } - if (userId === -1) { - // the creation was not successful, most probably because the user already exists, we need to update the group of the user - updateUserWithGroup(artemis, i + userOffset, baseUsername, course, asTutor); - } - } - } else { - console.log('Do not create users, assume the user exists in the external system, will update their groups'); - for (let i = 1 + userIdOffset; i <= iterations + userIdOffset; i++) { - // we need to log in once with the user, so that the user is synced and available for the update with the groups - login(baseUsername.replace('USERID', i + userOffset), basePassword.replace('USERID', i + userOffset)); - } - artemis = login(adminUsername, adminPassword); - for (let i = 1 + userIdOffset; i <= iterations + userIdOffset; i++) { - updateUserWithGroup(artemis, i + userOffset, baseUsername, course, asTutor); - } - } -} diff --git a/src/test/k6/resource/constants_c.js b/src/test/k6/resource/constants_c.js deleted file mode 100644 index a04e372a6c1b..000000000000 --- a/src/test/k6/resource/constants_c.js +++ /dev/null @@ -1,63 +0,0 @@ -export const programmingExerciseProblemStatementC = - '# Hello World\n' + - '\n' + - 'In dieser Aufgabe werden Sie Ihr erstes C Programm erstellen.\n' + - 'Die Aufgabenstellung entnehmen Sie bitte [https://gbs.cm.in.tum.de](https://gbs.cm.in.tum.de).\n' + - '\n' + - '#### Allgemein\n' + - '1. [task][Kompilieren](TestCompile)\n' + - '2. [task][Rückgabewert == 0](TestReturnCode)\n' + - '3. [task][Ausgabe prüfen](TestOutput)\n' + - '\n' + - '#### Address Sanitizer\n' + - '1. [task][Kompilieren mit Address Sanitizer](TestCompileASan)\n' + - '2. [task][Ausgabe prüfen mit Address Sanitizer](TestOutputASan)\n' + - '\n' + - '#### Undefined Behavior Sanitizer\n' + - '1. [task][Kompilieren mit Undefined Behavior Sanitizer](TestCompileUBSan)\n' + - '2. [task][Ausgabe prüfen mit Undefined Behavior Sanitizer](TestOutputUBSan)\n' + - '\n' + - '#### Leak Sanitizer\n' + - '1. [task][Kompilieren mit Leak Sanitizer](TestCompileLeak)\n' + - '2. [task][Ausgabe prüfen mit Leak Sanitizer](TestOutputLSan)\n' + - '\n' + - '#### GCC Static Analysis\n' + - '1. [task][GCC Static analysis](TestGccStaticAnalysis)'; - -export const buildErrorContentC = { - newFiles: [], - content: [ - { - fileName: 'helloWorld.c', - fileContent: 'a', - }, - ], -}; - -export const someSuccessfulErrorContentC = { - newFiles: [], - content: [ - { - fileName: 'helloWorld.c', - fileContent: '// Do magic ╰( ͡° ͜ʖ ͡° )つ──☆*:・゚ and implement your "Hello world" programm here.\n', - }, - ], -}; - -export const allSuccessfulContentC = { - newFiles: [], - content: [ - { - fileName: 'helloWorld.c', - fileContent: - '#include // For printf(...)\n' + - '#include // For EXIT_SUCCESS\n' + - '\n' + - 'int main(){\n' + - '\tprintf("Hello world!\n");\n' + - '\n' + - '\treturn EXIT_SUCCESS; // Same as "return 0;"\n' + - '}\n', - }, - ], -}; diff --git a/src/test/k6/resource/constants_java.js b/src/test/k6/resource/constants_java.js deleted file mode 100644 index c9a0b21a42d0..000000000000 --- a/src/test/k6/resource/constants_java.js +++ /dev/null @@ -1,441 +0,0 @@ -export const programmingExerciseProblemStatementJava = - '# Sorting with the Strategy Pattern\n' + - '\n' + - 'In this exercise, we want to implement sorting algorithms and choose them based on runtime specific variables.\n' + - '\n' + - '### Part 1: Sorting\n' + - '\n' + - 'First, we need to implement two sorting algorithms, in this case `MergeSort` and `BubbleSort`.\n' + - '\n' + - '**You have the following tasks:**\n' + - '\n' + - '1. [task][Implement Bubble Sort](testBubbleSort)\n' + - 'Implement the method `performSort(List)` in the class `BubbleSort`. Make sure to follow the Bubble Sort algorithm exactly.\n' + - '\n' + - '2. [task][Implement Merge Sort](testMergeSort)\n' + - 'Implement the method `performSort(List)` in the class `MergeSort`. Make sure to follow the Merge Sort algorithm exactly.\n' + - '\n' + - '### Part 2: Strategy Pattern\n' + - '\n' + - 'We want the application to apply different algorithms for sorting a `List` of `Date` objects.\n' + - 'Use the strategy pattern to select the right sorting algorithm at runtime.\n' + - '\n' + - '**You have the following tasks:**\n' + - '\n' + - '1. [task][SortStrategy Interface](testClass[SortStrategy],testMethods[SortStrategy])\n' + - 'Create a `SortStrategy` interface and adjust the sorting algorithms so that they implement this interface.\n' + - '\n' + - '2. [task][Context Class](testAttributes[Context],testMethods[Context])\n' + - 'Create and implement a `Context` class following the below class diagram\n' + - '\n' + - '3. [task][Context Policy](testConstructors[Policy],testAttributes[Policy],testMethods[Policy])\n' + - 'Create and implement a `Policy` class following the below class diagram with a simple configuration mechanism:\n' + - '\n' + - ' 1. [task][Select MergeSort](testClass[MergeSort],testUseMergeSortForBigList)\n' + - ' Select `MergeSort` when the List has more than 10 dates.\n' + - '\n' + - ' 2. [task][Select BubbleSort](testClass[BubbleSort],testUseBubbleSortForSmallList)\n' + - ' Select `BubbleSort` when the List has less or equal 10 dates.\n' + - '\n' + - '4. Complete the `Client` class which demonstrates switching between two strategies at runtime.\n' + - '\n' + - '@startuml\n' + - '\n' + - 'class Client {\n' + - '}\n' + - '\n' + - 'class Policy {\n' + - ' +configure()\n' + - '}\n' + - '\n' + - 'class Context {\n' + - ' -dates: List\n' + - ' +sort()\n' + - '}\n' + - '\n' + - 'interface SortStrategy {\n' + - ' +performSort(List)\n' + - '}\n' + - '\n' + - 'class BubbleSort {\n' + - ' +performSort(List)\n' + - '}\n' + - '\n' + - 'class MergeSort {\n' + - ' +performSort(List)\n' + - '}\n' + - '\n' + - 'MergeSort -up-|> SortStrategy #testsColor(testClass[MergeSort])\n' + - 'BubbleSort -up-|> SortStrategy #testsColor(testClass[BubbleSort])\n' + - 'Policy -right-> Context #testsColor(testAttributes[Policy]): context\n' + - 'Context -right-> SortStrategy #testsColor(testAttributes[Context]): sortAlgorithm\n' + - 'Client .down.> Policy\n' + - 'Client .down.> Context\n' + - '\n' + - '@enduml\n' + - '\n' + - '\n' + - '### Part 3: Optional Challenges\n' + - '\n' + - '(These are not tested)\n' + - '\n' + - '1. Create a new class `QuickSort` that implements `SortStrategy` and implement the Quick Sort algorithm.\n' + - '\n' + - '2. Make the method `performSort(List)` generic, so that other objects can also be sorted by the same method.\n' + - '**Hint:** Have a look at Java Generics and the interface `Comparable`.\n' + - '\n' + - '3. Think about a useful decision in `Policy` when to use the new `QuickSort` algorithm.\n'; - -export const buildErrorContentJava = { - newFiles: [], - content: [ - { - fileName: 'src/de/test/BubbleSort.java', - fileContent: 'a', - }, - ], -}; - -export const someSuccessfulErrorContentJava = (scaEnabled) => { - if (scaEnabled) { - // One Bad Practice, one Performance, one Documentation issue - return { - newFiles: ['src/de/test/SortStrategy.java'], - content: [ - { - fileName: 'src/de/test/SortStrategy.java', - fileContent: - 'package de.test;\n' + - '\n' + - 'import java.util.Date;\n' + - 'import java.util.List;\n' + - '\n' + - 'public interface SortStrategy {\n' + - '\n' + - ' public void performSort(List input);\n' + - '}', - }, - { - fileName: 'src/de/test/BubbleSort.java', - fileContent: - 'package de.test;\n' + - '\n' + - 'import java.util.*;\n' + - '\n' + - 'public class BubbleSort {\n' + - 'private int unused = 1;\n' + - 'public void performSort(List input) {\n' + - 'for (int i = input.size() - 1; i >= 0; i--) {\n' + - 'for (int j = 0; j < i; j++) {\n' + - 'if (input.get(j).compareTo(input.get(j + 1)) > 0) {\n' + - 'Date temp = input.get(j);\n' + - 'input.set(j, input.get(j + 1));\n' + - 'input.set(j + 1, temp);\n' + - '}\n' + - '}\n' + - '}\n' + - '}\n' + - '}', - }, - ], - }; - } else { - return { - newFiles: ['src/de/test/SortStrategy.java'], - content: [ - { - fileName: 'src/de/test/SortStrategy.java', - fileContent: - 'package de.test;\n' + - '\n' + - 'import java.util.Date;\n' + - 'import java.util.List;\n' + - '\n' + - 'public interface SortStrategy {\n' + - '\n' + - ' public void performSort(List input);\n' + - '}', - }, - ], - }; - } -}; - -export const allSuccessfulContentJava = { - newFiles: ['src/de/test/Context.java', 'src/de/test/Policy.java'], - content: [ - { - fileName: 'src/de/test/Context.java', - fileContent: - 'package de.test;\n' + - '\n' + - 'import java.util.*;\n' + - '\n' + - 'public class Context {\n' + - ' private SortStrategy sortAlgorithm;\n' + - '\n' + - ' private List dates;\n' + - '\n' + - ' public List getDates() {\n' + - ' return dates;\n' + - ' }\n' + - '\n' + - ' public void setDates(List dates) {\n' + - ' this.dates = dates;\n' + - ' }\n' + - '\n' + - ' public void setSortAlgorithm(SortStrategy sa) {\n' + - ' sortAlgorithm = sa;\n' + - ' }\n' + - '\n' + - ' public SortStrategy getSortAlgorithm() {\n' + - ' return sortAlgorithm;\n' + - ' }\n' + - '\n' + - ' /**\n' + - ' * Runs the configured sort algorithm.\n' + - ' */\n' + - ' public void sort() {\n' + - ' if (sortAlgorithm != null) {\n' + - ' sortAlgorithm.performSort(this.dates);\n' + - ' }\n' + - ' }\n' + - '}', - }, - { - fileName: 'src/de/test/BubbleSort.java', - fileContent: - 'package de.test;\n' + - '\n' + - 'import java.util.*;\n' + - '\n' + - 'public class BubbleSort implements SortStrategy {\n' + - '\n' + - ' /**\n' + - ' * Sorts dates with BubbleSort.\n' + - ' *\n' + - ' * @param input the List of Dates to be sorted\n' + - ' */\n' + - ' public void performSort(List input) {\n' + - '\n' + - ' for (int i = input.size() - 1; i >= 0; i--) {\n' + - ' for (int j = 0; j < i; j++) {\n' + - ' if (input.get(j).compareTo(input.get(j + 1)) > 0) {\n' + - ' Date temp = input.get(j);\n' + - ' input.set(j, input.get(j + 1));\n' + - ' input.set(j + 1, temp);\n' + - ' }\n' + - ' }\n' + - ' }\n' + - '\n' + - ' }\n' + - '}', - }, - { - fileName: 'src/de/test/Client.java', - fileContent: - 'package de.test;\n' + - '\n' + - 'import java.text.*;\n' + - 'import java.util.*;\n' + - 'import java.util.concurrent.ThreadLocalRandom;\n' + - '\n' + - 'public final class Client {\n' + - '\n' + - ' private static final int ITERATIONS = 10;\n' + - '\n' + - ' private static final int RANDOM_FLOOR = 5;\n' + - '\n' + - ' private static final int RANDOM_CEILING = 15;\n' + - '\n' + - ' private Client() {\n' + - ' }\n' + - '\n' + - ' /**\n' + - ' * Main method.\n' + - ' * Add code to demonstrate your implementation here.\n' + - ' *\n' + - ' * @param args command line arguments\n' + - ' */\n' + - ' public static void main(String[] args) throws ParseException {\n' + - '\n' + - ' // Init Context and Policy\n' + - '\n' + - ' Context sortingContext = new Context();\n' + - ' Policy policy = new Policy(sortingContext);\n' + - '\n' + - ' // Run multiple times to simulate different sorting strategies\n' + - ' for (int i = 0; i < ITERATIONS; i++) {\n' + - ' List dates = createRandomDatesList();\n' + - '\n' + - ' sortingContext.setDates(dates);\n' + - ' policy.configure();\n' + - '\n' + - ' System.out.print("Unsorted Array of course dates = ");\n' + - ' printDateList(dates);\n' + - '\n' + - ' sortingContext.sort();\n' + - '\n' + - ' System.out.print("Sorted Array of course dates = ");\n' + - ' printDateList(dates);\n' + - ' }\n' + - ' }\n' + - '\n' + - ' /**\n' + - ' * Generates a List of random Date objects with random List size between\n' + - ' * {@link #RANDOM_FLOOR} and {@link #RANDOM_CEILING}.\n' + - ' *\n' + - ' * @return a List of random Date objects\n' + - ' * @throws ParserException if date string cannot be parsed\n' + - ' */\n' + - ' private static List createRandomDatesList() throws ParseException {\n' + - ' int listLength = randomIntegerWithin(RANDOM_FLOOR, RANDOM_CEILING);\n' + - ' List list = new ArrayList<>();\n' + - '\n' + - ' SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy");\n' + - ' Date lowestDate = dateFormat.parse("08.11.2016");\n' + - ' Date highestDate = dateFormat.parse("03.11.2020");\n' + - '\n' + - ' for (int i = 0; i < listLength; i++) {\n' + - ' Date randomDate = randomDateWithin(lowestDate, highestDate);\n' + - ' list.add(randomDate);\n' + - ' }\n' + - ' return list;\n' + - ' }\n' + - '\n' + - ' /**\n' + - ' * Creates a random Date within the given range.\n' + - ' *\n' + - ' * @param low the lower bound\n' + - ' * @param high the upper bound\n' + - ' * @return random Date within the given range\n' + - ' */\n' + - ' private static Date randomDateWithin(Date low, Date high) {\n' + - ' long randomLong = randomLongWithin(low.getTime(), high.getTime());\n' + - ' return new Date(randomLong);\n' + - ' }\n' + - '\n' + - ' /**\n' + - ' * Creates a random long within the given range.\n' + - ' *\n' + - ' * @param low the lower bound\n' + - ' * @param high the upper bound\n' + - ' * @return random long within the given range\n' + - ' */\n' + - ' private static long randomLongWithin(long low, long high) {\n' + - ' return ThreadLocalRandom.current().nextLong(low, high + 1);\n' + - ' }\n' + - '\n' + - ' /**\n' + - ' * Creates a random int within the given range.\n' + - ' *\n' + - ' * @param low the lower bound\n' + - ' * @param high the upper bound\n' + - ' * @return random int within the given range\n' + - ' */\n' + - ' private static int randomIntegerWithin(int low, int high) {\n' + - ' return ThreadLocalRandom.current().nextInt(low, high + 1);\n' + - ' }\n' + - '\n' + - ' /**\n' + - ' * Prints out the given Array of Date objects.\n' + - ' *\n' + - ' * @param list of the dates to print\n' + - ' */\n' + - ' private static void printDateList(List list) {\n' + - ' System.out.println(list.toString());\n' + - ' }\n' + - '}', - }, - { - fileName: 'src/de/test/MergeSort.java', - fileContent: - 'package de.test;\n' + - '\n' + - 'import java.util.*;\n' + - '\n' + - 'public class MergeSort implements SortStrategy {\n' + - '\n' + - ' /**\n' + - ' * Wrapper method for the real MergeSort algorithm.\n' + - ' *\n' + - ' * @param input the List of Dates to be sorted\n' + - ' */\n' + - ' public void performSort(List input) {\n' + - ' mergesort(input, 0, input.size() - 1);\n' + - ' }\n' + - '\n' + - ' // Recursive merge sort method\n' + - ' private void mergesort(List input, int low, int high) {\n' + - ' if (high - low < 1) {\n' + - ' return;\n' + - ' }\n' + - ' int mid = (low + high) / 2;\n' + - ' mergesort(input, low, mid);\n' + - ' mergesort(input, mid + 1, high);\n' + - ' merge(input, low, mid, high);\n' + - ' }\n' + - '\n' + - ' // Merge method\n' + - ' private void merge(List input, int low, int middle, int high) {\n' + - '\n' + - ' Date[] temp = new Date[high - low + 1];\n' + - ' int leftIndex = low;\n' + - ' int rightIndex = middle + 1;\n' + - ' int wholeIndex = 0;\n' + - ' while (leftIndex <= middle && rightIndex <= high) {\n' + - ' if (input.get(leftIndex).compareTo(input.get(rightIndex)) <= 0) {\n' + - ' temp[wholeIndex] = input.get(leftIndex++);\n' + - ' }\n' + - ' else {\n' + - ' temp[wholeIndex] = input.get(rightIndex++);\n' + - ' }\n' + - ' wholeIndex++;\n' + - ' }\n' + - ' if (leftIndex <= middle && rightIndex > high) {\n' + - ' while (leftIndex <= middle) {\n' + - ' temp[wholeIndex++] = input.get(leftIndex++);\n' + - ' }\n' + - ' }\n' + - ' else {\n' + - ' while (rightIndex <= high) {\n' + - ' temp[wholeIndex++] = input.get(rightIndex++);\n' + - ' }\n' + - ' }\n' + - ' for (wholeIndex = 0; wholeIndex < temp.length; wholeIndex++) {\n' + - ' input.set(wholeIndex + low, temp[wholeIndex]);\n' + - ' }\n' + - ' }\n' + - '}', - }, - { - fileName: 'src/de/test/Policy.java', - fileContent: - 'package de.test;\n' + - '\n' + - 'public class Policy {\n' + - '\n' + - ' private static final int DATES_SIZE_THRESHOLD = 10;\n' + - '\n' + - ' private Context context;\n' + - '\n' + - ' public Policy(Context context) {\n' + - ' this.context = context;\n' + - ' }\n' + - '\n' + - ' /**\n' + - ' * Chooses a strategy depending on the number of date objects.\n' + - ' */\n' + - ' public void configure() {\n' + - ' if (this.context.getDates().size() > DATES_SIZE_THRESHOLD) {\n' + - ' System.out.println("More than " + DATES_SIZE_THRESHOLD + " dates, choosing merge sort!");\n' + - ' this.context.setSortAlgorithm(new MergeSort());\n' + - ' } else {\n' + - ' System.out.println("Less or equal than " + DATES_SIZE_THRESHOLD + " dates. choosing quick sort!");\n' + - ' this.context.setSortAlgorithm(new BubbleSort());\n' + - ' }\n' + - ' }\n' + - '}', - }, - ], -}; diff --git a/src/test/k6/resource/constants_python.js b/src/test/k6/resource/constants_python.js deleted file mode 100644 index e105600c76a1..000000000000 --- a/src/test/k6/resource/constants_python.js +++ /dev/null @@ -1,296 +0,0 @@ -export const programmingExerciseProblemStatementPython = - '# Sorting with the Strategy Pattern\n' + - '\n' + - 'In this exercise, we want to implement sorting algorithms and choose them based on runtime specific variables.\n' + - '\n' + - '### Part 1: Sorting\n' + - '\n' + - 'First, we need to implement two sorting algorithms, in this case `MergeSort` and `BubbleSort`.\n' + - '\n' + - '**You have the following tasks:**\n' + - '\n' + - '1. [task][Implement Bubble Sort](test_bubble_sort)\n' + - 'Implement the method `perform_sort(List)` in the class `BubbleSort`. Make sure to follow the Bubble Sort algorithm exactly.\n' + - '\n' + - '2. [task][Implement Merge Sort](test_merge_sort)\n' + - 'Implement the method `perform_sort(List)` in the class `MergeSort`. Make sure to follow the Merge Sort algorithm exactly.\n' + - '\n' + - '### Part 2: Strategy Pattern\n' + - '\n' + - 'We want the application to apply different algorithms for sorting a `List` of `Int` objects.\n' + - 'Use the strategy pattern to select the right sorting algorithm at runtime.\n' + - '\n' + - '**You have the following tasks:**\n' + - '\n' + - '1. [task][SortStrategy Interface](test_sort_strategy_class,test_sort_strategy_methods)\n' + - 'Create a `SortStrategy` abstract class with an abstract method and adjust the sorting algorithms so that they inherit from this class.\n' + - '\n' + - '2. [task][Context Class](test_context_attributes,test_context_methods)\n' + - 'Create and implement a `Context` class following the below class diagram\n' + - '\n' + - '3. [task][Context Policy](test_policy_constructor,test_policy_attributes,test_policy_methods)\n' + - 'Create and implement a `Policy` class following the below class diagram with a simple configuration mechanism:\n' + - '\n' + - '1. [task][Select MergeSort](test_merge_sort_struct,test_merge_sort_for_big_list)\n' + - 'Select `MergeSort` when the List has more than 10 dates.\n' + - '\n' + - '2. [task][Select BubbleSort](test_bubble_sort_struct,test_bubble_sort_for_small_list)\n' + - 'Select `BubbleSort` when the List has less or equal 10 dates.\n' + - '\n' + - '4. Complete the `Client` class which demonstrates switching between two strategies at runtime.\n' + - '\n' + - '@startuml\n' + - '\n' + - 'class Client {\n' + - '}\n' + - '\n' + - 'class Policy {\n' + - '+configure()\n' + - '}\n' + - '\n' + - 'class Context {\n' + - 'numbers: List\n' + - '+sort()\n' + - '}\n' + - '\n' + - 'abstract class SortStrategy {\n' + - '+perform_sort(List)\n' + - '}\n' + - '\n' + - 'class BubbleSort {\n' + - '+performSort(List)\n' + - '}\n' + - '\n' + - 'class MergeSort {\n' + - '+perform_sort(List)\n' + - '}\n' + - '\n' + - 'MergeSort -up-|> SortStrategy #testsColor(test_merge_sort_class)\n' + - 'BubbleSort -up-|> SortStrategy #testsColor(test_bubble_sort_class)\n' + - 'Policy -right-> Context #testsColor(test_policy_attributes): context\n' + - 'Context -right-> SortStrategy #testsColor(test_context_attributes): sortAlgorithm\n' + - 'Client .down.> Policy\n' + - 'Client .down.> Context\n' + - '\n' + - '@enduml\n' + - '\n' + - '\n' + - '### Part 3: Optional Challenges\n' + - '\n' + - '(These are not tested)\n' + - '\n' + - '1. Create a new class `QuickSort` that inherits from `SortStrategy` and implement the Quick Sort algorithm.\n' + - '\n' + - '2. Think about a useful decision in `Policy` when to use the new `QuickSort` algorithm.\n'; - -export const buildErrorContentPython = { - newFiles: [], - content: [ - { - fileName: 'context.py', - fileContent: 'a', - }, - ], -}; - -export const someSuccessfulErrorContentPython = { - newFiles: [], - content: [ - { - fileName: 'sort_strategy.py', - fileContent: - 'from abc import ABC, abstractmethod\n' + - '\n' + - '\n' + - 'class SortStrategy(ABC):\n' + - '\n' + - '\t@abstractmethod\n' + - '\tdef perform_sort(self, array):\n' + - '\t\tpass', - }, - ], -}; - -export const allSuccessfulContentPython = { - newFiles: [], - content: [ - { - fileName: 'sorting_algorithms.py', - fileContent: - 'from .sort_strategy import SortStrategy\n' + - '\n' + - '\n' + - 'class BubbleSort(SortStrategy):\n' + - '\n' + - '\tdef perform_sort(self, arr):\n' + - '\t\tif arr is None:\n' + - '\t\t\treturn\n' + - '\n' + - '\t\tfor i in range(len(arr))[::-1]:\n' + - '\t\t\tfor j in range(i):\n' + - '\t\t\t\tif arr[j] > arr[j + 1]:\n' + - '\t\t\t\t\tarr[j], arr[j + 1] = arr[j + 1], arr[j]\n' + - '\n' + - '\n' + - 'class MergeSort(SortStrategy):\n' + - '\n' + - '\tdef perform_sort(self, arr):\n' + - '\t\tself.__merge_sort(arr, 0, len(arr) - 1)\n' + - '\n' + - '\tdef __merge_sort(self, arr, low, high):\n' + - '\t\tif high - low < 1:\n' + - '\t\t\treturn\n' + - '\n' + - '\t\tmid = int((low + high) / 2)\n' + - '\t\tself.__merge_sort(arr, low, mid)\n' + - '\t\tself.__merge_sort(arr, mid + 1, high)\n' + - '\t\tself.__merge(arr, low, mid, high)\n' + - '\n' + - '\tdef __merge(self, arr, low, mid, high):\n' + - '\t\ttemp = [None] * (high - low + 1)\n' + - '\n' + - '\t\tleft_index = low\n' + - '\t\tright_index = mid + 1\n' + - '\t\twhole_index = 0\n' + - '\n' + - '\t\twhile left_index <= mid and right_index <= high:\n' + - '\t\t\tif arr[left_index] <= arr[right_index]:\n' + - '\t\t\t\ttemp[whole_index] = arr[left_index]\n' + - '\t\t\t\tleft_index += 1\n' + - '\t\t\telse:\n' + - '\t\t\t\ttemp[whole_index] = arr[right_index]\n' + - '\t\t\t\tright_index += 1\n' + - '\t\t\twhole_index += 1\n' + - '\n' + - '\t\tif left_index <= mid and right_index > high:\n' + - '\t\t\twhile left_index <= mid:\n' + - '\t\t\t\ttemp[whole_index] = arr[left_index]\n' + - '\t\t\t\twhole_index += 1\n' + - '\t\t\t\tleft_index += 1\n' + - '\t\telse:\n' + - '\t\t\twhile right_index <= high:\n' + - '\t\t\t\ttemp[whole_index] = arr[right_index]\n' + - '\t\t\t\twhole_index += 1\n' + - '\t\t\t\tright_index += 1\n' + - '\n' + - '\t\tfor whole_index in range(len(temp)):\n' + - '\t\t\tarr[whole_index + low] = temp[whole_index]', - }, - { - fileName: 'policy.py', - fileContent: - 'from .sorting_algorithms import *\n' + - '\n' + - '\n' + - 'class Policy:\n' + - '\tcontext = None\n' + - '\n' + - '\tdef __init__(self, context):\n' + - '\t\tself.context = context\n' + - '\n' + - '\tdef configure(self):\n' + - '\t\tif len(self.context.numbers) > 10:\n' + - "\t\t\tprint('More than 10 numbers, choosing merge sort!')\n" + - '\t\t\tself.context.sorting_algorithm = MergeSort()\n' + - '\t\telse:\n' + - "\t\t\tprint('Less or equal than 10 numbers, choosing bubble sort!')\n" + - '\t\t\tself.context.sorting_algorithm = BubbleSort()\n', - }, - { - fileName: 'context.py', - fileContent: - 'package de.test;\n' + - '\n' + - 'import java.text.*;\n' + - 'import java.util.*;\n' + - '\n' + - 'public class Client {\n' + - '\n' + - ' /**\n' + - ' * Main method.\n' + - ' * Add code to demonstrate your implementation here.\n' + - ' */\n' + - ' public static void main(String[] args) throws ParseException {\n' + - '\n' + - ' // Init Context and Policy\n' + - '\n' + - ' Context sortingContext = new Context();\n' + - ' Policy policy = new Policy(sortingContext);\n' + - '\n' + - ' // Run 10 times to simulate different sorting strategies\n' + - ' for (int i = 0; i < 10; i++) {\n' + - ' List dates = createRandomDatesList();\n' + - '\n' + - ' sortingContext.setDates(dates);\n' + - ' policy.configure();\n' + - '\n' + - ' System.out.print("Unsorted Array of course dates = ");\n' + - ' printDateList(dates);\n' + - '\n' + - ' sortingContext.sort();\n' + - '\n' + - ' System.out.print("Sorted Array of course dates = ");\n' + - ' printDateList(dates);\n' + - ' }\n' + - ' }\n' + - '\n' + - ' /**\n' + - ' * Generates an Array of random Date objects with random Array size between 5 and 15.\n' + - ' */\n' + - ' private static List createRandomDatesList() throws ParseException {\n' + - ' int listLength = randomIntegerWithin(5, 15);\n' + - ' List list = new ArrayList<>();\n' + - '\n' + - ' SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy");\n' + - ' Date lowestDate = dateFormat.parse("08.11.2016");\n' + - ' Date highestDate = dateFormat.parse("15.04.2017");\n' + - '\n' + - ' for (int i = 0; i < listLength; i++) {\n' + - ' Date randomDate = randomDateWithin(lowestDate, highestDate);\n' + - ' list.add(randomDate);\n' + - ' }\n' + - ' return list;\n' + - ' }\n' + - '\n' + - ' /**\n' + - ' * Creates a random Date within given Range\n' + - ' */\n' + - ' private static Date randomDateWithin(Date low, Date high) {\n' + - ' long randomLong = randomLongWithin(low.getTime(), high.getTime());\n' + - ' return new Date(randomLong);\n' + - ' }\n' + - '\n' + - ' /**\n' + - ' * Creates a random Long within given Range\n' + - ' */\n' + - ' private static long randomLongWithin(long low, long high) {\n' + - ' return low + (long) (Math.random() * (high - low));\n' + - ' }\n' + - '\n' + - ' /**\n' + - ' * Creates a random Integer within given Range\n' + - ' */\n' + - ' private static int randomIntegerWithin(int low, int high) {\n' + - ' return low + (int) (Math.random() * (high - low));\n' + - ' }\n' + - '\n' + - ' /**\n' + - ' * Prints out given Array of Date objects\n' + - ' */\n' + - ' private static void printDateList(List list) {\n' + - ' System.out.println(list.toString());\n' + - ' }\n' + - '}', - }, - { - fileName: 'context.py', - fileContent: - 'class Context:\n' + - '\tsorting_algorithm = None\n' + - '\tnumbers = None\n' + - '\n' + - '\tdef sort(self):\n' + - '\t\tself.sorting_algorithm.perform_sort(self.numbers)\n', - }, - ], -}; diff --git a/src/test/k6/util/utils.js b/src/test/k6/util/utils.js deleted file mode 100644 index 06789f267b2b..000000000000 --- a/src/test/k6/util/utils.js +++ /dev/null @@ -1,41 +0,0 @@ -// https://stackoverflow.com/questions/10726909/random-alpha-numeric-string-in-javascript -const randString = function randomString(length, chars) { - let result = ''; - for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)]; - return result; -}; - -export function nextAlphanumeric(length) { - // helpers for random titles - let allowedChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - - return randString(length, allowedChars); -} - -export function nextWSSubscriptionId() { - return Math.random() - .toString(36) - .replace(/[^a-z]+/g, '') - .slice(0, 12); -} - -export function randomArrayValue(array) { - return array[Math.floor(Math.random() * array.length)]; -} - -export function extractDestination(message) { - return extractHeader(message, 'destination'); -} - -export function extractHeader(message, header) { - const headers = extractSTOMPHeaders(message); - return headers.match('(?:.*:.*|\\n)*' + header + ':(.*)\\n(?:.*:.*|\\n)*')[1]; -} - -export function extractSTOMPHeaders(message) { - return message.match('MESSAGE\\n((?:.|\\n)*)\\n\\n(?:(?:.|\\n)*)')[1]; -} - -export function extractMessageContent(message) { - return message.match('MESSAGE\n(?:.|\\n)*\\n\\n((?:.|\\n)*)\u0000')[1]; -} From 0070ab879b4fdc1cf82934345c8905770704a067 Mon Sep 17 00:00:00 2001 From: Tobias Lippert <84102468+tobias-lippert@users.noreply.github.com> Date: Sat, 30 Sep 2023 09:21:06 +0200 Subject: [PATCH 08/19] General: Include quiz exercise information and submissions in course and exam archive (#7256) --- .../StudentParticipationRepository.java | 8 ++ .../export/CourseExamExportService.java | 13 +- ...DataExportQuizExerciseCreationService.java | 100 +++++++++++++-- .../service/export/DataExportUtil.java | 4 +- ...zExerciseWithSubmissionsExportService.java | 97 +++++++++++++++ .../artemis/course/CourseTestService.java | 114 ++++++++++++++++++ ...rseBitbucketBambooJiraIntegrationTest.java | 39 ++++++ 7 files changed, 357 insertions(+), 18 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/service/export/QuizExerciseWithSubmissionsExportService.java diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index 4a7d7bdace73..6abb8eae5d58 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -993,6 +993,14 @@ GROUP BY COALESCE(p.student.id, ts.id) """) Set sumPresentationScoreByStudentIdsAndCourseId(@Param("courseId") long courseId, @Param("studentIds") Set studentIds); + @Query(""" + SELECT p FROM StudentParticipation p + LEFT JOIN FETCH p.submissions s + WHERE p.exercise.id = :exerciseId + + """) + Set findByExerciseIdWithEagerSubmissions(long exerciseId); + /** * Helper interface to map the result of the {@link #sumPresentationScoreByStudentIdsAndCourseId(long, Set)} query to a map. */ diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/CourseExamExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/CourseExamExportService.java index ddd5c15e5a5a..371e7ce1c2f6 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/CourseExamExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/CourseExamExportService.java @@ -49,6 +49,8 @@ public class CourseExamExportService { private final ModelingExerciseWithSubmissionsExportService modelingExerciseWithSubmissionsExportService; + private final QuizExerciseWithSubmissionsExportService quizExerciseWithSubmissionsExportService; + private final FileService fileService; private final ExamRepository examRepository; @@ -58,14 +60,15 @@ public class CourseExamExportService { public CourseExamExportService(ProgrammingExerciseExportService programmingExerciseExportService, ZipFileService zipFileService, FileService fileService, TextExerciseWithSubmissionsExportService textExerciseWithSubmissionsExportService, FileUploadExerciseWithSubmissionsExportService fileUploadExerciseWithSubmissionsExportService, - ModelingExerciseWithSubmissionsExportService modelingExerciseWithSubmissionsExportService, WebsocketMessagingService websocketMessagingService, - ExamRepository examRepository) { + ModelingExerciseWithSubmissionsExportService modelingExerciseWithSubmissionsExportService, + QuizExerciseWithSubmissionsExportService quizExerciseWithSubmissionsExportService, WebsocketMessagingService websocketMessagingService, ExamRepository examRepository) { this.programmingExerciseExportService = programmingExerciseExportService; this.zipFileService = zipFileService; this.fileService = fileService; this.textExerciseWithSubmissionsExportService = textExerciseWithSubmissionsExportService; this.fileUploadExerciseWithSubmissionsExportService = fileUploadExerciseWithSubmissionsExportService; this.modelingExerciseWithSubmissionsExportService = modelingExerciseWithSubmissionsExportService; + this.quizExerciseWithSubmissionsExportService = quizExerciseWithSubmissionsExportService; this.websocketMessagingService = websocketMessagingService; this.examRepository = examRepository; } @@ -397,9 +400,9 @@ else if (exercise instanceof ModelingExercise) { exportedExercises.add(modelingExerciseWithSubmissionsExportService.exportModelingExerciseWithSubmissions(exercise, submissionsExportOptions, exerciseExportDir, exportErrors, reportData)); } - else if (exercise instanceof QuizExercise) { - // TODO: Quiz submissions aren't supported yet - continue; + else if (exercise instanceof QuizExercise quizExercise) { + exportedExercises.add(quizExerciseWithSubmissionsExportService.exportExerciseWithSubmissions(quizExercise, exerciseExportDir, exportErrors, reportData)); + } else { // Exercise is not supported so skip diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportQuizExerciseCreationService.java b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportQuizExerciseCreationService.java index dcb16d013298..df65b9cbb935 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportQuizExerciseCreationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportQuizExerciseCreationService.java @@ -7,6 +7,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.validation.constraints.NotNull; + import org.apache.commons.io.FileUtils; import org.springframework.stereotype.Service; @@ -14,13 +16,16 @@ import de.tum.in.www1.artemis.domain.quiz.*; import de.tum.in.www1.artemis.repository.QuizQuestionRepository; import de.tum.in.www1.artemis.repository.QuizSubmissionRepository; +import de.tum.in.www1.artemis.repository.StudentParticipationRepository; import de.tum.in.www1.artemis.service.DragAndDropQuizAnswerConversionService; +import de.tum.in.www1.artemis.service.archival.ArchivalReportEntry; /** * A service to create the data export for quiz exercise participations. * This includes creating a pdf highlighting the submitted answers for drag and drop questions and * txt files containing the submitted answers for multiple choice and short answer questions. * Additionally, the results can be included in the export if the due date is over. + * This service is also used to export the student submissions for archival. */ @Service public class DataExportQuizExerciseCreationService { @@ -33,11 +38,26 @@ public class DataExportQuizExerciseCreationService { private final DragAndDropQuizAnswerConversionService dragAndDropQuizAnswerConversionService; + private final StudentParticipationRepository studentParticipationRepository; + public DataExportQuizExerciseCreationService(QuizSubmissionRepository quizSubmissionRepository, QuizQuestionRepository quizQuestionRepository, - DragAndDropQuizAnswerConversionService dragAndDropQuizAnswerConversionService) { + DragAndDropQuizAnswerConversionService dragAndDropQuizAnswerConversionService, StudentParticipationRepository studentParticipationRepository) { this.quizSubmissionRepository = quizSubmissionRepository; this.quizQuestionRepository = quizQuestionRepository; this.dragAndDropQuizAnswerConversionService = dragAndDropQuizAnswerConversionService; + this.studentParticipationRepository = studentParticipationRepository; + } + + /** + * Creates an export for an exercise participation of a quiz exercise. + * + * @param quizExercise the quiz exercise for which the export should be created + * @param participation the participation for which the export should be created + * @param outputDir the directory in which the export should be stored + * @param includeResults true if the results should be included in the export (if the due date is over) + */ + public void createQuizAnswersExport(QuizExercise quizExercise, StudentParticipation participation, Path outputDir, boolean includeResults) { + createQuizAnswersExport(quizExercise, participation, outputDir, includeResults, Optional.empty()); } /** @@ -48,14 +68,15 @@ public DataExportQuizExerciseCreationService(QuizSubmissionRepository quizSubmis * @param participation the participation for which the export should be created * @param outputDir the directory in which the export should be stored * @param includeResults true if the results should be included in the export (if the due date is over) - * @throws IOException if an error occurs while accessing the file system. + * @param exportErrors an optional list of errors that occurred during the export + * @return true if the export was successful, false otherwise */ - public void createQuizAnswersExport(QuizExercise quizExercise, StudentParticipation participation, Path outputDir, boolean includeResults) throws IOException { + private boolean createQuizAnswersExport(QuizExercise quizExercise, StudentParticipation participation, Path outputDir, boolean includeResults, + Optional> exportErrors) { Set quizQuestions = quizQuestionRepository.getQuizQuestionsByExerciseId(quizExercise.getId()); - QuizSubmission quizSubmission; - + boolean errorOccurred = false; for (var submission : participation.getSubmissions()) { - quizSubmission = quizSubmissionRepository.findWithEagerSubmittedAnswersById(submission.getId()); + QuizSubmission quizSubmission = quizSubmissionRepository.findWithEagerSubmittedAnswersById(submission.getId()); List multipleChoiceQuestionsSubmissions = new ArrayList<>(); List shortAnswerQuestionsSubmissions = new ArrayList<>(); for (var question : quizQuestions) { @@ -63,7 +84,14 @@ public void createQuizAnswersExport(QuizExercise quizExercise, StudentParticipat // if this question wasn't answered, the submitted answer is null if (submittedAnswer != null) { if (submittedAnswer instanceof DragAndDropSubmittedAnswer dragAndDropSubmittedAnswer) { - dragAndDropQuizAnswerConversionService.convertDragAndDropQuizAnswerAndStoreAsPdf(dragAndDropSubmittedAnswer, outputDir, includeResults); + try { + dragAndDropQuizAnswerConversionService.convertDragAndDropQuizAnswerAndStoreAsPdf(dragAndDropSubmittedAnswer, outputDir, includeResults); + } + catch (IOException e) { + errorOccurred = true; + exportErrors.ifPresent(errors -> errors.add("Failed to export drag and drop answers for quiz submission " + submission.getId() + " of quiz exercise " + + quizExercise.getTitle() + " with id " + quizExercise.getId())); + } } else if (submittedAnswer instanceof ShortAnswerSubmittedAnswer shortAnswerSubmittedAnswer) { shortAnswerQuestionsSubmissions.add(createExportForShortAnswerQuestion(shortAnswerSubmittedAnswer, includeResults)); @@ -74,15 +102,65 @@ else if (submittedAnswer instanceof MultipleChoiceSubmittedAnswer multipleChoice } } if (!multipleChoiceQuestionsSubmissions.isEmpty()) { - FileUtils.writeLines(outputDir.resolve("quiz_submission_" + submission.getId() + "_multiple_choice_questions_answers" + TXT_FILE_EXTENSION).toFile(), - StandardCharsets.UTF_8.name(), multipleChoiceQuestionsSubmissions); + try { + FileUtils.writeLines(outputDir.resolve("quiz_submission_" + submission.getId() + "_multiple_choice_questions_answers" + TXT_FILE_EXTENSION).toFile(), + StandardCharsets.UTF_8.name(), multipleChoiceQuestionsSubmissions); + } + catch (IOException e) { + errorOccurred = true; + exportErrors.ifPresent(errors -> errors.add("Failed to export multiple choice answers for quiz submission " + submission.getId() + " of quiz exercise " + + quizExercise.getTitle() + " with id " + quizExercise.getId())); + } } if (!shortAnswerQuestionsSubmissions.isEmpty()) { - FileUtils.writeLines(outputDir.resolve("quiz_submission_" + submission.getId() + "_short_answer_questions_answers" + TXT_FILE_EXTENSION).toFile(), - StandardCharsets.UTF_8.name(), shortAnswerQuestionsSubmissions); + try { + FileUtils.writeLines(outputDir.resolve("quiz_submission_" + submission.getId() + "_short_answer_questions_answers" + TXT_FILE_EXTENSION).toFile(), + StandardCharsets.UTF_8.name(), shortAnswerQuestionsSubmissions); + } + catch (IOException e) { + errorOccurred = true; + exportErrors.ifPresent(errors -> errors.add("Failed to export short answer answers for quiz submission " + submission.getId() + " of quiz exercise " + + quizExercise.getTitle() + " with id " + quizExercise.getId())); + } } } + return !errorOccurred; + } + /** + * Exports the student submissions for a quiz exercise. + * + * @param quizExercise the quiz exercise for which the submissions should be exported + * @param exerciseDir the directory in which the submissions should be stored + * @param exportErrors a list of errors that occurred during the export + * @param archivalReportEntries a list of report entries to report failed/successful exports + */ + public void exportStudentSubmissionsForArchival(QuizExercise quizExercise, Path exerciseDir, @NotNull List exportErrors, + List archivalReportEntries) { + var participations = studentParticipationRepository.findByExerciseIdWithEagerSubmissions(quizExercise.getId()); + int participationsWithoutSubmission = 0; + int successfulExports = 0; + for (var participation : participations) { + if (participation.getSubmissions().isEmpty()) { + participationsWithoutSubmission++; + continue; + } + var outputDir = exerciseDir.resolve("participation-" + participation.getId() + "-" + participation.getParticipantIdentifier()); + try { + DataExportUtil.createDirectoryIfNotExistent(outputDir); + } + catch (IOException e) { + exportErrors.add("Failed to create directory for quiz exercise participation " + participation.getId() + "of quiz exercise " + quizExercise.getTitle() + " with id " + + quizExercise.getId() + ". Won't export this participation."); + continue; + } + boolean successful = createQuizAnswersExport(quizExercise, participation, outputDir, true, Optional.ofNullable(exportErrors)); + if (successful) { + successfulExports++; + } + } + archivalReportEntries + .add(new ArchivalReportEntry(quizExercise, quizExercise.getSanitizedExerciseTitle(), participations.size(), successfulExports, participationsWithoutSubmission)); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportUtil.java b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportUtil.java index fcd11d0267f0..7b27e55e7df7 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportUtil.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportUtil.java @@ -9,7 +9,7 @@ /** * A utility class for data export containing helper methods that are frequently used in the different services responsible for creating data exports. */ -final class DataExportUtil { +public final class DataExportUtil { private static final String COURSE_DIRECTORY_PREFIX = "course_"; @@ -23,7 +23,7 @@ private DataExportUtil() { * @param directory the directory to create * @throws IOException if an error occurs while accessing the file system */ - static void createDirectoryIfNotExistent(Path directory) throws IOException { + public static void createDirectoryIfNotExistent(Path directory) throws IOException { if (!Files.exists(directory)) { Files.createDirectories(directory); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/QuizExerciseWithSubmissionsExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/QuizExerciseWithSubmissionsExportService.java new file mode 100644 index 000000000000..38ce256caf01 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/export/QuizExerciseWithSubmissionsExportService.java @@ -0,0 +1,97 @@ +package de.tum.in.www1.artemis.service.export; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.in.www1.artemis.domain.quiz.DragAndDropQuestion; +import de.tum.in.www1.artemis.domain.quiz.QuizExercise; +import de.tum.in.www1.artemis.repository.QuizExerciseRepository; +import de.tum.in.www1.artemis.service.FilePathService; +import de.tum.in.www1.artemis.service.FileService; +import de.tum.in.www1.artemis.service.archival.ArchivalReportEntry; + +/** + * Service responsible for exporting quiz exercises with their submissions. + */ +@Service +public class QuizExerciseWithSubmissionsExportService { + + private final QuizExerciseRepository quizExerciseRepository; + + private final ObjectMapper objectMapper; + + private final DataExportQuizExerciseCreationService dataExportQuizExerciseCreationService; + + private final FileService fileService; + + private final FilePathService filePathService; + + public QuizExerciseWithSubmissionsExportService(QuizExerciseRepository quizExerciseRepository, MappingJackson2HttpMessageConverter springMvcJacksonConverter, + DataExportQuizExerciseCreationService dataExportQuizExerciseCreationService, FileService fileService, FilePathService filePathService) { + this.quizExerciseRepository = quizExerciseRepository; + this.objectMapper = springMvcJacksonConverter.getObjectMapper(); + this.dataExportQuizExerciseCreationService = dataExportQuizExerciseCreationService; + this.fileService = fileService; + this.filePathService = filePathService; + } + + /** + * Exports the given quiz exercise as JSON file with all its submissions and stores it in the given directory. + * + * @param quizExercise the quiz exercise to export + * @param exerciseExportDir the directory where the quiz exercise should be exported to + * @param exportErrors a list of errors that occurred during the export + * @param reportEntries a list of report entries that occurred during the export + * @return the path to the directory where the quiz exercise was exported to + */ + public Path exportExerciseWithSubmissions(QuizExercise quizExercise, Path exerciseExportDir, List exportErrors, List reportEntries) { + quizExercise = quizExerciseRepository.findByIdWithQuestionsAndStatisticsAndCompetenciesElseThrow(quizExercise.getId()); + // do not store unnecessary information in the JSON file + quizExercise.setCourse(null); + quizExercise.setExerciseGroup(null); + try { + fileService.writeObjectToJsonFile(quizExercise, objectMapper, exerciseExportDir.resolve("Exercise-Details-" + quizExercise.getSanitizedExerciseTitle() + ".json")); + } + catch (IOException e) { + exportErrors.add("Failed to export quiz exercise details " + quizExercise.getTitle() + " with id " + quizExercise.getId() + " due to a JSON processing error."); + } + List imagesToExport = new ArrayList<>(); + for (var quizQuestion : quizExercise.getQuizQuestions()) { + if (quizQuestion instanceof DragAndDropQuestion dragAndDropQuestion) { + if (dragAndDropQuestion.getBackgroundFilePath() != null) { + imagesToExport.add(filePathService.actualPathForPublicPath(URI.create(dragAndDropQuestion.getBackgroundFilePath()))); + } + for (var dragItem : dragAndDropQuestion.getDragItems()) { + if (dragItem.getPictureFilePath() != null) { + imagesToExport.add(filePathService.actualPathForPublicPath(URI.create(dragItem.getPictureFilePath()))); + + } + } + if (!imagesToExport.isEmpty()) { + var imagesDir = exerciseExportDir.resolve("images-for-drag-and-drop-question-" + dragAndDropQuestion.getId()); + fileService.createDirectory(imagesDir); + imagesToExport.forEach(path -> { + try { + FileUtils.copyFile(path.toFile(), imagesDir.resolve(path.getFileName()).toFile()); + } + catch (IOException e) { + exportErrors.add("Failed to export image file with file path " + path + " for drag and drop question with id " + dragAndDropQuestion.getId()); + } + }); + } + } + } + dataExportQuizExerciseCreationService.exportStudentSubmissionsForArchival(quizExercise, exerciseExportDir, exportErrors, reportEntries); + return exerciseExportDir; + } + +} diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java index 17598432a202..66515fcfd926 100644 --- a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java @@ -5,10 +5,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.mockStatic; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.file.Files; @@ -17,8 +19,10 @@ import java.time.Instant; import java.time.ZonedDateTime; import java.util.*; +import java.util.function.Predicate; import java.util.stream.Collectors; +import javax.imageio.ImageIO; import javax.validation.constraints.NotNull; import org.assertj.core.data.Offset; @@ -53,12 +57,14 @@ import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; import de.tum.in.www1.artemis.domain.participation.*; import de.tum.in.www1.artemis.domain.quiz.QuizExercise; +import de.tum.in.www1.artemis.domain.quiz.QuizSubmission; import de.tum.in.www1.artemis.exam.ExamFactory; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; import de.tum.in.www1.artemis.exercise.programmingexercise.MockDelegate; import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseUtilService; import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureUtilService; @@ -75,6 +81,7 @@ import de.tum.in.www1.artemis.service.dto.UserDTO; import de.tum.in.www1.artemis.service.dto.UserPublicInfoDTO; import de.tum.in.www1.artemis.service.export.CourseExamExportService; +import de.tum.in.www1.artemis.service.export.DataExportUtil; import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; import de.tum.in.www1.artemis.service.scheduled.ParticipantScoreScheduleService; import de.tum.in.www1.artemis.team.TeamUtilService; @@ -216,6 +223,9 @@ public class CourseTestService { @Autowired private ParticipantScoreScheduleService participantScoreScheduleService; + @Autowired + private QuizExerciseUtilService quizExerciseUtilService; + private static final int numberOfStudents = 8; private static final int numberOfTutors = 5; @@ -1914,6 +1924,109 @@ public Course testArchiveCourseWithTestModelingAndFileUploadExercises() throws E return updatedCourse; } + public void testArchiveCourseWithQuizExercise(String userPrefix) throws Exception { + var course = courseUtilService.createCourse(); + var quizSubmission = quizExerciseUtilService.addQuizExerciseToCourseWithParticipationAndSubmissionForUser(course, userPrefix + "student1", false); + var quizExercise = quizExerciseUtilService.createQuiz(ZonedDateTime.now().minusHours(5), ZonedDateTime.now().minusHours(2), QuizMode.INDIVIDUAL); + quizExercise = exerciseRepo.save(quizExercise); + participationUtilService.createAndSaveParticipationForExercise(quizExercise, userPrefix + "student2"); + var archivePath = courseExamExportService.exportCourse(course, courseArchivesDirPath, Collections.synchronizedList(new ArrayList<>())); + assertThat(archivePath).isNotEmpty(); + extractAndAssertContent(archivePath.orElseThrow(), quizSubmission); + } + + public void testArchiveCourseWithQuizExerciseCannotExportExerciseDetails() throws IOException { + var course = courseUtilService.createCourse(); + var quizSubmission = quizExerciseUtilService.addQuizExerciseToCourseWithParticipationAndSubmissionForUser(course, userPrefix + "student1", false); + var archivePath = courseExamExportService.exportCourse(course, courseArchivesDirPath, Collections.synchronizedList(new ArrayList<>())); + assertThat(archivePath).isNotEmpty(); + Predicate missingPathPredicate = path -> "Exercise-Details-quiz.json".equals(path.getFileName().toString()); + extractAndAssertMissingContent(archivePath.orElseThrow(), quizSubmission, missingPathPredicate); + } + + public void testArchiveCourseWithQuizExerciseCannotExportDragAndDropSubmission() throws IOException { + List exportErrors = Collections.synchronizedList(new ArrayList<>()); + var course = courseUtilService.createCourse(); + var quizSubmission = quizExerciseUtilService.addQuizExerciseToCourseWithParticipationAndSubmissionForUser(course, userPrefix + "student1", false); + try (MockedStatic mockedImageIO = mockStatic(ImageIO.class)) { + mockedImageIO.when(() -> ImageIO.read(any(File.class))).thenThrow(new IOException()); + var archivePath = courseExamExportService.exportCourse(course, courseArchivesDirPath, exportErrors); + assertThat(archivePath).isNotEmpty(); + Predicate missingPathPredicate = path -> path.getFileName().toString().contains("dragAndDropQuestion") && path.getFileName().toString().endsWith(".pdf"); + extractAndAssertMissingContent(archivePath.orElseThrow(), quizSubmission, missingPathPredicate); + } + } + + public void testArchiveCourseWithQuizExerciseCannotCreateParticipationDirectory() throws IOException { + List exportErrors = Collections.synchronizedList(new ArrayList<>()); + var course = courseUtilService.createCourse(); + quizExerciseUtilService.addQuizExerciseToCourseWithParticipationAndSubmissionForUser(course, userPrefix + "student1", false); + try (MockedStatic mockedFiles = mockStatic(DataExportUtil.class)) { + mockedFiles.when(() -> DataExportUtil.createDirectoryIfNotExistent(any())).thenThrow(new IOException()); + var archivePath = courseExamExportService.exportCourse(course, courseArchivesDirPath, exportErrors); + assertThat(archivePath).isNotEmpty(); + } + + } + + public void testArchiveCourseWithQuizExerciseCannotExportMCOrSAAnswersSubmission(String fileName, String dynamicErrorMsg) throws IOException { + List exportErrors = Collections.synchronizedList(new ArrayList<>()); + var course = courseUtilService.createCourse(); + var quizSubmission = quizExerciseUtilService.addQuizExerciseToCourseWithParticipationAndSubmissionForUser(course, userPrefix + "student1", false); + try (MockedStatic mockedFiles = mockStatic(org.apache.commons.io.FileUtils.class)) { + mockedFiles.when(() -> org.apache.commons.io.FileUtils.writeLines(argThat(file -> file.toString().contains(fileName)), anyString(), anyList())) + .thenThrow(new IOException()); + var archivePath = courseExamExportService.exportCourse(course, courseArchivesDirPath, exportErrors); + assertThat(archivePath).isNotEmpty(); + Predicate missingPathPredicate = path -> path.getFileName().toString().contains(fileName) && path.getFileName().toString().endsWith(".txt"); + extractAndAssertMissingContent(archivePath.orElseThrow(), quizSubmission, missingPathPredicate); + assertThat(exportErrors).hasSize(1); + assertThat(exportErrors.get(0)).contains("Failed to export " + dynamicErrorMsg + " answers"); + } + } + + private void extractAndAssertMissingContent(Path courseArchivePath, QuizSubmission quizSubmission, Predicate missingPathPredicate) throws IOException { + zipFileTestUtilService.extractZipFileRecursively(courseArchivePath.toString()); + var exercise = quizSubmission.getParticipation().getExercise(); + StudentParticipation studentParticipation = (StudentParticipation) quizSubmission.getParticipation(); + var courseArchiveDir = courseArchivePath.getParent().resolve(courseArchivePath.getFileName().toString().replace(".zip", "")); + assertThat(courseArchiveDir).exists(); + + try (var files = Files.walk(courseArchiveDir)) { + assertThat(files.filter(file -> Files.isDirectory(file) || Files.isRegularFile(file))) + // exercise directory + .anyMatch(file -> (exercise.getSanitizedExerciseTitle() + "_" + exercise.getId()).equals(file.getFileName().toString())) + // participation directory + .anyMatch( + file -> ("participation-" + studentParticipation.getId() + "-" + studentParticipation.getParticipantIdentifier()).equals(file.getFileName().toString())) + .noneMatch(missingPathPredicate); + } + } + + private void extractAndAssertContent(Path courseArchivePath, QuizSubmission quizSubmission) throws IOException { + zipFileTestUtilService.extractZipFileRecursively(courseArchivePath.toString()); + var exercise = quizSubmission.getParticipation().getExercise(); + StudentParticipation studentParticipation = (StudentParticipation) quizSubmission.getParticipation(); + var courseArchiveDir = courseArchivePath.getParent().resolve(courseArchivePath.getFileName().toString().replace(".zip", "")); + assertThat(courseArchiveDir).exists(); + try (var files = Files.walk(courseArchiveDir)) { + assertThat(files.filter(file -> Files.isDirectory(file) || Files.isRegularFile(file))) + // exercise directory + .anyMatch(file -> (exercise.getSanitizedExerciseTitle() + "_" + exercise.getId()).equals(file.getFileName().toString())) + // participation directory + .anyMatch( + file -> ("participation-" + studentParticipation.getId() + "-" + studentParticipation.getParticipantIdentifier()).equals(file.getFileName().toString())) + // exercise details file + .anyMatch(file -> ("Exercise-Details-quiz.json").equals(file.getFileName().toString())) + // drag and drop question submission pdf + .anyMatch(file -> file.getFileName().toString().contains("dragAndDropQuestion") && file.getFileName().toString().endsWith(".pdf")) + // MC submission txt file + .anyMatch(file -> file.getFileName().toString().contains("multiple_choice_questions_answers") && file.getFileName().toString().endsWith(".txt")) + // short answer submission txt file + .anyMatch(file -> file.getFileName().toString().contains("short_answer_questions_answers") && file.getFileName().toString().endsWith(".txt")); + } + } + /** * Test */ @@ -3115,4 +3228,5 @@ public void testUpdateCourseEnableLearningPaths() throws Exception { final var learningPath = learningPathRepository.findByCourseIdAndUserId(course.getId(), student.getId()); assertThat(learningPath).as("enable learning paths triggers generation").isPresent(); } + } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java index 1b94cc3cdf81..a39b981afe99 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java @@ -10,10 +10,13 @@ import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -673,6 +676,42 @@ void testArchiveCourseWithTestModelingAndFileUploadExercises() throws Exception courseTestService.testArchiveCourseWithTestModelingAndFileUploadExercises(); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testArchiveCourseWithQuizExercise() throws Exception { + courseTestService.testArchiveCourseWithQuizExercise(TEST_PREFIX); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testArchiveCourseWithQuizExerciseCannotExportExerciseDetails() throws Exception { + doThrow(new IOException("Error")).when(fileService).writeObjectToJsonFile(any(), any(ObjectMapper.class), any(Path.class)); + courseTestService.testArchiveCourseWithQuizExerciseCannotExportExerciseDetails(); + } + + @ParameterizedTest + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + @MethodSource("provideFileNameAndErrorMsg") + void testArchiveCourseWithQuizExerciseCannotExportMCOrSAAnswersSubmission(String dynamicFilenamePart, String dynamicErrorMsgPart) throws Exception { + courseTestService.testArchiveCourseWithQuizExerciseCannotExportMCOrSAAnswersSubmission(dynamicFilenamePart, dynamicErrorMsgPart); + } + + private static Stream provideFileNameAndErrorMsg() { + return Stream.of(Arguments.of("multiple_choice_questions_answers", "multiple choice"), Arguments.of("short_answer_questions_answers", "short answer")); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testArchiveCourseWithQuizExerciseCannotExportDragAndDropAnswersSubmission() throws Exception { + courseTestService.testArchiveCourseWithQuizExerciseCannotExportDragAndDropSubmission(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testArchiveCourseWithQuizExerciseCannotCreateParticipationDirectory() throws IOException { + courseTestService.testArchiveCourseWithQuizExerciseCannotCreateParticipationDirectory(); + } + @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportModelingExercise() throws Exception { From 233244a082ac8bb6689a8092b85e3b28933be29d Mon Sep 17 00:00:00 2001 From: Tobias Lippert <84102468+tobias-lippert@users.noreply.github.com> Date: Sat, 30 Sep 2023 09:22:06 +0200 Subject: [PATCH 09/19] Development: Add export documentation (#7249) --- docs/index.rst | 1 + docs/user/exports.rst | 91 ++++++++++++++++++++ docs/user/exports/archive_course.png | Bin 0 -> 1590 bytes docs/user/exports/archive_exam.png | Bin 0 -> 1259 bytes docs/user/exports/data_export.png | Bin 0 -> 942 bytes docs/user/exports/download_archive.png | Bin 0 -> 1665 bytes docs/user/exports/download_exercise.png | Bin 0 -> 1530 bytes docs/user/exports/download_repos.png | Bin 0 -> 1512 bytes docs/user/exports/export.png | Bin 0 -> 969 bytes docs/user/exports/export_quiz.png | Bin 0 -> 888 bytes docs/user/exports/export_results.png | Bin 0 -> 1374 bytes docs/user/exports/export_scores.png | Bin 0 -> 1003 bytes docs/user/exports/export_submissions.png | Bin 0 -> 1604 bytes docs/user/exports/privacy_statement.png | Bin 0 -> 1131 bytes docs/user/exports/scores.png | Bin 0 -> 955 bytes docs/user/exports/scores_navigation_bar.png | Bin 0 -> 934 bytes 16 files changed, 92 insertions(+) create mode 100644 docs/user/exports.rst create mode 100644 docs/user/exports/archive_course.png create mode 100644 docs/user/exports/archive_exam.png create mode 100644 docs/user/exports/data_export.png create mode 100644 docs/user/exports/download_archive.png create mode 100644 docs/user/exports/download_exercise.png create mode 100644 docs/user/exports/download_repos.png create mode 100644 docs/user/exports/export.png create mode 100644 docs/user/exports/export_quiz.png create mode 100644 docs/user/exports/export_results.png create mode 100644 docs/user/exports/export_scores.png create mode 100644 docs/user/exports/export_submissions.png create mode 100644 docs/user/exports/privacy_statement.png create mode 100644 docs/user/exports/scores.png create mode 100644 docs/user/exports/scores_navigation_bar.png diff --git a/docs/index.rst b/docs/index.rst index 2d769bb8de6e..912c3aa9570c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,7 @@ All these exercises are supposed to be run either live in the lecture with insta user/courses/customizable user/scaling user/markdown-support + user/exports user/mobile-applications diff --git a/docs/user/exports.rst b/docs/user/exports.rst new file mode 100644 index 000000000000..b2ac771276b7 --- /dev/null +++ b/docs/user/exports.rst @@ -0,0 +1,91 @@ +.. _exports: + +.. |archive_course| image:: exports/archive_course.png +.. |archive_exam| image:: exports/archive_exam.png +.. |download_archive| image:: exports/download_archive.png +.. |export_quiz| image:: exports/export_quiz.png +.. |export_results| image:: exports/export_results.png +.. |export_submissions| image:: exports/export_submissions.png +.. |download_exercise| image:: exports/download_exercise.png +.. |download_repos| image:: exports/download_repos.png +.. |download_scores| image:: exports/scores.png +.. |export_scores| image:: exports/export_scores.png +.. |export| image:: exports/export.png +.. |scores_navigation_bar| image:: exports/scores_navigation_bar.png +.. |privacy_statement| image:: exports/privacy_statement.png +.. |data_export| image:: exports/data_export.png + + +Exports +======= + +.. contents:: Table of Contents + :local: + :depth: 2 + +Overview +-------- +Artemis offers several options to export or archive different data. The following table gives an overview of the available export options. + +.. list-table:: Export options + :widths: 100 + :header-rows: 1 + + * - Export/Archive option + * - Archive course/exam + * - Export programming exercise material + * - Export programming exercise student repositories + * - Export exercise results + * - Export quiz questions + * - Export exercise submissions + * - Export user data + +Archive course/exam +------------------- +Export all course/exam data, including all exercises and student submissions. +To archive a course or an exam the end date of the entity needs to be in the past. +You can archive a course by clicking |archive_course| on the course management overview page or an exam by clicking |archive_exam| on the exam checklist page. This will create a zip file containing all exercises of the exam or course and all student submissions. For a course all exams are exported as well. +For each exercise the problem statement and a JSON file with exercise details such as points are exported. For programming exercises, the template, solution, test, and auxiliary (if existing) repository is exported as well. +The creation is done asynchronously. You will receive a notification once the archive is ready to download. You can then download the archive by clicking |download_archive| on the course management overview page or the exam checklist page. + +Export programming exercise material +------------------------------------ +Export the exercise material (template, solution, test, and auxiliary repositories as well as the problem statement and other general exercise information) of a programming exercise. +To export the material click the |download_exercise| button on the exercise details page. + +Export quiz exercise +-------------------- +Exports the questions and the sample solution of a quiz in JSON format. +You can export a a quiz exercise by clicking the |export_quiz| button on the exercises overview page. + + +Export programming exercise student repositories +------------------------------------------------ +Export the student repositories (this can include the repositories for both graded and practice participations) of a programming exercise. +To export the repositories click the |export| button and then the |download_repos| button on the |download_scores| page. + +Export exercise submissions +--------------------------- +Export the submissions of all students that participated in a specific exercise. This is supported for text, modeling and file upload exercises. +Text submissions are exported as a zip file containing all submissions as text files. +Modeling submissions are exported as a zip file containing all submissions as json files. +File upload submissions are exported as a zip file containing all submitted files. +To export the submissions click the |export_scores| button and then the |export_submissions| button on the |download_scores| page. + +Export exercise results +----------------------------------- +Export the results of students for a specific exercise as CSV file. This is supported for all exercise types. +To export the results click the |export| button and then the |export_results| button on the |download_scores| page. + +Export course/exam scores +------------------------- +Export the scores of all students that participated in a specific course or exam. This is supported for all exercise types. +The scores are exported in CSV format. +To export the scores of a course click on the |scores_navigation_bar| tab in the course management navigation bar and then the |export_scores| button. +For exams you can export the scores by clicking the |download_scores| button on the exam checklist page and then |export_scores| button. + +Export user data +---------------- +Export all data Artemis stores about a specific user. This includes information such as name or email, exercise submissions, results, feedbacks the user received, messages they've sent. +You can request a data export by clicking |privacy_statement| and |data_export|. Once the export has been created you will receive an email with a download link. + diff --git a/docs/user/exports/archive_course.png b/docs/user/exports/archive_course.png new file mode 100644 index 0000000000000000000000000000000000000000..29c0e8a086871a6c71e2d0ae58a09e73d87a99e9 GIT binary patch literal 1590 zcmV-62Fdw}P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1|bh%th)*_zsJ+E58d43@Z+P|yTCHKj;S){myBE}rw-@n{T6ucfs&h|g5iDC9;%YP25r*B}>Uo7jsjYl^V zIas5u9NEAy`;yqg7V@!t=2Kbq%Lv2*V%$J_(K8o&BE}dN5n~LCh%tsm#2CXOVvJ!C zF~+cn4uvtTJQ58j8sGL$*AvLeSJmu^{<)^MvLp1C`!S9$LOeFPR6epcPZC>eSUUT; z-1H=(_dfgClZbx%@w%#Ii@q3wvs9|A%_Gg5B_dDh%&mX~UOvMd4?IPp?}lFBxl2*J zOOfb{PcEoh7R=1U-x5XNAkY|4ZJr|03Od7oAT)SI>qkfIp9Xq9Mtg;7GA2C|^|x=J zX+%|thK?|PsLxZiV~S}c5@L8Bd+n;!et2GeIvdu}-@l4TD=_dxH60p-XrDYt0cpg6 zJK+rs)P}K5V^}4OhToSFQ-_=cJVhdrnuITMgYZLHo9lQSce;T4ZL|35Ht_W%P$PF7 zMkG26tVq-urawKoBvAH5`(m4By|_wU8N{9}0kuU4(xNxq0v^t&9g5iSQ}}!1lqSaD z4?jge-SHBU0>h%sDxLc0gfNr)<}sm~Wz?g7Y5$rBJEl;<{@p4w7E0jKtyCmCcOyXT)@j14r-6#+esrLNRWt)Dyf$g6gKlw5lmuFKX#Yc&I2M zJ!LBG5rF_2lTn}qeQg1?pm}I)BzeuPIZR$2iME79YYSrq54%g0poO>>5urhCVFc?h zBGUb8_h_^qGp{IGmE*oG4`EuA2Fq5*3EcG%kQ%o?I2B?x6qq!-eJAr?8gKw=DviY=)$;io8a z7pr*Yun56Y`ZGrn$*MXMTUe!Nm7fu<@)5eVh^Xg7k3K6u0|=m`=P-A?jMKlqLinsV-FQVhsJYPxBddN+&3 z?T(V+)%*h?#u&eBr&?8ZG1dZcyfFAvX(@0L(Q4aQiSMW?QKh9AY1}(?-}OHEmeiOJ zt1A&v`<$VOIY-e)@2^=!(~7e6ITFRKiFd&B53qu4e#*%DyCLRyVfq6s!yNdRjm(=3 zi-<9XMZ_4xB4UhT5i!QFh!|s7M2s;kBE}fMcwv!v-weZKjd(wi(2auMl*YJb$D7jl o?Xiq0?1>m-SVW8wxdmnT9|9YsF1Ow7C;$Ke07*qoM6N<$f<=PsKmY&$ literal 0 HcmV?d00001 diff --git a/docs/user/exports/archive_exam.png b/docs/user/exports/archive_exam.png new file mode 100644 index 0000000000000000000000000000000000000000..e767b4186ed994b9f4b174c446cae5184041cee2 GIT binary patch literal 1259 zcmZ{ke>Bqz0L3Sa@ZBt72kC{PrLxqU7f)LAW3BPp@nTO3Ewr*Ft2A-4IyFs<7y3ax z^aE+*NM(=h&|7|FWck(Dw26k=jyASt^!EOJ@4P?mz2|ez{rBFyUn0W+8y6c82n1jw zFa%R(o8)H=HhECk*)mg@We~#AAWom#j0r4K_Tu+~K#hgA#zadKuRa|?%m9HPul|u) zC$)xT>RiHN_C{wO`BL7Gb3Fm?6l`@F6n;jd>z9J9Sww80-pPPFe*$*{!S@_BsNWLvch(M@<`va-P*rf4ZuTo#N zCf8ZR*Cflcv)*h*JHcJ)d0_788`mK@9Hmn#K4qk1m)y>L8F zC!gc`z2H2FpR~92=cWQO@r?x<^l0gdvQRMJCmU&Ad}$BJAFt;RHh*WJdc!uGwK7BJ zmoWD!&{ZshvjX=iJ+SI-ux;zr5g&X(lA)L~i@ZqO&C(V4jznQ|P!9xBaG&>DSlDzJ z{Fb!i4#kPbqU!pFRgd-UIW53pSM<&uSE9x3O}j_tc%qi6v~g0|PfbAFQQ}=4Pb@OL zQYcsOBiiLmJILmHOPgIJpQm(BJ5wFbLe&94WqpnvRax6R1YlvHyYm>-pG)t%d7<}WLApR@hfrKT5cBde=6$u)2nKq=Swd){=x!8(4iLFbkHA`n}esB)|*RhYULT zqL=jIE<&3f<~d?{P#E(3RWc_s#VMHSs! zR})xoGo@;qql$#1euTyZbqF1!jSt$w{&m}h`pdXrMwOEy)}wOMNo%`Lumoo{;D2C` zqWjo%vSl`6G&I+u|F{cer!5~|$J13mgo{4Wl)%8qc{YCda8}8q*s)8I`qufON0{vq z#Z%L;kooRXI-^D;OpE$1VJj^e4%{^%6gnMIu%+qT*1V+*XoNOK8klwWci%P#dI$|w ziA^8o#F1Y%K0nJqr=PyM<{1!4KKvp8eaK}{n&B4O-tI`1g+#AuAS>kx@1p9C+j@!d zWKUdDS4}`LbH)e~zZkC{6FHFQUdFfw?851mRm)}JkP>`+;O&ln&vhhC#|fBHEgQSg zj1Z*G#IMTRW)#fa4*yZE*-+)mWKw?c;N>!vV2^);5r;H~;r?gXH_Y|UukYpc5E>1J6flH2?qr literal 0 HcmV?d00001 diff --git a/docs/user/exports/data_export.png b/docs/user/exports/data_export.png new file mode 100644 index 0000000000000000000000000000000000000000..a91999f778bac5b330d4122773ec47332b0cae1f GIT binary patch literal 942 zcmV;f15x~mP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D14BtfK~!i%?U~O@ z8$lGuzpY4UDBWP%l8aqvK_P;NfY?iWFd*~~SWjLHUcBYf{sFyoZyqf6nv=OJ2zt;< zqZm+3z=FaihX!lJloAONwllkLv$NYxLh2MsnGY=4H~Z#g=I#42u=i{R6jHcPzX}N` zXG%mlQzFWl5>d{Sh;pVxlrtrwoGFo>hxSV00LvB$9-4;2+X;-3s0hKG8AW-0-+s4! zNs+oxg?Dc^{;rJ zzr|v13TZAMmyz9SIgdEA8JgqhHBqE-zeLpV$*iM5!vjl4!2Sqwiy6F00arUE7-tXt zxsG0&p7a0s4t?Y8Fc3iu^%@yRv z_F=?Y1sjfjcx@5;5x8?VPa71Woasj}(1ukfIg05iEKSor#_?JQs+SFtgH%piELu{6 z{SIa|v@W(0*(p@gyM zTL{?f&+yKy-=K;ZbT3_aI-3W`I+u{XJ!xR`v!@?%V(8L_rlKf^N}*)f|ETltg0eaSXV=!4=8FGPE-*driQLmTvu=!M1ay zSY-BtJMtXXtfxPN6&9J%xSKcTeO>SJBd~^b=14Yg5$?F2ojPA1d$E1Hc_zk6fOiszW5qjx5MkSa{ZTg$ zckJ+f#ApBMXAAFW6g&$3ZSLI76+X(D5>a0#C88ozBFdQ(QO=Zza;7wZU*-+8J2?8& QT>t<807*qoM6N<$f@uq^z5oCK literal 0 HcmV?d00001 diff --git a/docs/user/exports/download_archive.png b/docs/user/exports/download_archive.png new file mode 100644 index 0000000000000000000000000000000000000000..03e4715ea1e8f8129490ac5606dd0f38789f62be GIT binary patch literal 1665 zcmV-{27dX8P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1{O&~K~#8N?VZ6( zTt^(ozv(t^#m!pQh}&)5SVR)YQZXW-f`>GRYJ2EOdno|}fn0KFmmc~D^w4g5=_RM+ zk{+t392)IGJOl!1F%mFP388jF#iBl%?Jh=(w7=iHnSHbC?&7YS?m&JY7q7Z05Sl?#MA;KpFe7*_8Vn4YwlYZVuNozu=-ND|;BaFoN4taeczeNqR5}&)Y#V z@!}SU&l;v_-lykvwM8^6O5WJSJHxsbz{je!)y@10E-vPP_^(TzN^ zu~&93=xyDk^ zKYt7bEO$gP6yC@3;sd0YsG;OuKz7BBfObEvpRwm}lTwBY$8pylAEy!av66e(*Et#u zMaqXy|HfDIzbO&2FbzhKpZ@TxI;S~oV50!UqP~Uug4`o97@OaOm+aMf&<5`Rxo_DV zM;ZEvMT(L3R3wD#x-U>gtQ%+7O}emyNHS3+8u<@0Mxr(8#r13(Ca#^vq&|ZM1bXMc^sNEv7A1@C{nSZS7#601 z1<9Cpw)mqHN1OtIm*dHJ4P9hW+nauiVK%o17$Gk0!qX3KvcCh~`YKH>Fu=B_tuPSu z;hE(3u!Y^giWSeXT_R$CcWd4rn3SM&42OsNA3-}N^z{~$0U}7sx0U_sY{ffkH4)fnm-tx=sol%^mC zP9>^iTp_rgDrCiZD;+sVjXA0to=e}t?KHxS=Rn`6G-Kypmncniw7E2zD2mV)AV*NP z@uNgb&L0<2ZCJi%HVfKPQYq`9P_9SGww)*XxmOGW1sxSRKO$=x>p{_e>PA^-6rWhH zrzWQov|tLjucAkJgwpw}=J)(QMHcHVg0g1)y%NKn7zmf88Vrx6EY~Zg!W_}PGF8v7 z4bNpVkvfp}sN*Hip?TCL%4)fyol_bH-#Vdo;N{0_LJ$3F(GEuZU9w5uHM27h{0&C& zJPHXVJ4-?dC`8t6uj~+6p%G9c@H;$zQ&sF=Pr=$O;0gWR_OOAaJNQI z$(xEu#Z*I!ZlL<6;=sKwkk>Y9=RB9OcG}=s`6EO5bLn%SVsHA6KwZCL%aQWZyNN>! zjb90wh$#rQU{M?7_r3)uutnMMDPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1&>KYK~#8N?VL?W zTvrsw|4NMv5v>|yAR`st2#RS6skV#+Duk)70&T%8%0R(oA={#Kt#lj6QYf`uMCis% zno7V$P>7AtR8i0e78Q)6CJvTi#zlJ0z4v|1O!PgCNkh*M9`D@`@4NTD|M|FLQt^_2 zAVi?j+yx;tA{rqw71sFumyiG@97P52P5TGje>Bs>XW@i`X} zjSv|TjSv|TjSv|TjSv|TjS!g=iDvqX&&-qXeX|oBde5dBv2@hBq3X22(e%nx;khuQOiY+oW{8XD_bd$={HeTrj1x z;f?1s*{x+H#%W>Znn0X3^L1PQe}~|DA0oYE8s1?-i{>lqGH!pAFeOd7x~Q;aOzhX7 zKh+Ivr2Mq2*RL26YMOxgHc7m#MF^Zn`TseVlcxaOJ&g-kn3Smk5g5-!`AB zF}2|dh7b;K-n#?ub^Gw1&^l7QX5T#39+&XNodhrrX1$>)r2dO zFG(;z^n9qEPs{Mm0csnXND#b%aWwg?lRc5zgry7?QHx$9fWh-*#f56LnDWSl8rVC= zE!V~I@s9ra(Jdf()h9L=`Vd`>$k>h-SjJ*QJ0A3KTPu2FS&TY!AW;uEACNxOP=nWY z7tKhX(@kL+p4@<4*Q~|d6GsxexI490GPb*RMHP~<7pnf6>APwH6Q=LzR^Ie;;PusF z@ekde7=MJ}a`iSa;5Wt)gci-6KUZ<*>8eVE85Yp5_NJ;oHP7?N^4-6IhT)Z6KnZFM z>$xg)#(5LzqtY}M_u!UWVh4CKqkaU~-$0*v9^1$wd#)O^zX1kQ&9Hp^K;x&^4`5wj zanSSR9L98m619dz-9qAA_u9s$Y|Iu+p$xT`+Koli<$jz6^x1hN+q!AIYRo?6@k+;5 z#uRfr*&$|Eyj(>YiR}o1=P~C#YYwnosKUj1Q#9yxKl8%m%nR6M_U9;$6%C(~*t_tx zd5}}WVOTJapY5r74pXN(P1RSUHkZ?z<=h>Xa>$rPxtrM0gwEUxq)8BP%E*I^rWF2p z^H@wa=ZL|;;*G`hhxNOD%O8RcvHjdUh9HpB-17v1nGMXw8qv9D2l5=tGVgTW>&zzr z8_99=AmQKJpaJz*+JhR%6T9z7$7ydpj~#|%Me}p&6CIZ5a>CKqzdy$2KRH!)>bc*^ zFc#*7mf}t7h@q)q1i@BO_dZ5QWEt+yf-q(5WrAW0foH`AN4zYUmb@D#61HDl#nxMc zixI<DzEpoj7_epn>Ge6EG$ zz+UHMo`!RncG^4>CX}me#J6r7CpX;I=o19H+-mfbOV?JmfG7L9ytv_ov@oc!MHc5- zpeNqJ^CUu_$Kz0-S;O?+A*>cJ2T<1cF}Z#N}STqdQvc?DYr=%nEF zVXIJypMp2bLXwijX=53~J9!MyS6h#IlwF#`HP9)di^G7f;<@M?$3`!SnO(Ok?R%bP zZ0aE_I-v}aYagO=c8kJHp;(~=*ly9uejm?yNxy48bwKM3j?H*;3c~XMowT^` z09O=tF-kX2zUlAuP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1${|GK~#8N?VNvT zR#h0skIVgVKb*DN%xP2iRu1-q%obW;6v{w~Ork%AMSoQg(jP_i10<**>W`5AsDFa8 z{)#AyU?oIBIyObw98GFdmrl30X6k;q%~t1g-ut}W_xifK`)=ggo)28^J?Gx{-19u= zd!FaH;9T^##VE=YWO-*rxhg6WB@`8j5=u%<^7^4;=A+J&MvIh%%OZO`QMKB0 z^NV!$(x!hdbIog#@ySW^UVFER?QL?$^ZVP)wu#4UyD+<3e@?#3 z55L*u?d%D=pdWOcaB~2J8!Ik!*Zm`xlJ5ckz3)%Q-FZ43{{6hOLGX_wc4B|y?W@cY z`|SNIOWnP$3qwh}!T-CUed)%GLdKB@&ey$7QtLz*P$IrXInv$jFv#wq@ z##y_-Pgh^Ldl|E?q}W9>o~+=7l?MbK?i+7F8gSu}Fv(d)m8P;R*=m^X}dmMG-&o z(J^;Md@SnA%^j}I&i(PsA8z;ck0hR{w{u_-bFa&C;hZ)?8T)I}v{)g*!T6uah`^%Y zRDUfX3xP@4qb zj}X{IB6v~^VNbKZxSftqI`>M3qb{(PQ_h_XW=VsE|%OZX+Z?Q z4kBO2iVNJeq!)@*5!rM=a5#}KD?&f`#fm^AqA>?TJxKlJj|f~NxG?xFc0L*_cd3&v zopiMlU?Zs)m>=`R2l9T#kSOusnw#DG#TP=RsVA~XB)v=p>E^`)N4?h61Vb!E>Pt36 zN)19?KmobkI)?j%P4cna7boFhu379;UYe4cWxBBA+6FF|ckZc8&gG3m$K1H=dWu^5 zsU`gB>b2HR=X|l@H=9PTR+9vwegJlOaGcJ@ttnB4!km8Q$2`RcL>uhKH{53v`M>aF zJcRZ3kzZVY6jJ{AA|Xkcdmt3PvP7`|1&B(B97&Xghl)fAMbnLv7EL!wBvd3yC@K;q zloXNh*ISAb)Io|!`13v$%ECte%+rtl|0GH%T9YWDs7RDhR3u6W#{2_)dH0}#ibuu( O0000a~60+7BevL9R^{>hhypPUa;>9ngbECOMZcnxD)OoDyb#1FwJX+|aA9tn8JaC14z-kQ>#)BN! z_`RG`Cd%Ea+`Rdnn%mtsDiSaMB+QuGuk+*f_j@@i^Y-&-#00BVwR%r+@I*s43pZwd zReHg|pS@n1x#GH%nHo!~*F57BmzS@VO-^uLS|Im?%S<&u2pNSlz1}O8bTRMV|MxqD zer#FR^}TfalmprR4;wwy`_opJChZB-5u1I?Y>lh6flgbVMyJ$_Tld{EYTUL|x@MoGIwv9jTOX}Ox?8li_zU$p$f8@bpfmb=3)aKbg{JdBEMCoz1U5_$Z zq;DArV1qBEy{t*B8^DtjtX-kf&J_@mFM#5+CDa&y93jahzYX>ah#7B2p? zS#ycu$z!_3&-XvKI=JzG{zuzA@%@P#EL-kXx9JGEC;hzjU1EN5^Ln!yF~g1FZ6-Q; z2|?@Kr_uk-JLZ(EPlk4nkKNp&yOyVpg@n_XYKGv>%w`$t`Oz5Tnj555mx zVsf@Z!}WnzW7?~rRYA%gNlI%M|9@>h@7yyf)6b&I1p;2lsri3g6Y27lm1S#@%sdIf zSC{wfv432~TC=O}CG&)bUX3dKe4Ikgy+JeX{kIFS+Fa$4JZrgS)|UMWTgn&oFManU zgvmQ$WsbduR`!~3*<0Z=k7;?|zUacG879AWmiU2+X7P`Uc@6b!Dwl5HS zbF-z5vJ|p6>)-H{T5~|iKQ`?6wYTi7$}^c}9j&{yeN(~}(=h(%9}AQ2Y4iMl*RxhF z?cnR>cQoH{_W8#D^3UutvND;oZ&`rt66d867qZVpzZG{1KH9zN^aA$e=U$CV-Bxmz zOg+TaEv;TqlK1b{B7Jc!Ce=U$`rJ@=Z=!GHI?tn(XYLPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0}V+;K~!i%?Up}C z8&Mp`zg|_erB7Q6dx(Ho#JZwm%IJnkD zu@p&$BAs-wO;V^58V8er5u2*-7vCLs*2H^Btk%9yc*(naKQ82x-|zQcNjmsoPuczk zNp5~u)MKmh9TX|c9v8O5>|J3mi4anlAX1nhQkWo8m>@#JCTHVNT30M%XflR{FI$L2 zfAOgEjgTvcY}@`_2Lwf{j{(MpoC%(k@UjXyGVbeXUH1p>Ymbk3#ul^6vJ`^ZGS z=uyD{iGU!rqApm-CA@@vWj#g_tN33Rq@ zAYjf+x^E&AYh)~o<@Yns^d&ImrTafI#6H~`Ff+VriL*=5O7bwSM_)6Wgb0p%4*0PK zjmncr8q-uP_EsJ&A_n*8OFSbR;15NNvLY$&P#xCwjGX}7Ch^b`KtHWr!%J@?7Ck`6 z^9FWZgBBiQmx6`0-nwWR1Z5e`si)B{dL&xph>R#jcYht0rddXOSi|iJ^A9r1V{u(7 zWB`dHt(BSJ5rdEpkziFsGvo+S6Lg@Xc;!MCqmZHd~fi zdYDO!T7?1E8hYT&-PhD|bnq!qfqILOQq;SWh-CS%3ZWojv?EgKnMZGXHA3%iW@PF@ zg`iRlsTg4;mviTg?O?`L#7AdRDoLCPdStL#`Q+uQ_HO@-0s|JIt zJ8fYk;W+e|V>8Rr!w$|x z0pBWmT)Y&UYOu&kv)0%RRaSyg9v{IHYub9rw)C<_POAt$HuS# O0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1oKHmK~#8N?VNpR zl~o+af15hzUUI(d&Fh$M*UE4kX&G~Y5gCI>OGyH;{)!4h`ojn$i2ey8{|F1JKU0EU z5Tb}i78yB5la#%fQFl|(={8f-Ic;iN=X35ox8u6I_j#Ig+BqLMJoh>Gx#yhce1C7x zzC+=U^n`11odTeq$ z&n{Ug)71EbSFXydtv}0JwGZ&ru6_E*HH@{T<)A)?WyP~(^}=$!c6&|F*HopPCl#A& zqq3A?pyj=I2n+?Nzo!v4gH%D*GqiPf2 z-HvX3MCkpe{z!V=Bb8L@wf8Kcl)L9;-A`J8g}kZ zD>kr>93>*c6LPC>%a@HxKzkkxZo?fF@_y%G-Cy_7H}YBc3H^R1emZqlp4)xknl&+} zJx@BZ0bvo?5JqeWBsiYUpjg-1s<^laZM@!IKq?T>q!cNl^c(KtL-;AsWxb7NzuOy1DcNZNrf^(i~%r)paOuswxz{IS_HuRzCZDMa!nW@ zk*DwCkX%+#q$DpMg~m(28ZT!`wv>h#&xjsYYzUmg&EV#7jN{9DJ9Nv0ohNON_~P(M z?NP^6IzZU5=)W|ScqKBRLa6PjM+OG<7-2Td#;lDz z)S-X8#TWOD;csoIO*%smwCEwlhO~m4P)_Kk0h5rgH+JsS((87XERGLW(7%Yanxw%~zdFj?31fm(^ ze|fZ5r!4#at~Y4~M{J5RgrpJDdH#M|iB3M}oPPRfTptuHnav`UA38gb+-Rs&dDv_s z9)#z$s=P`oP=ZtB82B6!B2#`20;vLn!VnqriD)G4=G);NgCrMoJ&n&2dBX5t!h2W( zRp~Yu0L`XnFz$V0eHml&q~YM1^D@lfu8Ik=uR|DeWMR1|;8 zuo=CkU(_C~j1=qlQr+wIChbq0@M4OzpkJes&r?0Rg#ppo=uFkBCpPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1As|HK~!i%?V8a` zQ&Akpza}<9n>8Gch0_Qza3pHJ2+@NN&Zi*K9*V*s@THd+{SmQx5h4hb!X8oxdZ|T( z4}mW+r!gVvA>B$G%BtH$W{92Nx#!NidAH{7krdAd4(Fb8@9wyt`}>{Wy>KhG7*OO; zNQp6T?x`K&BZV!LLOH)U|ComO~&yulEk5{I6IXHws5(#9zz!!^ii?Ga;OCs zft-+=d@Lm@frYVhqt-+TD2WnK5+$G{Nh&&Yz9&3Fe%$>q8~v%xq!jg2HM+N5NbY(=#=RqS#)J1FEWo0sY*1TVWRS7GeWZ( zP+yCFF;2%<5fa-nC|e>`VTwL#%UXtuBuL=wx8Ho8r+TT?I%iFcZC_+~713%ZOXDR3 zBVx{i#q_4wVytCyBPxi4O)^c+^od()aF*&q!(;d~D3qakikyT3csp zm{+8$HH|fuNNFPasvX&=<8(UUV(Z_c7|O<0B$^>+{g-qpQZ6^46oy0 zY275|8OB;ku)@^mTE`rvcQ}o>!-?yrH89xb1d_kXTJK7s?CMPEY8FKg* z`;Uo%PCEV*U+PcrYJ7>WZ~4`Eji-4#zek&hPEMQBJ-0fiS&`AtVN~bsF~|7-GOM%g zN1Xpb|3C+S#L0golEyA2+OE!u{q)9ifH{y7C&s7uE|tqbMWO_hLOH)pd?B_NtA$+C;=r=0!pF;ltkMA Ze*singaafmrMv(D002ovPDHLkV1l~`(L?|M literal 0 HcmV?d00001 diff --git a/docs/user/exports/export_submissions.png b/docs/user/exports/export_submissions.png new file mode 100644 index 0000000000000000000000000000000000000000..1181a1fbcbbbc2a220de1df5adb2e9a10327eec2 GIT binary patch literal 1604 zcmV-K2D|x*P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1=&eNK~#8N?VNvT zR#h0spH1DI+tgfb=8XAcmP_YKY%*6Uk+y$|N|Hb<>YwNzDJX&pLI@-BkC34HE6PT{ zF#ACuTA+}MTPfSb^<&Nrox0Vg?w4-b>U_@bdAqxA@80{qH)iyF;Bw!4?s?CB?sLB9 zd7ksiO#90qiV^|o-dRynM#ZE=hKfmv3|lk3_T$g;L1T+(@uY+y%^XjZ%#&AEEVkD+ zGO3W1WI!`$$6>{!LQ;~&uN|$Kl*mvqDUqRKQX)geq(p{_Nr{Y<#I*0>)!rvXNkL32 z3uejOoGg3%-+5@k9I4%KuRL3}$ZJWS-4EVvk1+83y2tL5trbhXmWg3P(%gq@?~@6N=5khWEp%8Bm2m~EdowaL8PY_EqB5BqO(I4x1u zFD#ToliCiS>yqkq<+5hiew*@W+g{h?^@>>v1H`noVu^g!eD)e~W8VvR-YRb%{nbVSVC*BmUyz#iE_r#yVmWF)-!tb% zYe#$kWqJNUgB&u?;C*i~{>d=y*v~!2VLW&3`Z8w6r2%V~^GWuU$s^vGKE*R~tPQ-w z7bn{6^Q(V36|>vlr|-&Dd9~)awHsUVvodWv_k+F+j_vo)SP;P>bYwy_o0k>K=CWdY z{fH6Q8>>p~=TzWD5z=Zi8B8hys_VQMd&$zg={8Cr{9UFmLc`e{e5Q?io6O5EoSr?R zen3b#v-IP=@^<|RIX^IHCa?Y)M=_bFdN0Ms4g*42GHbd#_5DxQjvgkkyu`aJGTVp- z`>_vRH}?6jpDA5n&klTm*zgJCx#;Z{B5fR_O*VaUjQ|;&K8(Rwj0uQ^MZmqjCXYjH3~07L}C)2yJL}LPD+<|8k)V{ zVqe+`>zk8jrPuT;H#Q*Vvu0(xdI3S~Kl6tyziFnu21Mzi$#~#EoWHGdS*Utx(5!I8 z!Si8vYrEu4&4_)!wWr_O&wbKp@Y+QS2krPGxWPGX#Ewv$FoN*Bk{fbI*n+>X&&^rW z#om~|j&xj*4CCv9X_@1WJst^*iI*Z8akQC-A}Byegn8OYfr;+!{54bM&V3^$;kKDM zHr2ScAkNVa(t@9k{sAk-WSxSeu?tZVL6X@VwDk1Jph>T(u%)KaMp+h|iyGL5_;#3U zw+MsqT%Pa1U!?kcBbXq5I};f@9tn%-qqXJ3>CGH2t!>;>BW-<`?6qsBUn-E^h%_$r zjt1?B1<^ooRpT_AH-)Ip^2 zZyJLBipO|k$0Jl^@`HFNz&SgJQmOpzgtG{+Z$>O-P+w4^95xY= zFkNJ7BIjrvN8LqD5EjP&^!}CJ)$mwMA~;cyT7l=eLKU!wg#dhy#X$X)s-VF|;;~Ym z-wx7w=jFi>ixEFMeRFq`!YsZR- zKk-#TDPeJc9`4ZppGk=ft(la_P%$Zyp<+@ZLga6Qh9r9eTl|Rt0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1OQ1yK~!i%?U~O@ z8$lGuzilZ}kyuPEd(l7(6(V>Lil=z6AovI5=2hs$+g|D)&`WdcLD8!x_pVTg2YX73 zfr=4QP}tGC_!X9Z|Ci1cC-0m4Q)*K1B>%!X5ajHKV~*Qumy;?*F5CE5%-wL zhMxK&-Jt&qp^=zDLh%4fnHQK9(Wp_BT0_v!tH@L-LSxv={6u1L8Vje5-b~)K zH}M*8pMS^7W)(TEPtS|GDHdR1XX8ZZlM*@5qIDARLYf1Qqj42MMl8mVO^qP!qF2)&RqlR^&+~Zo>6>6h3E7Ly0bq2esq)x z$%raecHe%N{lyYdvR6MG+67Ufwz&`c_Vy3iBHLV9EfeZ{hxPhzheC&}x9wl{_WBoP zypp|5W$LqDv@Nc65#?@Yyz<{~NajksW~>{?tOhUn`>Y#cK$A91;nv)!L! z?#em?^QrFv>j}s?OQ!VpdNeO1jU z%YS(b!K`?F<(z9&b$dG1qdmbm7jH}3i%q8nbRbVNFL2~UnD8ve1Ksn?y?ki(NF>g$ zLN86p)G$iq5K2V1hKwn6)A(GY;+{e7x{T@Okx2OVzDofw28=<_x3 zGA<*ma)lBGD04{2C|s23MO+ZPG+4%tTh`)8onK(Bs7AD(v)uxARIgTb2dG##n28Uv zWFdFlJQ69jA#eJ1{{h|7(~d41kyEN!Ln_JVoc0PjeWti6r6_KIvPD2=nuc*NeOF*U zbI(UQUurPU-H2{vWG)7C(@j<4aXvTrCO4%)M-^#NPd`Q^)9N@T%%@5u2Kv&R-_H*q zqv~#vrkipHJ(P*y%(oDlSyX!ZUs03-{hvD#VRStbyGI#Y`2j3O25d`D8j(KADYat0 xhkx$tA)^26_W?beP9r)cGGZSP8S&l&{05>kUX-4b-OvC4002ovPDHLkV1gfv4eI~^ literal 0 HcmV?d00001 diff --git a/docs/user/exports/scores.png b/docs/user/exports/scores.png new file mode 100644 index 0000000000000000000000000000000000000000..fc5494bae030adf7cff4b93e132b7e238860eb19 GIT binary patch literal 955 zcmV;s14R6ZP)001Qj1^@s63a}Kw00001b5ch_0Itp) z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D15imsK~!i%?V3$U z8$lGu{}!Swic+l~U=BiZFlaqkZAtJ@C{j;e3VJBggZEyG(qpmoD)i_640P5TJUX4-;{tZ_1s+u|*@+SESp*jA zS>ihGMLKzOKC_>)nEjaLQFY=KD{vU92t_>Xi*nCFY!Dmb4bYpAZM2ssssrVm_5S;z zs}U4(PMdECl28MU*i5HDo=k0_U|^D`tAW5_NS^RAw~I$BpV?x08-?fn*J~x=2s>n5 z*Cny~fafpPF~=r1y;zzEBf_IxE`_@*z~t~9Om&3VTEuGlEhaM5r}gdF-#bCPqXFjL z7W&_8yVw8TL3Htp?4D*ni+>Ko3zIp(CR#A%8+w*d5CTcBOaH_^nG-`|gC|&EPmHEb z^b8DPMm~oKA#VJVb0m~p-)!P3uU|kI8^c`{_8>y}q2yVr_+58qbOd8fqT{@~Vl&xm zV$-fobRjw3jka%=huP5cZ~Pz|dsfNQeAe67{c4C5P={1<8Phx!gbR5VvTTh>G`>ly1QyE{;8uHcsofNfpu?)uPU8whNGv6|PsCc$Q)_kS zc1)N};qtsF2Fb)W-W?W&r#L)8*hiZ3_}~c3GPE9u_njle(Fx4jx>6xg;lCY9y_M6> zDRCCaUD*WkXwf2kjVRB#t6^RwdpqxVbCyLZCo;0CLgX$ANr~rm&%m3rls@i|*976x zf84o@T&NmecC=UuXt5N~Vkw}-Qb3EPfE0^Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D13O7XK~!i%?UzAo z6G0Tm{~9qgU{ji+ixPtts@M`CPyz)#v{3B_NIfWeDtPfC!9zbl4-&leQuHQXBp;x* zkQ^)sMF<$$poLb_f~++SZiSW#MQ3*2CbQcat5Qo_=Lf^id%Lr{^WXR8%^v%~fMEy6 z>1YUGgwdc)CEMAe-tJh@F!hcON@1qvYIywWBU!s+wT3a+6T`i}bL2VvC{yYAn@`mH zJ?!-@OwX;6bu>^4!zcQu=*O&K^4VJqkR;4r1lep2-;cFZ&n98rj z-o!E@xGsxe9ehzZC+PTZuv%~)4qlgmD_Svlsx2YskQt34KhlME)~y#y$dtB)pOH>o z#8`ifEFO(tkVu7*D{TM?3x!Oa{5JC9BL5f`t)nj-%nAz`Ig$ftQ|WKmnM#bt%S14q z?Zy>y3xp^$oz0i86c=FicLA;_VQEY-ElGfeAWfhD8YbSI6oxGEtZKS>u2PO`KiDrf zcvs`6e9a^1=jo+|rOp@=tS|^X;EQj5g-$TaHgbp}+lT@dSxl66CfMxaHv6KokZ}yB zLdbuR#v_F6HBS^qc^!Ecal?(t@-e_nrCBkTYhGV33P+nte8Nv|T_#I#`zgx_M$rH8 z<@?<$S+X02r1)y1#fTdr^%@(~*6OB>pAA%<5E7@!B-%UJ2Uy%ho%e;yhkHL~1DRmN zCs?X6A%;fSfjxhqv?QgTntlEyh#}n>!t5Gu97L!)}j=J3D zUBlOyItiGco=1iJ?s_rJqT!PME4%lr+L#c59e|COgs@^E>Z#e2Ujh!_+w3gev_O)@ z@mCg3;2J5!gH_32MdXE*T%17Van~Vf@l{nn6-r?^<|k4k%>T|lFTRHdBa8+kj0Pi& z1|y7yJ^hJ1jPPBs3KKCJ%sxhgi5Lw=7!5`k4MrFZMi?c)Puv}JGdzyL>i_@%07*qo IM6N<$f`0X&YXATM literal 0 HcmV?d00001 From 4be1b1cfe460a40d3780cfd4bb8e04ac8b6d363d Mon Sep 17 00:00:00 2001 From: Julian Christl Date: Sat, 30 Sep 2023 09:39:17 +0200 Subject: [PATCH 10/19] Development: Simplify authentication (#7291) --- .../ArtemisAuthenticationProvider.java | 16 ----------- ...ArtemisInternalAuthenticationProvider.java | 19 ------------- .../jira/JiraAuthenticationProvider.java | 28 +++++-------------- .../ldap/LdapAuthenticationProvider.java | 27 ++++++------------ .../artemis/connectors/LtiServiceTest.java | 1 - 5 files changed, 16 insertions(+), 75 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/security/ArtemisAuthenticationProvider.java b/src/main/java/de/tum/in/www1/artemis/security/ArtemisAuthenticationProvider.java index 28ab83ca2853..2849126ec584 100644 --- a/src/main/java/de/tum/in/www1/artemis/security/ArtemisAuthenticationProvider.java +++ b/src/main/java/de/tum/in/www1/artemis/security/ArtemisAuthenticationProvider.java @@ -2,29 +2,13 @@ import java.util.Optional; -import javax.annotation.Nullable; - import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.core.Authentication; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.service.connectors.ConnectorHealth; public interface ArtemisAuthenticationProvider extends AuthenticationProvider { - /** - * Gets the user object for the specified authentication or creates one in Artemis based on the passed information (possibly asking an external authentication source). - * Note: This method does not create a new user in the external authentication source. - * - * @param authentication the Spring authentication object which includes the username and password - * @param firstName The first name of the user that should get created if not present - * @param lastName The last name of the user that should get created if not present - * @param email The email of the user that should get created if not present - * @param skipPasswordCheck whether the password against the by the user management system provided user should be skipped - * @return The Artemis user identified by the provided credentials - */ - User getOrCreateUser(Authentication authentication, @Nullable String firstName, @Nullable String lastName, @Nullable String email, boolean skipPasswordCheck); - /** * Adds a user to the specified group * diff --git a/src/main/java/de/tum/in/www1/artemis/security/ArtemisInternalAuthenticationProvider.java b/src/main/java/de/tum/in/www1/artemis/security/ArtemisInternalAuthenticationProvider.java index b043de565e85..696bdc0cbb08 100644 --- a/src/main/java/de/tum/in/www1/artemis/security/ArtemisInternalAuthenticationProvider.java +++ b/src/main/java/de/tum/in/www1/artemis/security/ArtemisInternalAuthenticationProvider.java @@ -4,7 +4,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.security.authentication.AuthenticationServiceException; -import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -39,24 +38,6 @@ public Authentication authenticate(Authentication authentication) throws Authent return new UsernamePasswordAuthenticationToken(user.get().getLogin(), user.get().getPassword(), user.get().getGrantedAuthorities()); } - @Override - public User getOrCreateUser(Authentication authentication, String firstName, String lastName, String email, boolean skipPasswordCheck) { - final var password = authentication.getCredentials().toString(); - final var optionalUser = userRepository.findOneByLogin(authentication.getName().toLowerCase()); - final User user; - if (optionalUser.isEmpty()) { - user = userCreationService.createUser(authentication.getName(), password, null, firstName, lastName, email, null, null, "en", true); - } - else { - user = optionalUser.get(); - if (!skipPasswordCheck && !passwordService.checkPasswordMatch(password, user.getPassword())) { - throw new InternalAuthenticationServiceException("Authentication failed for user " + user.getLogin()); - } - } - - return user; - } - @Override public void addUserToGroup(User user, String group) { // nothing to do, this was already done by the UserService, this method is only needed when external management is active diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jira/JiraAuthenticationProvider.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jira/JiraAuthenticationProvider.java index d72e1bc17c2f..03083d8659b0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jira/JiraAuthenticationProvider.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jira/JiraAuthenticationProvider.java @@ -101,20 +101,14 @@ public ConnectorHealth health() { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - User user = getOrCreateUser(authentication, false); + User user = getOrCreateUser(authentication); if (user != null) { return new UsernamePasswordAuthenticationToken(user.getLogin(), user.getPassword(), user.getGrantedAuthorities()); } return null; } - @Override - public User getOrCreateUser(Authentication authentication, String firstName, String lastName, String email, boolean skipPasswordCheck) { - // NOTE: firstName, lastName, email is not needed in this case since we always get these values from Jira - return getOrCreateUser(authentication, skipPasswordCheck); - } - - private User getOrCreateUser(Authentication authentication, Boolean skipPasswordCheck) { + private User getOrCreateUser(Authentication authentication) { String username = authentication.getName().toLowerCase(); String password = authentication.getCredentials().toString(); @@ -129,18 +123,10 @@ private User getOrCreateUser(Authentication authentication, Boolean skipPassword ResponseEntity authenticationResponse = null; try { final var path = jiraUrl + "/rest/api/2/user?username=" + username + "&expand=groups"; - // If we want to skip the password check, we can just use the ADMIN auth, which is already injected in the default restTemplate - // Otherwise, we create our own authorization and use the credentials of the user. - if (skipPasswordCheck) { - // this is only the case if the systems wants to log in a user automatically (e.g. based on Oauth in LTI) - // when we provide null, the default restTemplate header will be used automatically - authenticationResponse = restTemplate.exchange(path, HttpMethod.GET, null, JiraUserDTO.class); - } - else { - // this is the normal case, where we use the username and password provided by the user so that JIRA checks for us if this is valid - final var entity = new HttpEntity<>(HeaderUtil.createAuthorization(username, password)); - authenticationResponse = restTemplate.exchange(path, HttpMethod.GET, entity, JiraUserDTO.class); - } + // We create our own authorization and use the credentials of the user. + // We use the username and password provided by the user so that JIRA checks for us if this is valid + final var entity = new HttpEntity<>(HeaderUtil.createAuthorization(username, password)); + authenticationResponse = restTemplate.exchange(path, HttpMethod.GET, entity, JiraUserDTO.class); } catch (HttpStatusCodeException e) { if (e.getStatusCode().value() == 401 || e.getStatusCode().value() == 403) { @@ -162,7 +148,7 @@ else if (e.getStatusCode().is5xxServerError()) { if (authenticationResponse != null && authenticationResponse.getBody() != null) { final var jiraUserDTO = authenticationResponse.getBody(); - // If the user has already existed, the check has already been completed and we can continue + // If the user has already existed, the check has already been completed, and we can continue // Otherwise, we have to create it in the Artemis database User user = optionalUser.orElseGet(() -> userCreationService.createUser(jiraUserDTO.getName(), null, null, jiraUserDTO.getDisplayName(), "", jiraUserDTO.getEmailAddress(), null, null, "en", false)); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/ldap/LdapAuthenticationProvider.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/ldap/LdapAuthenticationProvider.java index 9ef60f8d72c4..32b88f0449ce 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/ldap/LdapAuthenticationProvider.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/ldap/LdapAuthenticationProvider.java @@ -53,20 +53,14 @@ public LdapAuthenticationProvider(UserRepository userRepository, LdapUserService @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - User user = getOrCreateUser(authentication, false); + User user = getOrCreateUser(authentication); if (user != null) { return new UsernamePasswordAuthenticationToken(user.getLogin(), user.getPassword(), user.getGrantedAuthorities()); } return null; } - @Override - public User getOrCreateUser(Authentication authentication, String firstName, String lastName, String email, boolean skipPasswordCheck) { - // NOTE: firstName, lastName, email is not needed in this case since we always get these values from LDAP - return getOrCreateUser(authentication, skipPasswordCheck); - } - - private User getOrCreateUser(Authentication authentication, Boolean skipPasswordCheck) { + private User getOrCreateUser(Authentication authentication) { String username = authentication.getName().toLowerCase(); String password = authentication.getCredentials().toString(); @@ -88,16 +82,13 @@ private User getOrCreateUser(Authentication authentication, Boolean skipPassword log.info("Finished ldapUserService.findByUsername in {}", TimeLogUtil.formatDurationFrom(start)); start = System.nanoTime(); - // If we want to skip the password check, we can just use the ADMIN auth, which is already injected in the default restTemplate - // Otherwise, we create our own authorization and use the credentials of the user. - if (!skipPasswordCheck) { - byte[] passwordBytes = Utf8.encode(password); - boolean passwordCorrect = ldapTemplate.compare(ldapUserDto.getUid().toString(), "userPassword", passwordBytes); - log.debug("Compare password with LDAP entry for user " + username + " to validate login"); - // this is the normal case, where the password is validated - if (!passwordCorrect) { - throw new BadCredentialsException("Wrong credentials"); - } + // We create our own authorization and use the credentials of the user. + byte[] passwordBytes = Utf8.encode(password); + boolean passwordCorrect = ldapTemplate.compare(ldapUserDto.getUid().toString(), "userPassword", passwordBytes); + log.debug("Compare password with LDAP entry for user " + username + " to validate login"); + // this is the normal case, where the password is validated + if (!passwordCorrect) { + throw new BadCredentialsException("Wrong credentials"); } log.info("Finished ldapTemplate.compare password in {}", TimeLogUtil.formatDurationFrom(start)); diff --git a/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java b/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java index 9fb564a7cee6..e18655b19189 100644 --- a/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java @@ -202,7 +202,6 @@ void authenticateLtiUser_newUser() { SecurityContextHolder.getContext().setAuthentication(null); when(artemisAuthenticationProvider.getUsernameForEmail("email")).thenReturn(Optional.of("username")); - when(artemisAuthenticationProvider.getOrCreateUser(any(), any(), any(), any(), anyBoolean())).thenReturn(user); assertThatExceptionOfType(InternalAuthenticationServiceException.class) .isThrownBy(() -> ltiService.authenticateLtiUser("email", "username", "firstname", "lastname", onlineCourseConfiguration.isRequireExistingUser())) From 0f16882013631ba3e13e752f8640f36af17bcd10 Mon Sep 17 00:00:00 2001 From: Jakub Riegel Date: Sat, 30 Sep 2023 12:39:37 +0200 Subject: [PATCH 11/19] Development: Extract plagiarism detection service (#7152) --- .../plagiarism/PlagiarismDetectionConfig.java | 7 + ...ogrammingExerciseGitDiffReportService.java | 48 +++++- .../ModelingPlagiarismDetectionService.java | 2 +- .../PlagiarismDetectionService.java | 131 ++++++++++++++++ ...portedForPlagiarismDetectionException.java | 10 ++ ...ProgrammingPlagiarismDetectionService.java | 74 ++++++--- .../TextPlagiarismDetectionService.java | 2 +- .../web/rest/ModelingExerciseResource.java | 25 ++-- ...ProgrammingExercisePlagiarismResource.java | 83 ++++++----- .../web/rest/TextExerciseResource.java | 25 ++-- .../plagiarism-inspector.component.html | 2 +- .../plagiarism-inspector.component.ts | 12 +- src/main/webapp/i18n/de/plagiarism.json | 6 +- src/main/webapp/i18n/en/plagiarism.json | 6 +- .../PlagiarismDetectionServiceTest.java | 140 ++++++++++++++++++ .../plagiarism-inspector.component.spec.ts | 8 +- 16 files changed, 461 insertions(+), 120 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java create mode 100644 src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java create mode 100644 src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingLanguageNotSupportedForPlagiarismDetectionException.java create mode 100644 src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionServiceTest.java diff --git a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java new file mode 100644 index 000000000000..6f455b2f539a --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java @@ -0,0 +1,7 @@ +package de.tum.in.www1.artemis.domain.plagiarism; + +/** + * Stores configuration for plagiarism detection. + */ +public record PlagiarismDetectionConfig(float similarityThreshold, int minimumScore, int minimumSize) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/hestia/ProgrammingExerciseGitDiffReportService.java b/src/main/java/de/tum/in/www1/artemis/service/hestia/ProgrammingExerciseGitDiffReportService.java index f34867ea864c..8444525dad34 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/hestia/ProgrammingExerciseGitDiffReportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/hestia/ProgrammingExerciseGitDiffReportService.java @@ -2,6 +2,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; @@ -19,6 +20,7 @@ import de.tum.in.www1.artemis.domain.DomainObject; import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseGitDiffEntry; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseGitDiffReport; import de.tum.in.www1.artemis.domain.participation.SolutionProgrammingExerciseParticipation; @@ -158,6 +160,33 @@ public ProgrammingExerciseGitDiffReport getOrCreateReportOfExercise(ProgrammingE } } + /** + * Calculates git diff between two repositories and returns the cumulative number of diff lines. + * + * @param urlRepoA url of the first repo to compare + * @param localPathRepoA local path to the checked out instance of the first repo to compare + * @param urlRepoB url of the second repo to compare + * @param localPathRepoB local path to the checked out instance of the second repo to compare + * @return cumulative number of lines in the git diff of given repositories + */ + public int calculateNumberOfDiffLinesBetweenRepos(VcsRepositoryUrl urlRepoA, Path localPathRepoA, VcsRepositoryUrl urlRepoB, Path localPathRepoB) { + var repoA = gitService.getExistingCheckedOutRepositoryByLocalPath(localPathRepoA, urlRepoA); + var repoB = gitService.getExistingCheckedOutRepositoryByLocalPath(localPathRepoB, urlRepoB); + + var treeParserRepoA = new FileTreeIterator(repoA); + var treeParserRepoB = new FileTreeIterator(repoB); + + try (var diffOutputStream = new ByteArrayOutputStream(); var git = Git.wrap(repoB)) { + git.diff().setOldTree(treeParserRepoB).setNewTree(treeParserRepoA).setOutputStream(diffOutputStream).call(); + var diff = diffOutputStream.toString(); + return extractDiffEntries(diff, true).stream().mapToInt(ProgrammingExerciseGitDiffEntry::getLineCount).sum(); + } + catch (IOException | GitAPIException e) { + log.error("Error calculating number of diff lines between repositories: urlRepoA={}, urlRepoB={}.", urlRepoA, urlRepoB, e); + return Integer.MAX_VALUE; + } + } + /** * Creates a new ProgrammingExerciseGitDiffReport for an exercise. * It will take the git-diff between the template and solution repositories and return all changes. @@ -183,7 +212,7 @@ private ProgrammingExerciseGitDiffReport generateReport(TemplateProgrammingExerc try (ByteArrayOutputStream diffOutputStream = new ByteArrayOutputStream(); Git git = Git.wrap(templateRepo)) { git.diff().setOldTree(oldTreeParser).setNewTree(newTreeParser).setOutputStream(diffOutputStream).call(); var diff = diffOutputStream.toString(); - var programmingExerciseGitDiffEntries = extractDiffEntries(diff); + var programmingExerciseGitDiffEntries = extractDiffEntries(diff, false); var report = new ProgrammingExerciseGitDiffReport(); for (ProgrammingExerciseGitDiffEntry gitDiffEntry : programmingExerciseGitDiffEntries) { gitDiffEntry.setGitDiffReport(report); @@ -199,7 +228,7 @@ private ProgrammingExerciseGitDiffReport generateReport(TemplateProgrammingExerc * @param diff The raw git-diff output * @return The extracted ProgrammingExerciseGitDiffEntries */ - private List extractDiffEntries(String diff) { + private List extractDiffEntries(String diff, boolean useAbsoluteLineCount) { var lines = diff.split("\n"); var parserState = new ParserState(); @@ -216,7 +245,7 @@ private List extractDiffEntries(String diff) { else if (!parserState.deactivateCodeReading) { switch (line.charAt(0)) { case '+' -> handleAddition(parserState); - case '-' -> handleRemoval(parserState); + case '-' -> handleRemoval(parserState, useAbsoluteLineCount); case ' ' -> handleUnchanged(parserState); default -> parserState.deactivateCodeReading = true; } @@ -262,7 +291,7 @@ private void handleUnchanged(ParserState parserState) { parserState.currentPreviousLineCount++; } - private void handleRemoval(ParserState parserState) { + private void handleRemoval(ParserState parserState, boolean useAbsoluteLineCount) { var entry = parserState.currentEntry; if (!parserState.lastLineRemoveOperation && !entry.isEmpty()) { parserState.entries.add(entry); @@ -274,7 +303,16 @@ private void handleRemoval(ParserState parserState) { entry.setPreviousLineCount(0); entry.setPreviousStartLine(parserState.currentPreviousLineCount); } - entry.setPreviousLineCount(entry.getPreviousLineCount() + 1); + if (useAbsoluteLineCount) { + if (parserState.currentEntry.getLineCount() == null) { + parserState.currentEntry.setLineCount(0); + parserState.currentEntry.setStartLine(parserState.currentLineCount); + } + parserState.currentEntry.setLineCount(parserState.currentEntry.getLineCount() + 1); + } + else { + entry.setPreviousLineCount(entry.getPreviousLineCount() + 1); + } parserState.currentEntry = entry; parserState.lastLineRemoveOperation = true; diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ModelingPlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ModelingPlagiarismDetectionService.java index 24ddbfbd39bb..026799d8661c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ModelingPlagiarismDetectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ModelingPlagiarismDetectionService.java @@ -149,7 +149,7 @@ public ModelingPlagiarismResult checkPlagiarism(List modelin final double similarity = model1.similarity(model2); log.debug("Compare result {} with {}: {}", i, j, similarity); - if (similarity < minimumSimilarity) { + if (similarity * 100 < minimumSimilarity) { // ignore comparison results with too small similarity continue; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java new file mode 100644 index 000000000000..d2bbef6a087d --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionService.java @@ -0,0 +1,131 @@ +package de.tum.in.www1.artemis.service.plagiarism; + +import java.io.File; +import java.io.IOException; +import java.util.Optional; + +import org.jvnet.hk2.annotations.Service; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import de.jplag.exceptions.ExitException; +import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.TextExercise; +import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismResult; +import de.tum.in.www1.artemis.domain.plagiarism.modeling.ModelingPlagiarismResult; +import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult; +import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismResultRepository; +import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeatureService; + +/** + * Service for triggering plagiarism checks. + */ +@Service +@Component +public class PlagiarismDetectionService { + + private static final Logger log = LoggerFactory.getLogger(PlagiarismDetectionService.class); + + private final TextPlagiarismDetectionService textPlagiarismDetectionService; + + private final Optional programmingLanguageFeatureService; + + private final ProgrammingPlagiarismDetectionService programmingPlagiarismDetectionService; + + private final ModelingPlagiarismDetectionService modelingPlagiarismDetectionService; + + private final PlagiarismResultRepository plagiarismResultRepository; + + public PlagiarismDetectionService(TextPlagiarismDetectionService textPlagiarismDetectionService, Optional programmingLanguageFeatureService, + ProgrammingPlagiarismDetectionService programmingPlagiarismDetectionService, ModelingPlagiarismDetectionService modelingPlagiarismDetectionService, + PlagiarismResultRepository plagiarismResultRepository) { + this.textPlagiarismDetectionService = textPlagiarismDetectionService; + this.programmingLanguageFeatureService = programmingLanguageFeatureService; + this.programmingPlagiarismDetectionService = programmingPlagiarismDetectionService; + this.modelingPlagiarismDetectionService = modelingPlagiarismDetectionService; + this.plagiarismResultRepository = plagiarismResultRepository; + } + + /** + * Check plagiarism in given text exercise + * + * @param exercise exercise to check plagiarism + * @param config configuration for plagiarism detection + * @return result of plagiarism checks + */ + public TextPlagiarismResult checkTextExercise(TextExercise exercise, PlagiarismDetectionConfig config) throws ExitException { + var plagiarismResult = textPlagiarismDetectionService.checkPlagiarism(exercise, config.similarityThreshold(), config.minimumScore(), config.minimumSize()); + log.info("Finished textPlagiarismDetectionService.checkPlagiarism for exercise {} with {} comparisons,", exercise.getId(), plagiarismResult.getComparisons().size()); + + trimAndSavePlagiarismResult(plagiarismResult); + return plagiarismResult; + } + + /** + * Check plagiarism in given programing exercise + * + * @param exercise exercise to check plagiarism + * @param config configuration for plagiarism detection + * @return result of plagiarism checks + */ + public TextPlagiarismResult checkProgrammingExercise(ProgrammingExercise exercise, PlagiarismDetectionConfig config) + throws ExitException, IOException, ProgrammingLanguageNotSupportedForPlagiarismDetectionException { + checkProgrammingLanguageSupport(exercise); + + var plagiarismResult = programmingPlagiarismDetectionService.checkPlagiarism(exercise.getId(), config.similarityThreshold(), config.minimumScore(), config.minimumSize()); + log.info("Finished programmingExerciseExportService.checkPlagiarism call for {} comparisons", plagiarismResult.getComparisons().size()); + + plagiarismResultRepository.prepareResultForClient(plagiarismResult); + + // make sure that participation is included in the exercise + plagiarismResult.setExercise(exercise); + return plagiarismResult; + } + + /** + * Check plagiarism in given programing exercise and outputs a Jplag report + * + * @param exercise exercise to check plagiarism + * @param config configuration for plagiarism detection + * @return Jplag report of plagiarism checks + */ + public File checkProgrammingExerciseWithJplagReport(ProgrammingExercise exercise, PlagiarismDetectionConfig config) + throws ProgrammingLanguageNotSupportedForPlagiarismDetectionException { + checkProgrammingLanguageSupport(exercise); + return programmingPlagiarismDetectionService.checkPlagiarismWithJPlagReport(exercise.getId(), config.similarityThreshold(), config.minimumScore(), config.minimumSize()); + } + + /** + * Check plagiarism in given modeling exercise + * + * @param exercise exercise to check plagiarism + * @param config configuration for plagiarism detection + * @return result of plagiarism checks + */ + public ModelingPlagiarismResult checkModelingExercise(ModelingExercise exercise, PlagiarismDetectionConfig config) { + var plagiarismResult = modelingPlagiarismDetectionService.checkPlagiarism(exercise, config.similarityThreshold(), config.minimumSize(), config.minimumScore()); + log.info("Finished modelingPlagiarismDetectionService.checkPlagiarism call for {} comparisons", plagiarismResult.getComparisons().size()); + + trimAndSavePlagiarismResult(plagiarismResult); + return plagiarismResult; + } + + private void trimAndSavePlagiarismResult(PlagiarismResult plagiarismResult) { + // Limit the amount temporarily because of database issues + plagiarismResult.sortAndLimit(100); + plagiarismResultRepository.savePlagiarismResultAndRemovePrevious(plagiarismResult); + + plagiarismResultRepository.prepareResultForClient(plagiarismResult); + } + + private void checkProgrammingLanguageSupport(ProgrammingExercise exercise) throws ProgrammingLanguageNotSupportedForPlagiarismDetectionException { + var language = exercise.getProgrammingLanguage(); + var programmingLanguageFeature = programmingLanguageFeatureService.orElseThrow().getProgrammingLanguageFeatures(language); + if (!programmingLanguageFeature.plagiarismCheckSupported()) { + throw new ProgrammingLanguageNotSupportedForPlagiarismDetectionException(language); + } + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingLanguageNotSupportedForPlagiarismDetectionException.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingLanguageNotSupportedForPlagiarismDetectionException.java new file mode 100644 index 000000000000..756feb454337 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingLanguageNotSupportedForPlagiarismDetectionException.java @@ -0,0 +1,10 @@ +package de.tum.in.www1.artemis.service.plagiarism; + +import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; + +public class ProgrammingLanguageNotSupportedForPlagiarismDetectionException extends Exception { + + ProgrammingLanguageNotSupportedForPlagiarismDetectionException(ProgrammingLanguage language) { + super("Artemis does not support plagiarism checks for the programming language " + language); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java index b135284995e9..3d0373e7c3f4 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java @@ -6,6 +6,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -40,12 +41,13 @@ import de.tum.in.www1.artemis.service.UrlService; import de.tum.in.www1.artemis.service.connectors.GitService; import de.tum.in.www1.artemis.service.export.ProgrammingExerciseExportService; +import de.tum.in.www1.artemis.service.hestia.ProgrammingExerciseGitDiffReportService; import de.tum.in.www1.artemis.service.plagiarism.cache.PlagiarismCacheService; import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @Service -public class ProgrammingPlagiarismDetectionService { +class ProgrammingPlagiarismDetectionService { @Value("${artemis.repo-download-clone-path}") private Path repoDownloadClonePath; @@ -72,10 +74,12 @@ public class ProgrammingPlagiarismDetectionService { private final UrlService urlService; + private final ProgrammingExerciseGitDiffReportService programmingExerciseGitDiffReportService; + public ProgrammingPlagiarismDetectionService(ProgrammingExerciseRepository programmingExerciseRepository, FileService fileService, GitService gitService, StudentParticipationRepository studentParticipationRepository, PlagiarismResultRepository plagiarismResultRepository, ProgrammingExerciseExportService programmingExerciseExportService, PlagiarismWebsocketService plagiarismWebsocketService, PlagiarismCacheService plagiarismCacheService, - UrlService urlService) { + UrlService urlService, ProgrammingExerciseGitDiffReportService programmingExerciseGitDiffReportService) { this.programmingExerciseRepository = programmingExerciseRepository; this.fileService = fileService; this.gitService = gitService; @@ -85,6 +89,7 @@ public ProgrammingPlagiarismDetectionService(ProgrammingExerciseRepository progr this.plagiarismWebsocketService = plagiarismWebsocketService; this.plagiarismCacheService = plagiarismCacheService; this.urlService = urlService; + this.programmingExerciseGitDiffReportService = programmingExerciseGitDiffReportService; } /** @@ -97,7 +102,7 @@ public ProgrammingPlagiarismDetectionService(ProgrammingExerciseRepository progr * @throws ExitException is thrown if JPlag exits unexpectedly * @throws IOException is thrown for file handling errors */ - public TextPlagiarismResult checkPlagiarism(long programmingExerciseId, float similarityThreshold, int minimumScore) throws ExitException, IOException { + public TextPlagiarismResult checkPlagiarism(long programmingExerciseId, float similarityThreshold, int minimumScore, int minimumSize) throws ExitException, IOException { long start = System.nanoTime(); String topic = plagiarismWebsocketService.getProgrammingExercisePlagiarismCheckTopic(programmingExerciseId); @@ -112,7 +117,7 @@ public TextPlagiarismResult checkPlagiarism(long programmingExerciseId, float si } plagiarismCacheService.setActivePlagiarismCheck(courseId); - JPlagResult jPlagResult = computeJPlagResult(programmingExercise, similarityThreshold, minimumScore); + JPlagResult jPlagResult = computeJPlagResult(programmingExercise, similarityThreshold, minimumScore, minimumSize); if (jPlagResult == null) { log.info("Insufficient amount of submissions for plagiarism detection. Return empty result."); TextPlagiarismResult textPlagiarismResult = new TextPlagiarismResult(); @@ -148,11 +153,11 @@ public TextPlagiarismResult checkPlagiarism(long programmingExerciseId, float si * @param minimumScore consider only submissions whose score is greater or equal to this value * @return a zip file that can be returned to the client */ - public File checkPlagiarismWithJPlagReport(long programmingExerciseId, float similarityThreshold, int minimumScore) { + public File checkPlagiarismWithJPlagReport(long programmingExerciseId, float similarityThreshold, int minimumScore, int minimumSize) { long start = System.nanoTime(); final var programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(programmingExerciseId); - JPlagResult result = computeJPlagResult(programmingExercise, similarityThreshold, minimumScore); + JPlagResult result = computeJPlagResult(programmingExercise, similarityThreshold, minimumScore, minimumSize); log.info("JPlag programming comparison finished with {} comparisons in {}", result.getAllComparisons().size(), TimeLogUtil.formatDurationFrom(start)); return generateJPlagReportZip(result, programmingExercise); @@ -167,7 +172,7 @@ public File checkPlagiarismWithJPlagReport(long programmingExerciseId, float sim * @return the JPlag result or null if there are not enough participations */ @NotNull - private JPlagResult computeJPlagResult(ProgrammingExercise programmingExercise, float similarityThreshold, int minimumScore) { + private JPlagResult computeJPlagResult(ProgrammingExercise programmingExercise, float similarityThreshold, int minimumScore, int minimumSize) { long programmingExerciseId = programmingExercise.getId(); final var targetPath = fileService.getTemporaryUniqueSubfolderPath(repoDownloadClonePath, 60); List participations = filterStudentParticipationsForComparison(programmingExercise, minimumScore); @@ -177,7 +182,7 @@ private JPlagResult computeJPlagResult(ProgrammingExercise programmingExercise, throw new BadRequestAlertException("Insufficient amount of valid and long enough submissions available for comparison", "Plagiarism Check", "notEnoughSubmissions"); } - List repositories = downloadRepositories(programmingExercise, participations, targetPath.toString()); + List repositories = downloadRepositories(programmingExercise, participations, targetPath.toString(), minimumSize); log.info("Downloading repositories done for programming exercise {}", programmingExerciseId); final var projectKey = programmingExercise.getProjectKey(); @@ -343,12 +348,41 @@ public List filterStudentParticipationsForComp }).toList(); } - private List downloadRepositories(ProgrammingExercise programmingExercise, List participations, String targetPath) { + private Optional cloneTemplateRepository(ProgrammingExercise programmingExercise, String targetPath) { + try { + var templateRepo = gitService.getOrCheckoutRepository(programmingExercise.getTemplateParticipation(), targetPath); + gitService.resetToOriginHead(templateRepo); // start with clean state + return Optional.of(templateRepo); + } + catch (GitException | GitAPIException ex) { + log.error("Clone template repository {} in exercise '{}' did not work as expected: {}", programmingExercise.getTemplateParticipation().getVcsRepositoryUrl(), + programmingExercise.getTitle(), ex.getMessage()); + return Optional.empty(); + } + } + + private boolean shouldAddRepo(int minimumSize, Repository repo, Optional templateRepo) { + if (templateRepo.isEmpty()) { + return true; + } + + var diffToTemplate = programmingExerciseGitDiffReportService.calculateNumberOfDiffLinesBetweenRepos(repo.getRemoteRepositoryUrl(), repo.getLocalPath(), + templateRepo.get().getRemoteRepositoryUrl(), templateRepo.get().getLocalPath()); + return diffToTemplate >= minimumSize; + } + + private List downloadRepositories(ProgrammingExercise programmingExercise, List participations, String targetPath, + int minimumSize) { // Used for sending progress notifications var topic = plagiarismWebsocketService.getProgrammingExercisePlagiarismCheckTopic(programmingExercise.getId()); int maxRepositories = participations.size() + 1; List downloadedRepositories = new ArrayList<>(); + + plagiarismWebsocketService.notifyInstructorAboutPlagiarismState(topic, PlagiarismCheckState.RUNNING, List.of("Downloading repositories: 0/" + maxRepositories)); + var templateRepo = cloneTemplateRepository(programmingExercise, targetPath); + templateRepo.ifPresent(downloadedRepositories::add); + participations.parallelStream().forEach(participation -> { try { var progressMessage = "Downloading repositories: " + (downloadedRepositories.size() + 1) + "/" + maxRepositories; @@ -356,7 +390,13 @@ private List downloadRepositories(ProgrammingExercise programmingExe Repository repo = gitService.getOrCheckoutRepositoryForJPlag(participation, targetPath); gitService.resetToOriginHead(repo); // start with clean state - downloadedRepositories.add(repo); + + if (shouldAddRepo(minimumSize, repo, templateRepo)) { + downloadedRepositories.add(repo); + } + else { + deleteTempLocalRepository(repo); + } } catch (GitException | GitAPIException | InvalidPathException ex) { log.error("Clone student repository {} in exercise '{}' did not work as expected: {}", participation.getVcsRepositoryUrl(), programmingExercise.getTitle(), @@ -364,20 +404,6 @@ private List downloadRepositories(ProgrammingExercise programmingExe } }); - // clone the template repo - try { - var progressMessage = "Downloading repositories: " + maxRepositories + "/" + maxRepositories; - plagiarismWebsocketService.notifyInstructorAboutPlagiarismState(topic, PlagiarismCheckState.RUNNING, List.of(progressMessage)); - - Repository templateRepo = gitService.getOrCheckoutRepository(programmingExercise.getTemplateParticipation(), targetPath); - gitService.resetToOriginHead(templateRepo); // start with clean state - downloadedRepositories.add(templateRepo); - } - catch (GitException | GitAPIException ex) { - log.error("Clone template repository {} in exercise '{}' did not work as expected: {}", programmingExercise.getTemplateParticipation().getVcsRepositoryUrl(), - programmingExercise.getTitle(), ex.getMessage()); - } - return downloadedRepositories; } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/TextPlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/TextPlagiarismDetectionService.java index c0e141e604dd..25e8ee3cb22c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/TextPlagiarismDetectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/TextPlagiarismDetectionService.java @@ -31,7 +31,7 @@ import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @Service -public class TextPlagiarismDetectionService { +class TextPlagiarismDetectionService { private final Logger log = LoggerFactory.getLogger(TextPlagiarismDetectionService.class); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java index dfcf760984a9..8ee88209dfd8 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java @@ -20,6 +20,7 @@ import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.metis.conversation.Channel; import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; import de.tum.in.www1.artemis.domain.plagiarism.modeling.ModelingPlagiarismResult; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; @@ -34,7 +35,7 @@ import de.tum.in.www1.artemis.service.feature.FeatureToggle; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationScheduleService; -import de.tum.in.www1.artemis.service.plagiarism.ModelingPlagiarismDetectionService; +import de.tum.in.www1.artemis.service.plagiarism.PlagiarismDetectionService; import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; @@ -86,7 +87,7 @@ public class ModelingExerciseResource { private final GradingCriterionRepository gradingCriterionRepository; - private final ModelingPlagiarismDetectionService modelingPlagiarismDetectionService; + private final PlagiarismDetectionService plagiarismDetectionService; private final ChannelService channelService; @@ -97,7 +98,7 @@ public ModelingExerciseResource(ModelingExerciseRepository modelingExerciseRepos ModelingExerciseService modelingExerciseService, ExerciseDeletionService exerciseDeletionService, PlagiarismResultRepository plagiarismResultRepository, ModelingExerciseImportService modelingExerciseImportService, SubmissionExportService modelingSubmissionExportService, ExerciseService exerciseService, GroupNotificationScheduleService groupNotificationScheduleService, GradingCriterionRepository gradingCriterionRepository, - ModelingPlagiarismDetectionService modelingPlagiarismDetectionService, ChannelService channelService, ChannelRepository channelRepository) { + PlagiarismDetectionService plagiarismDetectionService, ChannelService channelService, ChannelRepository channelRepository) { this.modelingExerciseRepository = modelingExerciseRepository; this.courseService = courseService; this.modelingExerciseService = modelingExerciseService; @@ -112,7 +113,7 @@ public ModelingExerciseResource(ModelingExerciseRepository modelingExerciseRepos this.groupNotificationScheduleService = groupNotificationScheduleService; this.exerciseService = exerciseService; this.gradingCriterionRepository = gradingCriterionRepository; - this.modelingPlagiarismDetectionService = modelingPlagiarismDetectionService; + this.plagiarismDetectionService = plagiarismDetectionService; this.channelService = channelService; this.channelRepository = channelRepository; } @@ -390,17 +391,11 @@ public ResponseEntity checkPlagiarism(@PathVariable lo var modelingExercise = modelingExerciseRepository.findByIdWithStudentParticipationsSubmissionsResultsElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.INSTRUCTOR, modelingExercise, null); long start = System.nanoTime(); - log.info("Start modelingPlagiarismDetectionService.checkPlagiarism for exercise {}", exerciseId); - var plagiarismResult = modelingPlagiarismDetectionService.checkPlagiarism(modelingExercise, similarityThreshold / 100, minimumSize, minimumScore); - log.info("Finished modelingPlagiarismDetectionService.checkPlagiarism call for {} comparisons in {}", plagiarismResult.getComparisons().size(), - TimeLogUtil.formatDurationFrom(start)); - // TODO: limit the amount temporarily because of database issues - plagiarismResult.sortAndLimit(100); - log.info("Limited number of comparisons to {} to avoid performance issues when saving to database", plagiarismResult.getComparisons().size()); - start = System.nanoTime(); - plagiarismResultRepository.savePlagiarismResultAndRemovePrevious(plagiarismResult); - log.info("Finished plagiarismResultRepository.savePlagiarismResultAndRemovePrevious call in {}", TimeLogUtil.formatDurationFrom(start)); - plagiarismResultRepository.prepareResultForClient(plagiarismResult); + log.info("Started manual plagiarism checks for modeling exercise: exerciseId={}.", exerciseId); + var config = new PlagiarismDetectionConfig(similarityThreshold, minimumScore, minimumSize); + var plagiarismResult = plagiarismDetectionService.checkModelingExercise(modelingExercise, config); + log.info("Finished manual plagiarism checks for modeling exercise: exerciseId={}, elapsed={}.", exerciseId, TimeLogUtil.formatDurationFrom(start)); + return ResponseEntity.ok(plagiarismResult); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExercisePlagiarismResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExercisePlagiarismResource.java index b170639fbb0d..9fe72ab56e03 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExercisePlagiarismResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExercisePlagiarismResource.java @@ -2,10 +2,8 @@ import static de.tum.in.www1.artemis.web.rest.ProgrammingExerciseResourceEndpoints.*; -import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,7 +16,7 @@ import de.jplag.exceptions.ExitException; import de.tum.in.www1.artemis.domain.ProgrammingExercise; -import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismResultRepository; @@ -27,9 +25,8 @@ import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.feature.Feature; import de.tum.in.www1.artemis.service.feature.FeatureToggle; -import de.tum.in.www1.artemis.service.plagiarism.ProgrammingPlagiarismDetectionService; -import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeature; -import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeatureService; +import de.tum.in.www1.artemis.service.plagiarism.PlagiarismDetectionService; +import de.tum.in.www1.artemis.service.plagiarism.ProgrammingLanguageNotSupportedForPlagiarismDetectionException; import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @@ -53,18 +50,14 @@ public class ProgrammingExercisePlagiarismResource { private final PlagiarismResultRepository plagiarismResultRepository; - private final Optional programmingLanguageFeatureService; - - private final ProgrammingPlagiarismDetectionService programmingPlagiarismDetectionService; + private final PlagiarismDetectionService plagiarismDetectionService; public ProgrammingExercisePlagiarismResource(ProgrammingExerciseRepository programmingExerciseRepository, AuthorizationCheckService authCheckService, - PlagiarismResultRepository plagiarismResultRepository, Optional programmingLanguageFeatureService, - ProgrammingPlagiarismDetectionService programmingPlagiarismDetectionService) { + PlagiarismResultRepository plagiarismResultRepository, PlagiarismDetectionService plagiarismDetectionService) { this.programmingExerciseRepository = programmingExerciseRepository; this.authCheckService = authCheckService; this.plagiarismResultRepository = plagiarismResultRepository; - this.programmingLanguageFeatureService = programmingLanguageFeatureService; - this.programmingPlagiarismDetectionService = programmingPlagiarismDetectionService; + this.plagiarismDetectionService = plagiarismDetectionService; } /** @@ -91,6 +84,7 @@ public ResponseEntity getPlagiarismResult(@PathVariable lo * @param exerciseId The ID of the programming exercise for which the plagiarism check should be executed * @param similarityThreshold ignore comparisons whose similarity is below this threshold (in % between 0 and 100) * @param minimumScore consider only submissions whose score is greater or equal to this value + * @param minimumSize consider only submissions whose number of diff to template lines is greate or equal to this value * @return the ResponseEntity with status 200 (OK) and the list of at most 500 pair-wise submissions with a similarity above the given threshold (e.g. 50%). * @throws ExitException is thrown if JPlag exits unexpectedly * @throws IOException is thrown for file handling errors @@ -98,25 +92,24 @@ public ResponseEntity getPlagiarismResult(@PathVariable lo @GetMapping(CHECK_PLAGIARISM) @EnforceAtLeastEditor @FeatureToggle({ Feature.ProgrammingExercises, Feature.PlagiarismChecks }) - public ResponseEntity checkPlagiarism(@PathVariable long exerciseId, @RequestParam float similarityThreshold, @RequestParam int minimumScore) - throws ExitException, IOException { + public ResponseEntity checkPlagiarism(@PathVariable long exerciseId, @RequestParam float similarityThreshold, @RequestParam int minimumScore, + @RequestParam int minimumSize) throws ExitException, IOException { ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, programmingExercise, null); - ProgrammingLanguage language = programmingExercise.getProgrammingLanguage(); - ProgrammingLanguageFeature programmingLanguageFeature = programmingLanguageFeatureService.orElseThrow().getProgrammingLanguageFeatures(language); - - if (!programmingLanguageFeature.plagiarismCheckSupported()) { - throw new BadRequestAlertException("Artemis does not support plagiarism checks for the programming language " + programmingExercise.getProgrammingLanguage(), - ENTITY_NAME, "programmingLanguageNotSupported"); - } long start = System.nanoTime(); - log.info("Start programmingPlagiarismDetectionService.checkPlagiarism for exercise {}", exerciseId); - var plagiarismResult = programmingPlagiarismDetectionService.checkPlagiarism(exerciseId, similarityThreshold, minimumScore); - log.info("Finished programmingExerciseExportService.checkPlagiarism call for {} comparisons in {}", plagiarismResult.getComparisons().size(), - TimeLogUtil.formatDurationFrom(start)); - plagiarismResultRepository.prepareResultForClient(plagiarismResult); - return ResponseEntity.ok(plagiarismResult); + log.info("Started manual plagiarism checks for programming exercise: exerciseId={}.", exerciseId); + var config = new PlagiarismDetectionConfig(similarityThreshold, minimumScore, minimumSize); + try { + var plagiarismResult = plagiarismDetectionService.checkProgrammingExercise(programmingExercise, config); + return ResponseEntity.ok(plagiarismResult); + } + catch (ProgrammingLanguageNotSupportedForPlagiarismDetectionException e) { + throw new BadRequestAlertException(e.getMessage(), ENTITY_NAME, "programmingLanguageNotSupported"); + } + finally { + log.info("Finished manual plagiarism checks for programming exercise: exerciseId={}, elapsed={}.", exerciseId, TimeLogUtil.formatDurationFrom(start)); + } } /** @@ -125,29 +118,37 @@ public ResponseEntity checkPlagiarism(@PathVariable long e * @param exerciseId The ID of the programming exercise for which the plagiarism check should be executed * @param similarityThreshold ignore comparisons whose similarity is below this threshold (in % between 0 and 100) * @param minimumScore consider only submissions whose score is greater or equal to this value + * @param minimumSize consider only submissions whose number of diff to template lines is greate or equal to this value * @return The ResponseEntity with status 201 (Created) or with status 400 (Bad Request) if the parameters are invalid * @throws IOException is thrown for file handling errors */ @GetMapping(value = CHECK_PLAGIARISM_JPLAG_REPORT) @EnforceAtLeastEditor @FeatureToggle(Feature.ProgrammingExercises) - public ResponseEntity checkPlagiarismWithJPlagReport(@PathVariable long exerciseId, @RequestParam float similarityThreshold, @RequestParam int minimumScore) - throws IOException { + public ResponseEntity checkPlagiarismWithJPlagReport(@PathVariable long exerciseId, @RequestParam float similarityThreshold, @RequestParam int minimumScore, + @RequestParam int minimumSize) throws IOException { log.debug("REST request to check plagiarism for ProgrammingExercise with id: {}", exerciseId); ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, programmingExercise, null); - var programmingLanguageFeature = programmingLanguageFeatureService.orElseThrow().getProgrammingLanguageFeatures(programmingExercise.getProgrammingLanguage()); - if (!programmingLanguageFeature.plagiarismCheckSupported()) { - throw new BadRequestAlertException("Artemis does not support plagiarism checks for the programming language " + programmingExercise.getProgrammingLanguage(), - "Plagiarism Check", "programmingLanguageNotSupported"); - } - File zipFile = programmingPlagiarismDetectionService.checkPlagiarismWithJPlagReport(exerciseId, similarityThreshold, minimumScore); - if (zipFile == null) { - throw new BadRequestAlertException("Insufficient amount of valid and long enough submissions available for comparison.", "Plagiarism Check", "notEnoughSubmissions"); + long start = System.nanoTime(); + log.info("Started manual plagiarism checks with Jplag report for programming exercise: exerciseId={}.", exerciseId); + var config = new PlagiarismDetectionConfig(similarityThreshold, minimumScore, minimumSize); + try { + var zipFile = plagiarismDetectionService.checkProgrammingExerciseWithJplagReport(programmingExercise, config); + if (zipFile == null) { + throw new BadRequestAlertException("Insufficient amount of valid and long enough submissions available for comparison.", "Plagiarism Check", + "notEnoughSubmissions"); + } + + var resource = new InputStreamResource(new FileInputStream(zipFile)); + return ResponseEntity.ok().contentLength(zipFile.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).header("filename", zipFile.getName()).body(resource); + } + catch (ProgrammingLanguageNotSupportedForPlagiarismDetectionException e) { + throw new BadRequestAlertException(e.getMessage(), ENTITY_NAME, "programmingLanguageNotSupported"); + } + finally { + log.info("Finished manual plagiarism checks with Jplag report for programming exercise: exerciseId={}, elapsed={}.", exerciseId, TimeLogUtil.formatDurationFrom(start)); } - - InputStreamResource resource = new InputStreamResource(new FileInputStream(zipFile)); - return ResponseEntity.ok().contentLength(zipFile.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).header("filename", zipFile.getName()).body(resource); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java index 2d4f41229288..ec59a8308d6b 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java @@ -16,6 +16,7 @@ import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.metis.conversation.Channel; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; @@ -29,7 +30,7 @@ import de.tum.in.www1.artemis.service.messaging.InstanceMessageSendService; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationScheduleService; -import de.tum.in.www1.artemis.service.plagiarism.TextPlagiarismDetectionService; +import de.tum.in.www1.artemis.service.plagiarism.PlagiarismDetectionService; import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; @@ -93,7 +94,7 @@ public class TextExerciseResource { private final InstanceMessageSendService instanceMessageSendService; - private final TextPlagiarismDetectionService textPlagiarismDetectionService; + private final PlagiarismDetectionService plagiarismDetectionService; private final CourseRepository courseRepository; @@ -107,7 +108,7 @@ public TextExerciseResource(TextExerciseRepository textExerciseRepository, TextE ParticipationRepository participationRepository, ResultRepository resultRepository, TextExerciseImportService textExerciseImportService, TextSubmissionExportService textSubmissionExportService, ExampleSubmissionRepository exampleSubmissionRepository, ExerciseService exerciseService, GradingCriterionRepository gradingCriterionRepository, TextBlockRepository textBlockRepository, GroupNotificationScheduleService groupNotificationScheduleService, - InstanceMessageSendService instanceMessageSendService, TextPlagiarismDetectionService textPlagiarismDetectionService, CourseRepository courseRepository, + InstanceMessageSendService instanceMessageSendService, PlagiarismDetectionService plagiarismDetectionService, CourseRepository courseRepository, ChannelService channelService, ChannelRepository channelRepository) { this.feedbackRepository = feedbackRepository; this.exerciseDeletionService = exerciseDeletionService; @@ -128,7 +129,7 @@ public TextExerciseResource(TextExerciseRepository textExerciseRepository, TextE this.exerciseService = exerciseService; this.gradingCriterionRepository = gradingCriterionRepository; this.instanceMessageSendService = instanceMessageSendService; - this.textPlagiarismDetectionService = textPlagiarismDetectionService; + this.plagiarismDetectionService = plagiarismDetectionService; this.courseRepository = courseRepository; this.channelService = channelService; this.channelRepository = channelRepository; @@ -495,18 +496,12 @@ public ResponseEntity checkPlagiarism(@PathVariable long e @RequestParam int minimumSize) throws ExitException { TextExercise textExercise = textExerciseRepository.findByIdWithStudentParticipationsAndSubmissionsElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, textExercise, null); - log.info("Start textPlagiarismDetectionService.checkPlagiarism for exercise {}", exerciseId); + long start = System.nanoTime(); - var plagiarismResult = textPlagiarismDetectionService.checkPlagiarism(textExercise, similarityThreshold, minimumScore, minimumSize); - log.info("Finished textPlagiarismDetectionService.checkPlagiarism for exercise {} with {} comparisons in {}", exerciseId, plagiarismResult.getComparisons().size(), - TimeLogUtil.formatDurationFrom(start)); - // TODO: limit the amount temporarily because of database issues - plagiarismResult.sortAndLimit(100); - log.info("Limited number of comparisons to {} to avoid performance issues when saving to database", plagiarismResult.getComparisons().size()); - start = System.nanoTime(); - plagiarismResultRepository.savePlagiarismResultAndRemovePrevious(plagiarismResult); - log.info("Finished plagiarismResultRepository.savePlagiarismResultAndRemovePrevious call in {}", TimeLogUtil.formatDurationFrom(start)); - plagiarismResultRepository.prepareResultForClient(plagiarismResult); + log.info("Started manual plagiarism checks for text exercise: exerciseId={}.", exerciseId); + var config = new PlagiarismDetectionConfig(similarityThreshold, minimumScore, minimumSize); + var plagiarismResult = plagiarismDetectionService.checkTextExercise(textExercise, config); + log.info("Finished manual plagiarism checks for text exercise: exerciseId={}, elapsed={}.", exerciseId, TimeLogUtil.formatDurationFrom(start)); return ResponseEntity.ok(plagiarismResult); } diff --git a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-inspector/plagiarism-inspector.component.html b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-inspector/plagiarism-inspector.component.html index b0cddf737e1b..9bd26fc8c330 100644 --- a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-inspector/plagiarism-inspector.component.html +++ b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-inspector/plagiarism-inspector.component.html @@ -91,7 +91,7 @@
-
+
diff --git a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-inspector/plagiarism-inspector.component.ts b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-inspector/plagiarism-inspector.component.ts index 47505af1cced..e69d8875925d 100644 --- a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-inspector/plagiarism-inspector.component.ts +++ b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-inspector/plagiarism-inspector.component.ts @@ -384,17 +384,15 @@ export class PlagiarismInspectorComponent implements OnInit { * Return the translation identifier of the minimum size tooltip for the current exercise type. */ getMinimumSizeTooltip() { - const tooltip = 'artemisApp.plagiarism.minimumSizeTooltip'; - switch (this.exercise.type) { + case ExerciseType.PROGRAMMING: { + return 'artemisApp.plagiarism.minimumSizeTooltipProgrammingExercise'; + } case ExerciseType.TEXT: { - return tooltip + 'Text'; + return 'artemisApp.plagiarism.minimumSizeTooltipTextExercise'; } case ExerciseType.MODELING: { - return tooltip + 'Modeling'; - } - default: { - return tooltip; + return 'artemisApp.plagiarism.minimumSizeTooltipModelingExercise'; } } } diff --git a/src/main/webapp/i18n/de/plagiarism.json b/src/main/webapp/i18n/de/plagiarism.json index 087164669afb..cc1737f42183 100644 --- a/src/main/webapp/i18n/de/plagiarism.json +++ b/src/main/webapp/i18n/de/plagiarism.json @@ -34,9 +34,9 @@ "minimumScore": "Minimale Bewertung in %", "minimumScoreTooltip": "Berücksichtige nur Einreichungen mit einer Bewertung, die mindestens diesem Wert entspricht", "minimumSize": "Minimale Größe", - "minimumSizeTooltip": "Berücksichtige nur Einreichungen, deren Größe mindestens diesem Wert entspricht", - "minimumSizeTooltipModeling": "Berücksichtige nur Einreichungen, die mindestens so viele Modeling-Elemente wie der angegebene Wert haben", - "minimumSizeTooltipText": "Berücksichtige nur Einreichungen, die mindestens so viele Zeichen wie der angegebene Wert haben", + "minimumSizeTooltipTextExercise": "Es werden nur diejenigen Abgaben berücksichtigt, die mindestens so viele Wörter beinhalten wie der angegebene Wert.", + "minimumSizeTooltipProgrammingExercise": "Es werden nur diejenigen Abgaben berücksichtigt, die mindestens so viele unterschiedliche Zeilen (im Vergleich zur Vorlage) beinhalten wie der angegebene Wert.", + "minimumSizeTooltipModelingExercise": "Es werden nur diejenigen Abgaben berücksichtigt, die mindestens so viele Modellelemente beinhalten wie der angegebene Wert.", "noInfoAvailable": "Keine Information verfügbar", "caution": "Die Plagiatskontrolle kann für große Kurse sehr lange dauern.", "detect": "Plagiate finden", diff --git a/src/main/webapp/i18n/en/plagiarism.json b/src/main/webapp/i18n/en/plagiarism.json index 2203a200344f..8bf6cb2a23f0 100644 --- a/src/main/webapp/i18n/en/plagiarism.json +++ b/src/main/webapp/i18n/en/plagiarism.json @@ -34,9 +34,9 @@ "minimumScore": "Minimum Score in %", "minimumScoreTooltip": "Consider only submissions with a score greater than or equal to this value", "minimumSize": "Minimum Size", - "minimumSizeTooltip": "Consider only submissions whose size is greater or equal to this value", - "minimumSizeTooltipModeling": "Consider only submissions that have at least as many modeling elements as the specified value", - "minimumSizeTooltipText": "Consider only submissions that have at least as many words as the specified value", + "minimumSizeTooltipTextExercise": "Consider only submissions that have at least as many words as the specified value.", + "minimumSizeTooltipProgrammingExercise": "Consider only submissions that have at least as many different lines in comparison to the template as the specified value.", + "minimumSizeTooltipModelingExercise": "Consider only submissions that have at least as many modeling elements as the specified value.", "noInfoAvailable": "No information available", "caution": "Plagiarism detection can take a long time for large courses.", "detect": "Detect plagiarism", diff --git a/src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionServiceTest.java new file mode 100644 index 000000000000..0f0e0fc8e033 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionServiceTest.java @@ -0,0 +1,140 @@ +package de.tum.in.www1.artemis.service.plagiarism; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import de.jplag.exceptions.ExitException; +import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.TextExercise; +import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; +import de.tum.in.www1.artemis.domain.plagiarism.modeling.ModelingPlagiarismResult; +import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult; +import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismResultRepository; +import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeature; +import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeatureService; + +class PlagiarismDetectionServiceTest { + + private final PlagiarismDetectionConfig config = new PlagiarismDetectionConfig(0.5f, 1, 2); + + private final TextPlagiarismDetectionService textPlagiarismDetectionService = mock(); + + private final ProgrammingLanguageFeatureService programmingLanguageFeatureService = mock(); + + private final ProgrammingPlagiarismDetectionService programmingPlagiarismDetectionService = mock(); + + private final ModelingPlagiarismDetectionService modelingPlagiarismDetectionService = mock(); + + private final PlagiarismResultRepository plagiarismResultRepository = mock(); + + private final PlagiarismDetectionService service = new PlagiarismDetectionService(textPlagiarismDetectionService, Optional.of(programmingLanguageFeatureService), + programmingPlagiarismDetectionService, modelingPlagiarismDetectionService, plagiarismResultRepository); + + @Test + void shouldExecuteChecksForTextExercise() throws ExitException { + // given + var textExercise = new TextExercise(); + var textPlagiarismResult = new TextPlagiarismResult(); + textPlagiarismResult.setComparisons(emptySet()); + when(textPlagiarismDetectionService.checkPlagiarism(textExercise, config.similarityThreshold(), config.minimumScore(), config.minimumSize())) + .thenReturn(textPlagiarismResult); + + // when + var result = service.checkTextExercise(textExercise, config); + + // then + assertThat(result).isEqualTo(textPlagiarismResult); + } + + @Test + void shouldExecuteChecksForModelingExercise() { + // given + var modelingExercise = new ModelingExercise(); + var modelingPlagiarismResult = new ModelingPlagiarismResult(); + modelingPlagiarismResult.setComparisons(emptySet()); + when(modelingPlagiarismDetectionService.checkPlagiarism(modelingExercise, config.similarityThreshold(), config.minimumSize(), config.minimumScore())) + .thenReturn(modelingPlagiarismResult); + + // when + var result = service.checkModelingExercise(modelingExercise, config); + + // then + assertThat(result).isEqualTo(modelingPlagiarismResult); + } + + @Test + void shouldExecuteChecksForProgrammingExercise() throws IOException, ExitException, ProgrammingLanguageNotSupportedForPlagiarismDetectionException { + // given + var programmingExercise = new ProgrammingExercise(); + programmingExercise.setId(1L); + var programmingPlagiarismResult = new TextPlagiarismResult(); + programmingPlagiarismResult.setComparisons(emptySet()); + when(programmingPlagiarismDetectionService.checkPlagiarism(1L, config.similarityThreshold(), config.minimumScore(), config.minimumSize())) + .thenReturn(programmingPlagiarismResult); + + // and + var programmingLanguageFeature = new ProgrammingLanguageFeature(null, false, false, true, false, false, emptyList(), false, false, false); + when(programmingLanguageFeatureService.getProgrammingLanguageFeatures(any())).thenReturn(programmingLanguageFeature); + + // when + var result = service.checkProgrammingExercise(programmingExercise, config); + + // then + assertThat(result).isEqualTo(programmingPlagiarismResult); + } + + @Test + void shouldThrowExceptionOnUnsupportedProgrammingLanguage() { + // given + var programmingExercise = new ProgrammingExercise(); + var programmingLanguageFeature = new ProgrammingLanguageFeature(null, false, false, false, false, false, emptyList(), false, false, false); + when(programmingLanguageFeatureService.getProgrammingLanguageFeatures(any())).thenReturn(programmingLanguageFeature); + + // expect + assertThatThrownBy(() -> service.checkProgrammingExercise(programmingExercise, config)).isInstanceOf(ProgrammingLanguageNotSupportedForPlagiarismDetectionException.class); + } + + @Test + void shouldExecuteChecksWithJplagReportForProgrammingExercise() throws ProgrammingLanguageNotSupportedForPlagiarismDetectionException { + // given + var programmingExercise = new ProgrammingExercise(); + programmingExercise.setId(1L); + var zipFile = new File(""); + when(programmingPlagiarismDetectionService.checkPlagiarismWithJPlagReport(1L, config.similarityThreshold(), config.minimumScore(), config.minimumSize())) + .thenReturn(zipFile); + + // and + var programmingLanguageFeature = new ProgrammingLanguageFeature(null, false, false, true, false, false, emptyList(), false, false, false); + when(programmingLanguageFeatureService.getProgrammingLanguageFeatures(any())).thenReturn(programmingLanguageFeature); + + // when + var result = service.checkProgrammingExerciseWithJplagReport(programmingExercise, config); + + // then + assertThat(result).isEqualTo(zipFile); + } + + @Test + void shouldThrowExceptionOnUnsupportedProgrammingLanguageForChecksWithJplagReport() { + // given + var programmingExercise = new ProgrammingExercise(); + var programmingLanguageFeature = new ProgrammingLanguageFeature(null, false, false, false, false, false, emptyList(), false, false, false); + when(programmingLanguageFeatureService.getProgrammingLanguageFeatures(any())).thenReturn(programmingLanguageFeature); + + // expect + assertThatThrownBy(() -> service.checkProgrammingExerciseWithJplagReport(programmingExercise, config)) + .isInstanceOf(ProgrammingLanguageNotSupportedForPlagiarismDetectionException.class); + } +} diff --git a/src/test/javascript/spec/component/plagiarism/plagiarism-inspector.component.spec.ts b/src/test/javascript/spec/component/plagiarism/plagiarism-inspector.component.spec.ts index 0a6981a1d5d3..63b05fd3d43c 100644 --- a/src/test/javascript/spec/component/plagiarism/plagiarism-inspector.component.spec.ts +++ b/src/test/javascript/spec/component/plagiarism/plagiarism-inspector.component.spec.ts @@ -146,22 +146,22 @@ describe('Plagiarism Inspector Component', () => { }); }); - it('should get the minimumSize tootip', () => { + it('should get the minimumSize tooltip for programming', () => { comp.exercise = { type: ExerciseType.PROGRAMMING } as Exercise; - expect(comp.getMinimumSizeTooltip()).toBe('artemisApp.plagiarism.minimumSizeTooltip'); + expect(comp.getMinimumSizeTooltip()).toBe('artemisApp.plagiarism.minimumSizeTooltipProgrammingExercise'); }); it('should get the minimumSize tootip for modeling', () => { comp.exercise = { type: ExerciseType.MODELING } as Exercise; - expect(comp.getMinimumSizeTooltip()).toBe('artemisApp.plagiarism.minimumSizeTooltipModeling'); + expect(comp.getMinimumSizeTooltip()).toBe('artemisApp.plagiarism.minimumSizeTooltipModelingExercise'); }); it('should get the minimumSize tootip for text', () => { comp.exercise = { type: ExerciseType.TEXT } as Exercise; - expect(comp.getMinimumSizeTooltip()).toBe('artemisApp.plagiarism.minimumSizeTooltipText'); + expect(comp.getMinimumSizeTooltip()).toBe('artemisApp.plagiarism.minimumSizeTooltipTextExercise'); }); it('should fetch the plagiarism detection results for modeling exercises', () => { From fa8a10bed68e92c973fcd8d9cf6cba635d9a994f Mon Sep 17 00:00:00 2001 From: Timor Morrien Date: Mon, 2 Oct 2023 09:30:03 +0200 Subject: [PATCH 12/19] Iris: Add per-user rate limit (#7246) --- .../iris/IrisMessageRepository.java | 21 +++++ .../service/iris/IrisRateLimitService.java | 80 +++++++++++++++++++ .../service/iris/IrisWebsocketService.java | 31 +++++-- .../service/iris/exception/IrisException.java | 6 ++ .../IrisRateLimitExceededException.java | 24 ++++++ .../web/rest/iris/IrisMessageResource.java | 23 ++++-- .../web/rest/iris/IrisSessionResource.java | 16 +++- .../app/entities/iris/iris-errors.model.ts | 2 + .../exercise-chat-widget.component.html | 35 ++++---- .../exercise-chat-widget.component.scss | 10 +++ .../exercise-chat-widget.component.ts | 40 ++++++---- src/main/webapp/app/iris/heartbeat.service.ts | 13 ++- .../webapp/app/iris/http-session.service.ts | 9 ++- .../iris-sub-settings-update.component.html | 3 +- src/main/webapp/app/iris/state-store.model.ts | 18 +++++ .../webapp/app/iris/state-store.service.ts | 15 ++++ src/main/webapp/app/iris/websocket.service.ts | 11 +++ src/main/webapp/i18n/de/exerciseChatbot.json | 6 +- src/main/webapp/i18n/de/iris.json | 2 + src/main/webapp/i18n/en/exerciseChatbot.json | 6 +- src/main/webapp/i18n/en/iris.json | 4 +- .../iris/IrisMessageIntegrationTest.java | 77 ++++++++++++++---- .../iris/IrisSessionIntegrationTest.java | 12 +-- .../www1/artemis/iris/IrisWebsocketTest.java | 2 +- .../iris/state-store.service.spec.ts | 35 ++++++++ .../spec/helpers/sample/iris-sample-data.ts | 2 + .../resources/config/application-iris.yml | 2 + 27 files changed, 424 insertions(+), 81 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/service/iris/IrisRateLimitService.java create mode 100644 src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisRateLimitExceededException.java diff --git a/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisMessageRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisMessageRepository.java index 0400efcdf8dc..749903a605b7 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisMessageRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisMessageRepository.java @@ -1,11 +1,13 @@ package de.tum.in.www1.artemis.repository.iris; +import java.time.ZonedDateTime; import java.util.List; import javax.validation.constraints.NotNull; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import de.tum.in.www1.artemis.domain.iris.IrisMessage; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; @@ -26,6 +28,25 @@ public interface IrisMessageRepository extends JpaRepository """) List findAllExceptSystemMessagesWithContentBySessionId(Long sessionId); + /** + * Counts the number of LLM responses the user got within the given timeframe. + * + * @param userId the id of the user + * @param start the start of the timeframe + * @param end the end of the timeframe + * @return the number of chat messages sent by the user within the given timeframe + */ + @Query(""" + SELECT COUNT(DISTINCT m) + FROM IrisMessage m + LEFT JOIN m.session as s + WHERE type(s) = de.tum.in.www1.artemis.domain.iris.session.IrisChatSession + AND s.user.id = :userId + AND m.sender = 'LLM' + AND m.sentAt BETWEEN :start AND :end + """) + int countLlmResponsesOfUserWithinTimeframe(@Param("userId") Long userId, @Param("start") ZonedDateTime start, @Param("end") ZonedDateTime end); + @NotNull default IrisMessage findByIdElseThrow(long messageId) throws EntityNotFoundException { return findById(messageId).orElseThrow(() -> new EntityNotFoundException("Iris Message", messageId)); diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/IrisRateLimitService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/IrisRateLimitService.java new file mode 100644 index 000000000000..b43979dd6168 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/IrisRateLimitService.java @@ -0,0 +1,80 @@ +package de.tum.in.www1.artemis.service.iris; + +import java.time.ZonedDateTime; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.repository.iris.IrisMessageRepository; +import de.tum.in.www1.artemis.service.iris.exception.IrisRateLimitExceededException; + +/** + * Service for the rate limit of the iris chatbot. + */ +@Service +@Profile("iris") +public class IrisRateLimitService { + + private final IrisMessageRepository irisMessageRepository; + + @Value("${artemis.iris.rate-limit:5}") + private int rateLimit; + + @Value("${artemis.iris.rate-limit-timeframe-hours:24}") + private int rateLimitTimeframeHours; + + public IrisRateLimitService(IrisMessageRepository irisMessageRepository) { + this.irisMessageRepository = irisMessageRepository; + } + + /** + * Get the rate limit information for the given user. + * See {@link IrisRateLimitInformation} and {@link IrisRateLimitInformation#isRateLimitExceeded()} for more information. + * + * @param user the user + * @return the rate limit information + */ + public IrisRateLimitInformation getRateLimitInformation(User user) { + var start = ZonedDateTime.now().minusHours(rateLimitTimeframeHours); + var end = ZonedDateTime.now(); + var currentMessageCount = irisMessageRepository.countLlmResponsesOfUserWithinTimeframe(user.getId(), start, end); + + return new IrisRateLimitInformation(currentMessageCount, rateLimit); + } + + /** + * Checks if the rate limit of the given user is exceeded. + * If it is exceeded, an {@link IrisRateLimitExceededException} is thrown. + * See {@link #getRateLimitInformation(User)} and {@link IrisRateLimitInformation#isRateLimitExceeded()} for more information. + * + * @param user the user + * @throws IrisRateLimitExceededException if the rate limit is exceeded + */ + public void checkRateLimitElseThrow(User user) { + var rateLimitInfo = getRateLimitInformation(user); + if (rateLimitInfo.isRateLimitExceeded()) { + throw new IrisRateLimitExceededException(rateLimitInfo); + } + } + + /** + * Contains information about the rate limit of a user. + * + * @param currentMessageCount the current rate limit + * @param rateLimit the max rate limit + */ + public record IrisRateLimitInformation(int currentMessageCount, int rateLimit) { + + /** + * Checks if the rate limit is exceeded. + * It is exceeded if the rateLimit is set and the currentMessageCount is greater or equal to the rateLimit. + * + * @return true if the rate limit is exceeded, false otherwise + */ + public boolean isRateLimitExceeded() { + return rateLimit != -1 && currentMessageCount >= rateLimit; + } + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/IrisWebsocketService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/IrisWebsocketService.java index 13ded18a2503..dbb6acfd6280 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/IrisWebsocketService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/IrisWebsocketService.java @@ -4,6 +4,7 @@ import java.util.Map; import java.util.Objects; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import com.fasterxml.jackson.annotation.JsonInclude; @@ -18,14 +19,18 @@ * A service to send a message over the websocket to a specific user */ @Service +@Profile("iris") public class IrisWebsocketService { private static final String IRIS_WEBSOCKET_TOPIC_PREFIX = "/topic/iris"; private final WebsocketMessagingService websocketMessagingService; - public IrisWebsocketService(WebsocketMessagingService websocketMessagingService) { + private final IrisRateLimitService rateLimitService; + + public IrisWebsocketService(WebsocketMessagingService websocketMessagingService, IrisRateLimitService rateLimitService) { this.websocketMessagingService = websocketMessagingService; + this.rateLimitService = rateLimitService; } /** @@ -38,9 +43,11 @@ public void sendMessage(IrisMessage irisMessage) { throw new UnsupportedOperationException("Only IrisChatSession is supported"); } Long irisSessionId = irisMessage.getSession().getId(); - String userLogin = ((IrisChatSession) irisMessage.getSession()).getUser().getLogin(); + var user = ((IrisChatSession) irisMessage.getSession()).getUser(); + String userLogin = user.getLogin(); String irisWebsocketTopic = String.format("%s/sessions/%d", IRIS_WEBSOCKET_TOPIC_PREFIX, irisSessionId); - websocketMessagingService.sendMessageToUser(userLogin, irisWebsocketTopic, new IrisWebsocketDTO(irisMessage)); + var rateLimitInfo = rateLimitService.getRateLimitInformation(user); + websocketMessagingService.sendMessageToUser(userLogin, irisWebsocketTopic, new IrisWebsocketDTO(irisMessage, rateLimitInfo)); } /** @@ -54,9 +61,11 @@ public void sendException(IrisSession irisSession, Throwable throwable) { throw new UnsupportedOperationException("Only IrisChatSession is supported"); } Long irisSessionId = irisSession.getId(); - String userLogin = ((IrisChatSession) irisSession).getUser().getLogin(); + var user = ((IrisChatSession) irisSession).getUser(); + String userLogin = user.getLogin(); String irisWebsocketTopic = String.format("%s/sessions/%d", IRIS_WEBSOCKET_TOPIC_PREFIX, irisSessionId); - websocketMessagingService.sendMessageToUser(userLogin, irisWebsocketTopic, new IrisWebsocketDTO(throwable)); + var rateLimitInfo = rateLimitService.getRateLimitInformation(user); + websocketMessagingService.sendMessageToUser(userLogin, irisWebsocketTopic, new IrisWebsocketDTO(throwable, rateLimitInfo)); } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -72,7 +81,10 @@ public static class IrisWebsocketDTO { private final Map translationParams; - public IrisWebsocketDTO(IrisMessage message) { + private final IrisRateLimitService.IrisRateLimitInformation rateLimitInfo; + + public IrisWebsocketDTO(IrisMessage message, IrisRateLimitService.IrisRateLimitInformation rateLimitInfo) { + this.rateLimitInfo = rateLimitInfo; this.type = IrisWebsocketMessageType.MESSAGE; this.message = message; this.errorMessage = null; @@ -80,7 +92,8 @@ public IrisWebsocketDTO(IrisMessage message) { this.translationParams = null; } - public IrisWebsocketDTO(Throwable throwable) { + public IrisWebsocketDTO(Throwable throwable, IrisRateLimitService.IrisRateLimitInformation rateLimitInfo) { + this.rateLimitInfo = rateLimitInfo; this.type = IrisWebsocketMessageType.ERROR; this.message = null; this.errorMessage = throwable.getMessage(); @@ -108,6 +121,10 @@ public Map getTranslationParams() { return translationParams != null ? Collections.unmodifiableMap(translationParams) : null; } + public IrisRateLimitService.IrisRateLimitInformation getRateLimitInfo() { + return rateLimitInfo; + } + public enum IrisWebsocketMessageType { MESSAGE, ERROR } diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisException.java b/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisException.java index c1e5ded8ee66..4aca58b81234 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisException.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisException.java @@ -21,6 +21,12 @@ public IrisException(String translationKey, Map translationParam this.translationParams = translationParams; } + public IrisException(String defaultMessage, Status status, String entityName, String translationKey, Map translationParams) { + super(ErrorConstants.DEFAULT_TYPE, defaultMessage, status, entityName, translationKey, getAlertParameters(translationKey, translationParams)); + this.translationKey = translationKey; + this.translationParams = translationParams; + } + public String getTranslationKey() { return translationKey; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisRateLimitExceededException.java b/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisRateLimitExceededException.java new file mode 100644 index 000000000000..68978d1e0812 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisRateLimitExceededException.java @@ -0,0 +1,24 @@ +package de.tum.in.www1.artemis.service.iris.exception; + +import java.util.Map; + +import org.zalando.problem.Status; + +import de.tum.in.www1.artemis.service.iris.IrisRateLimitService; + +/** + * Exception that is thrown when the rate limit of Iris is exceeded. + * See {@link IrisRateLimitService} for more information. + * It is mapped to the "429 Too Many Requests" HTTP status code. + */ +public class IrisRateLimitExceededException extends IrisException { + + public IrisRateLimitExceededException(int currentMessageCount, int rateLimit) { + super("You have exceeded the rate limit of Iris", Status.TOO_MANY_REQUESTS, "Iris", "artemisApp.exerciseChatbot.errors.rateLimitExceeded", + Map.of("currentMessageCount", currentMessageCount, "rateLimit", rateLimit)); + } + + public IrisRateLimitExceededException(IrisRateLimitService.IrisRateLimitInformation rateLimit) { + this(rateLimit.currentMessageCount(), rateLimit.rateLimit()); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisMessageResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisMessageResource.java index 2025a24d9de7..30eb0aab418e 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisMessageResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisMessageResource.java @@ -14,12 +14,11 @@ import de.tum.in.www1.artemis.domain.iris.IrisMessage; import de.tum.in.www1.artemis.domain.iris.IrisMessageSender; import de.tum.in.www1.artemis.domain.iris.session.IrisSession; +import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.repository.iris.IrisMessageRepository; import de.tum.in.www1.artemis.repository.iris.IrisSessionRepository; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; -import de.tum.in.www1.artemis.service.iris.IrisMessageService; -import de.tum.in.www1.artemis.service.iris.IrisSessionService; -import de.tum.in.www1.artemis.service.iris.IrisWebsocketService; +import de.tum.in.www1.artemis.service.iris.*; import de.tum.in.www1.artemis.web.rest.errors.ConflictException; /** @@ -40,13 +39,19 @@ public class IrisMessageResource { private final IrisWebsocketService irisWebsocketService; + private final IrisRateLimitService rateLimitService; + + private final UserRepository userRepository; + public IrisMessageResource(IrisSessionRepository irisSessionRepository, IrisSessionService irisSessionService, IrisMessageService irisMessageService, - IrisMessageRepository irisMessageRepository, IrisWebsocketService irisWebsocketService) { + IrisMessageRepository irisMessageRepository, IrisWebsocketService irisWebsocketService, IrisRateLimitService rateLimitService, UserRepository userRepository) { this.irisSessionRepository = irisSessionRepository; this.irisSessionService = irisSessionService; this.irisMessageService = irisMessageService; this.irisMessageRepository = irisMessageRepository; this.irisWebsocketService = irisWebsocketService; + this.rateLimitService = rateLimitService; + this.userRepository = userRepository; } /** @@ -77,7 +82,10 @@ public ResponseEntity> getMessages(@PathVariable Long sessionI public ResponseEntity createMessage(@PathVariable Long sessionId, @RequestBody IrisMessage message) throws URISyntaxException { var session = irisSessionRepository.findByIdElseThrow(sessionId); irisSessionService.checkIsIrisActivated(session); - irisSessionService.checkHasAccessToIrisSession(session, null); + var user = userRepository.getUser(); + irisSessionService.checkHasAccessToIrisSession(session, user); + rateLimitService.checkRateLimitElseThrow(user); + var savedMessage = irisMessageService.saveMessage(message, session, IrisMessageSender.USER); irisSessionService.requestMessageFromIris(session); savedMessage.setMessageDifferentiator(message.getMessageDifferentiator()); @@ -100,7 +108,10 @@ public ResponseEntity createMessage(@PathVariable Long sessionId, @ public ResponseEntity resendMessage(@PathVariable Long sessionId, @PathVariable Long messageId) { var session = irisSessionRepository.findByIdWithMessagesElseThrow(sessionId); irisSessionService.checkIsIrisActivated(session); - irisSessionService.checkHasAccessToIrisSession(session, null); + var user = userRepository.getUser(); + irisSessionService.checkHasAccessToIrisSession(session, user); + rateLimitService.checkRateLimitElseThrow(user); + var message = irisMessageRepository.findByIdElseThrow(messageId); if (session.getMessages().lastIndexOf(message) != session.getMessages().size() - 1) { throw new BadRequestException("Only the last message can be resent"); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSessionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSessionResource.java index 3d81309bd6da..6d80df7a6874 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSessionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSessionResource.java @@ -19,6 +19,7 @@ import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.connectors.iris.IrisHealthIndicator; import de.tum.in.www1.artemis.service.connectors.iris.dto.IrisStatusDTO; +import de.tum.in.www1.artemis.service.iris.IrisRateLimitService; import de.tum.in.www1.artemis.service.iris.IrisSessionService; import de.tum.in.www1.artemis.service.iris.IrisSettingsService; @@ -44,9 +45,11 @@ public class IrisSessionResource { private final IrisHealthIndicator irisHealthIndicator; + private final IrisRateLimitService irisRateLimitService; + public IrisSessionResource(ProgrammingExerciseRepository programmingExerciseRepository, AuthorizationCheckService authCheckService, IrisChatSessionRepository irisChatSessionRepository, UserRepository userRepository, IrisSessionService irisSessionService, IrisSettingsService irisSettingsService, - IrisHealthIndicator irisHealthIndicator) { + IrisHealthIndicator irisHealthIndicator, IrisRateLimitService irisRateLimitService) { this.programmingExerciseRepository = programmingExerciseRepository; this.authCheckService = authCheckService; this.irisChatSessionRepository = irisChatSessionRepository; @@ -54,6 +57,7 @@ public IrisSessionResource(ProgrammingExerciseRepository programmingExerciseRepo this.irisSessionService = irisSessionService; this.irisSettingsService = irisSettingsService; this.irisHealthIndicator = irisHealthIndicator; + this.irisRateLimitService = irisRateLimitService; } /** @@ -125,7 +129,7 @@ public ResponseEntity createSessionForProgrammingExercise(@PathVari */ @GetMapping("/sessions/{sessionId}/active") @EnforceAtLeastStudent - public ResponseEntity isIrisActive(@PathVariable Long sessionId) { + public ResponseEntity isIrisActive(@PathVariable Long sessionId) { var session = irisChatSessionRepository.findByIdElseThrow(sessionId); var user = userRepository.getUser(); irisSessionService.checkHasAccessToIrisSession(session, user); @@ -138,6 +142,12 @@ public ResponseEntity isIrisActive(@PathVariable Long sessionId) { specificModelStatus = Arrays.stream(modelStatuses).filter(x -> x.model().equals(settings.getIrisChatSettings().getPreferredModel())) .anyMatch(x -> x.status() == IrisStatusDTO.ModelStatus.UP); } - return ResponseEntity.ok(specificModelStatus); + + var rateLimitInfo = irisRateLimitService.getRateLimitInformation(user); + + return ResponseEntity.ok(new IrisHealthDTO(specificModelStatus, rateLimitInfo.currentMessageCount(), rateLimitInfo.rateLimit())); + } + + public record IrisHealthDTO(boolean active, int currentMessageCount, int rateLimit) { } } diff --git a/src/main/webapp/app/entities/iris/iris-errors.model.ts b/src/main/webapp/app/entities/iris/iris-errors.model.ts index a115874a1732..fc15951e105d 100644 --- a/src/main/webapp/app/entities/iris/iris-errors.model.ts +++ b/src/main/webapp/app/entities/iris/iris-errors.model.ts @@ -16,6 +16,7 @@ export enum IrisErrorMessageKey { PARSE_RESPONSE = 'artemisApp.exerciseChatbot.errors.parseResponse', TECHNICAL_ERROR_RESPONSE = 'artemisApp.exerciseChatbot.errors.technicalError', IRIS_NOT_AVAILABLE = 'artemisApp.exerciseChatbot.errors.irisNotAvailable', + RATE_LIMIT_EXCEEDED = 'artemisApp.exerciseChatbot.errors.rateLimitExceeded', } export interface IrisErrorType { @@ -42,6 +43,7 @@ const IrisErrors: IrisErrorType[] = [ { key: IrisErrorMessageKey.FORBIDDEN, fatal: true }, { key: IrisErrorMessageKey.TECHNICAL_ERROR_RESPONSE, fatal: true }, { key: IrisErrorMessageKey.IRIS_NOT_AVAILABLE, fatal: true }, + { key: IrisErrorMessageKey.RATE_LIMIT_EXCEEDED, fatal: true }, ]; export const errorMessages: Readonly<{ [key in IrisErrorMessageKey]: IrisErrorType }> = Object.freeze( diff --git a/src/main/webapp/app/iris/exercise-chatbot/exercise-chatwidget/exercise-chat-widget.component.html b/src/main/webapp/app/iris/exercise-chatbot/exercise-chatwidget/exercise-chat-widget.component.html index 0ad22902b511..e6b5e8c55eab 100644 --- a/src/main/webapp/app/iris/exercise-chatbot/exercise-chatwidget/exercise-chat-widget.component.html +++ b/src/main/webapp/app/iris/exercise-chatbot/exercise-chatwidget/exercise-chat-widget.component.html @@ -11,21 +11,26 @@

-
- - - - + - - + + + + +
@@ -59,7 +64,7 @@
Preferred Model: -
+
@@ -16,6 +16,7 @@
+
diff --git a/src/main/webapp/app/iris/state-store.model.ts b/src/main/webapp/app/iris/state-store.model.ts index b62d2d406a71..0137ae10141c 100644 --- a/src/main/webapp/app/iris/state-store.model.ts +++ b/src/main/webapp/app/iris/state-store.model.ts @@ -9,6 +9,7 @@ export enum ActionType { STUDENT_MESSAGE_SENT = 'student-message-sent', SESSION_CHANGED = 'session-changed', RATE_MESSAGE_SUCCESS = 'rate-message-success', + RATE_LIMIT_UPDATED = 'rate-limit-updated', } export interface MessageStoreAction { @@ -89,6 +90,17 @@ export class RateMessageSuccessAction implements MessageStoreAction { } } +export class RateLimitUpdatedAction implements MessageStoreAction { + readonly type: ActionType; + + public constructor( + public readonly currentMessageCount: number, + public readonly rateLimit: number, + ) { + this.type = ActionType.RATE_LIMIT_UPDATED; + } +} + export function isNumNewMessagesResetAction(action: MessageStoreAction): action is NumNewMessagesResetAction { return action.type === ActionType.NUM_NEW_MESSAGES_RESET; } @@ -117,6 +129,10 @@ export function isRateMessageSuccessAction(action: MessageStoreAction): action i return action.type === ActionType.RATE_MESSAGE_SUCCESS; } +export function isRateLimitUpdatedAction(action: MessageStoreAction): action is RateLimitUpdatedAction { + return action.type === ActionType.RATE_LIMIT_UPDATED; +} + export class MessageStoreState { public constructor( public messages: ReadonlyArray, @@ -125,5 +141,7 @@ export class MessageStoreState { public numNewMessages: number, public error: IrisErrorType | null, public serverResponseTimeout: ReturnType | null, + public currentMessageCount: number, + public rateLimit: number, ) {} } diff --git a/src/main/webapp/app/iris/state-store.service.ts b/src/main/webapp/app/iris/state-store.service.ts index f457c7d17002..53b7342a6b51 100644 --- a/src/main/webapp/app/iris/state-store.service.ts +++ b/src/main/webapp/app/iris/state-store.service.ts @@ -7,6 +7,7 @@ import { HistoryMessageLoadedAction, MessageStoreAction, MessageStoreState, + RateLimitUpdatedAction, RateMessageSuccessAction, SessionReceivedAction, StudentMessageSentAction, @@ -14,6 +15,7 @@ import { isConversationErrorOccurredAction, isHistoryMessageLoadedAction, isNumNewMessagesResetAction, + isRateLimitUpdatedAction, isRateMessageSuccessAction, isSessionReceivedAction, isStudentMessageSentAction, @@ -35,6 +37,8 @@ export class IrisStateStore implements OnDestroy { numNewMessages: 0, error: null, serverResponseTimeout: null, + currentMessageCount: -1, + rateLimit: -1, }; private readonly action = new Subject(); @@ -151,6 +155,8 @@ export class IrisStateStore implements OnDestroy { numNewMessages: state.numNewMessages + 1, error: defaultError, serverResponseTimeout: null, + currentMessageCount: state.currentMessageCount, + rateLimit: state.rateLimit, }; } if (isConversationErrorOccurredAction(action)) { @@ -219,6 +225,15 @@ export class IrisStateStore implements OnDestroy { return state; } + if (isRateLimitUpdatedAction(action)) { + const castedAction = action as RateLimitUpdatedAction; + return { + ...state, + error: castedAction.rateLimit >= 0 && castedAction.currentMessageCount >= castedAction.rateLimit ? errorMessages[IrisErrorMessageKey.RATE_LIMIT_EXCEEDED] : null, + currentMessageCount: castedAction.currentMessageCount, + rateLimit: castedAction.rateLimit, + }; + } IrisStateStore.exhaustiveCheck(action); return state; diff --git a/src/main/webapp/app/iris/websocket.service.ts b/src/main/webapp/app/iris/websocket.service.ts index 68660454f5e5..0b0ffd81dcc8 100644 --- a/src/main/webapp/app/iris/websocket.service.ts +++ b/src/main/webapp/app/iris/websocket.service.ts @@ -6,6 +6,7 @@ import { ActiveConversationMessageLoadedAction, ConversationErrorOccurredAction, MessageStoreAction, + RateLimitUpdatedAction, StudentMessageSentAction, isSessionReceivedAction, } from 'app/iris/state-store.model'; @@ -20,6 +21,11 @@ export enum IrisWebsocketMessageType { ERROR = 'ERROR', } +class IrisRateLimitInformation { + currentMessageCount: number; + rateLimit: number; +} + /** * The IrisWebsocketDTO is the data transfer object for messages sent over the websocket. * It either contains an IrisMessage or an error message. @@ -29,6 +35,7 @@ export class IrisWebsocketDTO { message?: IrisMessage; errorTranslationKey?: IrisErrorMessageKey; translationParams?: Map; + rateLimitInfo?: IrisRateLimitInformation; } /** @@ -87,6 +94,10 @@ export class IrisWebsocketService implements OnDestroy { this.subscriptionChannel = channel; this.jhiWebsocketService.subscribe(this.subscriptionChannel); this.jhiWebsocketService.receive(this.subscriptionChannel).subscribe((websocketResponse: IrisWebsocketDTO) => { + if (websocketResponse.rateLimitInfo) { + this.stateStore.dispatch(new RateLimitUpdatedAction(websocketResponse.rateLimitInfo.currentMessageCount, websocketResponse.rateLimitInfo.rateLimit)); + } + if (websocketResponse.type === IrisWebsocketMessageType.ERROR) { if (!websocketResponse.errorTranslationKey) { this.stateStore.dispatch(new ConversationErrorOccurredAction(IrisErrorMessageKey.TECHNICAL_ERROR_RESPONSE)); diff --git a/src/main/webapp/i18n/de/exerciseChatbot.json b/src/main/webapp/i18n/de/exerciseChatbot.json index 787c656943bf..a85f1f53a8ed 100644 --- a/src/main/webapp/i18n/de/exerciseChatbot.json +++ b/src/main/webapp/i18n/de/exerciseChatbot.json @@ -51,9 +51,11 @@ "noResponse": "Es wurde keine Antwort von Iris empfangen. Bitte kontaktiere einen Administrator, wenn das Problem weiterhin besteht.", "parseResponse": "Ein Fehler ist beim Parsen der Antwort von Iris aufgetreten. Ursache: {{ cause }}", "technicalError": "Es ist ein technischer Fehler aufgetreten. Bitte wende dich an Artemis Administratoren sollte der Fehler weiterhin zu sehen sein.", - "irisNotAvailable": "Iris antwortet nicht. Bitte versuche es später!" + "irisNotAvailable": "Iris antwortet nicht. Bitte versuche es später!", + "rateLimitExceeded": "Du hast die maximale Anzahl von Nachrichten, die du in einem 24-Stunden-Zeitfenster an Iris senden kannst, erreicht. Bitte versuche es später erneut!" }, - "firstMessage": "Hallo, ich bin Iris! Ich kann dir bei deiner Programmieraufgabe helfen. Du kannst hier mehr über mich erfahren." + "firstMessage": "Hallo, ich bin Iris! Ich kann dir bei deiner Programmieraufgabe helfen. Du kannst hier mehr über mich erfahren.", + "rateLimitTooltip": "Dies ist die maximale Anzahl von Nachrichten, die du in einem 24-Stunden-Zeitfenster an Iris senden kannst." } } } diff --git a/src/main/webapp/i18n/de/iris.json b/src/main/webapp/i18n/de/iris.json index 95e45c8259e6..a43220623fb0 100644 --- a/src/main/webapp/i18n/de/iris.json +++ b/src/main/webapp/i18n/de/iris.json @@ -27,6 +27,8 @@ "enabled-disabled": "Aktiviert/Deaktiviert", "preferredModel": "Präferiertes Modell", "inheritModel": "Vererbe Modell", + "rateLimit": "Rate Limit", + "rateLimitTooltip": "Die maximale Anzahl an Antworten, die ein Benutzer vom LLM in einem bestimmten Zeitraum erhalten kann. Der Zeitraum beträgt derzeit 3h.", "template": { "title": "Template", "inherit": "Inherit Template" diff --git a/src/main/webapp/i18n/en/exerciseChatbot.json b/src/main/webapp/i18n/en/exerciseChatbot.json index d1f56b6825bf..d433f97403f2 100644 --- a/src/main/webapp/i18n/en/exerciseChatbot.json +++ b/src/main/webapp/i18n/en/exerciseChatbot.json @@ -51,9 +51,11 @@ "noResponse": "No response from Iris was received.", "parseResponse": "An error occurred while parsing the response from Iris. Cause: {{ cause }}", "technicalError": "There has been a technical error. Please contact Artemis Administrators if this error message persists.", - "irisNotAvailable": "Iris is not available. Please try again later!" + "irisNotAvailable": "Iris is not available. Please try again later!", + "rateLimitExceeded": "You have reached the maximum number of messages you can send to Iris in a 24 hour window. Please try again later!" }, - "firstMessage": "Hi, I'm Iris! I can help you with your programming exercise. You can learn more about me here." + "firstMessage": "Hi, I'm Iris! I can help you with your programming exercise. You can learn more about me here.", + "rateLimitTooltip": "This is the maximum number of messages you can send to Iris in a 24 hour window." } } } diff --git a/src/main/webapp/i18n/en/iris.json b/src/main/webapp/i18n/en/iris.json index 64480dde3d5a..fb5cb28308ff 100644 --- a/src/main/webapp/i18n/en/iris.json +++ b/src/main/webapp/i18n/en/iris.json @@ -26,7 +26,9 @@ "inheritHestiaSettings": "Inherit Hestia Settings", "enabled-disabled": "Enabled/Disabled", "preferredModel": "Preferred Model", - "inheritModel": "Inherit Modell", + "inheritModel": "Inherit Model", + "rateLimit": "Rate Limit", + "rateLimitTooltip": "The maximum number of answers a user can receive from the LLM in a given time period. The time period is currently 3h.", "template": { "title": "Template", "inherit": "Inherit Template" diff --git a/src/test/java/de/tum/in/www1/artemis/iris/IrisMessageIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/IrisMessageIntegrationTest.java index 6ce1fea32261..f8a538730d9e 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/IrisMessageIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/IrisMessageIntegrationTest.java @@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.util.ReflectionTestUtils; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; @@ -23,6 +24,7 @@ import de.tum.in.www1.artemis.repository.iris.IrisMessageRepository; import de.tum.in.www1.artemis.repository.iris.IrisSessionRepository; import de.tum.in.www1.artemis.service.iris.IrisMessageService; +import de.tum.in.www1.artemis.service.iris.IrisRateLimitService; import de.tum.in.www1.artemis.service.iris.IrisSessionService; import de.tum.in.www1.artemis.util.IrisUtilTestService; import de.tum.in.www1.artemis.util.LocalRepository; @@ -49,6 +51,9 @@ class IrisMessageIntegrationTest extends AbstractIrisIntegrationTest { @Autowired private ParticipationUtilService participationUtilService; + @Autowired + private IrisRateLimitService irisRateLimitService; + private ProgrammingExercise exercise; private LocalRepository repository; @@ -72,10 +77,7 @@ void sendOneMessage() throws Exception { messageToSend.setMessageDifferentiator(1453); irisRequestMockProvider.mockMessageResponse("Hello World"); - var savedExercise = irisUtilTestService.setupTemplate(exercise, repository); - var exerciseParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(savedExercise, TEST_PREFIX + "student1"); - irisUtilTestService.setupStudentParticipation(exerciseParticipation, repository); - activateIrisFor(savedExercise); + setupExercise(); var irisMessage = request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, IrisMessage.class, HttpStatus.CREATED); assertThat(irisMessage.getSender()).isEqualTo(IrisMessageSender.USER); @@ -116,10 +118,7 @@ void sendTwoMessages() throws Exception { var irisSession = irisSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); IrisMessage messageToSend1 = createDefaultMockMessage(irisSession); - var savedExercise = irisUtilTestService.setupTemplate(exercise, repository); - var exerciseParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(savedExercise, TEST_PREFIX + "student1"); - irisUtilTestService.setupStudentParticipation(exerciseParticipation, repository); - activateIrisFor(savedExercise); + setupExercise(); var irisMessage1 = request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend1, IrisMessage.class, HttpStatus.CREATED); assertThat(irisMessage1.getSender()).isEqualTo(IrisMessageSender.USER); @@ -240,10 +239,7 @@ void sendOneMessageBadRequest() throws Exception { IrisMessage messageToSend = createDefaultMockMessage(irisSession); irisRequestMockProvider.mockMessageError(); - var savedExercise = irisUtilTestService.setupTemplate(exercise, repository); - var exerciseParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(savedExercise, TEST_PREFIX + "student1"); - irisUtilTestService.setupStudentParticipation(exerciseParticipation, repository); - activateIrisFor(savedExercise); + setupExercise(); request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, IrisMessage.class, HttpStatus.CREATED); @@ -259,10 +255,7 @@ void sendOneMessageEmptyBody() throws Exception { IrisMessage messageToSend = createDefaultMockMessage(irisSession); irisRequestMockProvider.mockMessageResponse(null); - var savedExercise = irisUtilTestService.setupTemplate(exercise, repository); - var exerciseParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(savedExercise, TEST_PREFIX + "student1"); - irisUtilTestService.setupStudentParticipation(exerciseParticipation, repository); - activateIrisFor(savedExercise); + setupExercise(); request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, IrisMessage.class, HttpStatus.CREATED); @@ -271,6 +264,58 @@ void sendOneMessageEmptyBody() throws Exception { verifyNothingElseWasSentOverWebsocket(TEST_PREFIX + "student1", irisSession.getId()); } + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void resendMessage() throws Exception { + var irisSession = irisSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + var messageToSend = createDefaultMockMessage(irisSession); + + irisRequestMockProvider.mockMessageResponse("Hello World"); + setupExercise(); + + var irisMessage = irisMessageService.saveMessage(messageToSend, irisSession, IrisMessageSender.USER); + request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages/" + irisMessage.getId() + "/resend", null, IrisMessage.class, HttpStatus.OK); + await().until(() -> irisSessionRepository.findByIdWithMessagesElseThrow(irisSession.getId()).getMessages().size() == 2); + verifyMessageWasSentOverWebsocket(TEST_PREFIX + "student1", irisSession.getId(), "Hello World"); + verifyNothingElseWasSentOverWebsocket(TEST_PREFIX + "student1", irisSession.getId()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student2", roles = "USER") + void sendMessageRateLimitReached() throws Exception { + var irisSession = irisSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student2")); + var messageToSend1 = createDefaultMockMessage(irisSession); + var messageToSend2 = createDefaultMockMessage(irisSession); + + irisRequestMockProvider.mockMessageResponse("Hello World"); + setupExercise(); + + var previousRateLimit = ReflectionTestUtils.getField(irisRateLimitService, "rateLimit"); + ReflectionTestUtils.setField(irisRateLimitService, "rateLimit", 1); + + try { + request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend1, IrisMessage.class, HttpStatus.CREATED); + await().until(() -> irisSessionRepository.findByIdWithMessagesElseThrow(irisSession.getId()).getMessages().size() == 2); + request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend2, IrisMessage.class, HttpStatus.TOO_MANY_REQUESTS); + var irisMessage = irisMessageService.saveMessage(messageToSend2, irisSession, IrisMessageSender.USER); + request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages/" + irisMessage.getId() + "/resend", null, IrisMessage.class, + HttpStatus.TOO_MANY_REQUESTS); + verifyMessageWasSentOverWebsocket(TEST_PREFIX + "student2", irisSession.getId(), messageToSend1); + verifyMessageWasSentOverWebsocket(TEST_PREFIX + "student2", irisSession.getId(), "Hello World"); + verifyNothingElseWasSentOverWebsocket(TEST_PREFIX + "student2", irisSession.getId()); + } + finally { + ReflectionTestUtils.setField(irisRateLimitService, "rateLimit", previousRateLimit); + } + } + + private void setupExercise() throws Exception { + var savedExercise = irisUtilTestService.setupTemplate(exercise, repository); + var exerciseParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(savedExercise, TEST_PREFIX + "student1"); + irisUtilTestService.setupStudentParticipation(exerciseParticipation, repository); + activateIrisFor(savedExercise); + } + private IrisMessage createDefaultMockMessage(IrisSession irisSession) { var messageToSend = new IrisMessage(); messageToSend.setSession(irisSession); diff --git a/src/test/java/de/tum/in/www1/artemis/iris/IrisSessionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/IrisSessionIntegrationTest.java index 263a2a048c9e..4e5a824f4023 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/IrisSessionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/IrisSessionIntegrationTest.java @@ -13,6 +13,7 @@ import de.tum.in.www1.artemis.domain.iris.session.IrisChatSession; import de.tum.in.www1.artemis.domain.iris.session.IrisSession; import de.tum.in.www1.artemis.repository.iris.IrisChatSessionRepository; +import de.tum.in.www1.artemis.web.rest.iris.IrisSessionResource.IrisHealthDTO; class IrisSessionIntegrationTest extends AbstractIrisIntegrationTest { @@ -70,17 +71,18 @@ void getCurrentSession_notFound() throws Exception { void isActive() throws Exception { var irisSession = request.postWithResponseBody("/api/iris/programming-exercises/" + exercise.getId() + "/sessions", null, IrisSession.class, HttpStatus.CREATED); var settings = irisSettingsService.getGlobalSettings(); - settings.getIrisChatSettings().setPreferredModel("TEST_MODEL_UP"); - irisSettingsService.saveGlobalIrisSettings(settings); irisRequestMockProvider.mockStatusResponse(); irisRequestMockProvider.mockStatusResponse(); irisRequestMockProvider.mockStatusResponse(); - assertThat(request.get("/api/iris/sessions/" + irisSession.getId() + "/active", HttpStatus.OK, Boolean.class)).isTrue(); + + settings.getIrisChatSettings().setPreferredModel("TEST_MODEL_UP"); + irisSettingsService.saveGlobalIrisSettings(settings); + assertThat(request.get("/api/iris/sessions/" + irisSession.getId() + "/active", HttpStatus.OK, IrisHealthDTO.class).active()).isTrue(); settings.getIrisChatSettings().setPreferredModel("TEST_MODEL_DOWN"); irisSettingsService.saveGlobalIrisSettings(settings); - assertThat(request.get("/api/iris/sessions/" + irisSession.getId() + "/active", HttpStatus.OK, Boolean.class)).isFalse(); + assertThat(request.get("/api/iris/sessions/" + irisSession.getId() + "/active", HttpStatus.OK, IrisHealthDTO.class).active()).isFalse(); settings.getIrisChatSettings().setPreferredModel("TEST_MODEL_NA"); irisSettingsService.saveGlobalIrisSettings(settings); - assertThat(request.get("/api/iris/sessions/" + irisSession.getId() + "/active", HttpStatus.OK, Boolean.class)).isFalse(); + assertThat(request.get("/api/iris/sessions/" + irisSession.getId() + "/active", HttpStatus.OK, IrisHealthDTO.class).active()).isFalse(); } } diff --git a/src/test/java/de/tum/in/www1/artemis/iris/IrisWebsocketTest.java b/src/test/java/de/tum/in/www1/artemis/iris/IrisWebsocketTest.java index 2bb97ffeba0d..dbfa2501ceb7 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/IrisWebsocketTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/IrisWebsocketTest.java @@ -57,7 +57,7 @@ void sendMessage() { message.setMessageDifferentiator(101010); irisWebsocketService.sendMessage(message); verify(websocketMessagingService, times(1)).sendMessageToUser(eq(TEST_PREFIX + "student1"), eq("/topic/iris/sessions/" + irisSession.getId()), - eq(new IrisWebsocketService.IrisWebsocketDTO(message))); + eq(new IrisWebsocketService.IrisWebsocketDTO(message, null))); } private IrisMessageContent createMockContent(IrisMessage message) { diff --git a/src/test/javascript/spec/component/iris/state-store.service.spec.ts b/src/test/javascript/spec/component/iris/state-store.service.spec.ts index 587b734fbbf0..ad5cdba0554e 100644 --- a/src/test/javascript/spec/component/iris/state-store.service.spec.ts +++ b/src/test/javascript/spec/component/iris/state-store.service.spec.ts @@ -6,6 +6,7 @@ import { HistoryMessageLoadedAction, MessageStoreState, NumNewMessagesResetAction, + RateLimitUpdatedAction, RateMessageSuccessAction, SessionReceivedAction, StudentMessageSentAction, @@ -364,6 +365,40 @@ describe('IrisStateStore', () => { expect(state2.messages).toHaveLength(2); }); + + it('should update below rate limit state', async () => { + const obs = stateStore.getState(); + + const promise = obs.pipe(skip(1), take(1)).toPromise(); + + stateStore.dispatch(new RateLimitUpdatedAction(1, 2)); + + const state = (await promise) as MessageStoreState; + + expect(state).toStrictEqual({ + ...mockState, + error: null, + currentMessageCount: 1, + rateLimit: 2, + }); + }); + + it('should update above rate limit state', async () => { + const obs = stateStore.getState(); + + const promise = obs.pipe(skip(1), take(1)).toPromise(); + + stateStore.dispatch(new RateLimitUpdatedAction(2, 2)); + + const state = (await promise) as MessageStoreState; + + expect(state).toStrictEqual({ + ...mockState, + error: errorMessages[IrisErrorMessageKey.RATE_LIMIT_EXCEEDED], + currentMessageCount: 2, + rateLimit: 2, + }); + }); }); describe('IrisStateStore with an empty session state', () => { diff --git a/src/test/javascript/spec/helpers/sample/iris-sample-data.ts b/src/test/javascript/spec/helpers/sample/iris-sample-data.ts index c4bf288bfdb2..b3e9809c9296 100644 --- a/src/test/javascript/spec/helpers/sample/iris-sample-data.ts +++ b/src/test/javascript/spec/helpers/sample/iris-sample-data.ts @@ -74,4 +74,6 @@ export const mockState = { numNewMessages: 0, sessionId: 0, serverResponseTimeout: null, + currentMessageCount: -1, + rateLimit: -1, }; diff --git a/src/test/resources/config/application-iris.yml b/src/test/resources/config/application-iris.yml index 074ded544b64..dced5f46f5a8 100644 --- a/src/test/resources/config/application-iris.yml +++ b/src/test/resources/config/application-iris.yml @@ -2,3 +2,5 @@ artemis: iris: url: "http://iris.fake" secret-token: "secret-token" + rate-limit: 100000 + rate-limit-timeframe-hours: 1 From c66cfe0c8d2426377c3cb1178f78ef5854b7d2f5 Mon Sep 17 00:00:00 2001 From: Lucas Welscher Date: Tue, 3 Oct 2023 09:32:44 +0200 Subject: [PATCH 13/19] Exam mode: Fix an issue when canceling assessments (#7285) --- .../shared/exercise-scores/exercise-scores.component.html | 2 +- .../shared/exercise-scores/exercise-scores.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.html b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.html index d15b1a5a3960..04db72ff7377 100644 --- a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.html @@ -348,7 +348,7 @@
!value.results[correctionRound].completionDate && value.results[correctionRound].assessmentType !== AssessmentType.AUTOMATIC " - (click)="cancelAssessment(value.results[correctionRound])" + (click)="cancelAssessment(value.results[correctionRound], value)" [disabled]="isLoading" class="btn btn-danger btn-sm mb-1" > diff --git a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts index ecfa39723900..9d42ed13d6ba 100644 --- a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts @@ -396,7 +396,7 @@ export class ExerciseScoresComponent implements OnInit, OnDestroy { /** * Cancel the current assessment and reload the submissions to reflect the change. */ - cancelAssessment(result: Result) { + cancelAssessment(result: Result, participation: Participation) { const confirmCancel = window.confirm(this.cancelConfirmationText); if (confirmCancel && result.submission?.id) { @@ -409,7 +409,7 @@ export class ExerciseScoresComponent implements OnInit, OnDestroy { cancelSubscription = this.modelingAssessmentService.cancelAssessment(result.submission.id); break; case ExerciseType.TEXT: - cancelSubscription = this.textAssessmentService.cancelAssessment(result.participation!.id!, result.submission.id); + cancelSubscription = this.textAssessmentService.cancelAssessment(participation.id!, result.submission.id); break; case ExerciseType.FILE_UPLOAD: cancelSubscription = this.fileUploadAssessmentService.cancelAssessment(result.submission.id); From 79521bd50d6882a82b38b26082537cafe8d19fba Mon Sep 17 00:00:00 2001 From: Lara Dvorsek <73339358+laadvo@users.noreply.github.com> Date: Tue, 3 Oct 2023 09:33:51 +0200 Subject: [PATCH 14/19] Development: Reduce server test code duplication (#7180) --- .../assessment/ComplaintUtilService.java | 20 +++ .../artemis/bonus/BonusIntegrationTest.java | 87 +++--------- .../FileUploadAssessmentIntegrationTest.java | 125 ++++-------------- .../ModelingAssessmentIntegrationTest.java | 115 ++++------------ .../ProgrammingAssessmentIntegrationTest.java | 25 ++-- .../textexercise/TextExerciseUtilService.java | 15 +++ .../text/TextAssessmentIntegrationTest.java | 103 ++++++--------- 7 files changed, 157 insertions(+), 333 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ComplaintUtilService.java b/src/test/java/de/tum/in/www1/artemis/assessment/ComplaintUtilService.java index eab4a7151d5a..e54910e5c7f3 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ComplaintUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ComplaintUtilService.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.assessment; +import java.util.ArrayList; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -84,4 +86,22 @@ public void addTeamComplaints(Team team, Participation participation, int number complaintRepo.save(complaint); } } + + /** + * Creates a complaint and a response for the passed text submission. The response is returned in a form of an assessment update. + * + * @param textResult result of the complaint. + * @param tutorLogin login of the tutor responding to the complaint. + * @return an assessment update with the complaint response. + */ + public AssessmentUpdate createComplaintAndResponse(Result textResult, String tutorLogin) { + Complaint complaint = new Complaint().result(textResult).complaintText("This is not fair"); + complaintRepo.save(complaint); + complaint.getResult().setParticipation(null); // Break infinite reference chain + + ComplaintResponse complaintResponse = createInitialEmptyResponse(tutorLogin, complaint); + complaintResponse.getComplaint().setAccepted(false); + complaintResponse.setResponseText("rejected"); + return new AssessmentUpdate().feedbacks(new ArrayList<>()).complaintResponse(complaintResponse); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/bonus/BonusIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/bonus/BonusIntegrationTest.java index f80bc1ea4342..cb225ebf105f 100644 --- a/src/test/java/de/tum/in/www1/artemis/bonus/BonusIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/bonus/BonusIntegrationTest.java @@ -10,6 +10,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -279,13 +281,14 @@ private GradingScale createSourceGradingScaleWithGradeStepsForGradesBonusStrateg return sourceGradingScale; } - @Test + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @WithMockUser(username = "admin", roles = "ADMIN") - void testCalculateRawBonusWithGradesContinuousBonusStrategy() throws Exception { + @EnumSource(value = BonusStrategy.class, names = { "GRADES_DISCRETE" }, mode = EnumSource.Mode.EXCLUDE) + void testCalculateRawBonus(BonusStrategy bonusStrategy) throws Exception { // Calculation results should be consistent with bonus.service.spec.ts - BonusStrategy bonusStrategy = BonusStrategy.GRADES_CONTINUOUS; - double weight = -1; + boolean isContinuous = bonusStrategy == BonusStrategy.GRADES_CONTINUOUS; + double weight = isContinuous ? -1 : 1; Exam bonusToExam = bonusToExamGradingScale.getExam(); Course sourceCourse = courseGradingScale.getCourse(); @@ -295,7 +298,14 @@ void testCalculateRawBonusWithGradesContinuousBonusStrategy() throws Exception { bonusToExamGradingScale = gradingScaleRepository.findWithEagerBonusFromByExamId(bonusToExam.getId()).orElseThrow(); gradingScaleRepository.deleteAll(List.of(bonusToExamGradingScale, courseGradingScale)); - GradingScale sourceGradingScale = createSourceGradingScaleWithGradeStepsForGradesBonusStrategy(sourceCourse); + GradingScale sourceGradingScale; + if (isContinuous) { + sourceGradingScale = createSourceGradingScaleWithGradeStepsForGradesBonusStrategy(sourceCourse); + } + else { + sourceGradingScale = createSourceGradingScaleWithGradeStepsForPointsBonusStrategy(sourceCourse); + } + GradingScale bonusToGradingScale = createBonusToGradingScale(bonusToExam); gradingScaleRepository.saveAll(List.of(bonusToGradingScale, sourceGradingScale)); @@ -313,10 +323,9 @@ void testCalculateRawBonusWithGradesContinuousBonusStrategy() throws Exception { bonusToPoints = 120; sourcePoints = 75; expectedExamGrade = "3.0"; - expectedBonusGrade = 0.1; - expectedFinalPoints = null; - expectedFinalGrade = "2.9"; - expectedExceedsMax = false; + expectedBonusGrade = isContinuous ? 0.1 : 10.0; + expectedFinalPoints = isContinuous ? null : 130.0; + expectedFinalGrade = isContinuous ? "2.9" : "3.0"; calculateFinalGradeAtServer(bonusStrategy, weight, bonusToPoints, sourcePoints, expectedExamGrade, expectedBonusGrade, expectedFinalPoints, expectedFinalGrade, expectedExceedsMax, sourceGradingScale.getId()); @@ -324,14 +333,13 @@ void testCalculateRawBonusWithGradesContinuousBonusStrategy() throws Exception { bonusToPoints = 200; sourcePoints = 200; expectedExamGrade = "1.0"; - expectedBonusGrade = 0.2; - expectedFinalPoints = null; + expectedBonusGrade = isContinuous ? 0.2 : 20.0; + expectedFinalPoints = isContinuous ? null : 200.0; expectedFinalGrade = "1.0"; expectedExceedsMax = true; calculateFinalGradeAtServer(bonusStrategy, weight, bonusToPoints, sourcePoints, expectedExamGrade, expectedBonusGrade, expectedFinalPoints, expectedFinalGrade, expectedExceedsMax, sourceGradingScale.getId()); - } @Test @@ -413,61 +421,6 @@ private GradingScale createSourceGradingScaleWithGradeStepsForPointsBonusStrateg return sourceGradingScale; } - @Test - @WithMockUser(username = "admin", roles = "ADMIN") - void testCalculateRawBonusWithPointsBonusStrategy() throws Exception { - // Calculation results should be consistent with bonus.service.spec.ts - - BonusStrategy bonusStrategy = BonusStrategy.POINTS; - double weight = 1; - - Exam bonusToExam = bonusToExamGradingScale.getExam(); - Course sourceCourse = courseGradingScale.getCourse(); - - bonusRepository.delete(courseBonus); - // Line below is needed to prevent EntityNotFoundException for the Bonus instance deleted above. - bonusToExamGradingScale = gradingScaleRepository.findWithEagerBonusFromByExamId(bonusToExam.getId()).orElseThrow(); - gradingScaleRepository.deleteAll(List.of(bonusToExamGradingScale, courseGradingScale)); - - GradingScale sourceGradingScale = createSourceGradingScaleWithGradeStepsForPointsBonusStrategy(sourceCourse); - GradingScale bonusToGradingScale = createBonusToGradingScale(bonusToExam); - gradingScaleRepository.saveAll(List.of(bonusToGradingScale, sourceGradingScale)); - - double bonusToPoints = 50; - double sourcePoints = 100; - String expectedExamGrade = "5.0"; - double expectedBonusGrade = 0.0; - double expectedFinalPoints = 50.0; - String expectedFinalGrade = "5.0"; - boolean expectedExceedsMax = false; - - calculateFinalGradeAtServer(bonusStrategy, weight, bonusToPoints, sourcePoints, expectedExamGrade, expectedBonusGrade, expectedFinalPoints, expectedFinalGrade, - expectedExceedsMax, sourceGradingScale.getId()); - - bonusToPoints = 120; - sourcePoints = 75; - expectedExamGrade = "3.0"; - expectedBonusGrade = 10.0; - expectedFinalPoints = 130.0; - expectedFinalGrade = "3.0"; - expectedExceedsMax = false; - - calculateFinalGradeAtServer(bonusStrategy, weight, bonusToPoints, sourcePoints, expectedExamGrade, expectedBonusGrade, expectedFinalPoints, expectedFinalGrade, - expectedExceedsMax, sourceGradingScale.getId()); - - bonusToPoints = 200; - sourcePoints = 200; - expectedExamGrade = "1.0"; - expectedBonusGrade = 20.0; - expectedFinalPoints = 200.0; - expectedFinalGrade = "1.0"; - expectedExceedsMax = true; - - calculateFinalGradeAtServer(bonusStrategy, weight, bonusToPoints, sourcePoints, expectedExamGrade, expectedBonusGrade, expectedFinalPoints, expectedFinalGrade, - expectedExceedsMax, sourceGradingScale.getId()); - - } - @Test @WithMockUser(username = "admin", roles = "ADMIN") void testCalculateRawBonusWithPointsBonusStrategy_nonNumericGrades() throws Exception { diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java index a50184014df5..f72a495c11cf 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java @@ -10,6 +10,8 @@ import org.assertj.core.data.Offset; import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -19,12 +21,10 @@ import de.tum.in.www1.artemis.assessment.ComplaintUtilService; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.*; -import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; -import de.tum.in.www1.artemis.domain.enumeration.ComplaintType; -import de.tum.in.www1.artemis.domain.enumeration.FeedbackType; -import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; +import de.tum.in.www1.artemis.domain.enumeration.*; import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; +import de.tum.in.www1.artemis.domain.participation.Participation; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; @@ -126,59 +126,14 @@ void testSubmitFileUploadAssessment_asInstructor() throws Exception { assertThat(exercise.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isEqualTo(1L); } - @Test - @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") - void testManualAssessmentSubmit_IncludedCompletelyWithBonusPointsExercise() throws Exception { - // setting up exercise - afterReleaseFileUploadExercise.setIncludedInOverallScore(IncludedInOverallScore.INCLUDED_COMPLETELY); - afterReleaseFileUploadExercise.setMaxPoints(10.0); - afterReleaseFileUploadExercise.setBonusPoints(10.0); - exerciseRepository.save(afterReleaseFileUploadExercise); - - // setting up student submission - FileUploadSubmission submission = ParticipationFactory.generateFileUploadSubmission(true); - submission = fileUploadExerciseUtilService.addFileUploadSubmission(afterReleaseFileUploadExercise, submission, TEST_PREFIX + "student1"); - List feedbacks = new ArrayList<>(); - - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 0.0, 0L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, -1.0, 0L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 1.0, 0L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 50L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 100L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 150L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 200L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 200L); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") - void testManualAssessmentSubmit_IncludedCompletelyWithoutBonusPointsExercise() throws Exception { - // setting up exercise - afterReleaseFileUploadExercise.setIncludedInOverallScore(IncludedInOverallScore.INCLUDED_COMPLETELY); - afterReleaseFileUploadExercise.setMaxPoints(10.0); - afterReleaseFileUploadExercise.setBonusPoints(0.0); - exerciseRepository.save(afterReleaseFileUploadExercise); - - // setting up student submission - FileUploadSubmission submission = ParticipationFactory.generateFileUploadSubmission(true); - submission = fileUploadExerciseUtilService.addFileUploadSubmission(afterReleaseFileUploadExercise, submission, TEST_PREFIX + "student1"); - List feedbacks = new ArrayList<>(); - - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 0.0, 0L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, -1.0, 0L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 1.0, 0L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 50L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 100L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 100L); - } - - @Test + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") - void testManualAssessmentSubmit_IncludedAsBonusExercise() throws Exception { + @CsvSource({ "INCLUDED_COMPLETELY,true", "INCLUDED_COMPLETELY,false", "INCLUDED_AS_BONUS,true", "INCLUDED_AS_BONUS,false", "NOT_INCLUDED,true", "INCLUDED_AS_BONUS,false" }) + void testManualAssessmentSubmitWithBonus(IncludedInOverallScore includedInOverallScore, boolean bonus) throws Exception { // setting up exercise - afterReleaseFileUploadExercise.setIncludedInOverallScore(IncludedInOverallScore.INCLUDED_AS_BONUS); - afterReleaseFileUploadExercise.setMaxPoints(10.0); - afterReleaseFileUploadExercise.setBonusPoints(0.0); + afterReleaseFileUploadExercise.setIncludedInOverallScore(includedInOverallScore); + afterReleaseFileUploadExercise.setMaxPoints(15.0); + afterReleaseFileUploadExercise.setBonusPoints(bonus ? 15.0 : 0.0); exerciseRepository.save(afterReleaseFileUploadExercise); // setting up student submission @@ -186,37 +141,17 @@ void testManualAssessmentSubmit_IncludedAsBonusExercise() throws Exception { submission = fileUploadExerciseUtilService.addFileUploadSubmission(afterReleaseFileUploadExercise, submission, TEST_PREFIX + "student1"); List feedbacks = new ArrayList<>(); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 0.0, 0L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, -1.0, 0L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 1.0, 0L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 50L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 100L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 100L); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") - void testManualAssessmentSubmit_NotIncludedExercise() throws Exception { - // setting up exercise - afterReleaseFileUploadExercise.setIncludedInOverallScore(IncludedInOverallScore.NOT_INCLUDED); - afterReleaseFileUploadExercise.setMaxPoints(10.0); - afterReleaseFileUploadExercise.setBonusPoints(0.0); - exerciseRepository.save(afterReleaseFileUploadExercise); - - // setting up student submission - FileUploadSubmission submission = ParticipationFactory.generateFileUploadSubmission(true); - submission = fileUploadExerciseUtilService.addFileUploadSubmission(afterReleaseFileUploadExercise, submission, TEST_PREFIX + "student1"); - List feedbacks = new ArrayList<>(); + addAssessmentFeedbackAndCheckScore(submission, feedbacks, 3.75, 25.0); + addAssessmentFeedbackAndCheckScore(submission, feedbacks, 7.5, 75.0); + addAssessmentFeedbackAndCheckScore(submission, feedbacks, 7.5, bonus ? 125.0 : 100.0); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 0.0, 0L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, -1.0, 0L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 1.0, 0L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 50L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 100L); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 100L); + if (bonus) { + addAssessmentFeedbackAndCheckScore(submission, feedbacks, 7.5, 175.0); + addAssessmentFeedbackAndCheckScore(submission, feedbacks, 15.0, 200.0); + } } - private void addAssessmentFeedbackAndCheckScore(FileUploadSubmission fileUploadSubmission, List feedbacks, double pointsAwarded, long expectedScore) + private void addAssessmentFeedbackAndCheckScore(FileUploadSubmission fileUploadSubmission, List feedbacks, Double pointsAwarded, Double expectedScore) throws Exception { var params = new LinkedMultiValueMap(); params.add("submit", "true"); @@ -498,20 +433,14 @@ void multipleCorrectionRoundsForExam() throws Exception { var submission = ParticipationFactory.generateFileUploadSubmission(true); submission = fileUploadExerciseUtilService.addFileUploadSubmission(exercise, submission, TEST_PREFIX + "student1"); - // verify setup - assertThat(exam.getNumberOfCorrectionRoundsInExam()).isEqualTo(2); - assertThat(exam.getEndDate()).isBefore(ZonedDateTime.now()); - var optionalFetchedExercise = exerciseRepository.findWithEagerStudentParticipationsStudentAndSubmissionsById(exercise.getId()); - assertThat(optionalFetchedExercise).isPresent(); - final var exerciseWithParticipation = optionalFetchedExercise.get(); - final var studentParticipation = exerciseWithParticipation.getStudentParticipations().stream().iterator().next(); + Participation studentParticipation = submission.getParticipation(); // request to manually assess latest submission (correction round: 0) LinkedMultiValueMap params = new LinkedMultiValueMap<>(); params.add("lock", "true"); params.add("correction-round", "0"); - FileUploadSubmission submissionWithoutFirstAssessment = request.get("/api/exercises/" + exerciseWithParticipation.getId() + "/file-upload-submission-without-assessment", - HttpStatus.OK, FileUploadSubmission.class, params); + FileUploadSubmission submissionWithoutFirstAssessment = request.get("/api/exercises/" + exercise.getId() + "/file-upload-submission-without-assessment", HttpStatus.OK, + FileUploadSubmission.class, params); // verify that no new submission was created assertThat(submissionWithoutFirstAssessment).isEqualTo(submission); // verify that the lock has been set @@ -523,7 +452,7 @@ void multipleCorrectionRoundsForExam() throws Exception { LinkedMultiValueMap paramsGetAssessedCR1Tutor1 = new LinkedMultiValueMap<>(); paramsGetAssessedCR1Tutor1.add("assessedByTutor", "true"); paramsGetAssessedCR1Tutor1.add("correction-round", "0"); - var assessedSubmissionList = request.getList("/api/exercises/" + exerciseWithParticipation.getId() + "/file-upload-submissions", HttpStatus.OK, FileUploadSubmission.class, + var assessedSubmissionList = request.getList("/api/exercises/" + exercise.getId() + "/file-upload-submissions", HttpStatus.OK, FileUploadSubmission.class, paramsGetAssessedCR1Tutor1); assertThat(assessedSubmissionList).hasSize(1); @@ -539,7 +468,7 @@ void multipleCorrectionRoundsForExam() throws Exception { feedbacks, Result.class, HttpStatus.OK, params); // make sure that new result correctly appears after the assessment for first correction round - assessedSubmissionList = request.getList("/api/exercises/" + exerciseWithParticipation.getId() + "/file-upload-submissions", HttpStatus.OK, FileUploadSubmission.class, + assessedSubmissionList = request.getList("/api/exercises/" + exercise.getId() + "/file-upload-submissions", HttpStatus.OK, FileUploadSubmission.class, paramsGetAssessedCR1Tutor1); assertThat(assessedSubmissionList).hasSize(1); @@ -577,8 +506,8 @@ void multipleCorrectionRoundsForExam() throws Exception { paramsSecondCorrection.add("lock", "true"); paramsSecondCorrection.add("correction-round", "1"); - final var submissionWithoutSecondAssessment = request.get("/api/exercises/" + exerciseWithParticipation.getId() + "/file-upload-submission-without-assessment", - HttpStatus.OK, FileUploadSubmission.class, paramsSecondCorrection); + final var submissionWithoutSecondAssessment = request.get("/api/exercises/" + exercise.getId() + "/file-upload-submission-without-assessment", HttpStatus.OK, + FileUploadSubmission.class, paramsSecondCorrection); // verify that the submission is not new assertThat(submissionWithoutSecondAssessment).isEqualTo(submission); @@ -617,7 +546,7 @@ void multipleCorrectionRoundsForExam() throws Exception { LinkedMultiValueMap paramsGetAssessedCR2 = new LinkedMultiValueMap<>(); paramsGetAssessedCR2.add("assessedByTutor", "true"); paramsGetAssessedCR2.add("correction-round", "1"); - assessedSubmissionList = request.getList("/api/exercises/" + exerciseWithParticipation.getId() + "/file-upload-submissions", HttpStatus.OK, FileUploadSubmission.class, + assessedSubmissionList = request.getList("/api/exercises/" + exercise.getId() + "/file-upload-submissions", HttpStatus.OK, FileUploadSubmission.class, paramsGetAssessedCR2); assertThat(assessedSubmissionList).hasSize(1); @@ -628,7 +557,7 @@ void multipleCorrectionRoundsForExam() throws Exception { LinkedMultiValueMap paramsGetAssessedCR1 = new LinkedMultiValueMap<>(); paramsGetAssessedCR1.add("assessedByTutor", "true"); paramsGetAssessedCR1.add("correction-round", "0"); - assessedSubmissionList = request.getList("/api/exercises/" + exerciseWithParticipation.getId() + "/file-upload-submissions", HttpStatus.OK, FileUploadSubmission.class, + assessedSubmissionList = request.getList("/api/exercises/" + exercise.getId() + "/file-upload-submissions", HttpStatus.OK, FileUploadSubmission.class, paramsGetAssessedCR1); assertThat(assessedSubmissionList).isEmpty(); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java index 4fb60c5b33c4..4a44b3440743 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java @@ -10,6 +10,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -23,6 +25,7 @@ import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; +import de.tum.in.www1.artemis.domain.participation.Participation; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismComparison; import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismStatus; @@ -399,37 +402,14 @@ void testManualAssessmentSaveAndSubmit() throws Exception { assertThat(storedResult.getParticipation()).isNotNull(); } - @Test - @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") - void testManualAssessmentSubmit_IncludedCompletelyWithBonusPointsExercise() throws Exception { - // setting up exercise - useCaseExercise.setIncludedInOverallScore(IncludedInOverallScore.INCLUDED_COMPLETELY); - useCaseExercise.setMaxPoints(10.0); - useCaseExercise.setBonusPoints(10.0); - exerciseRepo.save(useCaseExercise); - - // setting up student submission - ModelingSubmission submission = modelingExerciseUtilService.addModelingSubmissionFromResources(useCaseExercise, "test-data/model-submission/use-case-model.json", - TEST_PREFIX + "student1"); - List feedbacks = new ArrayList<>(); - - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 0.0, 0D); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, -1.0, 0D); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 1.0, 0D); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 50D); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 100D); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 150D); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 200D); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 200D); - } - - @Test + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") - void testManualAssessmentSubmit_IncludedCompletelyWithoutBonusPointsExercise() throws Exception { + @CsvSource({ "INCLUDED_COMPLETELY,true", "INCLUDED_COMPLETELY,false", "INCLUDED_AS_BONUS,true", "INCLUDED_AS_BONUS,false", "NOT_INCLUDED,true", "INCLUDED_AS_BONUS,false" }) + void testManualAssessmentSubmit(IncludedInOverallScore includedInOverallScore, boolean bonus) throws Exception { // setting up exercise - useCaseExercise.setIncludedInOverallScore(IncludedInOverallScore.INCLUDED_COMPLETELY); + useCaseExercise.setIncludedInOverallScore(includedInOverallScore); useCaseExercise.setMaxPoints(10.0); - useCaseExercise.setBonusPoints(0.0); + useCaseExercise.setBonusPoints(bonus ? 10.0 : 0.0); exerciseRepo.save(useCaseExercise); // setting up student submission @@ -437,53 +417,20 @@ void testManualAssessmentSubmit_IncludedCompletelyWithoutBonusPointsExercise() t TEST_PREFIX + "student1"); List feedbacks = new ArrayList<>(); - setupStudentSubmissions(submission, feedbacks); - } + addAssessmentFeedbackAndCheckScore(submission, feedbacks, 0.0, 0.0); + addAssessmentFeedbackAndCheckScore(submission, feedbacks, -1.0, 0.0); + addAssessmentFeedbackAndCheckScore(submission, feedbacks, 1.0, 0.0); + addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 50.0); + addAssessmentFeedbackAndCheckScore(submission, feedbacks, -2.5, 25.0); + addAssessmentFeedbackAndCheckScore(submission, feedbacks, 15.0, bonus ? 175.0 : 100.0); - @Test - @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") - void testManualAssessmentSubmit_IncludedAsBonusExercise() throws Exception { - // setting up exercise - useCaseExercise.setIncludedInOverallScore(IncludedInOverallScore.INCLUDED_AS_BONUS); - useCaseExercise.setMaxPoints(10.0); - useCaseExercise.setBonusPoints(0.0); - exerciseRepo.save(useCaseExercise); - - // setting up student submission - ModelingSubmission submission = modelingExerciseUtilService.addModelingSubmissionFromResources(useCaseExercise, "test-data/model-submission/use-case-model.json", - TEST_PREFIX + "student1"); - List feedbacks = new ArrayList<>(); - - setupStudentSubmissions(submission, feedbacks); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") - void testManualAssessmentSubmit_NotIncludedExercise() throws Exception { - // setting up exercise - useCaseExercise.setIncludedInOverallScore(IncludedInOverallScore.NOT_INCLUDED); - useCaseExercise.setMaxPoints(10.0); - useCaseExercise.setBonusPoints(0.0); - exerciseRepo.save(useCaseExercise); - - // setting up student submission - ModelingSubmission submission = modelingExerciseUtilService.addModelingSubmissionFromResources(useCaseExercise, "test-data/model-submission/use-case-model.json", - TEST_PREFIX + "student1"); - List feedbacks = new ArrayList<>(); - - setupStudentSubmissions(submission, feedbacks); - } - - private void setupStudentSubmissions(ModelingSubmission submission, List feedbacks) throws Exception { - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 0.0, 0D); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, -1.0, 0D); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 1.0, 0D); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 50D); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 100D); - addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 100D); + if (bonus) { + addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 200.0); + addAssessmentFeedbackAndCheckScore(submission, feedbacks, 5.0, 200.0); + } } - private void addAssessmentFeedbackAndCheckScore(ModelingSubmission submission, List feedbacks, double pointsAwarded, Double expectedScore) throws Exception { + private void addAssessmentFeedbackAndCheckScore(ModelingSubmission submission, List feedbacks, Double pointsAwarded, Double expectedScore) throws Exception { feedbacks.add(new Feedback().credits(pointsAwarded).type(FeedbackType.MANUAL_UNREFERENCED).detailText("gj")); createAssessment(submission, feedbacks, "/assessment?submit=true", HttpStatus.OK); ModelingSubmission storedSubmission = modelingSubmissionRepo.findWithEagerResultById(submission.getId()).orElseThrow(); @@ -1333,20 +1280,14 @@ void multipleCorrectionRoundsForExam() throws Exception { final var submission = modelingExerciseUtilService.addModelingSubmissionFromResources(exercise, "test-data/model-submission/model.54727.partial.json", TEST_PREFIX + "student1"); - // verify setup - assertThat(exam.getNumberOfCorrectionRoundsInExam()).isEqualTo(2); - assertThat(exam.getEndDate()).isBefore(ZonedDateTime.now()); - var optionalFetchedExercise = exerciseRepo.findWithEagerStudentParticipationsStudentAndSubmissionsById(exercise.getId()); - assertThat(optionalFetchedExercise).isPresent(); - final var exerciseWithParticipation = optionalFetchedExercise.get(); - final var studentParticipation = exerciseWithParticipation.getStudentParticipations().stream().iterator().next(); + Participation studentParticipation = submission.getParticipation(); // request to manually assess latest submission (correction round: 0) LinkedMultiValueMap params = new LinkedMultiValueMap<>(); params.add("lock", "true"); params.add("correction-round", "0"); - ModelingSubmission submissionWithoutFirstAssessment = request.get("/api/exercises/" + exerciseWithParticipation.getId() + "/modeling-submission-without-assessment", - HttpStatus.OK, ModelingSubmission.class, params); + ModelingSubmission submissionWithoutFirstAssessment = request.get("/api/exercises/" + exercise.getId() + "/modeling-submission-without-assessment", HttpStatus.OK, + ModelingSubmission.class, params); // verify that no new submission was created assertThat(submissionWithoutFirstAssessment).isEqualTo(submission); // verify that the lock has been set @@ -1358,7 +1299,7 @@ void multipleCorrectionRoundsForExam() throws Exception { LinkedMultiValueMap paramsGetAssessedCR1Tutor1 = new LinkedMultiValueMap<>(); paramsGetAssessedCR1Tutor1.add("assessedByTutor", "true"); paramsGetAssessedCR1Tutor1.add("correction-round", "0"); - var assessedSubmissionList = request.getList("/api/exercises/" + exerciseWithParticipation.getId() + "/modeling-submissions", HttpStatus.OK, ModelingSubmission.class, + var assessedSubmissionList = request.getList("/api/exercises/" + exercise.getId() + "/modeling-submissions", HttpStatus.OK, ModelingSubmission.class, paramsGetAssessedCR1Tutor1); assertThat(assessedSubmissionList).hasSize(1); @@ -1374,7 +1315,7 @@ void multipleCorrectionRoundsForExam() throws Exception { feedbacks, Result.class, HttpStatus.OK, params); // make sure that new result correctly appears after the assessment for first correction round - assessedSubmissionList = request.getList("/api/exercises/" + exerciseWithParticipation.getId() + "/modeling-submissions", HttpStatus.OK, ModelingSubmission.class, + assessedSubmissionList = request.getList("/api/exercises/" + exercise.getId() + "/modeling-submissions", HttpStatus.OK, ModelingSubmission.class, paramsGetAssessedCR1Tutor1); assertThat(assessedSubmissionList).hasSize(1); @@ -1412,7 +1353,7 @@ void multipleCorrectionRoundsForExam() throws Exception { paramsSecondCorrection.add("lock", "true"); paramsSecondCorrection.add("correction-round", "1"); - final var submissionWithoutSecondAssessment = request.get("/api/exercises/" + exerciseWithParticipation.getId() + "/modeling-submission-without-assessment", HttpStatus.OK, + final var submissionWithoutSecondAssessment = request.get("/api/exercises/" + exercise.getId() + "/modeling-submission-without-assessment", HttpStatus.OK, ModelingSubmission.class, paramsSecondCorrection); // verify that the submission is not new @@ -1452,8 +1393,7 @@ void multipleCorrectionRoundsForExam() throws Exception { LinkedMultiValueMap paramsGetAssessedCR2 = new LinkedMultiValueMap<>(); paramsGetAssessedCR2.add("assessedByTutor", "true"); paramsGetAssessedCR2.add("correction-round", "1"); - assessedSubmissionList = request.getList("/api/exercises/" + exerciseWithParticipation.getId() + "/modeling-submissions", HttpStatus.OK, ModelingSubmission.class, - paramsGetAssessedCR2); + assessedSubmissionList = request.getList("/api/exercises/" + exercise.getId() + "/modeling-submissions", HttpStatus.OK, ModelingSubmission.class, paramsGetAssessedCR2); assertThat(assessedSubmissionList).hasSize(1); assertThat(assessedSubmissionList.get(0).getId()).isEqualTo(submissionWithoutSecondAssessment.getId()); @@ -1463,8 +1403,7 @@ void multipleCorrectionRoundsForExam() throws Exception { LinkedMultiValueMap paramsGetAssessedCR1 = new LinkedMultiValueMap<>(); paramsGetAssessedCR1.add("assessedByTutor", "true"); paramsGetAssessedCR1.add("correction-round", "0"); - assessedSubmissionList = request.getList("/api/exercises/" + exerciseWithParticipation.getId() + "/modeling-submissions", HttpStatus.OK, ModelingSubmission.class, - paramsGetAssessedCR1); + assessedSubmissionList = request.getList("/api/exercises/" + exercise.getId() + "/modeling-submissions", HttpStatus.OK, ModelingSubmission.class, paramsGetAssessedCR1); assertThat(assessedSubmissionList).isEmpty(); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java index 3db8760bc506..c7b4a1587066 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java @@ -638,14 +638,7 @@ void multipleCorrectionRoundsForExam() throws Exception { final var thirdSubmission = programmingExerciseUtilService.createProgrammingSubmission(studentParticipation, false, dummyHash); participationUtilService.addResultToSubmission(thirdSubmission, AssessmentType.AUTOMATIC, null); - // verify setup - assertThat(exam.getNumberOfCorrectionRoundsInExam()).isEqualTo(2); - assertThat(exam.getEndDate()).isBefore(ZonedDateTime.now()); - var optionalFetchedExercise = programmingExerciseRepository.findWithAllParticipationsById(exercise.getId()); - assertThat(optionalFetchedExercise).isPresent(); - final var exerciseWithParticipation = optionalFetchedExercise.get(); - var submissionsOfParticipation = submissionRepository - .findAllWithResultsAndAssessorByParticipationId(exerciseWithParticipation.getStudentParticipations().stream().iterator().next().getId()); + var submissionsOfParticipation = submissionRepository.findAllWithResultsAndAssessorByParticipationId(studentParticipation.getId()); assertThat(submissionsOfParticipation).hasSize(3); for (final var submission : submissionsOfParticipation) { assertThat(submission.getResults()).isNotNull(); @@ -658,8 +651,8 @@ void multipleCorrectionRoundsForExam() throws Exception { LinkedMultiValueMap params = new LinkedMultiValueMap<>(); params.add("lock", "true"); params.add("correction-round", "0"); - ProgrammingSubmission submissionWithoutFirstAssessment = request.get("/api/exercises/" + exerciseWithParticipation.getId() + "/programming-submission-without-assessment", - HttpStatus.OK, ProgrammingSubmission.class, params); + ProgrammingSubmission submissionWithoutFirstAssessment = request.get("/api/exercises/" + exercise.getId() + "/programming-submission-without-assessment", HttpStatus.OK, + ProgrammingSubmission.class, params); // verify that a new submission was created // We want to get the third Submission, as it is the latest one, and contains an automatic result; assertThat(submissionWithoutFirstAssessment).isNotEqualTo(firstSubmission).isNotEqualTo(secondSubmission).isEqualTo(thirdSubmission); @@ -672,7 +665,7 @@ void multipleCorrectionRoundsForExam() throws Exception { LinkedMultiValueMap paramsGetAssessedCR1Tutor1 = new LinkedMultiValueMap<>(); paramsGetAssessedCR1Tutor1.add("assessedByTutor", "true"); paramsGetAssessedCR1Tutor1.add("correction-round", "0"); - var assessedSubmissionList = request.getList("/api/exercises/" + exerciseWithParticipation.getId() + "/programming-submissions", HttpStatus.OK, ProgrammingSubmission.class, + var assessedSubmissionList = request.getList("/api/exercises/" + exercise.getId() + "/programming-submissions", HttpStatus.OK, ProgrammingSubmission.class, paramsGetAssessedCR1Tutor1); assertThat(assessedSubmissionList).hasSize(1); @@ -695,7 +688,7 @@ void multipleCorrectionRoundsForExam() throws Exception { manualResultLockedFirstRound, Result.class, HttpStatus.OK, params); // make sure that new result correctly appears after the assessment for first correction round - assessedSubmissionList = request.getList("/api/exercises/" + exerciseWithParticipation.getId() + "/programming-submissions", HttpStatus.OK, ProgrammingSubmission.class, + assessedSubmissionList = request.getList("/api/exercises/" + exercise.getId() + "/programming-submissions", HttpStatus.OK, ProgrammingSubmission.class, paramsGetAssessedCR1Tutor1); assertThat(assessedSubmissionList).hasSize(1); @@ -737,8 +730,8 @@ void multipleCorrectionRoundsForExam() throws Exception { paramsSecondCorrection.add("lock", "true"); paramsSecondCorrection.add("correction-round", "1"); - final var submissionWithoutSecondAssessment = request.get("/api/exercises/" + exerciseWithParticipation.getId() + "/programming-submission-without-assessment", - HttpStatus.OK, ProgrammingSubmission.class, paramsSecondCorrection); + final var submissionWithoutSecondAssessment = request.get("/api/exercises/" + exercise.getId() + "/programming-submission-without-assessment", HttpStatus.OK, + ProgrammingSubmission.class, paramsSecondCorrection); // verify that the submission is not new // it should contain the latest automatic result, and the first manual result, and the lock for the second manual result @@ -783,7 +776,7 @@ void multipleCorrectionRoundsForExam() throws Exception { LinkedMultiValueMap paramsGetAssessedCR2 = new LinkedMultiValueMap<>(); paramsGetAssessedCR2.add("assessedByTutor", "true"); paramsGetAssessedCR2.add("correction-round", "1"); - assessedSubmissionList = request.getList("/api/exercises/" + exerciseWithParticipation.getId() + "/programming-submissions", HttpStatus.OK, ProgrammingSubmission.class, + assessedSubmissionList = request.getList("/api/exercises/" + exercise.getId() + "/programming-submissions", HttpStatus.OK, ProgrammingSubmission.class, paramsGetAssessedCR2); assertThat(assessedSubmissionList).hasSize(1); @@ -794,7 +787,7 @@ void multipleCorrectionRoundsForExam() throws Exception { LinkedMultiValueMap paramsGetAssessedCR1 = new LinkedMultiValueMap<>(); paramsGetAssessedCR1.add("assessedByTutor", "true"); paramsGetAssessedCR1.add("correction-round", "0"); - assessedSubmissionList = request.getList("/api/exercises/" + exerciseWithParticipation.getId() + "/programming-submissions", HttpStatus.OK, ProgrammingSubmission.class, + assessedSubmissionList = request.getList("/api/exercises/" + exercise.getId() + "/programming-submissions", HttpStatus.OK, ProgrammingSubmission.class, paramsGetAssessedCR1); assertThat(assessedSubmissionList).isEmpty(); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/textexercise/TextExerciseUtilService.java b/src/test/java/de/tum/in/www1/artemis/exercise/textexercise/TextExerciseUtilService.java index 2ea7741f69c0..17673d74339a 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/textexercise/TextExerciseUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/textexercise/TextExerciseUtilService.java @@ -344,4 +344,19 @@ public TextExercise createTextExerciseForExam(ExerciseGroup exerciseGroup) { textExerciseRepository.save(textExercise); return textExercise; } + + /** + * Creates a submission with a result and an assessor of the passed text exercise. + * + * @param textExercise exercise of the submission. + * @param studentLogin login of a student who created the submission. + * @param tutorLogin login of the tutor assessing the exercise. + * @return the created text submission. + */ + public TextSubmission createTextSubmissionWithResultAndAssessor(TextExercise textExercise, String studentLogin, String tutorLogin) { + TextSubmission textSubmission = ParticipationFactory.generateTextSubmission("Some text", Language.ENGLISH, true); + textSubmission = saveTextSubmissionWithResultAndAssessor(textExercise, textSubmission, studentLogin, tutorLogin); + + return textSubmission; + } } diff --git a/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java index 015bf3bfd296..220aa4853d6a 100644 --- a/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -232,18 +233,8 @@ void getTextSubmissionWithResultIdAsTutor_badRequest() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "tutor2", roles = "TA") void updateTextAssessmentAfterComplaint_wrongParticipationId() throws Exception { - TextSubmission textSubmission = ParticipationFactory.generateTextSubmission("Some text", Language.ENGLISH, true); - textSubmission = textExerciseUtilService.saveTextSubmissionWithResultAndAssessor(textExercise, textSubmission, TEST_PREFIX + "student1", TEST_PREFIX + "tutor1"); - Result textAssessment = textSubmission.getLatestResult(); - Complaint complaint = new Complaint().result(textAssessment).complaintText("This is not fair"); - - complaintRepo.save(complaint); - complaint.getResult().setParticipation(null); // Break infinite reference chain - - ComplaintResponse complaintResponse = complaintUtilService.createInitialEmptyResponse(TEST_PREFIX + "tutor2", complaint); - complaintResponse.getComplaint().setAccepted(false); - complaintResponse.setResponseText("rejected"); - AssessmentUpdate assessmentUpdate = new AssessmentUpdate().feedbacks(new ArrayList<>()).complaintResponse(complaintResponse); + TextSubmission textSubmission = textExerciseUtilService.createTextSubmissionWithResultAndAssessor(textExercise, TEST_PREFIX + "student1", TEST_PREFIX + "tutor1"); + AssessmentUpdate assessmentUpdate = complaintUtilService.createComplaintAndResponse(textSubmission.getLatestResult(), TEST_PREFIX + "tutor2"); long randomId = 12354; Result updatedResult = request.putWithResponseBody("/api/participations/" + randomId + "/submissions/" + textSubmission.getId() + "/text-assessment-after-complaint", @@ -255,18 +246,8 @@ void updateTextAssessmentAfterComplaint_wrongParticipationId() throws Exception @Test @WithMockUser(username = TEST_PREFIX + "tutor2", roles = "TA") void updateTextAssessmentAfterComplaint_studentHidden() throws Exception { - TextSubmission textSubmission = ParticipationFactory.generateTextSubmission("Some text", Language.ENGLISH, true); - textSubmission = textExerciseUtilService.saveTextSubmissionWithResultAndAssessor(textExercise, textSubmission, TEST_PREFIX + "student1", TEST_PREFIX + "tutor1"); - Result textAssessment = textSubmission.getLatestResult(); - Complaint complaint = new Complaint().result(textAssessment).complaintText("This is not fair"); - - complaintRepo.save(complaint); - complaint.getResult().setParticipation(null); // Break infinite reference chain - - ComplaintResponse complaintResponse = complaintUtilService.createInitialEmptyResponse(TEST_PREFIX + "tutor2", complaint); - complaintResponse.getComplaint().setAccepted(false); - complaintResponse.setResponseText("rejected"); - AssessmentUpdate assessmentUpdate = new AssessmentUpdate().feedbacks(new ArrayList<>()).complaintResponse(complaintResponse); + TextSubmission textSubmission = textExerciseUtilService.createTextSubmissionWithResultAndAssessor(textExercise, TEST_PREFIX + "student1", TEST_PREFIX + "tutor1"); + AssessmentUpdate assessmentUpdate = complaintUtilService.createComplaintAndResponse(textSubmission.getLatestResult(), TEST_PREFIX + "tutor2"); Result updatedResult = request.putWithResponseBody( "/api/participations/" + textSubmission.getParticipation().getId() + "/submissions/" + textSubmission.getId() + "/text-assessment-after-complaint", @@ -319,60 +300,30 @@ private TextSubmission prepareSubmission() throws Exception { return request.get("/api/exercises/" + textExercise.getId() + "/text-submission-without-assessment", HttpStatus.OK, TextSubmission.class, params); } - @Test - @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") - void saveTextAssessment_studentHidden() throws Exception { - TextSubmission submissionWithoutAssessment = prepareSubmission(); - - final TextAssessmentDTO textAssessmentDTO = new TextAssessmentDTO(); - textAssessmentDTO.setFeedbacks(new ArrayList<>()); - - Result result = request.putWithResponseBody("/api/participations/" + submissionWithoutAssessment.getParticipation().getId() + "/results/" - + submissionWithoutAssessment.getLatestResult().getId() + "/text-assessment", textAssessmentDTO, Result.class, HttpStatus.OK); - - assertThat(result).as("saved result found").isNotNull(); - assertThat(((StudentParticipation) result.getParticipation()).getStudent()).as("student of participation is hidden").isEmpty(); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") - void saveTextAssessment_wrongParticipationId() throws Exception { - TextSubmission submissionWithoutAssessment = prepareSubmission(); - - final TextAssessmentDTO textAssessmentDTO = new TextAssessmentDTO(); - textAssessmentDTO.setFeedbacks(new ArrayList<>()); - - long randomId = 1343; - Result result = request.putWithResponseBody("/api/participations/" + randomId + "/results/" + submissionWithoutAssessment.getLatestResult().getId() + "/text-assessment", - textAssessmentDTO, Result.class, HttpStatus.BAD_REQUEST); - assertThat(result).isNull(); - } - - @Test + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") - void submitTextAssessment_studentHidden() throws Exception { + @ValueSource(booleans = { true, false }) + void saveOrSubmitTextAssessment_studentHidden(boolean submit) throws Exception { TextSubmission submissionWithoutAssessment = prepareSubmission(); final TextAssessmentDTO textAssessmentDTO = new TextAssessmentDTO(); textAssessmentDTO.setFeedbacks(new ArrayList<>()); - Result result = request.postWithResponseBody("/api/participations/" + submissionWithoutAssessment.getParticipation().getId() + "/results/" - + submissionWithoutAssessment.getLatestResult().getId() + "/submit-text-assessment", textAssessmentDTO, Result.class, HttpStatus.OK); + Result result = saveOrSubmitTextAssessment(submissionWithoutAssessment.getParticipation().getId(), + Objects.requireNonNull(submissionWithoutAssessment.getLatestResult()).getId(), textAssessmentDTO, submit, HttpStatus.OK); assertThat(result).as("saved result found").isNotNull(); assertThat(((StudentParticipation) result.getParticipation()).getStudent()).as("student of participation is hidden").isEmpty(); } - @Test + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") - void submitTextAssessment_wrongParticipationId() throws Exception { + @ValueSource(booleans = { true, false }) + void saveOrSubmitTextAssessment_wrongParticipationId(boolean submit) throws Exception { TextSubmission submissionWithoutAssessment = prepareSubmission(); - final TextAssessmentDTO textAssessmentDTO = new TextAssessmentDTO(); textAssessmentDTO.setFeedbacks(new ArrayList<>()); - long randomId = 1548; - Result result = request.postWithResponseBody( - "/api/participations/" + randomId + "/results/" + submissionWithoutAssessment.getLatestResult().getId() + "/submit-text-assessment", textAssessmentDTO, - Result.class, HttpStatus.BAD_REQUEST); + Result result = saveOrSubmitTextAssessment(1343L, Objects.requireNonNull(submissionWithoutAssessment.getLatestResult()).getId(), textAssessmentDTO, submit, + HttpStatus.BAD_REQUEST); assertThat(result).isNull(); } @@ -1347,4 +1298,28 @@ void testConsecutiveSaveFailsAfterAddingOrRemovingReferencedFeedback() throws Ex Set textBlocks = textBlockRepository.findAllBySubmissionId(textSubmissionWithoutAssessment.getId()); assertThat(textBlocks).allSatisfy(block -> assertThat(block).isEqualToComparingFieldByField(blocksSubmission.get(block.getId()))); } + + /** + * Saves or submits a text assessment. + * + * @param participationId The participation id of a submission. + * @param latestResultId The id of the latest result. + * @param textAssessmentDTO The DTO of the text assessment. + * @param submit True, if the text assessment should be submitted. False, if it should only be saved. + * @param expectedStatus Expected response status of the request. + * @return The response of the request in form of a result. + * @throws Exception If the request fails. + */ + private Result saveOrSubmitTextAssessment(Long participationId, Long latestResultId, TextAssessmentDTO textAssessmentDTO, boolean submit, HttpStatus expectedStatus) + throws Exception { + if (submit) { + return request.postWithResponseBody("/api/participations/" + participationId + "/results/" + latestResultId + "/submit-text-assessment", textAssessmentDTO, + Result.class, expectedStatus); + + } + else { + return request.putWithResponseBody("/api/participations/" + participationId + "/results/" + latestResultId + "/text-assessment", textAssessmentDTO, Result.class, + expectedStatus); + } + } } From 65087f5d7eb50b59dfcbcb3e0807a82f0de2b919 Mon Sep 17 00:00:00 2001 From: Andreas Resch Date: Tue, 3 Oct 2023 09:45:00 +0200 Subject: [PATCH 15/19] Development: Make developer setup travel friendly with pull policy: if_not_present (#7290) --- docker/atlassian.yml | 8 ++++---- docker/broker-registry.yml | 4 ++-- docker/cypress.yml | 2 +- docker/gitlab-gitlabci.yml | 2 +- docker/mailhog.yml | 2 +- docker/monitoring.yml | 6 +++--- docker/mysql.yml | 2 +- docker/nginx.yml | 2 +- docker/postgres.yml | 2 +- docker/saml-test.yml | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docker/atlassian.yml b/docker/atlassian.yml index 6b8e33418747..ae663d5bc81a 100644 --- a/docker/atlassian.yml +++ b/docker/atlassian.yml @@ -9,7 +9,7 @@ services: extra_hosts: - "host.docker.internal:host-gateway" image: ghcr.io/ls1intum/artemis-jira:9.4.3 - pull_policy: always + pull_policy: if_not_present volumes: - artemis-jira-data:/var/atlassian/application-data/jira ports: @@ -25,7 +25,7 @@ services: extra_hosts: - "host.docker.internal:host-gateway" image: ghcr.io/ls1intum/artemis-bitbucket:8.13.1 - pull_policy: always + pull_policy: if_not_present volumes: - artemis-bitbucket-data:/var/atlassian/application-data/bitbucket environment: @@ -45,7 +45,7 @@ services: extra_hosts: - "host.docker.internal:host-gateway" image: ghcr.io/ls1intum/artemis-bamboo:9.3.3 - pull_policy: always + pull_policy: if_not_present volumes: - artemis-bamboo-data:/var/atlassian/application-data/bamboo ports: @@ -70,7 +70,7 @@ services: extra_hosts: - "host.docker.internal:host-gateway" image: ghcr.io/ls1intum/artemis-bamboo-build-agent:9.2.1 - pull_policy: always + pull_policy: if_not_present volumes: # The following path needs to be the same absolute path on the host because of the docker socket: # https://confluence.atlassian.com/bamkb/bamboo-in-docker-build-fails-due-to-a-missing-working-directory-when-using-docker-runner-1027119339.html diff --git a/docker/broker-registry.yml b/docker/broker-registry.yml index 74b9b79a3325..1e5dd59e0026 100644 --- a/docker/broker-registry.yml +++ b/docker/broker-registry.yml @@ -2,7 +2,7 @@ services: jhipster-registry: container_name: artemis-jhipster-registry image: docker.io/jhipster/jhipster-registry:v6.1.2 - pull_policy: always + pull_policy: if_not_present volumes: - ./registry:/central-config # When run with the "dev" Spring profile, the JHipster Registry will @@ -25,7 +25,7 @@ services: activemq-broker: container_name: artemis-activemq-broker image: docker.io/vromero/activemq-artemis:latest - pull_policy: always + pull_policy: if_not_present environment: ARTEMIS_USERNAME: guest ARTEMIS_PASSWORD: guest diff --git a/docker/cypress.yml b/docker/cypress.yml index 494d78473672..2e337c158c4d 100644 --- a/docker/cypress.yml +++ b/docker/cypress.yml @@ -6,7 +6,7 @@ services: artemis-cypress: # Cypress image with node and chrome browser installed (Cypress installation needs to be done separately because we require additional dependencies) image: docker.io/cypress/browsers:node-18.16.0-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1 - pull_policy: always + pull_policy: if_not_present environment: CYPRESS_baseUrl: "https://artemis-nginx" CYPRESS_video: "${bamboo_cypress_video_enabled}" diff --git a/docker/gitlab-gitlabci.yml b/docker/gitlab-gitlabci.yml index c454136ffa4e..d5dac2dc635f 100644 --- a/docker/gitlab-gitlabci.yml +++ b/docker/gitlab-gitlabci.yml @@ -34,7 +34,7 @@ services: shm_size: "256m" gitlab-runner: image: docker.io/gitlab/gitlab-runner:latest - pull_policy: always + pull_policy: if_not_present container_name: artemis-gitlab-runner volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/docker/mailhog.yml b/docker/mailhog.yml index 70af79096071..fb93d5784b4d 100644 --- a/docker/mailhog.yml +++ b/docker/mailhog.yml @@ -8,7 +8,7 @@ services: mailhog: container_name: artemis-mailhog image: docker.io/mailhog/mailhog - pull_policy: always + pull_policy: if_not_present ports: - "1025:1025" - "8025:8025" diff --git a/docker/monitoring.yml b/docker/monitoring.yml index 9c237f83b69b..f9f4f5b7ac08 100644 --- a/docker/monitoring.yml +++ b/docker/monitoring.yml @@ -3,14 +3,14 @@ # ---------------------------------------------------------------------------------------------------------------------- # This configuration is intended for development purpose, it's **your** responsibility to harden it for production # -# Out of the box this setup just works with a non-containerized Artemis instancezs +# Out of the box this setup just works with a non-containerized Artemis instances # ---------------------------------------------------------------------------------------------------------------------- services: prometheus: container_name: artemis-prometheus image: docker.io/prom/prometheus:v2.34.0 - pull_policy: always + pull_policy: if_not_present volumes: - ./monitoring/prometheus/:/etc/prometheus/ # If you want to expose these ports outside your dev PC, @@ -26,7 +26,7 @@ services: grafana: container_name: artemis-grafana image: docker.io/grafana/grafana:9.0.2 - pull_policy: always + pull_policy: if_not_present volumes: - ./monitoring/grafana/provisioning/:/etc/grafana/provisioning/ environment: diff --git a/docker/mysql.yml b/docker/mysql.yml index f7b79b40ba63..97ae038e67fa 100644 --- a/docker/mysql.yml +++ b/docker/mysql.yml @@ -6,7 +6,7 @@ services: mysql: container_name: artemis-mysql image: docker.io/library/mysql:8.0.33 - pull_policy: always + pull_policy: if_not_present volumes: - artemis-mysql-data:/var/lib/mysql # DO NOT use this default file for production systems! diff --git a/docker/nginx.yml b/docker/nginx.yml index aeb196c5ea07..43807eed4a37 100644 --- a/docker/nginx.yml +++ b/docker/nginx.yml @@ -7,7 +7,7 @@ services: # nginx setup based on artemis prod ansible repository container_name: artemis-nginx image: docker.io/library/nginx:1.23 - pull_policy: always + pull_policy: if_not_present volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/timeouts.conf:/etc/nginx/conf.d/timeouts.conf:ro diff --git a/docker/postgres.yml b/docker/postgres.yml index 097c9530baee..9feba0d54638 100644 --- a/docker/postgres.yml +++ b/docker/postgres.yml @@ -6,7 +6,7 @@ services: postgres: container_name: artemis-postgres image: docker.io/library/postgres:15.3-alpine - pull_policy: always + pull_policy: if_not_present user: postgres command: ["postgres", "-c", "max_connections=200"] volumes: diff --git a/docker/saml-test.yml b/docker/saml-test.yml index 63fb677b0741..a71d69dee02e 100644 --- a/docker/saml-test.yml +++ b/docker/saml-test.yml @@ -13,7 +13,7 @@ services: saml-test: container_name: artemis-saml-test image: docker.io/jamedjo/test-saml-idp - pull_policy: always + pull_policy: if_not_present ports: - "9980:8080" # expose the port to make it reachable docker internally even if the external port mapping changes From fa029c1a8c30eec367012c9306f7c2a525406847 Mon Sep 17 00:00:00 2001 From: Markus Paulsen <39456125+MarkusPaulsen@users.noreply.github.com> Date: Tue, 3 Oct 2023 09:49:40 +0200 Subject: [PATCH 16/19] Development: Improve background image input validation (#7170) --- .../in/www1/artemis/service/FileService.java | 23 +++++++++ .../service/QuizExerciseImportService.java | 14 +++++- .../www1/artemis/service/FileServiceTest.java | 49 +++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/FileService.java b/src/main/java/de/tum/in/www1/artemis/service/FileService.java index ac239e2b645d..2226bf6962eb 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/FileService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/FileService.java @@ -79,6 +79,10 @@ public class FileService implements DisposableBean { public static final String DEFAULT_FILE_SUBPATH = "/api/files/temp/"; + public static final String BACKGROUND_FILE_SUBPATH = "/api/files/drag-and-drop/backgrounds/"; + + public static final String PICTURE_FILE_SUBPATH = "/api/files/drag-and-drop/drag-items/"; + /** * Filenames for which the template filename differs from the filename it should have in the repository. */ @@ -248,6 +252,25 @@ public Path copyExistingFileToTarget(Path oldFilePath, Path targetFolder) { return null; } + /** + * Checks whether the path starts with the provided sub-path. + * + * @param path URI to check if it starts with the sub-pat + * @param subPath sub-path URI to search for + * @throws IllegalArgumentException if the provided path does not start with the provided sub-path or the provided legacy-sub-path + */ + public static void sanitizeByCheckingIfPathStartsWithSubPathElseThrow(@NotNull URI path, @NotNull URI subPath) { + // Removes redundant elements (e.g. ../ or ./) from the path and sub-path + URI normalisedPath = path.normalize(); + URI normalisedSubPath = subPath.normalize(); + // Indicates whether the path starts with the subPath + boolean normalisedPathStartsWithNormalisedSubPath = normalisedPath.getPath().startsWith(normalisedSubPath.getPath()); + // Throws a IllegalArgumentException in case the normalisedPath does not start with the normalisedSubPath + if (!normalisedPathStartsWithNormalisedSubPath) { + throw new IllegalArgumentException("Path is not valid!"); + } + } + /** * Generates a prefix for the filename based on the target folder * diff --git a/src/main/java/de/tum/in/www1/artemis/service/QuizExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/QuizExerciseImportService.java index 29638f7d66b5..7b4c96579929 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/QuizExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/QuizExerciseImportService.java @@ -104,8 +104,13 @@ private void copyQuizQuestions(QuizExercise importedExercise, QuizExercise newEx } else if (quizQuestion instanceof DragAndDropQuestion dndQuestion) { if (dndQuestion.getBackgroundFilePath() != null) { + URI backgroundFilePublicPath = URI.create(dndQuestion.getBackgroundFilePath()); + URI backgroundFileIntendedPath = URI.create(FileService.BACKGROUND_FILE_SUBPATH); + // Check whether pictureFilePublicPath is actually a picture file path + // (which is the case when its path starts with the path backgroundFileIntendedPath) + FileService.sanitizeByCheckingIfPathStartsWithSubPathElseThrow(backgroundFilePublicPath, backgroundFileIntendedPath); // Need to copy the file and get a new path, otherwise two different questions would share the same image and would cause problems in case one was deleted - Path oldPath = filePathService.actualPathForPublicPath(URI.create(dndQuestion.getBackgroundFilePath())); + Path oldPath = filePathService.actualPathForPublicPath(backgroundFilePublicPath); Path newPath = fileService.copyExistingFileToTarget(oldPath, FilePathService.getDragAndDropBackgroundFilePath()); dndQuestion.setBackgroundFilePath(filePathService.publicPathForActualPath(newPath, null).toString()); } @@ -121,8 +126,13 @@ else if (quizQuestion instanceof DragAndDropQuestion dndQuestion) { dragItem.setId(null); dragItem.setQuestion(dndQuestion); if (dragItem.getPictureFilePath() != null) { + URI pictureFilePublicPath = URI.create(dragItem.getPictureFilePath()); + URI pictureFileIntendedPath = URI.create(FileService.PICTURE_FILE_SUBPATH); + // Check whether pictureFilePublicPath is actually a picture file path + // (which is the case when its path starts with the path pictureFileIntendedPath) + FileService.sanitizeByCheckingIfPathStartsWithSubPathElseThrow(pictureFilePublicPath, pictureFileIntendedPath); // Need to copy the file and get a new path, same as above - Path oldDragItemPath = filePathService.actualPathForPublicPath(URI.create(dragItem.getPictureFilePath())); + Path oldDragItemPath = filePathService.actualPathForPublicPath(pictureFilePublicPath); Path newDragItemPath = fileService.copyExistingFileToTarget(oldDragItemPath, FilePathService.getDragItemFilePath()); dragItem.setPictureFilePath(filePathService.publicPathForActualPath(newDragItemPath, null).toString()); } diff --git a/src/test/java/de/tum/in/www1/artemis/service/FileServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/FileServiceTest.java index c38321adb4a1..7f441d5d2778 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/FileServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/FileServiceTest.java @@ -1,6 +1,7 @@ package de.tum.in.www1.artemis.service; import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -8,6 +9,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.net.URI; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -42,6 +44,18 @@ class FileServiceTest extends AbstractSpringIntegrationIndependentTest { // the resource loader allows to load resources from the file system for this prefix private final Path overridableBasePath = Path.of("templates", "jenkins"); + private static final URI VALID_BACKGROUND_PATH = URI.create("/api/uploads/images/drag-and-drop/backgrounds/1/BackgroundFile.jpg"); + + private static final URI VALID_INTENDED_BACKGROUND_PATH = URI.create("/api/" + FilePathService.getDragAndDropBackgroundFilePath() + "/"); + + private static final URI INVALID_BACKGROUND_PATH = URI.create("/api/uploads/images/drag-and-drop/backgrounds/1/../../../exam-users/signatures/some-file.png"); + + private static final URI VALID_DRAGITEM_PATH = URI.create("/api/uploads/images/drag-and-drop/drag-items/1/PictureFile.jpg"); + + private static final URI VALID_INTENDED_DRAGITEM_PATH = URI.create("/api/" + FilePathService.getDragItemFilePath() + "/"); + + private static final URI INVALID_DRAGITEM_PATH = URI.create("/api/uploads/images/drag-and-drop/drag-items/1/../../../exam-users/signatures/some-file.png"); + @AfterEach void cleanup() throws IOException { Files.deleteIfExists(javaPath); @@ -377,4 +391,39 @@ void testIgnoreDirectoryFalsePositives(@TempDir Path targetDir) throws IOExcepti final Path expectedTargetFile = targetDir.resolve("jenkins").resolve("package.xcworkspace"); assertThat(expectedTargetFile).doesNotExist(); } + + /** + * Tests whether FileService.sanitizeByCheckingIfPathContainsSubPathElseThrow correctly indicates, that VALID_BACKGROUND_PATH starts with VALID_INTENDED_BACKGROUND_PATH + */ + @Test + void testSanitizeByCheckingIfPathContainsSubPathElseThrow_Background_Valid() { + assertThatCode(() -> FileService.sanitizeByCheckingIfPathStartsWithSubPathElseThrow(VALID_BACKGROUND_PATH, VALID_INTENDED_BACKGROUND_PATH)).doesNotThrowAnyException(); + } + + /** + * Tests whether FileService.sanitizeByCheckingIfPathContainsSubPathElseThrow correctly indicates, that INVALID_BACKGROUND_PATH does not start with + * VALID_INTENDED_BACKGROUND_PATH + */ + @Test + void testSanitizeByCheckingIfPathContainsSubPathElseThrow_Background_Invalid_Path() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> FileService.sanitizeByCheckingIfPathStartsWithSubPathElseThrow(INVALID_BACKGROUND_PATH, VALID_INTENDED_BACKGROUND_PATH)); + } + + /** + * Tests whether FileService.sanitizeByCheckingIfPathContainsSubPathElseThrow correctly indicates, that VALID_DRAGITEM_PATH starts with VALID_INTENDED_DRAGITEM_PATH + */ + @Test + void testSanitizeByCheckingIfPathContainsSubPathElseThrow_Picture_Valid() { + assertThatCode(() -> FileService.sanitizeByCheckingIfPathStartsWithSubPathElseThrow(VALID_DRAGITEM_PATH, VALID_INTENDED_DRAGITEM_PATH)).doesNotThrowAnyException(); + } + + /** + * Tests whether FileService.sanitizeByCheckingIfPathContainsSubPathElseThrow correctly indicates, that INVALID_DRAGITEM_PATH does not start with VALID_INTENDED_DRAGITEM_PATH + */ + @Test + void testSanitizeByCheckingIfPathContainsSubPathElseThrow_Picture_Invalid_Path() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> FileService.sanitizeByCheckingIfPathStartsWithSubPathElseThrow(INVALID_DRAGITEM_PATH, VALID_INTENDED_DRAGITEM_PATH)); + } } From d2ff084a330140d22de4ac11af7c894032e4ae02 Mon Sep 17 00:00:00 2001 From: Lucas Welscher Date: Tue, 3 Oct 2023 09:51:11 +0200 Subject: [PATCH 17/19] Development: Reduce transferred data when creating ratings (#7168) --- .../artemis/web/rest/ComplaintResource.java | 4 +-- .../www1/artemis/web/rest/RatingResource.java | 36 ++++++++++--------- .../exam-exercise-import.component.ts | 3 +- .../shared/rating/rating.component.html | 4 +-- .../shared/rating/rating.component.ts | 34 ++++++++---------- .../exercises/shared/rating/rating.service.ts | 20 ++++++----- .../RatingResourceIntegrationTest.java | 29 +++++++-------- .../component/rating/rating.component.spec.ts | 22 +++++------- .../spec/service/rating.service.spec.ts | 4 +-- 9 files changed, 72 insertions(+), 84 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ComplaintResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ComplaintResource.java index aa44ed9df1b1..8158b1891e7d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ComplaintResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ComplaintResource.java @@ -102,7 +102,7 @@ public ResponseEntity createComplaint(@RequestBody Complaint complain Complaint savedComplaint = complaintService.createComplaint(complaint, OptionalLong.empty(), principal); // Remove assessor information from client request - savedComplaint.getResult().setAssessor(null); + savedComplaint.getResult().filterSensitiveInformation(); return ResponseEntity.created(new URI("/api/complaints/" + savedComplaint.getId())) .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, entityName, savedComplaint.getId().toString())).body(savedComplaint); @@ -138,7 +138,7 @@ public ResponseEntity createComplaintForExamExercise(@PathVariable Lo Complaint savedComplaint = complaintService.createComplaint(complaint, OptionalLong.of(examId), principal); // Remove assessor information from client request - savedComplaint.getResult().setAssessor(null); + savedComplaint.getResult().filterSensitiveInformation(); return ResponseEntity.created(new URI("/api/complaints/" + savedComplaint.getId())) .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, entityName, savedComplaint.getId().toString())).body(savedComplaint); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/RatingResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/RatingResource.java index dc35510c7c3c..e1c622f47262 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/RatingResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/RatingResource.java @@ -11,10 +11,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import de.tum.in.www1.artemis.domain.Course; -import de.tum.in.www1.artemis.domain.Rating; -import de.tum.in.www1.artemis.domain.Result; -import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ResultRepository; @@ -59,37 +56,37 @@ public RatingResource(RatingService ratingService, UserRepository userRepository } /** - * Return Rating referencing resultId or null + * GET /results/:resultId/rating : Return Rating referencing resultId or null * * @param resultId - Id of result that is referenced with the rating - * @return Rating or null + * @return saved star rating value or empty optional */ @GetMapping("/results/{resultId}/rating") @EnforceAtLeastStudent - public ResponseEntity> getRatingForResult(@PathVariable Long resultId) { + public ResponseEntity> getRatingForResult(@PathVariable Long resultId) { // TODO allow for Instructors if (!authCheckService.isAdmin()) { checkIfUserIsOwnerOfSubmissionElseThrow(resultId); } Optional rating = ratingService.findRatingByResultId(resultId); - return ResponseEntity.ok(rating); + return ResponseEntity.ok(rating.map(Rating::getRating)); } /** - * Persist a new Rating + * POST /results/:resultId/rating/:ratingValue : Persist a new Rating * * @param resultId - Id of result that is referenced with the rating that should be persisted * @param ratingValue - Value of the updated rating - * @return inserted Rating + * @return inserted star rating value * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("/results/{resultId}/rating/{ratingValue}") @EnforceAtLeastStudent - public ResponseEntity createRatingForResult(@PathVariable long resultId, @PathVariable int ratingValue) throws URISyntaxException { + public ResponseEntity createRatingForResult(@PathVariable long resultId, @PathVariable int ratingValue) throws URISyntaxException { checkRating(ratingValue); checkIfUserIsOwnerOfSubmissionElseThrow(resultId); Rating savedRating = ratingService.saveRating(resultId, ratingValue); - return ResponseEntity.created(new URI("/api/results/" + savedRating.getId() + "/rating")).body(savedRating); + return ResponseEntity.created(new URI("/api/results/" + savedRating.getId() + "/rating")).body(savedRating.getRating()); } private void checkRating(int ratingValue) { @@ -99,23 +96,23 @@ private void checkRating(int ratingValue) { } /** - * Update a Rating + * PUT /results/:resultId/rating/:ratingValue : Update a Rating * * @param resultId - Id of result that is referenced with the rating that should be updated * @param ratingValue - Value of the updated rating - * @return updated Rating + * @return updated star rating value */ @PutMapping("/results/{resultId}/rating/{ratingValue}") @EnforceAtLeastStudent - public ResponseEntity updateRatingForResult(@PathVariable long resultId, @PathVariable int ratingValue) { + public ResponseEntity updateRatingForResult(@PathVariable long resultId, @PathVariable int ratingValue) { checkRating(ratingValue); checkIfUserIsOwnerOfSubmissionElseThrow(resultId); Rating savedRating = ratingService.updateRating(resultId, ratingValue); - return ResponseEntity.ok(savedRating); + return ResponseEntity.ok(savedRating.getRating()); } /** - * Get all ratings for the "courseId" Course + * GET /course/:courseId/rating : Get all ratings for the "courseId" Course * * @param courseId - Id of the course that the ratings are fetched for * @return List of Ratings for the course @@ -126,6 +123,11 @@ public ResponseEntity> getRatingForInstructorDashboard(@PathVariabl Course course = courseRepository.findByIdElseThrow(courseId); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); List responseRatings = ratingService.getAllRatingsByCourse(courseId); + responseRatings.forEach(rating -> { + rating.getResult().setSubmission(null); + rating.getResult().getParticipation().getExercise().setCourse(null); + rating.getResult().getParticipation().getExercise().setExerciseGroup(null); + }); return ResponseEntity.ok(responseRatings); } diff --git a/src/main/webapp/app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component.ts b/src/main/webapp/app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component.ts index be3ba91c83fc..0f212060dccf 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component.ts @@ -1,10 +1,9 @@ import { Component, Input, OnInit } from '@angular/core'; import { Exam } from 'app/entities/exam.model'; import { faCheckDouble, faFont } from '@fortawesome/free-solid-svg-icons'; -import { Exercise, ExerciseType } from 'app/entities/exercise.model'; +import { Exercise, ExerciseType, getIcon } from 'app/entities/exercise.model'; import { ExerciseGroup } from 'app/entities/exercise-group.model'; import { SHORT_NAME_PATTERN } from 'app/shared/constants/input.constants'; -import { getIcon } from 'app/entities/exercise.model'; @Component({ selector: 'jhi-exam-exercise-import', diff --git a/src/main/webapp/app/exercises/shared/rating/rating.component.html b/src/main/webapp/app/exercises/shared/rating/rating.component.html index 4631f776caaa..6b55339ca456 100644 --- a/src/main/webapp/app/exercises/shared/rating/rating.component.html +++ b/src/main/webapp/app/exercises/shared/rating/rating.component.html @@ -1,6 +1,6 @@ -
+
- +
diff --git a/src/main/webapp/app/exercises/shared/rating/rating.component.ts b/src/main/webapp/app/exercises/shared/rating/rating.component.ts index 2dde045d5be2..d7c81c0c68eb 100644 --- a/src/main/webapp/app/exercises/shared/rating/rating.component.ts +++ b/src/main/webapp/app/exercises/shared/rating/rating.component.ts @@ -2,9 +2,9 @@ import { Component, Input, OnInit } from '@angular/core'; import { RatingService } from 'app/exercises/shared/rating/rating.service'; import { StarRatingComponent } from 'app/exercises/shared/rating/star-rating/star-rating.component'; import { Result } from 'app/entities/result.model'; -import { Rating } from 'app/entities/rating.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { AccountService } from 'app/core/auth/account.service'; +import { Observable } from 'rxjs'; @Component({ selector: 'jhi-rating', @@ -12,7 +12,7 @@ import { AccountService } from 'app/core/auth/account.service'; styleUrls: ['./rating.component.scss'], }) export class RatingComponent implements OnInit { - public rating: Rating; + public rating: number; public disableRating = false; @Input() result?: Result; @@ -22,16 +22,12 @@ export class RatingComponent implements OnInit { ) {} ngOnInit(): void { - if (!this.result || !this.result.id || !this.result.participation || !this.accountService.isOwnerOfParticipation(this.result.participation as StudentParticipation)) { + if (!this.result?.id || !this.result.participation || !this.accountService.isOwnerOfParticipation(this.result.participation as StudentParticipation)) { return; } this.ratingService.getRating(this.result.id).subscribe((rating) => { - if (rating) { - this.rating = rating; - } else { - this.rating = new Rating(this.result, 0); - } + this.rating = rating ?? 0; }); } @@ -41,24 +37,22 @@ export class RatingComponent implements OnInit { */ onRate(event: { oldValue: number; newValue: number; starRating: StarRatingComponent }) { // block rating to prevent double sending of post request - if (this.disableRating || !this.rating.result) { + if (this.disableRating || !this.result) { return; } - // update feedback locally - this.rating.rating = event.newValue; + const oldRating = this.rating; + this.rating = event.newValue; + this.disableRating = true; + let observable: Observable; // set/update feedback on the server - if (this.rating.id) { - this.ratingService.updateRating(this.rating).subscribe((rating) => { - this.rating = rating; - }); + if (oldRating) { + observable = this.ratingService.updateRating(this.rating, this.result.id!); } else { - this.disableRating = true; - this.ratingService.createRating(this.rating).subscribe((rating) => { - this.rating = rating; - this.disableRating = false; - }); + observable = this.ratingService.createRating(this.rating, this.result.id!); } + + observable.subscribe((rating) => (this.rating = rating)).add(() => (this.disableRating = false)); } } diff --git a/src/main/webapp/app/exercises/shared/rating/rating.service.ts b/src/main/webapp/app/exercises/shared/rating/rating.service.ts index 34df5d2d2d16..728e9415a6bd 100644 --- a/src/main/webapp/app/exercises/shared/rating/rating.service.ts +++ b/src/main/webapp/app/exercises/shared/rating/rating.service.ts @@ -13,26 +13,28 @@ export class RatingService { /** * Create the student rating for feedback on the server. - * @param rating - Rating for the result + * @param rating - star rating for the result + * @param resultId - id of the linked result */ - createRating(rating: Rating): Observable { - return this.http.post(this.ratingResourceUrl + `${rating.result!.id!}/rating/${rating.rating}`, null); + createRating(rating: number, resultId: number): Observable { + return this.http.post(this.ratingResourceUrl + `${resultId}/rating/${rating}`, null); } /** * Get rating for "resultId" Result - * @param ratingId - Id of Result who's rating is received + * @param resultId - id of result who's rating is received */ - getRating(ratingId: number): Observable { - return this.http.get(this.ratingResourceUrl + `${ratingId}/rating`); + getRating(resultId: number): Observable { + return this.http.get(this.ratingResourceUrl + `${resultId}/rating`); } /** * Update rating for "resultId" Result - * @param rating - Rating for the result + * @param rating - star rating for the result + * @param resultId - id of the linked result */ - updateRating(rating: Rating): Observable { - return this.http.put(this.ratingResourceUrl + `${rating.result!.id!}/rating/${rating.rating}`, null); + updateRating(rating: number, resultId: number): Observable { + return this.http.put(this.ratingResourceUrl + `${resultId}/rating/${rating}`, null); } /** diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/RatingResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/RatingResourceIntegrationTest.java index 6c200b0e88ef..955be181c070 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/RatingResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/RatingResourceIntegrationTest.java @@ -4,9 +4,10 @@ import java.util.Optional; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -18,7 +19,6 @@ import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; -import de.tum.in.www1.artemis.repository.RatingRepository; import de.tum.in.www1.artemis.service.RatingService; import de.tum.in.www1.artemis.user.UserUtilService; @@ -29,9 +29,6 @@ class RatingResourceIntegrationTest extends AbstractSpringIntegrationIndependent @Autowired private RatingService ratingService; - @Autowired - private RatingRepository ratingRepo; - @Autowired private UserUtilService userUtilService; @@ -70,24 +67,21 @@ void initTestCase() { userUtilService.createAndSaveUser(TEST_PREFIX + "instructor2"); } - @AfterEach - void tearDown() { - ratingRepo.deleteAllInBatch(); - } - @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testCreateRating_asUser() throws Exception { - request.post("/api/results/" + result.getId() + "/rating/" + rating.getRating(), null, HttpStatus.CREATED); + int response = request.postWithResponseBody("/api/results/" + result.getId() + "/rating/" + rating.getRating(), null, Integer.class, HttpStatus.CREATED); Rating savedRating = ratingService.findRatingByResultId(result.getId()).orElseThrow(); assertThat(savedRating.getRating()).isEqualTo(2); + assertThat(response).isEqualTo(2); assertThat(savedRating.getResult().getId()).isEqualTo(result.getId()); } - @Test + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") + @ValueSource(ints = { 7, 123, -5, 0 }) @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testCreateInvalidRating_asUser() throws Exception { - rating.setRating(7); + void testCreateInvalidRating_asUser(int value) throws Exception { + rating.setRating(value); request.post("/api/results/" + result.getId() + "/rating/" + rating.getRating(), null, HttpStatus.BAD_REQUEST); final Optional optionalRating = ratingService.findRatingByResultId(result.getId()); assertThat(optionalRating).isEmpty(); @@ -103,20 +97,21 @@ void testCreateRating_asTutor_FORBIDDEN() throws Exception { @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testGetRating_asUser() throws Exception { Rating savedRating = ratingService.saveRating(result.getId(), rating.getRating()); - request.get("/api/results/" + savedRating.getResult().getId() + "/rating", HttpStatus.OK, Rating.class); + int response = request.get("/api/results/" + savedRating.getResult().getId() + "/rating", HttpStatus.OK, Integer.class); + assertThat(response).isEqualTo(2); } @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testGetRating_asUser_FORBIDDEN() throws Exception { Rating savedRating = ratingService.saveRating(result.getId(), rating.getRating()); - request.get("/api/results/" + savedRating.getResult().getId() + "/rating", HttpStatus.FORBIDDEN, Rating.class); + request.get("/api/results/" + savedRating.getResult().getId() + "/rating", HttpStatus.FORBIDDEN, Integer.class); } @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testGetRating_asUser_Null() throws Exception { - Rating savedRating = request.get("/api/results/" + result.getId() + "/rating", HttpStatus.OK, Rating.class); + Integer savedRating = request.get("/api/results/" + result.getId() + "/rating", HttpStatus.OK, Integer.class); assertThat(savedRating).isNull(); } diff --git a/src/test/javascript/spec/component/rating/rating.component.spec.ts b/src/test/javascript/spec/component/rating/rating.component.spec.ts index 381fb680c0ee..517d46f96a9b 100644 --- a/src/test/javascript/spec/component/rating/rating.component.spec.ts +++ b/src/test/javascript/spec/component/rating/rating.component.spec.ts @@ -6,7 +6,6 @@ import { RatingService } from 'app/exercises/shared/rating/rating.service'; import { MockRatingService } from '../../helpers/mocks/service/mock-rating.service'; import { Result } from 'app/entities/result.model'; import { Submission } from 'app/entities/submission.model'; -import { Rating } from 'app/entities/rating.model'; import { of } from 'rxjs'; import { AccountService } from 'app/core/auth/account.service'; import { MockAccountService } from '../../helpers/mocks/service/mock-account.service'; @@ -62,20 +61,19 @@ describe('RatingComponent', () => { it('should create new local rating', () => { ratingComponent.ngOnInit(); - expect(ratingComponent.rating.result?.id).toBe(89); - expect(ratingComponent.rating.rating).toBe(0); + expect(ratingComponent.rating).toBe(0); }); it('should set rating received from server', () => { - jest.spyOn(ratingService, 'getRating').mockReturnValue(of(new Rating({ id: 90 } as Result, 1))); + jest.spyOn(ratingService, 'getRating').mockReturnValue(of(1)); ratingComponent.ngOnInit(); - expect(ratingComponent.rating.result?.id).toBe(90); - expect(ratingComponent.rating.rating).toBe(1); + expect(ratingComponent.rating).toBe(1); }); describe('OnRate', () => { beforeEach(() => { - ratingComponent.rating = new Rating({ id: 89 } as Result, 0); + ratingComponent.rating = 0; + ratingComponent.result = { id: 89 } as Result; jest.spyOn(ratingService, 'createRating'); jest.spyOn(ratingService, 'updateRating'); }); @@ -99,21 +97,19 @@ describe('RatingComponent', () => { }); expect(ratingService.createRating).toHaveBeenCalledOnce(); expect(ratingService.updateRating).not.toHaveBeenCalled(); - expect(ratingComponent.rating.result?.id).toBe(89); - expect(ratingComponent.rating.rating).toBe(2); + expect(ratingComponent.rating).toBe(2); }); it('should update rating', () => { - ratingComponent.rating.id = 89; + ratingComponent.rating = 1; ratingComponent.onRate({ - oldValue: 0, + oldValue: 1, newValue: 2, starRating: new StarRatingComponent(), }); expect(ratingService.updateRating).toHaveBeenCalledOnce(); expect(ratingService.createRating).not.toHaveBeenCalled(); - expect(ratingComponent.rating.result?.id).toBe(89); - expect(ratingComponent.rating.rating).toBe(2); + expect(ratingComponent.rating).toBe(2); }); }); }); diff --git a/src/test/javascript/spec/service/rating.service.spec.ts b/src/test/javascript/spec/service/rating.service.spec.ts index b6732e01969e..9ee310a08e27 100644 --- a/src/test/javascript/spec/service/rating.service.spec.ts +++ b/src/test/javascript/spec/service/rating.service.spec.ts @@ -25,7 +25,7 @@ describe('Rating Service', () => { id: 0, ...elemDefault, }; - service.createRating(new Rating(new Result(), 3)).pipe(take(1)).subscribe(); + service.createRating(3, 0).pipe(take(1)).subscribe(); const req = httpMock.expectOne({ method: 'POST' }); req.flush(returnedFromService); @@ -46,7 +46,7 @@ describe('Rating Service', () => { id: 0, ...elemDefault, }; - service.updateRating(new Rating(new Result(), 3)).pipe(take(1)).subscribe(); + service.updateRating(3, 0).pipe(take(1)).subscribe(); const req = httpMock.expectOne({ method: 'PUT' }); req.flush(returnedFromService); From a0e37589ee69ef62053636ce3d4562c3934c5c5a Mon Sep 17 00:00:00 2001 From: Raphael Stief <118574504+rstief@users.noreply.github.com> Date: Tue, 3 Oct 2023 09:52:44 +0200 Subject: [PATCH 18/19] Grading: Fix course statistics to show correct numbers in tooltips (#7159) --- .../exercise-scores-chart.component.html | 4 +- .../exercise-scores-chart.component.ts | 63 +++++++++++-------- .../exercise-scores-chart.component.spec.ts | 10 +-- 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/main/webapp/app/overview/visualizations/exercise-scores-chart/exercise-scores-chart.component.html b/src/main/webapp/app/overview/visualizations/exercise-scores-chart/exercise-scores-chart.component.html index 7390a72a5907..02a0bccffc96 100644 --- a/src/main/webapp/app/overview/visualizations/exercise-scores-chart/exercise-scores-chart.component.html +++ b/src/main/webapp/app/overview/visualizations/exercise-scores-chart/exercise-scores-chart.component.html @@ -46,12 +46,12 @@

{{ 'artemisApp.exercise-scores-chart.title' | artemisTranslate > {{ model.name }}
- {{ model.series }}: {{ Math.max(model.value - 1, 0) }} % + {{ model.series }}: {{ Math.max(model.value, 0) }} %
{{ model[0].name }}
-
{{ entry.series }}: {{ Math.max(entry.value - 1, 0) }}%
+
{{ entry.series }}: {{ Math.max(entry.value, 0) }}%
{{ 'artemisApp.exercise-scores-chart.exerciseType' | artemisTranslate }} {{ 'artemisApp.exercise-scores-chart.' + model[0].exerciseType.toLowerCase() | artemisTranslate }} { const extraInformation = { - exerciseId: exerciseScoreDTO.exerciseId, + exerciseId: exerciseScoreDTO.exerciseId!, exerciseType: exerciseScoreDTO.exerciseType, }; // adapt the y-axis max @@ -132,9 +148,9 @@ export class ExerciseScoresChartComponent implements AfterViewInit, OnChanges { round(exerciseScoreDTO.maxScoreAchieved!), this.maxScale, ); - scoreSeries.push({ name: exerciseScoreDTO.exerciseTitle, value: round(exerciseScoreDTO.scoreOfStudent!) + 1, ...extraInformation }); - averageSeries.push({ name: exerciseScoreDTO.exerciseTitle, value: round(exerciseScoreDTO.averageScoreAchieved!) + 1, ...extraInformation }); - bestScoreSeries.push({ name: exerciseScoreDTO.exerciseTitle, value: round(exerciseScoreDTO.maxScoreAchieved!) + 1, ...extraInformation }); + scoreSeries.push({ name: exerciseScoreDTO.exerciseTitle!, value: round(exerciseScoreDTO.scoreOfStudent!), ...extraInformation }); + averageSeries.push({ name: exerciseScoreDTO.exerciseTitle!, value: round(exerciseScoreDTO.averageScoreAchieved!), ...extraInformation }); + bestScoreSeries.push({ name: exerciseScoreDTO.exerciseTitle!, value: round(exerciseScoreDTO.maxScoreAchieved!), ...extraInformation }); }); const studentScore = { name: this.yourScoreLabel, series: scoreSeries }; @@ -144,7 +160,7 @@ export class ExerciseScoresChartComponent implements AfterViewInit, OnChanges { this.ngxData.push(averageScore); this.ngxData.push(bestScore); this.ngxData = [...this.ngxData]; - this.backUpData = [...this.ngxData]; + this.backUpData = cloneDeep(this.ngxData); } /** @@ -153,34 +169,29 @@ export class ExerciseScoresChartComponent implements AfterViewInit, OnChanges { * If the users click on an entry in the legend, the corresponding line disappears or reappears depending on its previous state * @param data the event sent by the framework */ - onSelect(data: any): void { - // delegate to the corresponding exercise if chart node is clicked - if (data.exerciseId) { - this.navigateToExercise(data.exerciseId); - } else { - // if a legend label is clicked, the corresponding line has to disappear or reappear - const name = JSON.parse(JSON.stringify(data)) as string; + onSelect(data: ChartNode | string): void { + if (typeof data === 'string') { + // if a legend label is clicked, the visibility of the corresponding line is toggled + const name: string = data; // find the affected line in the dataset const index = this.ngxData.findIndex((dataPack: any) => { const dataName = dataPack.name as string; return dataName === name; }); - // check whether the line is currently displayed if (this.ngxColor.domain[index] !== 'rgba(255,255,255,0)') { - const placeHolder = cloneDeep(this.ngxData[index]); - placeHolder.series.forEach((piece: any) => { - piece.value = 0; - }); - // exchange actual line with all-zero line and make color transparent - this.ngxData[index] = placeHolder; + //if the line is displayed, remove its values and make it transparent + this.ngxData[index].series = []; this.ngxColor.domain[index] = 'rgba(255,255,255,0)'; } else { - // if the line is currently hidden, the color and the values are reset + // if the line is currently hidden, the values and the color are reset + this.ngxData[index].series = cloneDeep(this.backUpData[index].series); this.ngxColor.domain[index] = this.colorBase[index]; - this.ngxData[index] = this.backUpData[index]; } // trigger a chart update this.ngxData = [...this.ngxData]; + } else { + // if a chart node is clicked, navigate to the corresponding exercise + this.navigateToExercise(data.exerciseId); } } diff --git a/src/test/javascript/spec/component/overview/course-statistics/visualizations/exercise-scores-chart.component.spec.ts b/src/test/javascript/spec/component/overview/course-statistics/visualizations/exercise-scores-chart.component.spec.ts index 4cac347eb88d..76416a42e4f9 100644 --- a/src/test/javascript/spec/component/overview/course-statistics/visualizations/exercise-scores-chart.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-statistics/visualizations/exercise-scores-chart.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateService } from '@ngx-translate/core'; import { AlertService } from 'app/core/util/alert.service'; import { MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks'; -import { ExerciseScoresChartComponent } from 'app/overview/visualizations/exercise-scores-chart/exercise-scores-chart.component'; +import { ChartNode, ExerciseScoresChartComponent } from 'app/overview/visualizations/exercise-scores-chart/exercise-scores-chart.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { of } from 'rxjs'; import { ActivatedRoute } from '@angular/router'; @@ -136,11 +136,11 @@ describe('ExerciseScoresChartComponent', () => { component.onSelect(legendClickEvent); expect(component.ngxColor.domain[2]).toBe('rgba(255,255,255,0)'); - expect(component.ngxData[2].series.map((exercise: any) => exercise.value)).toEqual([0, 0]); + expect(component.ngxData[2].series).toEqual([]); component.onSelect(legendClickEvent); expect(component.ngxColor.domain[2]).toBe(GraphColors.GREEN); - expect(component.ngxData[2].series.map((exercise: any) => exercise.value)).toEqual([61, 71]); + expect(component.ngxData[2].series.map((exercise: any) => exercise.value)).toEqual([60, 70]); }); it('should react correct if chart point is clicked', () => { @@ -150,7 +150,7 @@ describe('ExerciseScoresChartComponent', () => { setUpServiceAndStartComponent([firstExercise, secondExercise]); const routingService = TestBed.inject(ArtemisNavigationUtilService); const routingStub = jest.spyOn(routingService, 'routeInNewTab'); - const pointClickEvent = { exerciseId: 2 }; + const pointClickEvent: ChartNode = { exerciseType: '', name: '', series: '', value: 0, exerciseId: 2 }; component.onSelect(pointClickEvent); @@ -177,7 +177,7 @@ describe('ExerciseScoresChartComponent', () => { }); function validateStructureOfDataPoint(dataPoint: any, exerciseScoresDTO: ExerciseScoresDTO, score: number) { - const expectedStructure = { name: exerciseScoresDTO.exerciseTitle, value: score + 1, exerciseId: exerciseScoresDTO.exerciseId, exerciseType: exerciseScoresDTO.exerciseType }; + const expectedStructure = { name: exerciseScoresDTO.exerciseTitle, value: score, exerciseId: exerciseScoresDTO.exerciseId, exerciseType: exerciseScoresDTO.exerciseType }; expect(dataPoint).toEqual(expectedStructure); } From a55031ad2112277b12ae1f73158856627497a083 Mon Sep 17 00:00:00 2001 From: Jonathan Ostertag Date: Tue, 3 Oct 2023 10:27:56 +0200 Subject: [PATCH 19/19] Quiz exercises: Improve user experience when creating modeling drag and drop exercises (#7261) --- .../apollon-diagram-detail.component.ts | 5 ++++- .../quiz-exercise-generator.ts | 4 ++-- .../drag-and-drop-question.component.html | 4 +++- .../drag-and-drop-question.component.scss | 2 +- src/main/webapp/i18n/de/apollonDiagram.json | 3 ++- src/main/webapp/i18n/en/apollonDiagram.json | 3 ++- .../quiz-exercise-generator.spec.ts | 18 ++---------------- 7 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/main/webapp/app/exercises/quiz/manage/apollon-diagrams/apollon-diagram-detail.component.ts b/src/main/webapp/app/exercises/quiz/manage/apollon-diagrams/apollon-diagram-detail.component.ts index 9ca9b4796321..6caa91f19a16 100644 --- a/src/main/webapp/app/exercises/quiz/manage/apollon-diagrams/apollon-diagram-detail.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/apollon-diagrams/apollon-diagram-detail.component.ts @@ -1,5 +1,5 @@ import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { ApollonEditor, ApollonMode, Locale, UMLModel } from '@ls1intum/apollon'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { JhiLanguageHelper } from 'app/core/language/language.helper'; @@ -52,6 +52,7 @@ export class ApollonDiagramDetailComponent implements OnInit, OnDestroy { private languageHelper: JhiLanguageHelper, private modalService: NgbModal, private route: ActivatedRoute, + private router: Router, ) {} /** @@ -153,6 +154,7 @@ export class ApollonDiagramDetailComponent implements OnInit, OnDestroy { */ async generateExercise() { if (!this.hasInteractive) { + this.alertService.error('artemisApp.apollonDiagram.create.validationError'); return; } @@ -165,6 +167,7 @@ export class ApollonDiagramDetailComponent implements OnInit, OnDestroy { const result = await modalRef.result; if (result) { this.alertService.success('artemisApp.apollonDiagram.create.success', { title: result.title }); + this.router.navigate(['course-management', this.courseId, 'quiz-exercises', result.id, 'edit']); } } catch (error) { this.alertService.error('artemisApp.apollonDiagram.create.error'); diff --git a/src/main/webapp/app/exercises/quiz/manage/apollon-diagrams/exercise-generation/quiz-exercise-generator.ts b/src/main/webapp/app/exercises/quiz/manage/apollon-diagrams/exercise-generation/quiz-exercise-generator.ts index 10128f71a4d1..0222ed96cf77 100644 --- a/src/main/webapp/app/exercises/quiz/manage/apollon-diagrams/exercise-generation/quiz-exercise-generator.ts +++ b/src/main/webapp/app/exercises/quiz/manage/apollon-diagrams/exercise-generation/quiz-exercise-generator.ts @@ -68,9 +68,9 @@ export async function generateDragAndDropQuizExercise( const quizExercise = createDragAndDropQuizExercise(course, title, dragAndDropQuestion); // Save the quiz exercise - await lastValueFrom(quizExerciseService.create(quizExercise)); + const creationResponse = await lastValueFrom(quizExerciseService.create(quizExercise)); - return quizExercise; + return creationResponse.body ?? quizExercise; } /** diff --git a/src/main/webapp/app/exercises/quiz/shared/questions/drag-and-drop-question/drag-and-drop-question.component.html b/src/main/webapp/app/exercises/quiz/shared/questions/drag-and-drop-question/drag-and-drop-question.component.html index 08f652e0df4a..6f8f8d96dfb7 100644 --- a/src/main/webapp/app/exercises/quiz/shared/questions/drag-and-drop-question/drag-and-drop-question.component.html +++ b/src/main/webapp/app/exercises/quiz/shared/questions/drag-and-drop-question/drag-and-drop-question.component.html @@ -96,6 +96,7 @@

(onDragOver)="preventDefault($event)" (onDragLeave)="preventDefault($event)" cdkDropList + [cdkDropListAutoScrollStep]="60" >

-
+
@@ -244,6 +245,7 @@

(onDragOver)="preventDefault($event)" (onDragLeave)="preventDefault($event)" cdkDropList + [cdkDropListAutoScrollStep]="60" > { return { width: 0, height: 0 }; }; -const question1 = { id: 1, type: QuizQuestionType.DRAG_AND_DROP, points: 1 } as QuizQuestion; -const question2 = { id: 2, type: QuizQuestionType.MULTIPLE_CHOICE, points: 2, answerOptions: [], invalid: false, exportQuiz: false, randomizeOrder: true } as QuizQuestion; -const question3 = { id: 3, type: QuizQuestionType.SHORT_ANSWER, points: 3 } as QuizQuestion; -const now = dayjs(); - -const quizExercise = { - id: 1, - quizQuestions: [question1, question2, question3], - releaseDate: dayjs(now).subtract(2, 'minutes'), - dueDate: dayjs(now).add(2, 'minutes'), - quizStarted: true, -} as QuizExercise; - describe('QuizExercise Generator', () => { let quizExerciseService: QuizExerciseService; let fileUploaderService: FileUploaderService; @@ -80,7 +66,7 @@ describe('QuizExercise Generator', () => { configureServices(); const examplePath = '/path/to/file'; jest.spyOn(fileUploaderService, 'uploadFile').mockReturnValue(Promise.resolve({ path: examplePath })); - jest.spyOn(quizExerciseService, 'create').mockReturnValue(of({ body: quizExercise } as HttpResponse)); + jest.spyOn(quizExerciseService, 'create').mockImplementation((generatedExercise) => of({ body: generatedExercise } as HttpResponse)); jest.spyOn(svgRenderer, 'convertRenderedSVGToPNG').mockReturnValue(new Blob()); // @ts-ignore const classDiagram: UMLModel = testClassDiagram as UMLModel;