@if (!isDownloadingLink) {
-
+
{{ attachment.name }}
}
diff --git a/src/main/webapp/app/overview/course-lectures/course-lecture-details.component.ts b/src/main/webapp/app/overview/course-lectures/course-lecture-details.component.ts
index 28f73df62dc5..14cc75d10ef7 100644
--- a/src/main/webapp/app/overview/course-lectures/course-lecture-details.component.ts
+++ b/src/main/webapp/app/overview/course-lectures/course-lecture-details.component.ts
@@ -128,10 +128,10 @@ export class CourseLectureDetailsComponent extends AbstractScienceComponent impl
return attachment.link.split('.').pop()!;
}
- downloadAttachment(downloadUrl?: string): void {
- if (!this.isDownloadingLink && downloadUrl) {
+ downloadAttachment(downloadUrl?: string, downloadName?: string): void {
+ if (!this.isDownloadingLink && downloadUrl && downloadName) {
this.isDownloadingLink = downloadUrl;
- this.fileService.downloadFile(this.fileService.replaceLectureAttachmentPrefixAndUnderscores(downloadUrl));
+ this.fileService.downloadFileByAttachmentName(downloadUrl, downloadName);
this.isDownloadingLink = undefined;
}
}
diff --git a/src/main/webapp/app/shared/http/file.service.ts b/src/main/webapp/app/shared/http/file.service.ts
index 05960940f7aa..1c9f264f41d3 100644
--- a/src/main/webapp/app/shared/http/file.service.ts
+++ b/src/main/webapp/app/shared/http/file.service.ts
@@ -82,6 +82,23 @@ export class FileService {
return newWindow;
}
+ /**
+ * Downloads the file from the provided downloadUrl and the attachment name
+ *
+ * @param downloadUrl url that is stored in the attachment model
+ * @param downloadName the name given to the attachment
+ */
+ downloadFileByAttachmentName(downloadUrl: string, downloadName: string) {
+ const downloadUrlComponents = downloadUrl.split('/');
+ // take the last element
+ const extension = downloadUrlComponents.pop()!.split('.').pop();
+ const restOfUrl = downloadUrlComponents.join('/');
+ const normalizedDownloadUrl = restOfUrl + '/' + encodeURIComponent(downloadName + '.' + extension);
+ const newWindow = window.open('about:blank');
+ newWindow!.location.href = normalizedDownloadUrl;
+ return newWindow;
+ }
+
/**
* Downloads the merged PDF file.
*
@@ -124,12 +141,4 @@ export class FileService {
replaceAttachmentPrefixAndUnderscores(link: string): string {
return link.replace(/AttachmentUnit_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}_/, '').replace(/_/g, ' ');
}
-
- /**
- * Removes the prefix from the file name, and replaces underscore with spaces
- * @param link
- */
- replaceLectureAttachmentPrefixAndUnderscores(link: string): string {
- return link.replace(/LectureAttachment_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}_/, '').replace(/_/g, ' ');
- }
}
diff --git a/src/test/java/de/tum/cit/aet/artemis/lecture/AttachmentResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/lecture/AttachmentResourceIntegrationTest.java
index 001b2ea8cda3..1bae5e4927f6 100644
--- a/src/test/java/de/tum/cit/aet/artemis/lecture/AttachmentResourceIntegrationTest.java
+++ b/src/test/java/de/tum/cit/aet/artemis/lecture/AttachmentResourceIntegrationTest.java
@@ -1,5 +1,6 @@
package de.tum.cit.aet.artemis.lecture;
+import static org.apache.velocity.shaded.commons.io.FilenameUtils.getExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -69,8 +70,11 @@ void initTestCase() {
void createAttachment() throws Exception {
Attachment actualAttachment = request.postWithMultipartFile("/api/attachments", attachment, "attachment",
new MockMultipartFile("file", "test.txt", MediaType.TEXT_PLAIN_VALUE, "testContent".getBytes()), Attachment.class, HttpStatus.CREATED);
- assertThat(actualAttachment.getLink()).isNotNull();
- MvcResult file = request.performMvcRequest(get(actualAttachment.getLink())).andExpect(status().isOk()).andExpect(content().contentType(MediaType.TEXT_PLAIN_VALUE))
+ String actualLink = actualAttachment.getLink();
+ assertThat(actualLink).isNotNull();
+ // getLectureAttachment uses the provided file name to fetch the attachment which has that attachment name (not filename)
+ String linkWithCorrectFileName = actualLink.substring(0, actualLink.lastIndexOf('/') + 1) + attachment.getName() + "." + getExtension(actualAttachment.getLink());
+ MvcResult file = request.performMvcRequest(get(linkWithCorrectFileName)).andExpect(status().isOk()).andExpect(content().contentType(MediaType.TEXT_PLAIN_VALUE))
.andReturn();
assertThat(file.getResponse().getContentAsByteArray()).isNotEmpty();
var expectedAttachment = attachmentRepository.findById(actualAttachment.getId()).orElseThrow();
diff --git a/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit.component.spec.ts b/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit.component.spec.ts
index a65980b3725a..287b08031ac1 100644
--- a/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit.component.spec.ts
+++ b/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit.component.spec.ts
@@ -82,7 +82,7 @@ describe('AttachmentUnitComponent', () => {
});
it('should handle download', () => {
- const downloadFileSpy = jest.spyOn(fileService, 'downloadFile');
+ const downloadFileSpy = jest.spyOn(fileService, 'downloadFileByAttachmentName');
const onCompletionEmitSpy = jest.spyOn(component.onCompletion, 'emit');
fixture.detectChanges();
@@ -113,7 +113,7 @@ describe('AttachmentUnitComponent', () => {
});
it('should download attachment when clicked', () => {
- const downloadFileSpy = jest.spyOn(fileService, 'downloadFile');
+ const downloadFileSpy = jest.spyOn(fileService, 'downloadFileByAttachmentName');
fixture.detectChanges();
diff --git a/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts b/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts
index 0ec3ea836ec9..825eefd0ef1b 100644
--- a/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts
+++ b/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts
@@ -377,7 +377,7 @@ describe('LectureAttachmentsComponent', () => {
fixture.detectChanges();
comp.isDownloadingAttachmentLink = undefined;
expect(comp.isDownloadingAttachmentLink).toBeUndefined();
- comp.downloadAttachment('https://my/own/download/url');
+ comp.downloadAttachment('https://my/own/download/url', 'test');
expect(comp.isDownloadingAttachmentLink).toBeUndefined();
}));
diff --git a/src/test/javascript/spec/component/overview/course-lectures/course-lecture-details.component.spec.ts b/src/test/javascript/spec/component/overview/course-lectures/course-lecture-details.component.spec.ts
index 101871555716..fce4233eb246 100644
--- a/src/test/javascript/spec/component/overview/course-lectures/course-lecture-details.component.spec.ts
+++ b/src/test/javascript/spec/component/overview/course-lectures/course-lecture-details.component.spec.ts
@@ -279,13 +279,13 @@ describe('CourseLectureDetailsComponent', () => {
it('should download file for attachment', fakeAsync(() => {
const fileService = TestBed.inject(FileService);
- const downloadFileSpy = jest.spyOn(fileService, 'downloadFile');
+ const downloadFileSpy = jest.spyOn(fileService, 'downloadFileByAttachmentName');
const attachment = getAttachmentUnit(lecture, 1, dayjs()).attachment!;
- courseLecturesDetailsComponent.downloadAttachment(attachment.link);
+ courseLecturesDetailsComponent.downloadAttachment(attachment.link, attachment.name);
expect(downloadFileSpy).toHaveBeenCalledOnce();
- expect(downloadFileSpy).toHaveBeenCalledWith(attachment.link);
+ expect(downloadFileSpy).toHaveBeenCalledWith(attachment.link, attachment.name);
expect(courseLecturesDetailsComponent.isDownloadingLink).toBeUndefined();
}));
diff --git a/src/test/javascript/spec/component/shared/http/file.service.spec.ts b/src/test/javascript/spec/component/shared/http/file.service.spec.ts
index 68a2b56a7faf..843fd61a12d3 100644
--- a/src/test/javascript/spec/component/shared/http/file.service.spec.ts
+++ b/src/test/javascript/spec/component/shared/http/file.service.spec.ts
@@ -3,6 +3,7 @@ import { HttpTestingController, provideHttpClientTesting } from '@angular/common
import { TestBed } from '@angular/core/testing';
import { v4 as uuid } from 'uuid';
import { provideHttpClient } from '@angular/common/http';
+import { ProgrammingLanguage, ProjectType } from 'app/entities/programming/programming-exercise.model';
jest.mock('uuid', () => ({
v4: jest.fn(),
@@ -77,4 +78,157 @@ describe('FileService', () => {
expect(v4Mock).toHaveBeenCalledTimes(3);
});
});
+
+ describe('getTemplateFile', () => {
+ it('should fetch the template file without project type', () => {
+ const language = ProgrammingLanguage.JAVA;
+ const expectedUrl = `api/files/templates/JAVA`;
+ const response = 'template content';
+
+ fileService.getTemplateFile(language).subscribe((data) => {
+ expect(data).toEqual(response);
+ });
+
+ const req = httpMock.expectOne({
+ url: expectedUrl,
+ method: 'GET',
+ });
+ expect(req.request.responseType).toBe('text');
+ req.flush(response);
+ });
+
+ it('should fetch the template file with project type', () => {
+ const language = ProgrammingLanguage.JAVA;
+ const projectType = ProjectType.PLAIN_MAVEN;
+ const expectedUrl = `api/files/templates/JAVA/PLAIN_MAVEN`;
+ const response = 'template content';
+
+ fileService.getTemplateFile(language, projectType).subscribe((data) => {
+ expect(data).toEqual(response);
+ });
+
+ const req = httpMock.expectOne({
+ url: expectedUrl,
+ method: 'GET',
+ });
+ expect(req.request.responseType).toBe('text');
+ req.flush(response);
+ });
+ });
+
+ describe('downloadMergedFile', () => {
+ it('should download the merged PDF file', () => {
+ const lectureId = 123;
+ const expectedUrl = `api/files/attachments/lecture/${lectureId}/merge-pdf`;
+ const blobResponse = new Blob(['PDF content'], { type: 'application/pdf' });
+
+ fileService.downloadMergedFile(lectureId).subscribe((response) => {
+ expect(response.body).toEqual(blobResponse);
+ expect(response.status).toBe(200);
+ });
+
+ const req = httpMock.expectOne({
+ url: expectedUrl,
+ method: 'GET',
+ });
+ expect(req.request.responseType).toBe('blob');
+ req.flush(blobResponse, { status: 200, statusText: 'OK' });
+ });
+ });
+
+ describe('getAeolusTemplateFile', () => {
+ it('should fetch the aeolus template file with all parameters', () => {
+ const language = ProgrammingLanguage.PYTHON;
+ const projectType = ProjectType.PLAIN;
+ const staticAnalysis = true;
+ const sequentialRuns = false;
+ const coverage = true;
+ const expectedUrl = `api/files/aeolus/templates/PYTHON/PLAIN?staticAnalysis=true&sequentialRuns=false&testCoverage=true`;
+ const response = 'aeolus template content';
+
+ fileService.getAeolusTemplateFile(language, projectType, staticAnalysis, sequentialRuns, coverage).subscribe((data) => {
+ expect(data).toEqual(response);
+ });
+
+ const req = httpMock.expectOne({
+ url: expectedUrl,
+ method: 'GET',
+ });
+ expect(req.request.responseType).toBe('text');
+ req.flush(response);
+ });
+
+ it('should fetch the aeolus template file with missing optional parameters', () => {
+ const expectedUrl = `api/files/aeolus/templates/PYTHON?staticAnalysis=false&sequentialRuns=false&testCoverage=false`;
+ const response = 'aeolus template content';
+
+ fileService.getAeolusTemplateFile(ProgrammingLanguage.PYTHON).subscribe((data) => {
+ expect(data).toEqual(response);
+ });
+
+ const req = httpMock.expectOne({
+ url: expectedUrl,
+ method: 'GET',
+ });
+ expect(req.request.responseType).toBe('text');
+ req.flush(response);
+ });
+ });
+
+ describe('getTemplateCodeOfConduct', () => {
+ it('should fetch the template code of conduct', () => {
+ const expectedUrl = `api/files/templates/code-of-conduct`;
+ const response = 'code of conduct content';
+
+ fileService.getTemplateCodeOfCondcut().subscribe((data) => {
+ expect(data.body).toEqual(response);
+ });
+
+ const req = httpMock.expectOne({
+ url: expectedUrl,
+ method: 'GET',
+ });
+ expect(req.request.responseType).toBe('text');
+ req.flush(response);
+ });
+ });
+
+ describe('downloadFile', () => {
+ it('should open a new window with the normalized URL', () => {
+ const downloadUrl = 'http://example.com/files/some file name.txt';
+ const encodedUrl = 'http://example.com/files/some%20file%20name.txt';
+ const newWindowMock = { location: { href: '' } } as Window;
+
+ jest.spyOn(window, 'open').mockReturnValue(newWindowMock);
+
+ const newWindow = fileService.downloadFile(downloadUrl);
+ expect(newWindow).not.toBeNull();
+ expect(newWindow!.location.href).toBe(encodedUrl);
+ });
+ });
+
+ describe('downloadFileByAttachmentName', () => {
+ it('should open a new window with the normalized URL and attachment name', () => {
+ const downloadUrl = 'http://example.com/files/attachment.txt';
+ const downloadName = 'newAttachment';
+ const encodedUrl = 'http://example.com/files/newAttachment.txt';
+ const newWindowMock = { location: { href: '' } } as Window;
+
+ jest.spyOn(window, 'open').mockReturnValue(newWindowMock);
+
+ const newWindow = fileService.downloadFileByAttachmentName(downloadUrl, downloadName);
+ expect(newWindow).not.toBeNull();
+ expect(newWindow!.location.href).toBe(encodedUrl);
+ });
+ });
+
+ describe('replaceAttachmentPrefixAndUnderscores', () => {
+ it('should replace the prefix and underscores in a file name', () => {
+ const fileName = 'AttachmentUnit_2023-01-01T00-00-00-000_some_file_name';
+ const expected = 'some file name';
+
+ const result = fileService.replaceAttachmentPrefixAndUnderscores(fileName);
+ expect(result).toBe(expected);
+ });
+ });
});
diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-file.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-file.service.ts
index ae528e16f0ea..0e13c1e21090 100644
--- a/src/test/javascript/spec/helpers/mocks/service/mock-file.service.ts
+++ b/src/test/javascript/spec/helpers/mocks/service/mock-file.service.ts
@@ -8,6 +8,9 @@ export class MockFileService {
downloadFile = () => {
return { subscribe: (fn: (value: any) => void) => fn({ body: new Window() }) };
};
+ downloadFileByAttachmentName = () => {
+ return { subscribe: (fn: (value: any) => void) => fn({ body: new Window() }) };
+ };
getTemplateFile = () => {
return of();
From fef20284ea9fa567c2135b5f4e32e4c03af8ab4b Mon Sep 17 00:00:00 2001
From: Stephan Krusche
Date: Mon, 2 Dec 2024 22:01:02 +0100
Subject: [PATCH 05/38] Development: Update Spring Boot to 3.4.0 (#9852)
---
build.gradle | 54 +++++++----------
docs/dev/guidelines/server-tests.rst | 14 ++---
gradle.properties | 8 ++-
.../buildagent/BuildAgentConfiguration.java | 4 +-
...on.java => EurekaClientConfiguration.java} | 37 +++++++-----
.../core/config/LiquibaseConfiguration.java | 4 +-
.../config/RestTemplateConfiguration.java | 5 --
.../communication/PostingServiceUnitTest.java | 4 +-
...InternalAuthenticationIntegrationTest.java | 8 +--
.../UserJenkinsGitlabIntegrationTest.java | 2 +-
.../connector/GitlabRequestMockProvider.java | 4 ++
.../connector/JenkinsRequestMockProvider.java | 11 ++--
...ountResourceWithGitLabIntegrationTest.java | 3 +-
.../aet/artemis/exam/ExamIntegrationTest.java | 2 +-
.../ExamParticipationIntegrationTest.java | 2 +-
.../artemis/exam/ExamUserIntegrationTest.java | 2 +-
.../exam/StudentExamIntegrationTest.java | 2 +-
.../ParticipationIntegrationTest.java | 4 +-
.../service/ParticipationServiceTest.java | 2 +-
...ogrammingIntegrationJenkinsGitlabTest.java | 4 --
...encyCheckGitlabJenkinsIntegrationTest.java | 2 +-
.../CourseGitlabJenkinsIntegrationTest.java | 2 +-
...gExerciseGitlabJenkinsIntegrationTest.java | 2 +-
...gExerciseIntegrationJenkinsGitlabTest.java | 2 +-
...rammingExerciseIntegrationTestService.java | 14 ++---
...ammingExerciseTemplateIntegrationTest.java | 4 +-
.../programming/ProgrammingExerciseTest.java | 4 +-
...AndResultGitlabJenkinsIntegrationTest.java | 2 +-
.../ProgrammingSubmissionIntegrationTest.java | 16 ++---
.../icl/LocalVCInfoContributorTest.java | 4 +-
.../JenkinsAuthorizationInterceptorTest.java | 2 +-
.../JenkinsJobPermissionServiceTest.java | 2 +-
.../service/JenkinsJobServiceTest.java | 2 +-
.../service/JenkinsServiceTest.java | 2 +-
.../util/ProgrammingExerciseTestService.java | 2 +-
.../base/AbstractArtemisIntegrationTest.java | 58 +++++++++----------
...ringIntegrationGitlabCIGitlabSamlTest.java | 17 +++---
...tractSpringIntegrationIndependentTest.java | 10 ++--
...actSpringIntegrationJenkinsGitlabTest.java | 21 ++++---
...ctSpringIntegrationLocalCILocalVCTest.java | 18 +++---
40 files changed, 176 insertions(+), 186 deletions(-)
rename src/main/java/de/tum/cit/aet/artemis/core/config/{EurekaClientRestTemplateConfiguration.java => EurekaClientConfiguration.java} (52%)
diff --git a/build.gradle b/build.gradle
index 8c830f0784f8..618553ed0366 100644
--- a/build.gradle
+++ b/build.gradle
@@ -212,17 +212,16 @@ repositories {
maven {
url "https://build.shibboleth.net/maven/releases"
}
- // required for latest jgit 7.0.0 dependency
- // TODO: remove this when jgit is available in the official maven repository
+ // TODO: remove this when spring cloud is available in the official maven repository
maven {
- url "https://repo.eclipse.org/content/repositories/jgit-releases"
+ url "https://repo.spring.io/milestone"
}
}
ext["jackson.version"] = fasterxml_version
ext["junit-jupiter.version"] = junit_version
-ext { qDoxVersionReusable = "com.thoughtworks.qdox:qdox:2.1.0" }
+ext { qDoxVersionReusable = "com.thoughtworks.qdox:qdox:2.2.0" }
ext { springBootStarterWeb = "org.springframework.boot:spring-boot-starter-web:${spring_boot_version}" }
dependencies {
@@ -231,9 +230,8 @@ dependencies {
// implementation "com.offbytwo.jenkins:jenkins-client:0.3.8"
implementation files("libs/jenkins-client-0.4.1.jar")
// The following 4 dependencies are explicitly integrated as transitive dependencies of jenkins-client-0.4.0.jar
- // NOTE: we cannot upgrade to the latest version for org.apache.httpcomponents because of exceptions in Docker Java
- implementation "org.apache.httpcomponents.client5:httpclient5:5.3.1" // also used by Docker Java
- implementation "org.apache.httpcomponents.core5:httpcore5:5.2.5"
+ implementation "org.apache.httpcomponents.client5:httpclient5:5.4.1"
+ implementation "org.apache.httpcomponents.core5:httpcore5:5.3.1"
implementation "org.apache.httpcomponents:httpmime:4.5.14"
implementation("org.dom4j:dom4j:2.1.4") {
// Note: avoid org.xml.sax.SAXNotRecognizedException: unrecognized feature http://xml.org/sax/features/external-general-entities
@@ -246,7 +244,7 @@ dependencies {
exclude module: "jaxb-api"
}
- implementation "org.gitlab4j:gitlab4j-api:6.0.0-rc.6"
+ implementation "org.gitlab4j:gitlab4j-api:6.0.0-rc.7"
implementation "de.jplag:jplag:${jplag_version}"
@@ -268,7 +266,7 @@ dependencies {
implementation "org.apache.lucene:lucene-queryparser:${lucene_version}"
implementation "org.apache.lucene:lucene-core:${lucene_version}"
implementation "org.apache.lucene:lucene-analyzers-common:${lucene_version}"
- implementation "com.google.protobuf:protobuf-java:4.28.3"
+ implementation "com.google.protobuf:protobuf-java:4.29.0"
// we have to override those values to use the latest version
implementation "org.slf4j:jcl-over-slf4j:${slf4j_version}"
@@ -279,7 +277,7 @@ dependencies {
}
}
- implementation "org.apache.logging.log4j:log4j-to-slf4j:2.24.1"
+ implementation "org.apache.logging.log4j:log4j-to-slf4j:2.24.2"
// Note: spring-security-lti13 does not work with jakarta yet, so we built our own custom version and declare its transitive dependencies below
// implementation "uk.ac.ox.ctl:spring-security-lti13:0.1.11"
@@ -327,7 +325,7 @@ dependencies {
// required by Saml2
implementation "org.apache.santuario:xmlsec:4.0.3"
- implementation "org.jsoup:jsoup:1.18.1"
+ implementation "org.jsoup:jsoup:1.18.2"
implementation "commons-codec:commons-codec:1.17.1" // needed for spring security saml2
// TODO: decide if we want to use OpenAPI and Swagger v3
@@ -335,16 +333,18 @@ dependencies {
// implementation "org.springdoc:springdoc-openapi-ui:1.8.0"
// use the latest version to avoid security vulnerabilities
- implementation "org.springframework:spring-webmvc:6.1.14"
+ implementation "org.springframework:spring-webmvc:${spring_framework_version}"
implementation "com.vdurmont:semver4j:3.1.0"
implementation "com.github.docker-java:docker-java-core:${docker_java_version}"
- implementation "com.github.docker-java:docker-java-transport-httpclient5:${docker_java_version}"
+ // Note: we explicitly use docker-java-transport-zerodep, because docker-java-transport-httpclient5 uses an outdated http5 version which is not compatible with Spring Boot >= 3.4.0
+ implementation "com.github.docker-java:docker-java-transport-zerodep:${docker_java_version}"
// use newest version of commons-compress to avoid security issues through outdated dependencies
implementation "org.apache.commons:commons-compress:1.27.1"
+
// import JHipster dependencies BOM
implementation platform("tech.jhipster:jhipster-dependencies:${jhipster_dependencies_version}")
@@ -403,24 +403,19 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:${spring_boot_version}"
implementation "org.springframework.ldap:spring-ldap-core:3.2.8"
- implementation "org.springframework.data:spring-data-ldap:3.3.5"
+ implementation "org.springframework.data:spring-data-ldap:3.4.0"
- implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:4.1.3") {
+ implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:${spring_cloud_version}") {
// NOTE: these modules contain security vulnerabilities and are not needed
exclude module: "commons-jxpath"
exclude module: "woodstox-core"
}
- implementation "org.springframework.cloud:spring-cloud-starter-config:4.1.3"
- implementation "org.springframework.cloud:spring-cloud-commons:4.1.4"
+ implementation "org.springframework.cloud:spring-cloud-starter-config:${spring_cloud_version}"
+ implementation "org.springframework.cloud:spring-cloud-commons:${spring_cloud_version}"
+ implementation "io.netty:netty-all:4.1.115.Final"
implementation "io.projectreactor.netty:reactor-netty:1.2.0"
- implementation("io.netty:netty-common") {
- version {
- strictly netty_version
- }
- }
-
- implementation "org.springframework:spring-messaging:6.1.14"
+ implementation "org.springframework:spring-messaging:${spring_framework_version}"
implementation "org.springframework.retry:spring-retry:2.0.10"
implementation "org.springframework.security:spring-security-config:${spring_security_version}"
@@ -428,7 +423,6 @@ dependencies {
implementation "org.springframework.security:spring-security-core:${spring_security_version}"
implementation "org.springframework.security:spring-security-oauth2-core:${spring_security_version}"
implementation "org.springframework.security:spring-security-oauth2-client:${spring_security_version}"
- implementation "org.springframework.security:spring-security-oauth2-resource-server:${spring_security_version}"
// use newest version of nimbus-jose-jwt to avoid security issues through outdated dependencies
implementation "com.nimbusds:nimbus-jose-jwt:9.47"
@@ -547,18 +541,10 @@ dependencies {
testImplementation "org.gradle:gradle-tooling-api:8.11.1"
testImplementation "org.apache.maven.surefire:surefire-report-parser:3.5.2"
testImplementation "com.opencsv:opencsv:5.9"
- testImplementation("io.zonky.test:embedded-database-spring-test:2.5.1") {
+ testImplementation("io.zonky.test:embedded-database-spring-test:2.6.0") {
exclude group: "org.testcontainers", module: "mariadb"
exclude group: "org.testcontainers", module: "mssqlserver"
}
- testImplementation "org.testcontainers:testcontainers:${testcontainer_version}"
- testImplementation "org.testcontainers:mysql:${testcontainer_version}"
- testImplementation "org.testcontainers:postgresql:${testcontainer_version}"
- testImplementation "org.testcontainers:testcontainers:${testcontainer_version}"
- testImplementation "org.testcontainers:junit-jupiter:${testcontainer_version}"
- testImplementation "org.testcontainers:jdbc:${testcontainer_version}"
- testImplementation "org.testcontainers:database-commons:${testcontainer_version}"
-
testImplementation "com.tngtech.archunit:archunit:1.3.0"
testImplementation("org.skyscreamer:jsonassert:1.5.3") {
exclude module: "android-json"
diff --git a/docs/dev/guidelines/server-tests.rst b/docs/dev/guidelines/server-tests.rst
index 1e95860b8064..ef2b61d586a4 100644
--- a/docs/dev/guidelines/server-tests.rst
+++ b/docs/dev/guidelines/server-tests.rst
@@ -151,19 +151,19 @@ Follow these tips to write performant tests:
* Limit object creation in tests and the test setup.
-6. Avoid using @MockBean
-=========================
+6. Avoid using @MockitoBean
+===========================
-Do not use the ``@SpyBean`` or ``@MockBean`` annotation unless absolutely necessary or possibly in an abstract Superclass. `Here `__ you can see why in more detail.
-Whenever``@MockBean`` appears in a class, the application context cache gets marked as dirty, meaning the runner will clean the cache after finishing the test class. The application context is restarted, which leads to an additional server start with runtime overhead.
+Do not use the ``@MockitoSpyBean`` or ``@MockitoBean`` annotation unless absolutely necessary or possibly in an abstract Superclass. `Here `__ you can see why in more detail.
+Whenever``@MockitoBean`` appears in a class, the application context cache gets marked as dirty, meaning the runner will clean the cache after finishing the test class. The application context is restarted, which leads to an additional server start with runtime overhead.
We want to keep the number of server starts minimal.
-Below is an example of how to replace a ``@SpyBean``. To test an edge case where an ``IOException`` is thrown, we mocked the service method so it threw an Exception.
+Below is an example of how to replace a ``@MockitoSpyBean``. To test an edge case where an ``IOException`` is thrown, we mocked the service method so it threw an Exception.
.. code-block:: java
class TestExport extends AbstractSpringIntegrationIndependentTest {
- @SpyBean
+ @MockitoSpyBean
private FileUploadSubmissionExportService fileUploadSubmissionExportService;
@Test
@@ -174,7 +174,7 @@ Below is an example of how to replace a ``@SpyBean``. To test an edge case where
}
}
-To avoid new SpyBeans, we now use `static mocks `__. Upon examining the ``export()`` method, we find a ``File.newOutputStream(..)`` call.
+To avoid new MockitoSpyBeans, we now use `static mocks `__. Upon examining the ``export()`` method, we find a ``File.newOutputStream(..)`` call.
Now, instead of mocking the whole service, we can mock the static method:
.. code-block:: java
diff --git a/gradle.properties b/gradle.properties
index b234044bcc8f..d3efd6837318 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -9,6 +9,10 @@ npm_version=10.8.0
jhipster_dependencies_version=8.7.2
spring_boot_version=3.3.6
spring_security_version=6.3.5
+spring_boot_version=3.4.0
+spring_framework_version=6.2.0
+spring_cloud_version=4.2.0-RC1
+spring_security_version=6.4.1
# TODO: upgrading to 6.6.0 currently leads to issues due to internal changes in Hibernate and potentially wrong use in Artemis server code
hibernate_version=6.4.10.Final
# TODO: can we update to 5.x?
@@ -16,7 +20,7 @@ opensaml_version=4.3.2
jwt_version=0.12.6
jaxb_runtime_version=4.0.5
hazelcast_version=5.5.0
-fasterxml_version=2.18.1
+fasterxml_version=2.18.2
jgit_version=7.0.0.202409031743-r
sshd_version=2.14.0
checkstyle_version=10.20.1
@@ -25,7 +29,7 @@ jplag_version=5.1.0
# NOTE: we do not need to use the latest version 9.x here as long as Stanford CoreNLP does not reference it
lucene_version=8.11.4
slf4j_version=2.0.16
-sentry_version=7.18.0
+sentry_version=7.18.1
liquibase_version=4.30.0
docker_java_version=3.4.0
logback_version=1.5.12
diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/BuildAgentConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/BuildAgentConfiguration.java
index b58d6dbd2fa8..186ca2433639 100644
--- a/src/main/java/de/tum/cit/aet/artemis/buildagent/BuildAgentConfiguration.java
+++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/BuildAgentConfiguration.java
@@ -23,8 +23,8 @@
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.DockerClientImpl;
-import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient;
+import com.github.dockerjava.zerodep.ZerodepDockerHttpClient;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import de.tum.cit.aet.artemis.core.config.ProgrammingLanguageConfiguration;
@@ -151,7 +151,7 @@ public ExecutorService localCIBuildExecutorService() {
public DockerClient dockerClient() {
log.debug("Create bean dockerClient");
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().withDockerHost(dockerConnectionUri).build();
- DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder().dockerHost(config.getDockerHost()).sslConfig(config.getSSLConfig()).build();
+ DockerHttpClient httpClient = new ZerodepDockerHttpClient.Builder().dockerHost(config.getDockerHost()).sslConfig(config.getSSLConfig()).build();
DockerClient dockerClient = DockerClientImpl.getInstance(config, httpClient);
log.debug("Docker client created with connection URI: {}", dockerConnectionUri);
diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/EurekaClientRestTemplateConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/EurekaClientConfiguration.java
similarity index 52%
rename from src/main/java/de/tum/cit/aet/artemis/core/config/EurekaClientRestTemplateConfiguration.java
rename to src/main/java/de/tum/cit/aet/artemis/core/config/EurekaClientConfiguration.java
index b66038d20469..26634548eb3b 100644
--- a/src/main/java/de/tum/cit/aet/artemis/core/config/EurekaClientRestTemplateConfiguration.java
+++ b/src/main/java/de/tum/cit/aet/artemis/core/config/EurekaClientConfiguration.java
@@ -8,38 +8,42 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.configuration.SSLContextFactory;
import org.springframework.cloud.configuration.TlsProperties;
+import org.springframework.cloud.netflix.eureka.RestClientTimeoutProperties;
+import org.springframework.cloud.netflix.eureka.http.DefaultEurekaClientHttpRequestFactorySupplier;
import org.springframework.cloud.netflix.eureka.http.EurekaClientHttpRequestFactorySupplier;
-import org.springframework.cloud.netflix.eureka.http.RestTemplateDiscoveryClientOptionalArgs;
-import org.springframework.cloud.netflix.eureka.http.RestTemplateTransportClientFactories;
+import org.springframework.cloud.netflix.eureka.http.RestClientDiscoveryClientOptionalArgs;
+import org.springframework.cloud.netflix.eureka.http.RestClientTransportClientFactories;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
+import org.springframework.web.client.RestClient;
/**
* This class is necessary to avoid using Jersey (which has an issue deserializing Eureka responses) after the spring boot upgrade.
- * It provides the RestTemplateTransportClientFactories and RestTemplateDiscoveryClientOptionalArgs that would normally not be instantiated
+ * It provides the RestClientTransportClientFactories and RestClientDiscoveryClientOptionalArgs that would normally not be instantiated
* when Jersey is found by Eureka.
*/
@Profile({ PROFILE_CORE, PROFILE_BUILDAGENT })
@Configuration
-public class EurekaClientRestTemplateConfiguration {
+public class EurekaClientConfiguration {
- private static final Logger log = LoggerFactory.getLogger(EurekaClientRestTemplateConfiguration.class);
+ private static final Logger log = LoggerFactory.getLogger(EurekaClientConfiguration.class);
/**
- * Configures and returns {@link RestTemplateDiscoveryClientOptionalArgs} for Eureka client communication,
+ * Configures and returns {@link RestClientDiscoveryClientOptionalArgs} for Eureka client communication,
* with optional TLS/SSL setup based on provided configuration.
*
- * This method leverages the {@link EurekaClientHttpRequestFactorySupplier} to configure the RestTemplate
+ * This method leverages the {@link EurekaClientHttpRequestFactorySupplier} to configure the RestClient
* specifically for Eureka client interactions. If TLS is enabled in the provided {@link TlsProperties},
* a custom SSLContext is set up to ensure secure communication.
*
*
- * @param tlsProperties The TLS configuration properties, used to check if TLS is enabled and to configure it accordingly.
- * @param eurekaClientHttpRequestFactorySupplier Supplies the HTTP request factory for the Eureka client RestTemplate.
- * @return A configured instance of {@link RestTemplateDiscoveryClientOptionalArgs} for Eureka client,
+ * @param tlsProperties The TLS configuration properties, used to check if TLS is enabled and to configure it accordingly.
+ * @param restClientBuilderProvider The provider for the {@link RestClient.Builder} instance, if available.
+ * @return A configured instance of {@link RestClientDiscoveryClientOptionalArgs} for Eureka client,
* potentially with SSL/TLS enabled if specified in the {@code tlsProperties}.
* @throws GeneralSecurityException If there's an issue with setting up the SSL/TLS context.
* @throws IOException If there's an I/O error during the setup.
@@ -47,12 +51,13 @@ public class EurekaClientRestTemplateConfiguration {
* @see EurekaClientHttpRequestFactorySupplier
*/
@Bean
- public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs(TlsProperties tlsProperties,
- EurekaClientHttpRequestFactorySupplier eurekaClientHttpRequestFactorySupplier) throws GeneralSecurityException, IOException {
- log.debug("Using RestTemplate for the Eureka client.");
+ public RestClientDiscoveryClientOptionalArgs restClientDiscoveryClientOptionalArgs(TlsProperties tlsProperties, ObjectProvider restClientBuilderProvider)
+ throws GeneralSecurityException, IOException {
+ log.debug("Using RestClient for the Eureka client.");
// The Eureka DiscoveryClientOptionalArgsConfiguration invokes a private method setupTLS.
// This code is taken from that method.
- var args = new RestTemplateDiscoveryClientOptionalArgs(eurekaClientHttpRequestFactorySupplier);
+ var supplier = new DefaultEurekaClientHttpRequestFactorySupplier(new RestClientTimeoutProperties());
+ var args = new RestClientDiscoveryClientOptionalArgs(supplier, () -> restClientBuilderProvider.getIfAvailable(RestClient::builder));
if (tlsProperties.isEnabled()) {
SSLContextFactory factory = new SSLContextFactory(tlsProperties);
args.setSSLContext(factory.createSSLContext());
@@ -61,7 +66,7 @@ public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOption
}
@Bean
- public RestTemplateTransportClientFactories restTemplateTransportClientFactories(RestTemplateDiscoveryClientOptionalArgs optionalArgs) {
- return new RestTemplateTransportClientFactories(optionalArgs);
+ public RestClientTransportClientFactories restClientTransportClientFactories(RestClientDiscoveryClientOptionalArgs optionalArgs) {
+ return new RestClientTransportClientFactories(optionalArgs);
}
}
diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/LiquibaseConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/LiquibaseConfiguration.java
index 5aa0b02bcc36..5a0847aeedcb 100644
--- a/src/main/java/de/tum/cit/aet/artemis/core/config/LiquibaseConfiguration.java
+++ b/src/main/java/de/tum/cit/aet/artemis/core/config/LiquibaseConfiguration.java
@@ -75,14 +75,14 @@ public SpringLiquibase liquibase(@LiquibaseDataSource ObjectProvider
SpringLiquibase liquibase = SpringLiquibaseUtil.createSpringLiquibase(liquibaseDataSource.getIfAvailable(), liquibaseProperties, dataSource, dataSourceProperties);
Scope.setScopeManager(new SingletonScopeManager());
liquibase.setChangeLog("classpath:config/liquibase/master.xml");
- liquibase.setContexts(liquibaseProperties.getContexts());
+ liquibase.setContexts(liquibaseProperties.getContexts() != null ? liquibaseProperties.getContexts().getFirst() : null);
liquibase.setDefaultSchema(liquibaseProperties.getDefaultSchema());
liquibase.setLiquibaseSchema(liquibaseProperties.getLiquibaseSchema());
liquibase.setLiquibaseTablespace(liquibaseProperties.getLiquibaseTablespace());
liquibase.setDatabaseChangeLogLockTable(liquibaseProperties.getDatabaseChangeLogLockTable());
liquibase.setDatabaseChangeLogTable(liquibaseProperties.getDatabaseChangeLogTable());
liquibase.setDropFirst(liquibaseProperties.isDropFirst());
- liquibase.setLabelFilter(liquibaseProperties.getLabelFilter());
+ liquibase.setLabelFilter(liquibaseProperties.getLabelFilter() != null ? liquibaseProperties.getLabelFilter().getFirst() : null);
liquibase.setChangeLogParameters(liquibaseProperties.getParameters());
liquibase.setRollbackFile(liquibaseProperties.getRollbackFile());
liquibase.setTestRollbackOnUpdate(liquibaseProperties.isTestRollbackOnUpdate());
diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/RestTemplateConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/RestTemplateConfiguration.java
index 0730267fb379..922e3f072f23 100644
--- a/src/main/java/de/tum/cit/aet/artemis/core/config/RestTemplateConfiguration.java
+++ b/src/main/java/de/tum/cit/aet/artemis/core/config/RestTemplateConfiguration.java
@@ -9,7 +9,6 @@
import jakarta.validation.constraints.NotNull;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@@ -43,14 +42,12 @@ public class RestTemplateConfiguration {
@Bean
@Profile("gitlab | gitlabci")
- @Autowired // ok
public RestTemplate gitlabRestTemplate(GitLabAuthorizationInterceptor gitlabInterceptor) {
return initializeRestTemplateWithInterceptors(gitlabInterceptor, createRestTemplate());
}
@Bean
@Profile("jenkins")
- @Autowired // ok
public RestTemplate jenkinsRestTemplate(JenkinsAuthorizationInterceptor jenkinsInterceptor) {
return initializeRestTemplateWithInterceptors(jenkinsInterceptor, createRestTemplate());
}
@@ -89,14 +86,12 @@ public RestTemplate pyrisRestTemplate(PyrisAuthorizationInterceptor pyrisAuthori
@Bean
@Profile("gitlab | gitlabci")
- @Autowired // ok
public RestTemplate shortTimeoutGitlabRestTemplate(GitLabAuthorizationInterceptor gitlabInterceptor) {
return initializeRestTemplateWithInterceptors(gitlabInterceptor, createShortTimeoutRestTemplate());
}
@Bean
@Profile("jenkins")
- @Autowired // ok
public RestTemplate shortTimeoutJenkinsRestTemplate(JenkinsAuthorizationInterceptor jenkinsInterceptor) {
return initializeRestTemplateWithInterceptors(jenkinsInterceptor, createShortTimeoutRestTemplate());
}
diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/PostingServiceUnitTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/PostingServiceUnitTest.java
index 554da34e0c7f..ced0b420a7a7 100644
--- a/src/test/java/de/tum/cit/aet/artemis/communication/PostingServiceUnitTest.java
+++ b/src/test/java/de/tum/cit/aet/artemis/communication/PostingServiceUnitTest.java
@@ -157,7 +157,7 @@ void testParseUserMentionsWithNonExistentUser() {
void testParseUserMentionsWithInvalidName() {
Course course = new Course();
String content = "[user]Test User 2(test_user_1)[/user]";
- User user = this.createUser("Test User 1", "test_user_1"); // Different name than mentioned
+ User user = createUser("Test User 1", "test_user_1"); // Different name than mentioned
setupUserRepository(Set.of("test_user_1"), Set.of(user));
when(authorizationCheckService.isAtLeastStudentInCourse(eq(course), any(User.class))).thenReturn(true);
@@ -169,7 +169,7 @@ void testParseUserMentionsWithInvalidName() {
void testParseUserMentionsWithUserNotInCourse() {
Course course = new Course();
String content = "[user]Test User 1(test_user_1)[/user]";
- User user = this.createUser("Test User 1", "test_user_1");
+ User user = createUser("Test User 1", "test_user_1");
setupUserRepository(Set.of("test_user_1"), Set.of(user));
when(authorizationCheckService.isAtLeastStudentInCourse(eq(course), any(User.class))).thenReturn(false);
diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java
index 7ade409a5287..3a756bd65ede 100644
--- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java
+++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java
@@ -37,12 +37,12 @@
import de.tum.cit.aet.artemis.core.security.SecurityUtils;
import de.tum.cit.aet.artemis.core.service.user.PasswordService;
import de.tum.cit.aet.artemis.core.util.CourseFactory;
-import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository;
import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService;
import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest;
import de.tum.cit.aet.artemis.tutorialgroup.util.TutorialGroupUtilService;
+// TODO: rewrite this test to use LocalVC instead of GitLab
class InternalAuthenticationIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest {
private static final String TEST_PREFIX = "internalauth";
@@ -81,17 +81,13 @@ class InternalAuthenticationIntegrationTest extends AbstractSpringIntegrationJen
private static final String USERNAME = TEST_PREFIX + "student1";
- private ProgrammingExercise programmingExercise;
-
@BeforeEach
void setUp() {
- jenkinsRequestMockProvider.enableMockingOfRequests(jenkinsServer);
+ jenkinsRequestMockProvider.enableMockingOfRequests(jenkinsServer, jenkinsJobPermissionsService);
userUtilService.addUsers(TEST_PREFIX, 1, 0, 0, 0);
Course course = programmingExerciseUtilService.addCourseWithOneProgrammingExercise();
courseUtilService.addOnlineCourseConfigurationToCourse(course);
- programmingExercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class);
- programmingExercise = programmingExerciseRepository.findWithEagerStudentParticipationsById(programmingExercise.getId()).orElseThrow();
final var userAuthority = new Authority(Role.STUDENT.getAuthority());
final var instructorAuthority = new Authority(Role.INSTRUCTOR.getAuthority());
diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserJenkinsGitlabIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserJenkinsGitlabIntegrationTest.java
index b302e83631f5..50ff7b932516 100644
--- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserJenkinsGitlabIntegrationTest.java
+++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserJenkinsGitlabIntegrationTest.java
@@ -66,7 +66,7 @@ class UserJenkinsGitlabIntegrationTest extends AbstractSpringIntegrationJenkinsG
void setUp() throws Exception {
userTestService.setup(TEST_PREFIX, this);
gitlabRequestMockProvider.enableMockingOfRequests();
- jenkinsRequestMockProvider.enableMockingOfRequests(jenkinsServer);
+ jenkinsRequestMockProvider.enableMockingOfRequests(jenkinsServer, jenkinsJobPermissionsService);
}
@AfterEach
diff --git a/src/test/java/de/tum/cit/aet/artemis/core/connector/GitlabRequestMockProvider.java b/src/test/java/de/tum/cit/aet/artemis/core/connector/GitlabRequestMockProvider.java
index 6ac2c36c00db..c551387bdb68 100644
--- a/src/test/java/de/tum/cit/aet/artemis/core/connector/GitlabRequestMockProvider.java
+++ b/src/test/java/de/tum/cit/aet/artemis/core/connector/GitlabRequestMockProvider.java
@@ -108,6 +108,8 @@
@Component
@Profile("gitlab")
+// Gitlab support will be removed in 8.0.0. Please migrate to LocalVC using e.g. the PR https://github.com/ls1intum/Artemis/pull/8972
+@Deprecated(since = "7.5.0", forRemoval = true)
public class GitlabRequestMockProvider {
private static final Logger log = LoggerFactory.getLogger(GitlabRequestMockProvider.class);
@@ -129,6 +131,7 @@ public class GitlabRequestMockProvider {
private MockRestServiceServer mockServerShortTimeout;
+ // NOTE: we currently cannot convert this into @MockitoSpyBean because then @InjectMocks doesn't work
@SpyBean
@InjectMocks
private GitLabApi gitLabApi;
@@ -154,6 +157,7 @@ public class GitlabRequestMockProvider {
@Mock
private PipelineApi pipelineApi;
+ // NOTE: we currently cannot convert this into @MockitoSpyBean because then @InjectMocks (see above) doesn't work
@SpyBean
private GitLabUserManagementService gitLabUserManagementService;
diff --git a/src/test/java/de/tum/cit/aet/artemis/core/connector/JenkinsRequestMockProvider.java b/src/test/java/de/tum/cit/aet/artemis/core/connector/JenkinsRequestMockProvider.java
index 827953bc0b68..2adaa451e563 100644
--- a/src/test/java/de/tum/cit/aet/artemis/core/connector/JenkinsRequestMockProvider.java
+++ b/src/test/java/de/tum/cit/aet/artemis/core/connector/JenkinsRequestMockProvider.java
@@ -22,12 +22,10 @@
import org.apache.http.client.HttpResponseException;
import org.hamcrest.Matchers;
-import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
@@ -70,12 +68,10 @@ public class JenkinsRequestMockProvider {
private MockRestServiceServer shortTimeoutMockServer;
- @SpyBean
- @InjectMocks
+ // will be assigned in enableMockingOfRequests(), can be used like a MockitoSpyBean
private JenkinsServer jenkinsServer;
- @SpyBean
- @InjectMocks
+ // will be assigned in enableMockingOfRequests(), can be used like a MockitoSpyBean
private JenkinsJobPermissionsService jenkinsJobPermissionsService;
@Autowired
@@ -98,10 +94,11 @@ public JenkinsRequestMockProvider(@Qualifier("jenkinsRestTemplate") RestTemplate
this.shortTimeoutRestTemplate.setInterceptors(List.of());
}
- public void enableMockingOfRequests(JenkinsServer jenkinsServer) {
+ public void enableMockingOfRequests(JenkinsServer jenkinsServer, JenkinsJobPermissionsService jenkinsJobPermissionsService) {
mockServer = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).bufferContent().build();
shortTimeoutMockServer = MockRestServiceServer.bindTo(shortTimeoutRestTemplate).ignoreExpectOrder(true).bufferContent().build();
this.jenkinsServer = jenkinsServer;
+ this.jenkinsJobPermissionsService = jenkinsJobPermissionsService;
closeable = MockitoAnnotations.openMocks(this);
}
diff --git a/src/test/java/de/tum/cit/aet/artemis/core/user/AccountResourceWithGitLabIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/user/AccountResourceWithGitLabIntegrationTest.java
index ea8267af3d4e..6c2f0472be45 100644
--- a/src/test/java/de/tum/cit/aet/artemis/core/user/AccountResourceWithGitLabIntegrationTest.java
+++ b/src/test/java/de/tum/cit/aet/artemis/core/user/AccountResourceWithGitLabIntegrationTest.java
@@ -24,6 +24,7 @@
import de.tum.cit.aet.artemis.core.user.util.UserFactory;
import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest;
+// TODO: rewrite this test to use LocalVC instead of GitLab
class AccountResourceWithGitLabIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest {
@Autowired
@@ -32,7 +33,7 @@ class AccountResourceWithGitLabIntegrationTest extends AbstractSpringIntegration
@BeforeEach
void setUp() {
gitlabRequestMockProvider.enableMockingOfRequests();
- jenkinsRequestMockProvider.enableMockingOfRequests(jenkinsServer);
+ jenkinsRequestMockProvider.enableMockingOfRequests(jenkinsServer, jenkinsJobPermissionsService);
}
@AfterEach
diff --git a/src/test/java/de/tum/cit/aet/artemis/exam/ExamIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exam/ExamIntegrationTest.java
index 52f93e2d6741..e86f315c26fc 100644
--- a/src/test/java/de/tum/cit/aet/artemis/exam/ExamIntegrationTest.java
+++ b/src/test/java/de/tum/cit/aet/artemis/exam/ExamIntegrationTest.java
@@ -991,7 +991,7 @@ void testDeleteExamWithOneTestRuns() throws Exception {
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testDeleteExamWithMultipleTestRuns() throws Exception {
gitlabRequestMockProvider.enableMockingOfRequests();
- jenkinsRequestMockProvider.enableMockingOfRequests(jenkinsServer);
+ jenkinsRequestMockProvider.enableMockingOfRequests(jenkinsServer, jenkinsJobPermissionsService);
var exam = examUtilService.addExam(course1);
exam = examUtilService.addTextModelingProgrammingExercisesToExam(exam, true, true);
diff --git a/src/test/java/de/tum/cit/aet/artemis/exam/ExamParticipationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exam/ExamParticipationIntegrationTest.java
index ec49d8ec78a7..c0bf3576b18b 100644
--- a/src/test/java/de/tum/cit/aet/artemis/exam/ExamParticipationIntegrationTest.java
+++ b/src/test/java/de/tum/cit/aet/artemis/exam/ExamParticipationIntegrationTest.java
@@ -756,7 +756,7 @@ private void lockAndAssessForSecondCorrection(Exam exam, Course course, List
Date: Mon, 2 Dec 2024 22:02:14 +0100
Subject: [PATCH 06/38] Development: Update client tests documentation (#9913)
---
docs/dev/guidelines/client-tests.rst | 80 ++++++++++++++--------------
1 file changed, 40 insertions(+), 40 deletions(-)
diff --git a/docs/dev/guidelines/client-tests.rst b/docs/dev/guidelines/client-tests.rst
index 30733fec2d0f..947ec2fe8fe9 100644
--- a/docs/dev/guidelines/client-tests.rst
+++ b/docs/dev/guidelines/client-tests.rst
@@ -18,10 +18,9 @@ The most basic test looks similar to this:
let someComponentFixture: ComponentFixture;
let someComponent: SomeComponent;
- beforeEach(() => {
- TestBed.configureTestingModule({
- imports: [],
- declarations: [
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
SomeComponent,
MockPipe(SomePipeUsedInTemplate),
MockComponent(SomeComponentUsedInTemplate),
@@ -31,11 +30,10 @@ The most basic test looks similar to this:
MockProvider(SomeServiceUsedInComponent),
],
})
- .compileComponents()
- .then(() => {
- someComponentFixture = TestBed.createComponent(SomeComponent);
- someComponent = someComponentFixture.componentInstance;
- });
+ .compileComponents();
+
+ someComponentFixture = TestBed.createComponent(SomeComponent);
+ someComponent = someComponentFixture.componentInstance;
});
afterEach(() => {
@@ -60,24 +58,25 @@ Some guidelines:
describe('ParticipationSubmissionComponent', () => {
...
- beforeEach(() => {
- return TestBed.configureTestingModule({
- imports: [ArtemisTestModule, NgxDatatableModule, ArtemisResultModule, ArtemisSharedModule, TranslateModule.forRoot(), RouterTestingModule],
- declarations: [
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ ArtemisTestModule,
+ NgxDatatableModule,
+ ArtemisResultModule,
+ ArtemisSharedModule,
+ TranslateModule.forRoot(),
ParticipationSubmissionComponent,
MockComponent(UpdatingResultComponent),
MockComponent(AssessmentDetailComponent),
MockComponent(ComplaintsForTutorComponent),
],
providers: [
- ...
+ provideRouter([]),
],
})
.overrideModule(ArtemisTestModule, { set: { declarations: [], exports: [] } })
- .compileComponents()
- .then(() => {
- ...
- });
+ .compileComponents();
});
});
@@ -94,10 +93,12 @@ Some guidelines:
describe('ParticipationSubmissionComponent', () => {
...
- beforeEach(() => {
- return TestBed.configureTestingModule({
- imports: [ArtemisTestModule, RouterTestingModule, NgxDatatableModule],
- declarations: [
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ ArtemisTestModule,
+ RouterTestingModule,
+ NgxDatatableModule,
ParticipationSubmissionComponent,
MockComponent(UpdatingResultComponent),
MockComponent(AssessmentDetailComponent),
@@ -110,13 +111,10 @@ Some guidelines:
MockComponent(ResultComponent),
],
providers: [
- ...
+ provideRouter([]),
],
})
- .compileComponents()
- .then(() => {
- ...
- });
+ .compileComponents();
});
});
@@ -158,11 +156,16 @@ Some guidelines:
.. code:: ts
- import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+ import { provideHttpClient } from '@angular/common/http';
+ import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
describe('SomeComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
- imports: [HttpClientTestingModule],
+ imports: [...],
+ providers: [
+ provideHttpClient(),
+ provideHttpClientTesting(),
+ ],
});
...
@@ -221,21 +224,18 @@ Some guidelines:
let someComponentFixture: ComponentFixture;
let someComponent: SomeComponent;
- beforeEach(() => {
- TestBed.configureTestingModule({
- imports: [],
- declarations: [
- SomeComponent,
- ],
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [SomeComponent],
providers: [
+ ...
],
})
.overrideTemplate(SomeComponent, '') // DO NOT DO THIS
- .compileComponents()
- .then(() => {
- someComponentFixture = TestBed.createComponent(SomeComponent);
- someComponent = someComponentFixture.componentInstance;
- });
+ .compileComponents();
+
+ someComponentFixture = TestBed.createComponent(SomeComponent);
+ someComponent = someComponentFixture.componentInstance;
});
});
From 701dd72549d1062853d2288a9c44590f4f0de776 Mon Sep 17 00:00:00 2001
From: Johannes Wiest
Date: Mon, 2 Dec 2024 22:03:19 +0100
Subject: [PATCH 07/38] Development: Update adaptive learning documentation
(#9915)
---
.../adaptive-learning-instructor.rst | 6 ++++--
.../adaptive-learning-student.rst | 11 +++++------
.../instructor/learning-path-management.png | Bin 42508 -> 335679 bytes
.../student/students-learning-path-graph.png | Bin 93271 -> 293709 bytes
.../students-learning-path-participation.png | Bin 113853 -> 217743 bytes
5 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/docs/user/adaptive-learning/adaptive-learning-instructor.rst b/docs/user/adaptive-learning/adaptive-learning-instructor.rst
index 0f78a71e920d..524f4287d55a 100644
--- a/docs/user/adaptive-learning/adaptive-learning-instructor.rst
+++ b/docs/user/adaptive-learning/adaptive-learning-instructor.rst
@@ -152,8 +152,10 @@ Learning Paths
Instructors can enable learning paths for their courses either by editing the course or on the dedicated learning path management page. This will generate individualized learning paths for all course participants.
-Once the feature is enabled, instructors get access to each student's learning path. Instructors can search for students by login or name and view their respective learning path graph.
-
+Once the feature is enabled, instructors gain access to the Learning Paths Management page, where they can view an overview of the status of the learning paths feature.
+For example, if competencies have not yet been created or relationships between them are missing, the State panel will notify instructors of these issues.
+Instructors can also review the individual learning paths of students. The table on this page displays each student's login, name, and progress within their learning path. By clicking on a student's progress, the instructor can open the learning path graph, which illustrates the relationships between competencies and prerequisites and shows the student's mastery level for each.
+At the bottom of the page, instructors can find generalized information about the learning paths of all students. This includes a graph that presents the average mastery level for each competency or prerequisite across the entire class.
|instructors-learning-path-management|
.. |instructor-competency-management| image:: instructor/manage-competencies.png
diff --git a/docs/user/adaptive-learning/adaptive-learning-student.rst b/docs/user/adaptive-learning/adaptive-learning-student.rst
index e47c1965fb07..dd8a3e8e723d 100644
--- a/docs/user/adaptive-learning/adaptive-learning-student.rst
+++ b/docs/user/adaptive-learning/adaptive-learning-student.rst
@@ -38,15 +38,14 @@ Learning Paths
--------------
Students can access their learning path in the learning path tab. Here, they can access recommended lecture units and participate in exercises.
-Recommendations (visualized on the left) are generated via an intelligent agent that accounts for multiple metrics, e.g. prior performance, confidence, relations, and due dates, to support students in their selection of learning resources.
-Students can use the up and down buttons to navigate to the previous or next recommendation respectively. Hovering over a node in the list will display more information about the learning resource.
+Recommendations are generated via an intelligent agent that accounts for multiple metrics, e.g. prior performance, confidence, relations, and due dates, to support students in their selection of learning resources.
+Students can use the "Previous" and "Next" buttons to navigate to the previous or next recommendation respectively.
|students-learning-path-participation|
-Students can access their learning path graph via the eye icon on the top left. The graph displays all competencies, lecture units, exercises, and their relations. Each competency consists of a start node, visualized by the competency rings displaying progress, confidence, and overall mastery, and an end node represented by a checkered flag. Edges link learning resources to a competency via the respective start and end nodes. If the resource is still pending, it displays as a play symbol. Upon completion of the task, it appears as a checkmark.
-Users can read the graph from top to bottom, starting with the competencies that have no prerequisites, continuing downwards toward competencies that build upon prior knowledge. Students can zoom, pan, and drag the graph to navigate. For better orientation, the top right corner contains a mini-map.
-On the bottom right of the graph, users can view a legend describing the different types of nodes.
-Hovering over any node, e.g. exercise or competency, opens a popover containing essential information about the item, e.g. the type of exercise and title, or for competencies, the details, including the description.
+Students can access all scheduled competencies and prerequisites by clicking on the title of the learning object they are currently viewing. Expanding a competency or prerequisite in the list reveals its associated learning objects, each indicating whether it has been completed.
+To navigate to a specific learning object, students can simply click on its title.
+For a broader view of how competencies and prerequisites are interconnected, students can open the course competency graph. This graph starts with competencies that have no prerequisites and progresses to those that build upon earlier knowledge. To aid navigation, a mini-map is available in the top-right corner.
|students-learning-path-graph|
diff --git a/docs/user/adaptive-learning/instructor/learning-path-management.png b/docs/user/adaptive-learning/instructor/learning-path-management.png
index 871203c7d67232187d0c4ababb2c696626495e54..df2c773bd862de2b3a25f36ad47de54b90ea1948 100644
GIT binary patch
literal 335679
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
- * This method leverages the {@link EurekaClientHttpRequestFactorySupplier} to configure the RestTemplate + * This method leverages the {@link EurekaClientHttpRequestFactorySupplier} to configure the RestClient * specifically for Eureka client interactions. If TLS is enabled in the provided {@link TlsProperties}, * a custom SSLContext is set up to ensure secure communication. *
* - * @param tlsProperties The TLS configuration properties, used to check if TLS is enabled and to configure it accordingly. - * @param eurekaClientHttpRequestFactorySupplier Supplies the HTTP request factory for the Eureka client RestTemplate. - * @return A configured instance of {@link RestTemplateDiscoveryClientOptionalArgs} for Eureka client, + * @param tlsProperties The TLS configuration properties, used to check if TLS is enabled and to configure it accordingly. + * @param restClientBuilderProvider The provider for the {@link RestClient.Builder} instance, if available. + * @return A configured instance of {@link RestClientDiscoveryClientOptionalArgs} for Eureka client, * potentially with SSL/TLS enabled if specified in the {@code tlsProperties}. * @throws GeneralSecurityException If there's an issue with setting up the SSL/TLS context. * @throws IOException If there's an I/O error during the setup. @@ -47,12 +51,13 @@ public class EurekaClientRestTemplateConfiguration { * @see EurekaClientHttpRequestFactorySupplier */ @Bean - public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs(TlsProperties tlsProperties, - EurekaClientHttpRequestFactorySupplier eurekaClientHttpRequestFactorySupplier) throws GeneralSecurityException, IOException { - log.debug("Using RestTemplate for the Eureka client."); + public RestClientDiscoveryClientOptionalArgs restClientDiscoveryClientOptionalArgs(TlsProperties tlsProperties, ObjectProvidert8^BZlh_Bb+ OG
zHa9l|^Us5VHe*CZzHz3gCuU?!WeV*}_X+~qrE1wZ*aeR5p4c+ykIcVMu;=73_#Y4Y
zfBO%@F=B03>3eD9#wTd2OuzEC3p2+7O-hs$WI`Se;}10J(SC`8h|Z>1&n9bQZ#4 +2*XM8iBTQ<&_f86*`O1E$IeCRTg@}M|
zblSMIAK@MJymlG3NGF?1m
zfNgiMceg4a2fjgn%^SZ5)2c)zO5;ml@N2J2+EZcw-~mz9h_mCg9c$cC9Z=3k>4!r>
zTJJbdg@4V_=}UdH)*m|-e>h*TDk|h2j7&I*q!iX6Abc)(WOuy}1JWWXa>v$DKy~h+
zA?K33*2+fidD`THVt$Ux0$rA@hTT^C+$M!1j5QvQ6Y_+_w*`s^OjE!_2M>bahW%D5
z`Pc|J=Gx%2-dtA<=tIou74kxSbI5qrxLf#iv`0(-49*{nNJeZ~u~o@vrhj7-^{t@A
z8U7j`XP7FeV&~-)?#%KWnJTQHq^GxNuiTh76Jj{-X0E)y;YX^C8M1hPUSf;?Mk(n3
z3Rdoj;6MVBYXByW%Znz}&^>$F>4%5TXwXHA0kGb~