From 8aadfc6280cbffbe6919f72268ae1e9efbf31b94 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Mon, 20 Jan 2025 21:37:26 +0100 Subject: [PATCH 01/14] Development: Compress specific websocket messages with gzip to reduce payload size (#10087) --- package-lock.json | 39 +- package.json | 2 + .../service/WebsocketMessagingService.java | 69 +++- .../websocket/GzipMessageConverter.java | 138 +++++++ .../websocket/WebsocketConfiguration.java | 17 +- .../ParticipationTeamWebsocketService.java | 2 +- .../quiz/service/QuizMessagingService.java | 2 + .../webapp/app/core/auth/account.service.ts | 4 +- .../app/core/websocket/websocket.service.ts | 186 ++++++++-- .../generate-competencies.component.ts | 4 +- .../app/exam/manage/exam-status.component.ts | 4 +- .../exam-checklist.component.ts | 4 +- .../student-exams/student-exams.component.ts | 4 +- .../exam-participation-live-events.service.ts | 4 +- .../exam-participation.component.ts | 4 +- .../modeling-submission.component.ts | 4 +- .../programming-exercise-grading.service.ts | 4 +- .../programming-exercise-websocket.service.ts | 4 +- .../programming-build-run.service.ts | 4 +- .../programming-submission.service.ts | 4 +- .../code-editor-conflict-state.service.ts | 4 +- ...ditor-domain-dependent-endpoint.service.ts | 4 +- .../drag-and-drop-question-edit.component.ts | 2 +- .../question-statistic.component.ts | 4 +- .../quiz-point-statistic.component.ts | 4 +- .../quiz-statistic.component.ts | 4 +- .../quiz-statistics-footer.component.ts | 4 +- .../quiz-participation.component.ts | 4 +- .../plagiarism-inspector.component.ts | 4 +- .../team-submission-sync.component.ts | 8 +- .../team-students-online-list.component.ts | 8 +- .../app/exercises/shared/team/team.service.ts | 4 +- .../text/participate/text-editor.component.ts | 10 +- .../webapp/app/iris/iris-status.service.ts | 4 +- .../webapp/app/iris/iris-websocket.service.ts | 4 +- .../build-agent-details.component.ts | 4 +- .../build-agent-summary.component.ts | 4 +- .../build-queue/build-queue.component.ts | 4 +- .../app/overview/course-overview.component.ts | 4 +- .../webapp/app/overview/courses.component.ts | 4 +- .../participation-websocket.service.ts | 4 +- .../course-exam-archive-button.component.ts | 4 +- .../connection-status.component.ts | 4 +- .../connection-warning.component.ts | 4 +- .../feature-toggle/feature-toggle.service.ts | 4 +- .../metis/metis-conversation.service.ts | 4 +- .../webapp/app/shared/metis/metis.service.ts | 4 +- .../notification/notification.service.ts | 4 +- .../system-notification.component.ts | 4 +- .../websocket/GzipMessageConverterTest.java | 127 +++++++ .../generate-competencies.component.spec.ts | 4 +- .../connection-warning.spec.ts | 4 +- .../course/course-overview.component.spec.ts | 8 +- .../exams/exam-checklist.component.spec.ts | 4 +- .../exams/exam-detail.component.spec.ts | 4 +- .../exam-status.component.spec.ts | 4 +- .../student-exams.component.spec.ts | 4 +- .../exam-participation.component.spec.ts | 4 +- .../quiz/quiz-participation.component.spec.ts | 10 +- .../team-submission-sync.component.spec.ts | 8 +- .../iris/iris-status.service.spec.ts | 4 +- .../component/iris/websocket.service.spec.ts | 107 ------ .../build-agent-details.component.spec.ts | 4 +- .../build-agent-summary.component.spec.ts | 4 +- ...modeling-submission-team.component.spec.ts | 4 +- .../modeling-submission.component.spec.ts | 4 +- .../metis-conversation.service.spec.ts | 8 +- .../plagiarism-inspector.component.spec.ts | 4 +- .../system-notification.component.spec.ts | 8 +- .../user-settings.directive.spec.ts | 4 +- .../user-settings.service.spec.ts | 4 +- .../text-editor/text-editor.component.spec.ts | 1 + .../spec/core/websocket.service.spec.ts | 350 ++++++++++++++++++ .../code-editor-container.integration.spec.ts | 4 +- ...code-editor-instructor.integration.spec.ts | 4 +- .../code-editor-student.integration.spec.ts | 4 +- .../spec/service/account.service.spec.ts | 4 +- ...-participation-live-events.service.spec.ts | 10 +- .../spec/service/login.service.spec.ts | 4 +- .../spec/service/metis/metis.service.spec.ts | 6 +- .../spec/service/notification.service.spec.ts | 8 +- .../participation-websocket.service.spec.ts | 8 +- ...ogramming-exercise-grading.service.spec.ts | 8 +- .../programming-submission.service.spec.ts | 8 +- 84 files changed, 1054 insertions(+), 332 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/config/websocket/GzipMessageConverter.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/core/config/websocket/GzipMessageConverterTest.java delete mode 100644 src/test/javascript/spec/component/iris/websocket.service.spec.ts create mode 100644 src/test/javascript/spec/core/websocket.service.spec.ts diff --git a/package-lock.json b/package-lock.json index 540d2b4903df..5cb056c3dfaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,7 @@ "monaco-editor": "0.52.2", "ngx-infinite-scroll": "19.0.0", "ngx-webstorage": "19.0.1", + "pako": "2.1.0", "papaparse": "5.5.1", "pdf-lib": "1.17.1", "pdfjs-dist": "4.10.38", @@ -102,6 +103,7 @@ "@types/lodash-es": "4.17.12", "@types/markdown-it": "14.1.2", "@types/node": "22.10.7", + "@types/pako": "2.0.3", "@types/papaparse": "5.3.15", "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", @@ -6490,6 +6492,12 @@ "pako": "^1.0.6" } }, + "node_modules/@pdf-lib/standard-fonts/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/@pdf-lib/upng": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", @@ -6499,6 +6507,12 @@ "pako": "^1.0.10" } }, + "node_modules/@pdf-lib/upng/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -7746,6 +7760,13 @@ "@types/node": "*" } }, + "node_modules/@types/pako": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.3.tgz", + "integrity": "sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/papaparse": { "version": "5.3.15", "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.15.tgz", @@ -15321,6 +15342,12 @@ "setimmediate": "^1.0.5" } }, + "node_modules/jszip/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/karma-source-map-support": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", @@ -17561,9 +17588,9 @@ } }, "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", "license": "(MIT AND Zlib)" }, "node_modules/papaparse": { @@ -17883,6 +17910,12 @@ "tslib": "^1.11.1" } }, + "node_modules/pdf-lib/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/pdf-lib/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", diff --git a/package.json b/package.json index aee17ca68d2f..0449da8c7368 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "monaco-editor": "0.52.2", "ngx-infinite-scroll": "19.0.0", "ngx-webstorage": "19.0.1", + "pako": "2.1.0", "papaparse": "5.5.1", "pdf-lib": "1.17.1", "pdfjs-dist": "4.10.38", @@ -151,6 +152,7 @@ "@types/lodash-es": "4.17.12", "@types/markdown-it": "14.1.2", "@types/node": "22.10.7", + "@types/pako": "2.0.3", "@types/papaparse": "5.3.15", "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/WebsocketMessagingService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/WebsocketMessagingService.java index b28b70cfe6e8..50eb995f1d3b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/WebsocketMessagingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/WebsocketMessagingService.java @@ -1,9 +1,13 @@ package de.tum.cit.aet.artemis.communication.service; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import static de.tum.cit.aet.artemis.core.config.websocket.GzipMessageConverter.COMPRESSION_HEADER; +import java.util.Collection; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,16 +59,17 @@ public CompletableFuture sendMessage(String topic, Message message) { * The message will be sent asynchronously. * * @param topic the destination to which subscription the message should be sent - * @param message any object that should be sent to the destination (topic), this will typically get transformed into json + * @param payload the payload to send in the message (e.g. a record DTO), which will be transformed into json and potentially compressed * @return a future that can be used to check if the message was sent successfully or resulted in an exception */ - public CompletableFuture sendMessage(String topic, Object message) { + public CompletableFuture sendMessage(String topic, Object payload) { try { - return CompletableFuture.runAsync(() -> messagingTemplate.convertAndSend(topic, message), asyncExecutor); + Map headers = shouldCompress(topic, payload) ? COMPRESSION_HEADER : Map.of(); + return CompletableFuture.runAsync(() -> messagingTemplate.convertAndSend(topic, payload, headers), asyncExecutor); } // Note: explicitly catch ALL kinds of exceptions here and do NOT rethrow, because the actual task should NEVER be interrupted when the server cannot send WS messages catch (Exception ex) { - log.error("Error when sending message {} to topic {}", message, topic, ex); + log.error("Error when sending payload {} to topic {}", payload, topic, ex); return CompletableFuture.failedFuture(ex); } } @@ -75,17 +80,67 @@ public CompletableFuture sendMessage(String topic, Object message) { * * @param user the user that should receive the message. * @param topic the destination to send the message to - * @param payload the payload to send + * @param payload the payload to send in the message (e.g. a record DTO), which will be transformed into json and potentially compressed * @return a future that can be used to check if the message was sent successfully or resulted in an exception */ public CompletableFuture sendMessageToUser(String user, String topic, Object payload) { try { - return CompletableFuture.runAsync(() -> messagingTemplate.convertAndSendToUser(user, topic, payload), asyncExecutor); + Map headers = shouldCompress(topic, payload) ? COMPRESSION_HEADER : Map.of(); + return CompletableFuture.runAsync(() -> messagingTemplate.convertAndSendToUser(user, topic, payload, headers), asyncExecutor); } // Note: explicitly catch ALL kinds of exceptions here and do NOT rethrow, because the actual task should NEVER be interrupted when the server cannot send WS messages catch (Exception ex) { - log.error("Error when sending message {} on topic {} to user {}", payload, topic, user, ex); + log.error("Error when sending payload {} on topic {} to user {}", payload, topic, user, ex); return CompletableFuture.failedFuture(ex); } } + + /** + * A regex pattern to match compressible WebSocket topics. + *

+ * The topics covered by this pattern are: + * 1. Topics for course-specific job statuses: + * - `/topic/courses/{courseId}/queued-jobs` + * - `/topic/courses/{courseId}/running-jobs` + * - `{courseId}` is a numeric identifier (long). + *

+ * 2. Topics for admin-level job statuses and build agents: + * - `/topic/admin/queued-jobs` + * - `/topic/admin/running-jobs` + * - `/topic/admin/build-agents` + *

+ * 3. Topics for specific build agent details: + * - `/topic/admin/build-agent/{buildAgentName}` + * - `{buildAgentName}` is a string that does not contain a forward slash (`/`). + *

+ * Regex Details: + * - `^/topic/courses/\\d+/(queued-jobs|running-jobs)`: + * Matches topics for course-specific jobs with a numeric `courseId`. + * - `|^/topic/admin/(queued-jobs|running-jobs|build-agents)`: + * Matches admin-level job and build agent topics. + * - `|^/topic/admin/build-agent/[^/]+$`: + * Matches specific build agent topics, where `{buildAgentName}` is any string excluding `/`. + */ + private static final Pattern COMPRESSIBLE_TOPICS = Pattern + .compile("^/topic/courses/\\d+/(queued-jobs|running-jobs)|" + "^/topic/admin/(queued-jobs|running-jobs|build-agents)|" + "^/topic/admin/build-agent/[^/]+$"); + + /** + * Determine if a message for a specific topic should be compressed. + */ + private static boolean shouldCompress(String topic, Object payload) { + // Only compress messages for specific topics + if (topic == null) { + return false; + } + if (isEmpty(payload)) { + return false; + } + // Match the topic against the regex + return COMPRESSIBLE_TOPICS.matcher(topic).matches(); + } + + private static boolean isEmpty(Object payload) { + return payload == null || payload.toString().isEmpty() || (payload instanceof Collection collection && collection.isEmpty()) + || (payload instanceof Map map && map.isEmpty()); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/GzipMessageConverter.java b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/GzipMessageConverter.java new file mode 100644 index 000000000000..486b512295a4 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/GzipMessageConverter.java @@ -0,0 +1,138 @@ +package de.tum.cit.aet.artemis.core.config.websocket; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.support.NativeMessageHeaderAccessor; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class GzipMessageConverter extends MappingJackson2MessageConverter { + + private static final Logger log = LoggerFactory.getLogger(GzipMessageConverter.class); + + public static final String COMPRESSION_HEADER_KEY = "X-Compressed"; + + public static final Map COMPRESSION_HEADER = Map.of(COMPRESSION_HEADER_KEY, true); + + public GzipMessageConverter(ObjectMapper objectMapper) { + super(objectMapper); + } + + @Override + protected boolean supports(Class clazz) { + return true; + } + + // Incoming message from client, potentially compressed, needs to be decompressed + @Override + protected Object convertFromInternal(Message message, Class targetClass, Object conversionHint) { + var nativeHeaders = message.getHeaders().get(NativeMessageHeaderAccessor.NATIVE_HEADERS); + if (nativeHeaders instanceof Map nativeMapHeaders) { + final var messageIsCompressed = containsCompressionHeader(nativeMapHeaders); + if (messageIsCompressed) { + log.info("Decompressing message payload for incoming message"); + Object payload = message.getPayload(); + if (payload instanceof byte[] bytePayload) { + byte[] decompressed = decodeAndDecompress(bytePayload); + return super.convertFromInternal(new Message<>() { + + @Override + public Object getPayload() { + return decompressed; + } + + @Override + public MessageHeaders getHeaders() { + return message.getHeaders(); + } + }, targetClass, conversionHint); + } + } + } + return super.convertFromInternal(message, targetClass, conversionHint); + } + + private static boolean containsCompressionHeader(Map headers) { + var value = headers.get(COMPRESSION_HEADER_KEY); + if (value instanceof List list && !list.isEmpty()) { + return checkSimpleValue(list.getFirst()); + } + return checkSimpleValue(value); + } + + private static boolean checkSimpleValue(Object value) { + if (value instanceof Boolean booleanValue) { + return Boolean.TRUE.equals(booleanValue); + } + if (value instanceof String stringValue) { + return Boolean.parseBoolean(stringValue); + } + return false; + } + + // Outgoing message to client, potentially compressible, needs to be compressed + // NOTE: headers is immutable here and cannot be modified + @Override + protected Object convertToInternal(Object payload, MessageHeaders headers, Object conversionHint) { + Object original = super.convertToInternal(payload, headers, conversionHint); + if (original instanceof byte[] originalBytes) { + // Check the native headers to see if the message should be compressed + var nativeHeaders = headers.get(NativeMessageHeaderAccessor.NATIVE_HEADERS); + if (nativeHeaders instanceof Map nativeMapHeaders) { + boolean shouldCompress = containsCompressionHeader(nativeMapHeaders); + if (shouldCompress) { + String compressedBase64String = compressAndEncode(originalBytes); + byte[] compressed = compressedBase64String.getBytes(StandardCharsets.UTF_8); + double percentageSaved = 100 * (1 - (double) compressed.length / originalBytes.length); + log.debug("Compressed message payload from {} to {} (saved {}% payload size)", originalBytes.length, compressed.length, String.format("%.1f", percentageSaved)); + return compressed; + } + } + return originalBytes; + } + return original; + } + + // NOTE: we use a hybrid approach here mixing string based and binary data when compression is active. + // As a compromise, we use Base64 encoding to ensure that the compressed data can be safely transmitted as a string (without interfering with the WebSocket protocol). + // This can still reduce the payload size by up to 95% (for large payloads) compared to the original binary data (in standard json). + private String compressAndEncode(byte[] data) { + try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteStream)) { + gzipOutputStream.write(data); + gzipOutputStream.finish(); + return Base64.getEncoder().encodeToString(byteStream.toByteArray()); + } + catch (Exception e) { + throw new RuntimeException("Failed to compressAndEncode message payload", e); + } + } + + private byte[] decodeAndDecompress(byte[] data) { + // Step 1: Decode Base64 to binary + byte[] binaryData = Base64.getDecoder().decode(data); + + // Step 2: Decompress the binary data + try (ByteArrayInputStream byteStream = new ByteArrayInputStream(binaryData); + GZIPInputStream gzipStream = new GZIPInputStream(byteStream); + ByteArrayOutputStream outStream = new ByteArrayOutputStream()) { + // Efficiently transfers all bytes + gzipStream.transferTo(outStream); + return outStream.toByteArray(); + } + catch (Exception e) { + throw new RuntimeException("Failed to decodeAndDecompress message payload", e); + } + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java index 4a5dfdacfa24..9501569ac6c3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java @@ -35,6 +35,7 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.messaging.simp.stomp.StompCommand; @@ -141,6 +142,13 @@ protected void configureMessageBroker(@NotNull MessageBrokerRegistry config) { } } + @Override + protected boolean configureMessageConverters(List messageConverters) { + GzipMessageConverter gzipMessageConverter = new GzipMessageConverter(objectMapper); + messageConverters.add(gzipMessageConverter); + return false; + } + /** * Create a TCP client that will connect to the broker defined in the config. * If multiple brokers are configured, the client will connect to the first one and fail over to the next one in case a broker goes down. @@ -181,12 +189,7 @@ public void configureClientInboundChannel(ChannelRegistration registration) { @NotNull @Override protected MappingJackson2MessageConverter createJacksonConverter() { - // NOTE: We need to adapt the default messageConverter for WebSocket messages - // with a messageConverter that uses the same ObjectMapper that our REST endpoints use. - // This gives us consistency in how specific data types are serialized (e.g. timestamps) - MappingJackson2MessageConverter converter = super.createJacksonConverter(); - converter.setObjectMapper(objectMapper); - return converter; + return new GzipMessageConverter(objectMapper); } /** @@ -308,7 +311,7 @@ private boolean allowSubscription(@Nullable Principal principal, String destinat return isParticipationOwnedByUser(principal, participationId); } if (isNonPersonalExerciseResultDestination(destination)) { - final var exerciseId = getExerciseIdFromNonPersonalExerciseResultDestination(destination).orElseThrow(); + final long exerciseId = getExerciseIdFromNonPersonalExerciseResultDestination(destination).orElseThrow(); // TODO: Is it right that TAs are not allowed to subscribe to exam exercises? if (exerciseRepository.isExamExercise(exerciseId)) { diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationTeamWebsocketService.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationTeamWebsocketService.java index ad7d9b0fa16a..acc8727f17ea 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationTeamWebsocketService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationTeamWebsocketService.java @@ -184,7 +184,7 @@ public void patchModelingSubmission(@DestinationVariable Long participationId, @ public void updateTextSubmission(@DestinationVariable Long participationId, @Payload TextSubmission textSubmission, Principal principal) { long start = System.currentTimeMillis(); updateSubmission(participationId, textSubmission, principal, "/text-submissions", true); - log.info("Websocket endpoint updateTextSubmission took {}ms for submission with id {}", System.currentTimeMillis() - start, textSubmission.getId()); + log.debug("Websocket endpoint updateTextSubmission took {}ms for submission with id {}", System.currentTimeMillis() - start, textSubmission.getId()); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizMessagingService.java b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizMessagingService.java index 0c5900072048..0382b523df6f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizMessagingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizMessagingService.java @@ -51,6 +51,7 @@ public void sendQuizExerciseToSubscribedClients(QuizExercise quizExercise, @Null try { long start = System.currentTimeMillis(); Class view = quizExercise.viewForStudentsInQuizExercise(quizBatch); + // TODO: use a proper DTO and avoid sending the whole quiz exercise based on the view byte[] payload = objectMapper.writerWithView(view).writeValueAsBytes(quizExercise); // For each change we send the same message. The client needs to decide how to handle the date based on the quiz status if (quizExercise.isVisibleToStudents() && quizExercise.isCourseExercise()) { @@ -63,6 +64,7 @@ public void sendQuizExerciseToSubscribedClients(QuizExercise quizExercise, @Null if (quizChange == START_BATCH && quizBatch != null) { destination = destination + "/" + quizBatch.getId(); } + // TODO the view could also be passed as conversion hint to the message converter websocketMessagingService.sendMessage(destination, MessageBuilder.withPayload(payload).build()); log.info("Sent '{}' for quiz {} to all listening clients in {} ms", quizChange, quizExercise.getId(), System.currentTimeMillis() - start); } diff --git a/src/main/webapp/app/core/auth/account.service.ts b/src/main/webapp/app/core/auth/account.service.ts index 8745ae5357b9..8cc91bc9e586 100644 --- a/src/main/webapp/app/core/auth/account.service.ts +++ b/src/main/webapp/app/core/auth/account.service.ts @@ -5,7 +5,7 @@ import { BehaviorSubject, Observable, lastValueFrom, of } from 'rxjs'; import { catchError, distinctUntilChanged, map } from 'rxjs/operators'; import { Course } from 'app/entities/course.model'; import { User } from 'app/core/user/user.model'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { FeatureToggleService } from 'app/shared/feature-toggle/feature-toggle.service'; import { setUser } from '@sentry/angular'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; @@ -37,7 +37,7 @@ export class AccountService implements IAccountService { private translateService = inject(TranslateService); private sessionStorage = inject(SessionStorageService); private http = inject(HttpClient); - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private featureToggleService = inject(FeatureToggleService); // cached value of the user to avoid unnecessary requests to the server diff --git a/src/main/webapp/app/core/websocket/websocket.service.ts b/src/main/webapp/app/core/websocket/websocket.service.ts index 3d99a958f140..3b70c0451dbe 100644 --- a/src/main/webapp/app/core/websocket/websocket.service.ts +++ b/src/main/webapp/app/core/websocket/websocket.service.ts @@ -1,7 +1,19 @@ import { Injectable, OnDestroy } from '@angular/core'; import { BehaviorSubject, Observable, Subscriber, Subscription, first } from 'rxjs'; import SockJS from 'sockjs-client'; -import Stomp, { Client, ConnectionHeaders, Subscription as StompSubscription } from 'webstomp-client'; +import Stomp, { Client, ConnectionHeaders, Message, Subscription as StompSubscription } from 'webstomp-client'; +import { gzip, ungzip } from 'pako'; +import { captureException } from '@sentry/angular'; + +interface SockJSExtended extends WebSocket { + _transport?: { + url?: string; + }; +} + +// must be the same as in GzipMessageConverter.java +export const COMPRESSION_HEADER_KEY = 'X-Compressed'; +export const COMPRESSION_HEADER: Record = { [COMPRESSION_HEADER_KEY]: 'true' }; export interface IWebsocketService { /** @@ -27,20 +39,20 @@ export interface IWebsocketService { /** * Send data through the websocket connection - * @param path {string} the path for the websocket connection - * @param data {object} the data to send through the websocket connection + * @param path the path for the websocket connection + * @param data the data to send through the websocket connection */ - send(path: string, data: any): void; + send(path: string, data: T): void; /** * Subscribe to a channel. - * @param channel + * @param channel topic */ subscribe(channel: string): IWebsocketService; /** * Unsubscribe a channel. - * @param channel + * @param channel topic */ unsubscribe(channel: string): void; @@ -76,7 +88,7 @@ export class ConnectionState { * Server <1--1> Stomp <1--1> websocket.service.ts <1--n*m> Angular components * channel topic */ @Injectable({ providedIn: 'root' }) -export class JhiWebsocketService implements IWebsocketService, OnDestroy { +export class WebsocketService implements IWebsocketService, OnDestroy { private stompClient?: Client; // we store the STOMP subscriptions per channel so that we can unsubscribe in case we are not interested any more @@ -93,7 +105,7 @@ export class JhiWebsocketService implements IWebsocketService, OnDestroy { private readonly connectionStateInternal: BehaviorSubject; private consecutiveFailedAttempts = 0; private connecting = false; - private socket: any = undefined; + private socket: SockJSExtended | undefined = undefined; private subscriptionCounter = 0; constructor() { @@ -161,10 +173,11 @@ export class JhiWebsocketService implements IWebsocketService, OnDestroy { debug: false, protocols: ['v12.stomp'], }; + // TODO: consider to switch to RxStomp (like in the latest jhipster version) this.stompClient = Stomp.over(this.socket, options); // Note: at the moment, debugging is deactivated to prevent console log statements this.stompClient.debug = () => {}; - const headers = {}; + const headers = {} as ConnectionHeaders; this.stompClient.connect( headers, @@ -177,7 +190,7 @@ export class JhiWebsocketService implements IWebsocketService, OnDestroy { if (this.alreadyConnectedOnce) { // (re)connect to all existing channels if (this.observables.size !== 0) { - this.observables.forEach((observable, channel) => this.addSubscription(channel)); + this.observables.forEach((_observable, channel) => this.addSubscription(channel)); } } else { this.alreadyConnectedOnce = true; @@ -192,22 +205,89 @@ export class JhiWebsocketService implements IWebsocketService, OnDestroy { * @param channel the path (e.g. '/courses/5/exercises/10') that should be subscribed */ private addSubscription(channel: string) { - const subscription = this.stompClient!.subscribe( - channel, - (message) => { - // this code is invoked if a new websocket message was received from the server - // we pass the message to the subscriber (e.g. a component who will be notified and can handle the message) - if (this.subscribers.has(channel)) { - this.subscribers.get(channel)!.next(JhiWebsocketService.parseJSON(message.body)); + const subscription = this.stompClient!.subscribe(channel, this.handleIncomingMessage(channel), { + id: this.getSessionId() + '-' + this.subscriptionCounter++, + }); + this.stompSubscriptions.set(channel, subscription); + } + + /** + * Handle incoming messages from the server, which are potentially compressed: + * 1. Decode the Base64 string to binary data + * 2. Decompress the binary data to a string payload (JSON) + * 3. Parse the JSON payload and pass it to the subscribers + * @param channel the channel the message was received on + */ + private handleIncomingMessage(channel: string) { + return (message: Message) => { + // this code is invoked if a new websocket message was received from the server + // we pass the message to the subscriber (e.g. a component who will be notified and can handle the message) + if (this.subscribers.has(channel)) { + const isCompressed = message.headers[COMPRESSION_HEADER_KEY] === 'true'; + let payload = message.body; + + if (isCompressed) { + try { + payload = WebsocketService.decodeAndDecompress(payload); + } catch (error) { + captureException('Failed to decompress message', error); + } } - }, - { - id: this.getSessionId() + '-' + this.subscriptionCounter++, - }, + + this.subscribers.get(channel)!.next(WebsocketService.parseJSON(payload)); + } + }; + } + + /** + * Compresses a given string payload using GZIP and encodes the compressed data into a Base64 string. + * + *

This method performs the following steps: + *

    + *
  1. Compresses the input string using GZIP.
  2. + *
  3. Converts the compressed binary data into a Base64-encoded string.
  4. + *
+ * + * @param payload The string payload to be compressed and encoded. + * @returns A Base64-encoded string representing the compressed payload. + * @throws Error If compression or Base64 encoding fails. + */ + private static compressAndEncode(payload: string): string { + // 1. Compress if larger than 1 KB + const compressedPayload = gzip(payload); + // 2. Convert binary data to base64 string + return window.btoa( + Array.from(compressedPayload) + .map((byte) => String.fromCharCode(byte)) + .join(''), ); - this.stompSubscriptions.set(channel, subscription); } + /** + * Decodes a Base64-encoded string and decompresses the resulting binary data using GZIP. + * + *

This method performs the following steps: + *

    + *
  1. Decodes the Base64-encoded string into binary data.
  2. + *
  3. Decompresses the binary data using GZIP.
  4. + *
+ * + * @param payload The Base64-encoded string representing compressed data. + * @returns The decompressed string. + * @throws Error If decoding or decompression fails. + */ + private static decodeAndDecompress(payload: string): string { + // 1. Decode the Base64 string to binary (ArrayBuffer) and convert to Uint8Array + const binaryData = Uint8Array.from(window.atob(payload), (char) => char.charCodeAt(0)); + // 2. Decompress using pako + return ungzip(binaryData, { to: 'string' }); + } + + /** + * Checks whether the WebSocket connection is currently established. + * + * @returns true if the WebSocket connection is active; otherwise, false. + */ public isConnected(): boolean { return this.stompClient?.connected || false; } @@ -216,7 +296,7 @@ export class JhiWebsocketService implements IWebsocketService, OnDestroy { * Close the connection to the websocket (e.g. due to logout), unsubscribe all observables and set alreadyConnectedOnce to false */ disconnect() { - this.observables.forEach((observable, channel) => this.unsubscribe(channel)); + this.observables.forEach((_observable, channel) => this.unsubscribe(channel)); this.waitUntilConnectionSubscriptions.forEach((subscription) => subscription.unsubscribe()); if (this.stompClient) { this.stompClient.disconnect(); @@ -241,13 +321,33 @@ export class JhiWebsocketService implements IWebsocketService, OnDestroy { } /** - * Send data through the websocket connection - * @param path {string} the path for the websocket connection - * @param data {object} the data to send through the websocket connection + * Send data through the websocket connection, potentially compressing the payload. + * Only compresses data if the JSON stringified payload size is larger than 1 KB. + * 1. Convert the data into JSON + * 2. Compress the JSON payload into binary data if it is larger than 1 KB + * 3. Convert the binary data into a Base64 string + * + * @param path the path for the websocket connection + * @param data the data to send through the websocket connection */ - send(path: string, data: any): void { + send(path: string, data: T): void { if (this.isConnected()) { - this.stompClient!.send(path, JSON.stringify(data), {}); + const jsonPayload = JSON.stringify(data); + const payloadSize = new Blob([jsonPayload]).size; // Measure payload size + + if (payloadSize > 1024) { + try { + const base64StringPayload = WebsocketService.compressAndEncode(jsonPayload); + this.stompClient!.send(path, base64StringPayload, COMPRESSION_HEADER); + } catch (error) { + captureException('Failed to compress websocket message', error); + // Send uncompressed payload if an error occurs + this.stompClient!.send(path, jsonPayload, {}); + } + } else { + // Send uncompressed payload + this.stompClient!.send(path, jsonPayload, {}); + } } } @@ -272,8 +372,8 @@ export class JhiWebsocketService implements IWebsocketService, OnDestroy { } /** - * Unsubscribe a channel. - * @param channel + * Unsubscribe a channel if the component is not interested in the messages anymore + * @param channel topic for which the component wants to unsubscribe */ unsubscribe(channel: string) { if (this && this.stompSubscriptions && this.stompSubscriptions.has(channel)) { @@ -322,20 +422,30 @@ export class JhiWebsocketService implements IWebsocketService, OnDestroy { this.disconnect(); } - private static parseJSON(response: string): any { + /** + * Parses a JSON string into an object of the specified generic type. + * + *

This method attempts to parse the provided JSON string. If parsing fails, + * it returns the input string cast to the specified type. This can be useful + * for handling cases where the response might not always be a valid JSON string.

+ * + * @param response The JSON string to be parsed. + * @returns The parsed object of the specified type, or the input string cast to the type if parsing fails. + * @template T The type of the object to return after parsing. + * @throws Error If JSON parsing fails and the input is not a valid string cast to the specified type. + */ + private static parseJSON(response: string): T { try { return JSON.parse(response); } catch { - return response; + return response as T; } } - // https://stackoverflow.com/a/35651029/3802758 + // see https://stackoverflow.com/a/35651029/3802758 private getSessionId(): string { - if (this.socket && this.socket._transport && this.socket._transport.url) { - return this.socket._transport.url.match('.*\\/websocket\\/\\d*\\/(.*)\\/websocket.*')[1]; - } else { - return 'unsubscribed'; - } + const url = this.socket?._transport?.url; + const match = url?.match('.*\\/websocket\\/\\d*\\/(.*)\\/websocket.*'); + return match ? match[1] : 'unsubscribed'; } } diff --git a/src/main/webapp/app/course/competencies/generate-competencies/generate-competencies.component.ts b/src/main/webapp/app/course/competencies/generate-competencies/generate-competencies.component.ts index 65256d6ed5b8..4525a2ad3536 100644 --- a/src/main/webapp/app/course/competencies/generate-competencies/generate-competencies.component.ts +++ b/src/main/webapp/app/course/competencies/generate-competencies/generate-competencies.component.ts @@ -15,7 +15,7 @@ import { Observable, firstValueFrom, map } from 'rxjs'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { TranslateService } from '@ngx-translate/core'; import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { IrisStageDTO, IrisStageStateDTO } from 'app/entities/iris/iris-stage-dto.model'; import { CourseCompetencyService } from 'app/course/competencies/course-competency.service'; import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; @@ -62,7 +62,7 @@ export class GenerateCompetenciesComponent implements OnInit, ComponentCanDeacti private modalService = inject(NgbModal); private artemisTranslatePipe = inject(ArtemisTranslatePipe); private translateService = inject(TranslateService); - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); @ViewChild(CourseDescriptionFormComponent) courseDescriptionForm: CourseDescriptionFormComponent; diff --git a/src/main/webapp/app/exam/manage/exam-status.component.ts b/src/main/webapp/app/exam/manage/exam-status.component.ts index 3becf156aa5a..1c0a38b4aed7 100644 --- a/src/main/webapp/app/exam/manage/exam-status.component.ts +++ b/src/main/webapp/app/exam/manage/exam-status.component.ts @@ -6,7 +6,7 @@ import { ExamChecklist } from 'app/entities/exam/exam-checklist.model'; import dayjs from 'dayjs/esm'; import { round } from 'app/shared/util/utils'; import { Course } from 'app/entities/course.model'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { NgClass } from '@angular/common'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { TranslateDirective } from 'app/shared/language/translate.directive'; @@ -36,7 +36,7 @@ export enum ExamConductionState { }) export class ExamStatusComponent implements OnChanges, OnInit, OnDestroy { private examChecklistService = inject(ExamChecklistService); - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); @Input() public exam: Exam; diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.ts b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.ts index 88e6412574a4..4f29b11592ca 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.ts @@ -3,7 +3,7 @@ import { Exam } from 'app/entities/exam/exam.model'; import { ExamChecklist } from 'app/entities/exam/exam-checklist.model'; import { faChartBar, faEye, faListAlt, faThList, faUser, faWrench } from '@fortawesome/free-solid-svg-icons'; import { ExamChecklistService } from 'app/exam/manage/exams/exam-checklist-component/exam-checklist.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { ExamManagementService } from 'app/exam/manage/exam-management.service'; import { AlertService } from 'app/core/util/alert.service'; import { HttpErrorResponse } from '@angular/common/http'; @@ -40,7 +40,7 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; }) export class ExamChecklistComponent implements OnChanges, OnInit, OnDestroy { private examChecklistService = inject(ExamChecklistService); - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private examManagementService = inject(ExamManagementService); private alertService = inject(AlertService); private studentExamService = inject(StudentExamService); diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exams.component.ts b/src/main/webapp/app/exam/manage/student-exams/student-exams.component.ts index e1f30113fd9b..abe4efef4c3d 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exams.component.ts +++ b/src/main/webapp/app/exam/manage/student-exams/student-exams.component.ts @@ -19,7 +19,7 @@ import { AccountService } from 'app/core/auth/account.service'; import { onError } from 'app/shared/util/global.utils'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { convertDateFromServer } from 'app/utils/date.utils'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { PROFILE_LOCALVC } from 'app/app.constants'; @@ -66,7 +66,7 @@ export class StudentExamsComponent implements OnInit, OnDestroy { private modalService = inject(NgbModal); private accountService = inject(AccountService); private artemisTranslatePipe = inject(ArtemisTranslatePipe); - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private profileService = inject(ProfileService); courseId: number; diff --git a/src/main/webapp/app/exam/participate/exam-participation-live-events.service.ts b/src/main/webapp/app/exam/participate/exam-participation-live-events.service.ts index 1058fb8fbbba..3daa6a3e2d6f 100644 --- a/src/main/webapp/app/exam/participate/exam-participation-live-events.service.ts +++ b/src/main/webapp/app/exam/participate/exam-participation-live-events.service.ts @@ -1,6 +1,6 @@ import { HttpClient } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; -import { ConnectionState, JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { ConnectionState, WebsocketService } from 'app/core/websocket/websocket.service'; import { ExamParticipationService } from 'app/exam/participate/exam-participation.service'; import dayjs from 'dayjs/esm'; import { LocalStorageService } from 'ngx-webstorage'; @@ -54,7 +54,7 @@ export type ProblemStatementUpdateEvent = ExamLiveEvent & { @Injectable({ providedIn: 'root' }) export class ExamParticipationLiveEventsService { - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private examParticipationService = inject(ExamParticipationService); private localStorageService = inject(LocalStorageService); private httpClient = inject(HttpClient); diff --git a/src/main/webapp/app/exam/participate/exam-participation.component.ts b/src/main/webapp/app/exam/participate/exam-participation.component.ts index c4b8c658aff2..69dde679bdbb 100644 --- a/src/main/webapp/app/exam/participate/exam-participation.component.ts +++ b/src/main/webapp/app/exam/participate/exam-participation.component.ts @@ -1,6 +1,6 @@ import { Component, OnDestroy, OnInit, QueryList, ViewChildren, inject } from '@angular/core'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { ExamParticipationService } from 'app/exam/participate/exam-participation.service'; import { StudentExam } from 'app/entities/student-exam.model'; import { Exercise, ExerciseType } from 'app/entities/exercise.model'; @@ -90,7 +90,7 @@ type GenerateParticipationStatus = 'generating' | 'failed' | 'success'; ], }) export class ExamParticipationComponent implements OnInit, OnDestroy, ComponentCanDeactivate { - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private route = inject(ActivatedRoute); private router = inject(Router); private examParticipationService = inject(ExamParticipationService); diff --git a/src/main/webapp/app/exercises/modeling/participate/modeling-submission.component.ts b/src/main/webapp/app/exercises/modeling/participate/modeling-submission.component.ts index e0826f6413d0..09b504dcb669 100644 --- a/src/main/webapp/app/exercises/modeling/participate/modeling-submission.component.ts +++ b/src/main/webapp/app/exercises/modeling/participate/modeling-submission.component.ts @@ -2,7 +2,7 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Component, Input, OnDestroy, OnInit, ViewChild, inject } from '@angular/core'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { Patch, Selection, UMLDiagramType, UMLElementType, UMLModel, UMLRelationshipType } from '@ls1intum/apollon'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { ComplaintType } from 'app/entities/complaint.model'; import { Feedback, buildFeedbackTextForReview, checkSubsequentFeedbackInAssessment } from 'app/entities/feedback.model'; import { ModelingExercise } from 'app/entities/modeling-exercise.model'; @@ -87,7 +87,7 @@ import { captureException } from '@sentry/angular'; ], }) export class ModelingSubmissionComponent implements OnInit, OnDestroy, ComponentCanDeactivate { - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private modelingSubmissionService = inject(ModelingSubmissionService); private modelingAssessmentService = inject(ModelingAssessmentService); private alertService = inject(AlertService); diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-grading.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-grading.service.ts index 9150326c5570..7d573cd15061 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-grading.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-grading.service.ts @@ -2,7 +2,7 @@ import { Injectable, OnDestroy, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { BehaviorSubject, Observable, of } from 'rxjs'; import { catchError, map, switchMap, tap } from 'rxjs/operators'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { ProgrammingExerciseTestCase, Visibility } from 'app/entities/programming/programming-exercise-test-case.model'; import { StaticCodeAnalysisCategory } from 'app/entities/programming/static-code-analysis-category.model'; import { ProgrammingExerciseGradingStatistics } from 'app/entities/programming/programming-exercise-test-case-statistics.model'; @@ -47,7 +47,7 @@ export interface IProgrammingExerciseGradingService { @Injectable({ providedIn: 'root' }) export class ProgrammingExerciseGradingService implements IProgrammingExerciseGradingService, OnDestroy { - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private http = inject(HttpClient); public resourceUrl = 'api/programming-exercises'; diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-websocket.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-websocket.service.ts index 125d0c0397a2..b59ba39a887e 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-websocket.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-websocket.service.ts @@ -2,7 +2,7 @@ import { Injectable, OnDestroy, inject } from '@angular/core'; import { HttpResponse } from '@angular/common/http'; import { BehaviorSubject, Observable } from 'rxjs'; import { filter, tap } from 'rxjs/operators'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; export type EntityResponseType = HttpResponse; @@ -20,7 +20,7 @@ export interface IProgrammingExerciseWebsocketService { @Injectable({ providedIn: 'root' }) export class ProgrammingExerciseWebsocketService implements OnDestroy, IProgrammingExerciseWebsocketService { - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private connections: string[] = []; // Uses undefined for initial value. diff --git a/src/main/webapp/app/exercises/programming/participate/programming-build-run.service.ts b/src/main/webapp/app/exercises/programming/participate/programming-build-run.service.ts index 88ef2c010196..80a836ce4005 100644 --- a/src/main/webapp/app/exercises/programming/participate/programming-build-run.service.ts +++ b/src/main/webapp/app/exercises/programming/participate/programming-build-run.service.ts @@ -1,7 +1,7 @@ import { Injectable, OnDestroy, inject } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { filter, tap } from 'rxjs/operators'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; /** * Describes the build run state @@ -24,7 +24,7 @@ export interface IProgrammingBuildRunService { */ @Injectable({ providedIn: 'root' }) export class ProgrammingBuildRunService implements OnDestroy { - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); // Boolean subject: true == build is running, false == build is not running. private buildRunSubjects: { [programmingExerciseId: number]: BehaviorSubject } = {}; diff --git a/src/main/webapp/app/exercises/programming/participate/programming-submission.service.ts b/src/main/webapp/app/exercises/programming/participate/programming-submission.service.ts index 655c4d7dab6b..48812dfd7fae 100644 --- a/src/main/webapp/app/exercises/programming/participate/programming-submission.service.ts +++ b/src/main/webapp/app/exercises/programming/participate/programming-submission.service.ts @@ -5,7 +5,7 @@ import { catchError, distinctUntilChanged, filter, map, reduce, switchMap, tap } import { ParticipationWebsocketService } from 'app/overview/participation-websocket.service'; import { Result } from 'app/entities/result.model'; import { createRequestOption } from 'app/shared/util/request.util'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { Exercise, ExerciseType } from 'app/entities/exercise.model'; import { ProgrammingSubmission } from 'app/entities/programming/programming-submission.model'; import { SubmissionType, getLatestSubmissionResult, setLatestSubmissionResult } from 'app/entities/submission.model'; @@ -66,7 +66,7 @@ export interface IProgrammingSubmissionService { @Injectable({ providedIn: 'root' }) export class ProgrammingSubmissionService implements IProgrammingSubmissionService, OnDestroy { - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private http = inject(HttpClient); private participationWebsocketService = inject(ParticipationWebsocketService); private participationService = inject(ProgrammingExerciseParticipationService); diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-conflict-state.service.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-conflict-state.service.ts index e3429e7e4901..0c991b5b07c6 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-conflict-state.service.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-conflict-state.service.ts @@ -1,7 +1,7 @@ import { BehaviorSubject, Observable } from 'rxjs'; import { Injectable, OnDestroy, inject } from '@angular/core'; import { distinctUntilChanged } from 'rxjs/operators'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { DomainType, GitConflictState } from 'app/exercises/programming/shared/code-editor/model/code-editor.model'; import { DomainDependentService } from 'app/exercises/programming/shared/code-editor/service/code-editor-domain-dependent.service'; @@ -16,7 +16,7 @@ export interface IConflictStateService { */ @Injectable({ providedIn: 'root' }) export class CodeEditorConflictStateService extends DomainDependentService implements IConflictStateService, OnDestroy { - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private conflictSubjects: Map> = new Map(); private websocketConnections: Map = new Map(); diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-domain-dependent-endpoint.service.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-domain-dependent-endpoint.service.ts index bd34fcd567a0..4fd362b3d137 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-domain-dependent-endpoint.service.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-domain-dependent-endpoint.service.ts @@ -1,6 +1,6 @@ import { DomainDependentService } from 'app/exercises/programming/shared/code-editor/service/code-editor-domain-dependent.service'; import { HttpClient } from '@angular/common/http'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { DomainChange, DomainType } from 'app/exercises/programming/shared/code-editor/model/code-editor.model'; import { inject } from '@angular/core'; @@ -10,7 +10,7 @@ import { inject } from '@angular/core'; export abstract class DomainDependentEndpointService extends DomainDependentService { protected restResourceUrl?: string; protected http = inject(HttpClient); - protected jhiWebsocketService = inject(JhiWebsocketService); + protected jhiWebsocketService = inject(WebsocketService); protected constructor() { super(); diff --git a/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.ts b/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.ts index 15f2a2d2459a..f27804094982 100644 --- a/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.ts @@ -1000,7 +1000,7 @@ export class DragAndDropQuestionEditComponent implements OnInit, OnChanges, Afte * @returns returns a blob created from the data url */ dataUrlToBlob(dataUrl: string): Blob { - // Seperate metadata from base64-encoded content + // Separate metadata from base64-encoded content const byteString = window.atob(dataUrl.split(',')[1]); // Isolate the MIME type (e.g "image/png") const mimeString = dataUrl.split(',')[0].split(':')[1].split(';')[0]; diff --git a/src/main/webapp/app/exercises/quiz/manage/statistics/question-statistic.component.ts b/src/main/webapp/app/exercises/quiz/manage/statistics/question-statistic.component.ts index 7b79e4dfb324..cb70f6d4af3b 100644 --- a/src/main/webapp/app/exercises/quiz/manage/statistics/question-statistic.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/statistics/question-statistic.component.ts @@ -3,7 +3,7 @@ import { QuizQuestionStatistic } from 'app/entities/quiz/quiz-question-statistic import { QuizExercise } from 'app/entities/quiz/quiz-exercise.model'; import { AccountService } from 'app/core/auth/account.service'; import { QuizExerciseService } from 'app/exercises/quiz/manage/quiz-exercise.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { Authority } from 'app/shared/constants/authority.constants'; import { Subscription } from 'rxjs'; import { SafeHtml } from '@angular/platform-browser'; @@ -26,7 +26,7 @@ export abstract class QuestionStatisticComponent extends AbstractQuizStatisticCo protected router = inject(Router); protected accountService = inject(AccountService); protected quizExerciseService = inject(QuizExerciseService); - protected jhiWebsocketService = inject(JhiWebsocketService); + protected jhiWebsocketService = inject(WebsocketService); protected changeDetector = inject(ChangeDetectorRef); question: QuizQuestion; diff --git a/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-point-statistic/quiz-point-statistic.component.ts b/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-point-statistic/quiz-point-statistic.component.ts index bb31e1baee65..008c03884dd6 100644 --- a/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-point-statistic/quiz-point-statistic.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-point-statistic/quiz-point-statistic.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angula import { ActivatedRoute, Router } from '@angular/router'; import { AbstractQuizStatisticComponent } from 'app/exercises/quiz/manage/statistics/quiz-statistics'; import { AccountService } from 'app/core/auth/account.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { PointCounter } from 'app/entities/quiz/point-counter.model'; import { QuizExerciseService } from 'app/exercises/quiz/manage/quiz-exercise.service'; import { QuizPointStatistic } from 'app/entities/quiz/quiz-point-statistic.model'; @@ -31,7 +31,7 @@ export class QuizPointStatisticComponent extends AbstractQuizStatisticComponent private router = inject(Router); private accountService = inject(AccountService); private quizExerciseService = inject(QuizExerciseService); - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private changeDetector = inject(ChangeDetectorRef); private serverDateService = inject(ArtemisServerDateService); diff --git a/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-statistic/quiz-statistic.component.ts b/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-statistic/quiz-statistic.component.ts index 45b010a9e8df..130bcbd51bb6 100644 --- a/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-statistic/quiz-statistic.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-statistic/quiz-statistic.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angula import { ActivatedRoute, Router } from '@angular/router'; import { HttpResponse } from '@angular/common/http'; import { AccountService } from 'app/core/auth/account.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { QuizExercise } from 'app/entities/quiz/quiz-exercise.model'; import { QuizExerciseService } from 'app/exercises/quiz/manage/quiz-exercise.service'; import { Authority } from 'app/shared/constants/authority.constants'; @@ -26,7 +26,7 @@ export class QuizStatisticComponent extends AbstractQuizStatisticComponent imple private router = inject(Router); private accountService = inject(AccountService); private quizExerciseService = inject(QuizExerciseService); - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private changeDetector = inject(ChangeDetectorRef); quizExercise: QuizExercise; diff --git a/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-statistics-footer/quiz-statistics-footer.component.ts b/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-statistics-footer/quiz-statistics-footer.component.ts index e563711c2c49..e44308391ae9 100644 --- a/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-statistics-footer/quiz-statistics-footer.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-statistics-footer/quiz-statistics-footer.component.ts @@ -5,7 +5,7 @@ import { ShortAnswerQuestionUtil } from 'app/exercises/quiz/shared/short-answer- import { TranslateService } from '@ngx-translate/core'; import { HttpResponse } from '@angular/common/http'; import { AccountService } from 'app/core/auth/account.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { QuizQuestion, QuizQuestionType } from 'app/entities/quiz/quiz-question.model'; import { QuizExerciseService } from 'app/exercises/quiz/manage/quiz-exercise.service'; import { MultipleChoiceQuestionStatistic } from 'app/entities/quiz/multiple-choice-question-statistic.model'; @@ -36,7 +36,7 @@ export class QuizStatisticsFooterComponent implements OnInit, OnDestroy { private translateService = inject(TranslateService); private quizExerciseService = inject(QuizExerciseService); private quizStatisticUtil = inject(QuizStatisticUtil); - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private serverDateService = inject(ArtemisServerDateService); @Input() isQuizPointStatistic: boolean; diff --git a/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.ts b/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.ts index 560071f503f6..c08a35c2514e 100644 --- a/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.ts +++ b/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.ts @@ -14,7 +14,7 @@ import { TranslateService } from '@ngx-translate/core'; import * as smoothscroll from 'smoothscroll-polyfill'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { ShortAnswerSubmittedAnswer } from 'app/entities/quiz/short-answer-submitted-answer.model'; import { QuizExerciseService } from 'app/exercises/quiz/manage/quiz-exercise.service'; import { DragAndDropMapping } from 'app/entities/quiz/drag-and-drop-mapping.model'; @@ -70,7 +70,7 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; ], }) export class QuizParticipationComponent implements OnInit, OnDestroy { - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private quizExerciseService = inject(QuizExerciseService); private participationService = inject(ParticipationService); private route = inject(ActivatedRoute); 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 c2b80acd012e..130e05209f1e 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 @@ -14,7 +14,7 @@ import { ModelingSubmissionElement } from 'app/exercises/shared/plagiarism/types import { TextSubmissionElement } from 'app/exercises/shared/plagiarism/types/text/TextSubmissionElement'; import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { PlagiarismOptions } from 'app/exercises/shared/plagiarism/types/PlagiarismOptions'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { faChevronRight, faExclamationTriangle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; @@ -65,7 +65,7 @@ export class PlagiarismInspectorComponent implements OnInit { private modelingExerciseService = inject(ModelingExerciseService); private programmingExerciseService = inject(ProgrammingExerciseService); private textExerciseService = inject(TextExerciseService); - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private translateService = inject(TranslateService); private inspectorService = inject(PlagiarismInspectorService); private plagiarismCasesService = inject(PlagiarismCasesService); diff --git a/src/main/webapp/app/exercises/shared/team-submission-sync/team-submission-sync.component.ts b/src/main/webapp/app/exercises/shared/team-submission-sync/team-submission-sync.component.ts index 2a8d455dd6ff..14a6f23b0819 100644 --- a/src/main/webapp/app/exercises/shared/team-submission-sync/team-submission-sync.component.ts +++ b/src/main/webapp/app/exercises/shared/team-submission-sync/team-submission-sync.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, OnInit, Output, inject } from '@angular/core'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { throttleTime } from 'rxjs/operators'; import { AlertService } from 'app/core/util/alert.service'; @@ -18,7 +18,7 @@ import { SubmissionPatchPayload, isSubmissionPatchPayload } from 'app/entities/s }) export class TeamSubmissionSyncComponent implements OnInit { private accountService = inject(AccountService); - private teamSubmissionWebsocketService = inject(JhiWebsocketService); + private teamSubmissionWebsocketService = inject(WebsocketService); private alertService = inject(AlertService); // Sync settings @@ -76,14 +76,14 @@ export class TeamSubmissionSyncComponent implements OnInit { submission.participation.exercise = undefined; submission.participation.submissions = []; } - this.teamSubmissionWebsocketService.send(this.buildWebsocketTopic('/update'), submission); + this.teamSubmissionWebsocketService.send(this.buildWebsocketTopic('/update'), submission); }, error: (error) => this.onError(error), }); this.submissionPatchObservable?.subscribe({ next: (submissionPatch: SubmissionPatch) => { - this.teamSubmissionWebsocketService.send(this.buildWebsocketTopic('/patch'), submissionPatch); + this.teamSubmissionWebsocketService.send(this.buildWebsocketTopic('/patch'), submissionPatch); }, error: (error) => this.onError(error), }); diff --git a/src/main/webapp/app/exercises/shared/team/team-participate/team-students-online-list.component.ts b/src/main/webapp/app/exercises/shared/team/team-participate/team-students-online-list.component.ts index c62d3c6538bd..1191ee1a4ecc 100644 --- a/src/main/webapp/app/exercises/shared/team/team-participate/team-students-online-list.component.ts +++ b/src/main/webapp/app/exercises/shared/team/team-participate/team-students-online-list.component.ts @@ -6,7 +6,7 @@ import { orderBy } from 'lodash-es'; import { Observable } from 'rxjs'; import { map, throttleTime } from 'rxjs/operators'; import dayjs from 'dayjs/esm'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { faCircle, faHistory } from '@fortawesome/free-solid-svg-icons'; import { NgClass } from '@angular/common'; @@ -23,7 +23,7 @@ import { captureException } from '@sentry/angular'; }) export class TeamStudentsOnlineListComponent implements OnInit, OnDestroy { private accountService = inject(AccountService); - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); readonly SHOW_TYPING_DURATION = 2000; // ms readonly SEND_TYPING_INTERVAL = this.SHOW_TYPING_DURATION / 1.5; @@ -68,14 +68,14 @@ export class TeamStudentsOnlineListComponent implements OnInit, OnDestroy { error: (error) => captureException(error), }); setTimeout(() => { - this.jhiWebsocketService.send(this.buildWebsocketTopic('/trigger'), {}); + this.jhiWebsocketService.send(this.buildWebsocketTopic('/trigger'), {}); }, 700); } private setupTypingIndicatorSender() { if (this.typing$) { this.typing$.pipe(throttleTime(this.SEND_TYPING_INTERVAL)).subscribe({ - next: () => this.jhiWebsocketService.send(this.buildWebsocketTopic('/typing'), {}), + next: () => this.jhiWebsocketService.send(this.buildWebsocketTopic('/typing'), {}), error: (error) => captureException(error), }); } diff --git a/src/main/webapp/app/exercises/shared/team/team.service.ts b/src/main/webapp/app/exercises/shared/team/team.service.ts index b7125de9aab1..deef69604bf4 100644 --- a/src/main/webapp/app/exercises/shared/team/team.service.ts +++ b/src/main/webapp/app/exercises/shared/team/team.service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; import { Injectable, OnDestroy, inject } from '@angular/core'; import { AccountService } from 'app/core/auth/account.service'; import { User } from 'app/core/user/user.model'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { Course } from 'app/entities/course.model'; import { Exercise } from 'app/entities/exercise.model'; import { StudentWithTeam, Team, TeamAssignmentPayload, TeamImportStrategyType } from 'app/entities/team.model'; @@ -103,7 +103,7 @@ export interface ITeamService { @Injectable({ providedIn: 'root' }) export class TeamService implements ITeamService, OnDestroy { protected http = inject(HttpClient); - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private accountService = inject(AccountService); // Team Assignment Update Stream diff --git a/src/main/webapp/app/exercises/text/participate/text-editor.component.ts b/src/main/webapp/app/exercises/text/participate/text-editor.component.ts index 12c56eaa84b2..fc9698863136 100644 --- a/src/main/webapp/app/exercises/text/participate/text-editor.component.ts +++ b/src/main/webapp/app/exercises/text/participate/text-editor.component.ts @@ -397,6 +397,11 @@ export class TextEditorComponent implements OnInit, OnDestroy, ComponentCanDeact this.textSubmissionService.update(submissionToCreateOrUpdate, this.textExercise.id!).subscribe({ next: (response) => { this.submission = response.body!; + if (this.participation.team) { + // Make sure the team is not lost during update + const studentParticipation = this.submission.participation as StudentParticipation; + studentParticipation.team = this.participation.team; + } setLatestSubmissionResult(this.submission, getLatestSubmissionResult(this.submission)); this.submissionChange.next(this.submission); // reconnect so that the submission status is displayed correctly in the result.component @@ -447,7 +452,10 @@ export class TextEditorComponent implements OnInit, OnDestroy, ComponentCanDeact onReceiveSubmissionFromTeam(submission: TextSubmission) { submission.participation!.exercise = this.textExercise; submission.participation!.submissions = [submission]; - this.updateParticipation(submission.participation as StudentParticipation); + // Keep the existing team on the participation + const studentParticipation = submission.participation as StudentParticipation; + studentParticipation.team = this.participation.team; + this.updateParticipation(studentParticipation); } onTextEditorInput(event: Event) { diff --git a/src/main/webapp/app/iris/iris-status.service.ts b/src/main/webapp/app/iris/iris-status.service.ts index fc39d067860f..c0921699f32a 100644 --- a/src/main/webapp/app/iris/iris-status.service.ts +++ b/src/main/webapp/app/iris/iris-status.service.ts @@ -1,7 +1,7 @@ import { Injectable, OnDestroy, inject } from '@angular/core'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { BehaviorSubject, Observable, Subscription, firstValueFrom } from 'rxjs'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { Response } from 'app/iris/iris-chat-http.service'; import { IrisStatusDTO } from 'app/entities/iris/iris-health.model'; import { IrisRateLimitInformation } from 'app/entities/iris/iris-ratelimit-info.model'; @@ -14,7 +14,7 @@ import { IrisRateLimitInformation } from 'app/entities/iris/iris-ratelimit-info. */ @Injectable({ providedIn: 'root' }) export class IrisStatusService implements OnDestroy { - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private httpClient = inject(HttpClient); intervalId: ReturnType | undefined; diff --git a/src/main/webapp/app/iris/iris-websocket.service.ts b/src/main/webapp/app/iris/iris-websocket.service.ts index 24c200d1650e..e6a828b6a0ef 100644 --- a/src/main/webapp/app/iris/iris-websocket.service.ts +++ b/src/main/webapp/app/iris/iris-websocket.service.ts @@ -1,5 +1,5 @@ import { Injectable, OnDestroy, inject } from '@angular/core'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { Observable, Subject, Subscription } from 'rxjs'; type SubscribedChannel = { wsSubscription: Subscription; subject: Subject }; @@ -9,7 +9,7 @@ type SubscribedChannel = { wsSubscription: Subscription; subject: Subject } */ @Injectable({ providedIn: 'root' }) export class IrisWebsocketService implements OnDestroy { - protected jhiWebsocketService = inject(JhiWebsocketService); + protected jhiWebsocketService = inject(WebsocketService); private subscribedChannels: Map = new Map(); diff --git a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts index 07defd4cac33..e60773e68607 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts +++ b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts @@ -6,7 +6,7 @@ import { faCircleCheck, faExclamationCircle, faExclamationTriangle, faPause, faP import dayjs from 'dayjs/esm'; import { TriggeredByPushTo } from 'app/entities/programming/repository-info.model'; import { ActivatedRoute } from '@angular/router'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { BuildQueueService } from 'app/localci/build-queue/build-queue.service'; import { AlertService, AlertType } from 'app/core/util/alert.service'; import { ArtemisSharedModule } from 'app/shared/shared.module'; @@ -21,7 +21,7 @@ import { SubmissionResultStatusModule } from 'app/overview/submission-result-sta imports: [ArtemisSharedModule, NgxDatatableModule, ArtemisDataTableModule, SubmissionResultStatusModule], }) export class BuildAgentDetailsComponent implements OnInit, OnDestroy { - private readonly websocketService = inject(JhiWebsocketService); + private readonly websocketService = inject(WebsocketService); private readonly buildAgentsService = inject(BuildAgentsService); private readonly route = inject(ActivatedRoute); private readonly buildQueueService = inject(BuildQueueService); diff --git a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts index 08d8772163bd..01d0b2b48389 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts +++ b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts @@ -1,6 +1,6 @@ import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { BuildAgentInformation, BuildAgentStatus } from 'app/entities/programming/build-agent-information.model'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; import { Subscription } from 'rxjs'; import { faPause, faPlay, faTimes } from '@fortawesome/free-solid-svg-icons'; @@ -20,7 +20,7 @@ import { NgxDatatableModule } from '@siemens/ngx-datatable'; imports: [ArtemisSharedModule, NgxDatatableModule, ArtemisDataTableModule], }) export class BuildAgentSummaryComponent implements OnInit, OnDestroy { - private readonly websocketService = inject(JhiWebsocketService); + private readonly websocketService = inject(WebsocketService); private readonly buildAgentsService = inject(BuildAgentsService); private readonly buildQueueService = inject(BuildQueueService); private readonly router = inject(Router); diff --git a/src/main/webapp/app/localci/build-queue/build-queue.component.ts b/src/main/webapp/app/localci/build-queue/build-queue.component.ts index 0f0a904c4375..159eda1eee97 100644 --- a/src/main/webapp/app/localci/build-queue/build-queue.component.ts +++ b/src/main/webapp/app/localci/build-queue/build-queue.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit, ViewChild, inject } from '@angular/core'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { BuildJob, BuildJobStatistics, FinishedBuildJob, SpanType } from 'app/entities/programming/build-job.model'; import { faAngleDown, faAngleRight, faCircleCheck, faExclamationCircle, faExclamationTriangle, faFilter, faSort, faSync, faTimes } from '@fortawesome/free-solid-svg-icons'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { BuildQueueService } from 'app/localci/build-queue/build-queue.service'; import { debounceTime, distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators'; import { TriggeredByPushTo } from 'app/entities/programming/repository-info.model'; @@ -145,7 +145,7 @@ export enum FinishedBuildJobFilterStorageKey { }) export class BuildQueueComponent implements OnInit, OnDestroy { private route = inject(ActivatedRoute); - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private buildQueueService = inject(BuildQueueService); private alertService = inject(AlertService); private modalService = inject(NgbModal); diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index 3056f4857a61..7e4554eff93a 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -41,7 +41,7 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { NgbDropdown, NgbDropdownButtonItem, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle, NgbModal, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { AlertService, AlertType } from 'app/core/util/alert.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { CourseAccessStorageService } from 'app/course/course-access-storage.service'; import { CourseStorageService } from 'app/course/manage/course-storage.service'; import { Course, isCommunicationEnabled, isMessagingEnabled } from 'app/entities/course.model'; @@ -132,7 +132,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit private courseStorageService = inject(CourseStorageService); private route = inject(ActivatedRoute); private teamService = inject(TeamService); - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private serverDateService = inject(ArtemisServerDateService); private alertService = inject(AlertService); private changeDetectorRef = inject(ChangeDetectorRef); diff --git a/src/main/webapp/app/overview/courses.component.ts b/src/main/webapp/app/overview/courses.component.ts index 156b295f450c..72e3fba935e7 100644 --- a/src/main/webapp/app/overview/courses.component.ts +++ b/src/main/webapp/app/overview/courses.component.ts @@ -6,7 +6,7 @@ import { HttpResponse } from '@angular/common/http'; import { GuidedTourService } from 'app/guided-tour/guided-tour.service'; import { courseOverviewTour } from 'app/guided-tour/tours/course-overview-tour'; import { TeamService } from 'app/exercises/shared/team/team.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import dayjs from 'dayjs/esm'; import { Exam } from 'app/entities/exam/exam.model'; import { Router, RouterLink } from '@angular/router'; @@ -43,7 +43,7 @@ export class CoursesComponent implements OnInit, OnDestroy { private courseService = inject(CourseManagementService); private guidedTourService = inject(GuidedTourService); private teamService = inject(TeamService); - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private router = inject(Router); private courseAccessStorageService = inject(CourseAccessStorageService); diff --git a/src/main/webapp/app/overview/participation-websocket.service.ts b/src/main/webapp/app/overview/participation-websocket.service.ts index 7e7f40e98f8d..7ec0ebeff526 100644 --- a/src/main/webapp/app/overview/participation-websocket.service.ts +++ b/src/main/webapp/app/overview/participation-websocket.service.ts @@ -6,7 +6,7 @@ import { Result } from 'app/entities/result.model'; import { Exercise, ExerciseType } from 'app/entities/exercise.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import dayjs from 'dayjs/esm'; import { cloneDeep } from 'lodash-es'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; @@ -25,7 +25,7 @@ export interface IParticipationWebsocketService { @Injectable({ providedIn: 'root' }) export class ParticipationWebsocketService implements IParticipationWebsocketService { - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private participationService = inject(ParticipationService); cachedParticipations: Map = new Map(); diff --git a/src/main/webapp/app/shared/components/course-exam-archive-button/course-exam-archive-button.component.ts b/src/main/webapp/app/shared/components/course-exam-archive-button/course-exam-archive-button.component.ts index 46eb02356885..216a4fd2e232 100644 --- a/src/main/webapp/app/shared/components/course-exam-archive-button/course-exam-archive-button.component.ts +++ b/src/main/webapp/app/shared/components/course-exam-archive-button/course-exam-archive-button.component.ts @@ -1,7 +1,7 @@ import { Component, Input, OnDestroy, OnInit, TemplateRef, ViewChild, inject } from '@angular/core'; import { AlertService } from 'app/core/util/alert.service'; import { CourseManagementService } from 'app/course/manage/course-management.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { TranslateService } from '@ngx-translate/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { tap } from 'rxjs/operators'; @@ -37,7 +37,7 @@ export class CourseExamArchiveButtonComponent implements OnInit, OnDestroy { private courseService = inject(CourseManagementService); private examService = inject(ExamManagementService); private alertService = inject(AlertService); - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private translateService = inject(TranslateService); private modalService = inject(NgbModal); private accountService = inject(AccountService); diff --git a/src/main/webapp/app/shared/connection-status/connection-status.component.ts b/src/main/webapp/app/shared/connection-status/connection-status.component.ts index b29faab0f34a..34ce5ded38d5 100644 --- a/src/main/webapp/app/shared/connection-status/connection-status.component.ts +++ b/src/main/webapp/app/shared/connection-status/connection-status.component.ts @@ -1,7 +1,7 @@ import { Component, ContentChild, ElementRef, Input, OnDestroy, OnInit, inject } from '@angular/core'; import { faCircle, faExclamation, faTowerBroadcast } from '@fortawesome/free-solid-svg-icons'; import { Subscription } from 'rxjs'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { NgClass } from '@angular/common'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { TranslateDirective } from '../language/translate.directive'; @@ -13,7 +13,7 @@ import { TranslateDirective } from '../language/translate.directive'; imports: [NgClass, FaIconComponent, TranslateDirective], }) export class JhiConnectionStatusComponent implements OnInit, OnDestroy { - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); @ContentChild('innerContent', { static: false }) innerContent: ElementRef; @Input() isExamMode = false; diff --git a/src/main/webapp/app/shared/connection-warning/connection-warning.component.ts b/src/main/webapp/app/shared/connection-warning/connection-warning.component.ts index eae68db19f00..f9619d6c1986 100644 --- a/src/main/webapp/app/shared/connection-warning/connection-warning.component.ts +++ b/src/main/webapp/app/shared/connection-warning/connection-warning.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit, ViewChild, inject } from '@angular/core'; import { faExclamationCircle, faWifi } from '@fortawesome/free-solid-svg-icons'; import { Subscription, filter } from 'rxjs'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; import { NavigationEnd, Router } from '@angular/router'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; @@ -16,7 +16,7 @@ import { NgClass } from '@angular/common'; imports: [FaIconComponent, TranslateDirective, CloseCircleComponent, NgClass, NgbPopover], }) export class JhiConnectionWarningComponent implements OnInit, OnDestroy { - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private router = inject(Router); @ViewChild('popover') popover: NgbPopover; diff --git a/src/main/webapp/app/shared/feature-toggle/feature-toggle.service.ts b/src/main/webapp/app/shared/feature-toggle/feature-toggle.service.ts index 283f60e2d313..244a5b7d5ccc 100644 --- a/src/main/webapp/app/shared/feature-toggle/feature-toggle.service.ts +++ b/src/main/webapp/app/shared/feature-toggle/feature-toggle.service.ts @@ -1,6 +1,6 @@ import { Injectable, inject } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { HttpClient } from '@angular/common/http'; @@ -25,7 +25,7 @@ const defaultActiveFeatureState: ActiveFeatureToggles = Object.values(FeatureTog @Injectable({ providedIn: 'root' }) export class FeatureToggleService { - private websocketService = inject(JhiWebsocketService); + private websocketService = inject(WebsocketService); private http = inject(HttpClient); private readonly TOPIC = `/topic/management/feature-toggles`; diff --git a/src/main/webapp/app/shared/metis/metis-conversation.service.ts b/src/main/webapp/app/shared/metis/metis-conversation.service.ts index c0d4e0e0d50d..ffa6498da3df 100644 --- a/src/main/webapp/app/shared/metis/metis-conversation.service.ts +++ b/src/main/webapp/app/shared/metis/metis-conversation.service.ts @@ -2,7 +2,7 @@ import { Injectable, OnDestroy, inject } from '@angular/core'; import { EMPTY, Observable, ReplaySubject, Subject, Subscription, catchError, finalize, map, of, switchMap, tap } from 'rxjs'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { ConversationService } from 'app/shared/metis/conversations/conversation.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { AccountService } from 'app/core/auth/account.service'; import { User } from 'app/core/user/user.model'; import { ConversationWebsocketDTO } from 'app/entities/metis/conversation/conversation-websocket-dto.model'; @@ -30,7 +30,7 @@ export class MetisConversationService implements OnDestroy { private oneToOneChatService = inject(OneToOneChatService); private channelService = inject(ChannelService); protected conversationService = inject(ConversationService); - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private accountService = inject(AccountService); private alertService = inject(AlertService); private router = inject(Router); diff --git a/src/main/webapp/app/shared/metis/metis.service.ts b/src/main/webapp/app/shared/metis/metis.service.ts index 629c005d9509..441bda75f35e 100644 --- a/src/main/webapp/app/shared/metis/metis.service.ts +++ b/src/main/webapp/app/shared/metis/metis.service.ts @@ -23,7 +23,7 @@ import { } from 'app/shared/metis/metis.util'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { Params } from '@angular/router'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { MetisPostDTO } from 'app/entities/metis/metis-post-dto.model'; import dayjs from 'dayjs/esm'; import { PlagiarismCase } from 'app/exercises/shared/plagiarism/types/PlagiarismCase'; @@ -41,7 +41,7 @@ export class MetisService implements OnDestroy { protected reactionService = inject(ReactionService); protected accountService = inject(AccountService); protected exerciseService = inject(ExerciseService); - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private conversationService = inject(ConversationService); private posts$: ReplaySubject = new ReplaySubject(1); diff --git a/src/main/webapp/app/shared/notification/notification.service.ts b/src/main/webapp/app/shared/notification/notification.service.ts index fb009fdab982..fc35afe7af6e 100644 --- a/src/main/webapp/app/shared/notification/notification.service.ts +++ b/src/main/webapp/app/shared/notification/notification.service.ts @@ -6,7 +6,7 @@ import { filter, map } from 'rxjs/operators'; import { createRequestOption } from 'app/shared/util/request.util'; import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router'; import { AccountService } from 'app/core/auth/account.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { User } from 'app/core/user/user.model'; import { GroupNotification, GroupNotificationType } from 'app/entities/group-notification.model'; import { @@ -65,7 +65,7 @@ const MESSAGING_NOTIFICATION_TEXTS = [ @Injectable({ providedIn: 'root' }) export class NotificationService { - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private router = inject(Router); private http = inject(HttpClient); private accountService = inject(AccountService); diff --git a/src/main/webapp/app/shared/notification/system-notification/system-notification.component.ts b/src/main/webapp/app/shared/notification/system-notification/system-notification.component.ts index a6e05379c1d9..bbec4ac631fa 100644 --- a/src/main/webapp/app/shared/notification/system-notification/system-notification.component.ts +++ b/src/main/webapp/app/shared/notification/system-notification/system-notification.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import dayjs from 'dayjs/esm'; import { SystemNotification, SystemNotificationType } from 'app/entities/system-notification.model'; import { AccountService } from 'app/core/auth/account.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { User } from 'app/core/user/user.model'; import { SystemNotificationService } from 'app/shared/notification/system-notification/system-notification.service'; import { faExclamationTriangle, faInfoCircle, faTimes } from '@fortawesome/free-solid-svg-icons'; @@ -21,7 +21,7 @@ export const WEBSOCKET_CHANNEL = '/topic/system-notification'; }) export class SystemNotificationComponent implements OnInit, OnDestroy { private accountService = inject(AccountService); - private jhiWebsocketService = inject(JhiWebsocketService); + private jhiWebsocketService = inject(WebsocketService); private systemNotificationService = inject(SystemNotificationService); readonly INFO = SystemNotificationType.INFO; diff --git a/src/test/java/de/tum/cit/aet/artemis/core/config/websocket/GzipMessageConverterTest.java b/src/test/java/de/tum/cit/aet/artemis/core/config/websocket/GzipMessageConverterTest.java new file mode 100644 index 000000000000..2b610f39bf67 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/core/config/websocket/GzipMessageConverterTest.java @@ -0,0 +1,127 @@ +package de.tum.cit.aet.artemis.core.config.websocket; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.zip.GZIPOutputStream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.NativeMessageHeaderAccessor; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.cit.aet.artemis.communication.dto.AuthorDTO; + +class GzipMessageConverterTest { + + private GzipMessageConverter converter; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + converter = new GzipMessageConverter(objectMapper); + } + + @Test + void testSupportsAnyClass() { + assertThat(converter.supports(String.class)).isTrue(); + assertThat(converter.supports(Object.class)).isTrue(); + } + + @Test + void testConvertFromInternalWithCompressedPayload() throws Exception { + // Arrange + var author = new AuthorDTO(1L, "Test", "Test"); + String payload = objectMapper.writeValueAsString(author); + byte[] compressedPayload = compressAndEncode(payload.getBytes()).getBytes(); + + Message message = mock(Message.class); + MessageHeaders headers = mock(MessageHeaders.class); + Map nativeHeaders = Map.of(GzipMessageConverter.COMPRESSION_HEADER_KEY, List.of("true")); + + when(message.getPayload()).thenReturn(compressedPayload); + when(message.getHeaders()).thenReturn(headers); + when(headers.get(NativeMessageHeaderAccessor.NATIVE_HEADERS)).thenReturn(nativeHeaders); + + // Act + Object result = converter.convertFromInternal(message, AuthorDTO.class, null); + + // Assert + assertThat(result).isNotNull(); + assertThat(result).isEqualTo(author); + } + + @Test + void testConvertFromInternalWithoutCompressedPayload() throws Exception { + // Arrange + var author = new AuthorDTO(1L, "Test", "Test"); + String payload = objectMapper.writeValueAsString(author); + Message message = mock(Message.class); + MessageHeaders headers = mock(MessageHeaders.class); + + when(message.getPayload()).thenReturn(payload); + when(message.getHeaders()).thenReturn(headers); + when(headers.get(NativeMessageHeaderAccessor.NATIVE_HEADERS)).thenReturn(null); + + // Act + Object result = converter.convertFromInternal(message, AuthorDTO.class, null); + + // Assert + assertThat(result).isEqualTo(author); + } + + @Test + void testConvertToInternalWithCompressionEnabled() throws Exception { + // Arrange + var author = new AuthorDTO(1L, "Test", "Test"); + String payload = objectMapper.writeValueAsString(author); + byte[] payloadBytes = payload.getBytes(); + + MessageHeaders headers = mock(MessageHeaders.class); + Map nativeHeaders = Map.of(GzipMessageConverter.COMPRESSION_HEADER_KEY, List.of("true")); + + when(headers.get(NativeMessageHeaderAccessor.NATIVE_HEADERS)).thenReturn(nativeHeaders); + + // Act + Object result = converter.convertToInternal(payloadBytes, headers, null); + + // Assert + assertThat(result).isNotNull(); + assertThat(result).isInstanceOf(byte[].class); + } + + @Test + void testConvertToInternalWithoutCompression() throws Exception { + // Arrange + var author = new AuthorDTO(1L, "Test", "Test"); + String payload = objectMapper.writeValueAsString(author); + byte[] payloadBytes = payload.getBytes(); + + MessageHeaders headers = mock(MessageHeaders.class); + when(headers.get(NativeMessageHeaderAccessor.NATIVE_HEADERS)).thenReturn(null); + + // Act + Object result = converter.convertToInternal(author, headers, null); + + // Assert + assertThat(result).isEqualTo(payloadBytes); + } + + // Utility for compression + private String compressAndEncode(byte[] data) throws Exception { + try (var byteStream = new ByteArrayOutputStream(); var gzipStream = new GZIPOutputStream(byteStream)) { + gzipStream.write(data); + gzipStream.finish(); + return Base64.getEncoder().encodeToString(byteStream.toByteArray()); + } + } +} diff --git a/src/test/javascript/spec/component/competencies/generate-competencies/generate-competencies.component.spec.ts b/src/test/javascript/spec/component/competencies/generate-competencies/generate-competencies.component.spec.ts index 94e72e39d3de..db8195309ef2 100644 --- a/src/test/javascript/spec/component/competencies/generate-competencies/generate-competencies.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/generate-competencies/generate-competencies.component.spec.ts @@ -21,7 +21,7 @@ import { HttpResponse } from '@angular/common/http'; import { By } from '@angular/platform-browser'; import { CompetencyRecommendationDetailComponent } from 'app/course/competencies/generate-competencies/competency-recommendation-detail.component'; import { DocumentationButtonComponent } from 'app/shared/components/documentation-button/documentation-button.component'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { IrisStageStateDTO } from 'app/entities/iris/iris-stage-dto.model'; import { CourseDescriptionFormComponent } from 'app/course/competencies/generate-competencies/course-description-form.component'; import { CourseCompetencyService } from 'app/course/competencies/course-competency.service'; @@ -57,7 +57,7 @@ describe('GenerateCompetenciesComponent', () => { }, { provide: Router, useClass: MockRouter }, { - provide: JhiWebsocketService, + provide: WebsocketService, useValue: { subscribe: jest.fn(), receive: jest.fn(() => mockWebSocketSubject.asObservable()), diff --git a/src/test/javascript/spec/component/connection-warning/connection-warning.spec.ts b/src/test/javascript/spec/component/connection-warning/connection-warning.spec.ts index 94387b6668c3..d2fab2d8908e 100644 --- a/src/test/javascript/spec/component/connection-warning/connection-warning.spec.ts +++ b/src/test/javascript/spec/component/connection-warning/connection-warning.spec.ts @@ -2,7 +2,7 @@ import { ArtemisTestModule } from '../../test.module'; import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { BehaviorSubject } from 'rxjs'; import { JhiConnectionWarningComponent } from 'app/shared/connection-warning/connection-warning.component'; -import { ConnectionState, JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { ConnectionState, WebsocketService } from 'app/core/websocket/websocket.service'; import { By } from '@angular/platform-browser'; describe('ConnectionWarning', () => { @@ -16,7 +16,7 @@ describe('ConnectionWarning', () => { imports: [ArtemisTestModule], providers: [ { - provide: JhiWebsocketService, + provide: WebsocketService, useValue: { connectionState: subject.asObservable(), }, diff --git a/src/test/javascript/spec/component/course/course-overview.component.spec.ts b/src/test/javascript/spec/component/course/course-overview.component.spec.ts index 1ea458f9426b..49a92b5402fc 100644 --- a/src/test/javascript/spec/component/course/course-overview.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-overview.component.spec.ts @@ -20,7 +20,7 @@ import { MockRouter } from '../../helpers/mocks/mock-router'; import { SecuredImageComponent } from 'app/shared/image/secured-image.component'; import { OrionFilterDirective } from 'app/shared/orion/orion-filter.directive'; import { TeamService } from 'app/exercises/shared/team/team.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { QuizExercise } from 'app/entities/quiz/quiz-exercise.model'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { SortDirective } from 'app/shared/sort/sort.directive'; @@ -137,7 +137,7 @@ describe('CourseOverviewComponent', () => { let teamService: TeamService; let tutorialGroupsService: TutorialGroupsService; let tutorialGroupsConfigurationService: TutorialGroupsConfigurationService; - let jhiWebsocketService: JhiWebsocketService; + let jhiWebsocketService: WebsocketService; let courseAccessStorageService: CourseAccessStorageService; let router: MockRouter; let jhiWebsocketServiceReceiveStub: jest.SpyInstance; @@ -192,7 +192,7 @@ describe('CourseOverviewComponent', () => { MockProvider(CourseExerciseService), MockProvider(CompetencyService), MockProvider(TeamService), - MockProvider(JhiWebsocketService), + MockProvider(WebsocketService), MockProvider(ArtemisServerDateService), MockProvider(AlertService), MockProvider(ChangeDetectorRef), @@ -222,7 +222,7 @@ describe('CourseOverviewComponent', () => { teamService = TestBed.inject(TeamService); tutorialGroupsService = TestBed.inject(TutorialGroupsService); tutorialGroupsConfigurationService = TestBed.inject(TutorialGroupsConfigurationService); - jhiWebsocketService = TestBed.inject(JhiWebsocketService); + jhiWebsocketService = TestBed.inject(WebsocketService); courseAccessStorageService = TestBed.inject(CourseAccessStorageService); metisConversationService = fixture.debugElement.injector.get(MetisConversationService); itemsDrop = component.itemsDrop; diff --git a/src/test/javascript/spec/component/exam/manage/exams/exam-checklist.component.spec.ts b/src/test/javascript/spec/component/exam/manage/exams/exam-checklist.component.spec.ts index 814df06dd42b..bec6bd9498dd 100644 --- a/src/test/javascript/spec/component/exam/manage/exams/exam-checklist.component.spec.ts +++ b/src/test/javascript/spec/component/exam/manage/exams/exam-checklist.component.spec.ts @@ -11,7 +11,7 @@ import { ArtemisTestModule } from '../../../../test.module'; import { ExamChecklistService } from 'app/exam/manage/exams/exam-checklist-component/exam-checklist.service'; import { MockExamChecklistService } from '../../../../helpers/mocks/service/mock-exam-checklist.service'; import { of } from 'rxjs'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { MockWebsocketService } from '../../../../helpers/mocks/service/mock-websocket.service'; import { ExamEditWorkingTimeComponent } from 'app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component'; @@ -67,7 +67,7 @@ describe('ExamChecklistComponent', () => { ], providers: [ { provide: ExamChecklistService, useClass: MockExamChecklistService }, - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, ], }) .compileComponents() diff --git a/src/test/javascript/spec/component/exam/manage/exams/exam-detail.component.spec.ts b/src/test/javascript/spec/component/exam/manage/exams/exam-detail.component.spec.ts index 811b2574c994..586bba32db3c 100644 --- a/src/test/javascript/spec/component/exam/manage/exams/exam-detail.component.spec.ts +++ b/src/test/javascript/spec/component/exam/manage/exams/exam-detail.component.spec.ts @@ -28,7 +28,7 @@ import { DeleteButtonDirective } from 'app/shared/delete-dialog/delete-button.di import { MockAccountService } from '../../../../helpers/mocks/service/mock-account.service'; import { AlertService } from 'app/core/util/alert.service'; import { ArtemisDurationFromSecondsPipe } from 'app/shared/pipes/artemis-duration-from-seconds.pipe'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { MockWebsocketService } from '../../../../helpers/mocks/service/mock-websocket.service'; import { ExamEditWorkingTimeComponent } from 'app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component'; import { ExamLiveAnnouncementCreateButtonComponent } from 'app/exam/manage/exams/exam-checklist-component/exam-announcement-dialog/exam-live-announcement-create-button.component'; @@ -118,7 +118,7 @@ describe('ExamDetailComponent', () => { safeHtmlForMarkdown: () => exampleHTML, }), MockProvider(AlertService), - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, { provide: TranslateService, useClass: MockTranslateService }, { provide: LocalStorageService, useClass: MockLocalStorageService }, MockProvider(ArtemisDurationFromSecondsPipe), diff --git a/src/test/javascript/spec/component/exam/manage/student-exams/exam-status.component.spec.ts b/src/test/javascript/spec/component/exam/manage/student-exams/exam-status.component.spec.ts index 453df0f6be55..d3de065f45a5 100644 --- a/src/test/javascript/spec/component/exam/manage/student-exams/exam-status.component.spec.ts +++ b/src/test/javascript/spec/component/exam/manage/student-exams/exam-status.component.spec.ts @@ -13,7 +13,7 @@ import { MockExamChecklistService } from '../../../../helpers/mocks/service/mock import { ExamChecklist } from 'app/entities/exam/exam-checklist.model'; import { of } from 'rxjs'; import { Course } from 'app/entities/course.model'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { MockWebsocketService } from '../../../../helpers/mocks/service/mock-websocket.service'; enum DateOffsetType { @@ -61,7 +61,7 @@ describe('ExamStatusComponent', () => { providers: [ { provide: TranslateService, useClass: MockTranslateService }, { provide: ExamChecklistService, useClass: MockExamChecklistService }, - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, ], }) .compileComponents() diff --git a/src/test/javascript/spec/component/exam/manage/student-exams/student-exams.component.spec.ts b/src/test/javascript/spec/component/exam/manage/student-exams/student-exams.component.spec.ts index 6212f37d7a93..6be540969bb9 100644 --- a/src/test/javascript/spec/component/exam/manage/student-exams/student-exams.component.spec.ts +++ b/src/test/javascript/spec/component/exam/manage/student-exams/student-exams.component.spec.ts @@ -23,7 +23,7 @@ import { MockAccountService } from '../../../../helpers/mocks/service/mock-accou import { AlertService } from 'app/core/util/alert.service'; import { TranslateDirective } from 'app/shared/language/translate.directive'; import { MockTranslateService } from '../../../../helpers/mocks/service/mock-translate.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { MockWebsocketService } from '../../../../helpers/mocks/service/mock-websocket.service'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { ArtemisTestModule } from '../../../../test.module'; @@ -165,7 +165,7 @@ describe('StudentExamsComponent', () => { }, { provide: AccountService, useClass: MockAccountService }, { provide: TranslateService, useClass: MockTranslateService }, - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, { provide: NgbModal, useClass: MockNgbModalService }, MockProvider(ProfileService, { getProfileInfo: () => of({ activeProfiles: [] }) }, 'useValue'), ]; diff --git a/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts index 1e4bda79525a..90384410cd51 100644 --- a/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts @@ -5,7 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { UMLDiagramType } from '@ls1intum/apollon'; import { TranslateService } from '@ngx-translate/core'; import { AlertService } from 'app/core/util/alert.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { CourseStorageService } from 'app/course/manage/course-storage.service'; import { Course } from 'app/entities/course.model'; @@ -114,7 +114,7 @@ describe('ExamParticipationComponent', () => { MockPipe(ArtemisDatePipe), ], providers: [ - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, { provide: LocalStorageService, useClass: MockLocalStorageService }, { provide: ActivatedRoute, diff --git a/src/test/javascript/spec/component/exercises/quiz/quiz-participation.component.spec.ts b/src/test/javascript/spec/component/exercises/quiz/quiz-participation.component.spec.ts index e0df4e004522..0a2b061d9f94 100644 --- a/src/test/javascript/spec/component/exercises/quiz/quiz-participation.component.spec.ts +++ b/src/test/javascript/spec/component/exercises/quiz/quiz-participation.component.spec.ts @@ -3,7 +3,7 @@ import { HttpTestingController, provideHttpClientTesting } from '@angular/common import { ComponentFixture, TestBed, discardPeriodicTasks, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { QuizBatch, QuizExercise, QuizMode } from 'app/entities/quiz/quiz-exercise.model'; import { QuizQuestion, QuizQuestionType } from 'app/entities/quiz/quiz-question.model'; @@ -151,7 +151,7 @@ describe('QuizParticipationComponent', () => { .provide({ provide: TranslateService, useClass: MockTranslateService }) .provide({ provide: LocalStorageService, useClass: MockLocalStorageService }) .provide({ provide: SessionStorageService, useClass: MockSyncStorage }) - .provide({ provide: JhiWebsocketService, useClass: MockWebsocketService }) + .provide({ provide: WebsocketService, useClass: MockWebsocketService }) .provide({ provide: ActivatedRoute, useValue: { @@ -518,7 +518,7 @@ describe('QuizParticipationComponent', () => { .provide({ provide: TranslateService, useClass: MockTranslateService }) .provide({ provide: LocalStorageService, useClass: MockLocalStorageService }) .provide({ provide: SessionStorageService, useClass: MockSyncStorage }) - .provide({ provide: JhiWebsocketService, useClass: MockWebsocketService }) + .provide({ provide: WebsocketService, useClass: MockWebsocketService }) .provide({ provide: ActivatedRoute, useValue: { @@ -591,7 +591,7 @@ describe('QuizParticipationComponent', () => { .keep(AlertService) .keep(ArtemisServerDateService) .keep(QuizExerciseService) - .mock(JhiWebsocketService) + .mock(WebsocketService) .provide(provideHttpClient()) .provide(provideHttpClientTesting()) .provide({ provide: TranslateService, useClass: MockTranslateService }) @@ -680,7 +680,7 @@ describe('QuizParticipationComponent', () => { .provide({ provide: TranslateService, useClass: MockTranslateService }) .provide({ provide: LocalStorageService, useClass: MockLocalStorageService }) .provide({ provide: SessionStorageService, useClass: MockSyncStorage }) - .provide({ provide: JhiWebsocketService, useClass: MockWebsocketService }) + .provide({ provide: WebsocketService, useClass: MockWebsocketService }) .provide({ provide: ActivatedRoute, useValue: { diff --git a/src/test/javascript/spec/component/exercises/shared/team-submission-sync.component.spec.ts b/src/test/javascript/spec/component/exercises/shared/team-submission-sync.component.spec.ts index 6bab9646a59b..422551bf318a 100644 --- a/src/test/javascript/spec/component/exercises/shared/team-submission-sync.component.spec.ts +++ b/src/test/javascript/spec/component/exercises/shared/team-submission-sync.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TeamSubmissionSyncComponent } from 'app/exercises/shared/team-submission-sync/team-submission-sync.component'; import { MockProvider } from 'ng-mocks'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { MockAccountService } from '../../../helpers/mocks/service/mock-account.service'; import { ExerciseType } from 'app/entities/exercise.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; @@ -24,7 +24,7 @@ import { SubmissionPatch } from 'app/entities/submission-patch.model'; describe('Team Submission Sync Component', () => { let fixture: ComponentFixture; let component: TeamSubmissionSyncComponent; - let websocketService: JhiWebsocketService; + let websocketService: WebsocketService; let textSubmissionWithParticipation: Submission; let submissionObservableWithParticipation: Observable; let submissionSyncPayload: SubmissionSyncPayload; @@ -35,7 +35,7 @@ describe('Team Submission Sync Component', () => { providers: [ MockProvider(AlertService), MockProvider(SessionStorageService), - MockProvider(JhiWebsocketService), + MockProvider(WebsocketService), { provide: AccountService, useClass: MockAccountService }, { provide: TranslateService, useClass: MockTranslateService }, { provide: HttpClient, useClass: MockHttpService }, @@ -46,7 +46,7 @@ describe('Team Submission Sync Component', () => { fixture = TestBed.createComponent(TeamSubmissionSyncComponent); component = fixture.componentInstance; - websocketService = TestBed.inject(JhiWebsocketService); + websocketService = TestBed.inject(WebsocketService); component.exerciseType = ExerciseType.TEXT; const participation = new StudentParticipation(ParticipationType.STUDENT); diff --git a/src/test/javascript/spec/component/iris/iris-status.service.spec.ts b/src/test/javascript/spec/component/iris/iris-status.service.spec.ts index 54f7ee7ced58..c3c06455d960 100644 --- a/src/test/javascript/spec/component/iris/iris-status.service.spec.ts +++ b/src/test/javascript/spec/component/iris/iris-status.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { of } from 'rxjs'; import { IrisStatusService } from 'app/iris/iris-status.service'; import { IrisRateLimitInformation } from 'app/entities/iris/iris-ratelimit-info.model'; @@ -17,7 +17,7 @@ describe('IrisStatusService', () => { provideHttpClient(), provideHttpClientTesting(), IrisStatusService, - { provide: JhiWebsocketService, useValue: { connectionState: of({ connected: true, intendedDisconnect: false, wasEverConnectedBefore: true }) } }, + { provide: WebsocketService, useValue: { connectionState: of({ connected: true, intendedDisconnect: false, wasEverConnectedBefore: true }) } }, ], }); diff --git a/src/test/javascript/spec/component/iris/websocket.service.spec.ts b/src/test/javascript/spec/component/iris/websocket.service.spec.ts deleted file mode 100644 index b1f317220d9c..000000000000 --- a/src/test/javascript/spec/component/iris/websocket.service.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; -import { MockProvider } from 'ng-mocks'; -import { AccountService } from 'app/core/auth/account.service'; -import { MockAccountService } from '../../helpers/mocks/service/mock-account.service'; -import { IrisWebsocketService } from 'app/iris/iris-websocket.service'; -import { defer, of } from 'rxjs'; -import { provideHttpClient } from '@angular/common/http'; - -describe('IrisWebsocketService', () => { - let irisWebsocketService: IrisWebsocketService; - let jhiWebsocketService: JhiWebsocketService; - - const sessionId = 1; - const channel = `/user/topic/iris/${sessionId}`; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [], - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - IrisWebsocketService, - MockProvider(JhiWebsocketService), - { provide: AccountService, useClass: MockAccountService }, - ], - }); - irisWebsocketService = TestBed.inject(IrisWebsocketService); - jhiWebsocketService = TestBed.inject(JhiWebsocketService); - }); - - afterEach(() => { - irisWebsocketService.ngOnDestroy(); - jest.restoreAllMocks(); - }); - - it('should subscribe to a channel', fakeAsync(() => { - const subscribeSpy = jest.spyOn(jhiWebsocketService, 'subscribe').mockReturnValue(jhiWebsocketService); - const receiveSpy = jest.spyOn(jhiWebsocketService, 'receive').mockReturnValue(of(null)); - - irisWebsocketService.subscribeToSession(sessionId); - - expect(subscribeSpy).toHaveBeenCalledWith(channel); - expect(receiveSpy).toHaveBeenCalledWith(channel); - expect(irisWebsocketService['subscribedChannels'].has(sessionId)).toBeTrue(); - })); - - it('should return an existing channel', fakeAsync(() => { - // Spy on the JhiWebsocketService's subscribe and receive methods - const subscribeSpy = jest.spyOn(jhiWebsocketService, 'subscribe').mockReturnValue(jhiWebsocketService); - const receiveSpy = jest.spyOn(jhiWebsocketService, 'receive').mockReturnValue(of(null)); - - // Call subscribeToSession for the first time - const firstObservable = irisWebsocketService.subscribeToSession(sessionId); - - // Call subscribeToSession for the second time - const secondObservable = irisWebsocketService.subscribeToSession(sessionId); - - // Check that subscribe and receive were called only once - expect(subscribeSpy).toHaveBeenCalledOnce(); - expect(receiveSpy).toHaveBeenCalledOnce(); - - // Check that the same observable was returned both times - expect(firstObservable).toStrictEqual(secondObservable); - })); - - it('should emit a message', fakeAsync(() => { - const testMessage = 'Test message'; - - // Spy on the JhiWebsocketService's subscribe and receive methods - const subscribeSpy = jest.spyOn(jhiWebsocketService, 'subscribe').mockReturnValue(jhiWebsocketService); - const receiveSpy = jest.spyOn(jhiWebsocketService, 'receive').mockReturnValue(defer(() => Promise.resolve(testMessage))); - - // Call subscribeToSession and subscribe to the returned observable - const observable = irisWebsocketService.subscribeToSession(sessionId); - let receivedMessage: any; - observable.subscribe((message) => { - // Store the message emitted by the observable - receivedMessage = message; - }); - tick(); - expect(receivedMessage).toEqual(testMessage); - // Check that subscribe and receive were called with the correct channel - expect(subscribeSpy).toHaveBeenCalledWith(channel); - expect(receiveSpy).toHaveBeenCalledWith(channel); - })); - - it('should unsubscribe from a channel', fakeAsync(() => { - jest.spyOn(jhiWebsocketService, 'subscribe').mockReturnValue(jhiWebsocketService); - jest.spyOn(jhiWebsocketService, 'receive').mockReturnValue(of(null)); - const unsubscribeSpy = jest.spyOn(jhiWebsocketService, 'unsubscribe'); - - irisWebsocketService.subscribeToSession(sessionId); - expect(irisWebsocketService['subscribedChannels'].has(sessionId)).toBeTrue(); - - const result = irisWebsocketService.unsubscribeFromSession(sessionId); - - expect(unsubscribeSpy).toHaveBeenCalledWith(channel); - - // Check that the sessionId was removed from the subscribedChannels map - expect(irisWebsocketService['subscribedChannels'].has(sessionId)).toBeFalse(); - - // Check that the method returned true - expect(result).toBeTrue(); - })); -}); diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts index 7224aaf85123..a68424ec4ef0 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; import { of, throwError } from 'rxjs'; import { BuildJob } from 'app/entities/programming/build-job.model'; @@ -125,7 +125,7 @@ describe('BuildAgentDetailsComponent', () => { imports: [ArtemisTestModule], declarations: [], providers: [ - { provide: JhiWebsocketService, useValue: mockWebsocketService }, + { provide: WebsocketService, useValue: mockWebsocketService }, { provide: ActivatedRoute, useValue: new MockActivatedRoute({ key: 'ABC123' }) }, { provide: BuildAgentsService, useValue: mockBuildAgentsService }, { provide: DataTableComponent, useClass: DataTableComponent }, diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts index 623f92e6482f..40967076b0c9 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { BuildAgentSummaryComponent } from 'app/localci/build-agents/build-agent-summary/build-agent-summary.component'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; import { of, throwError } from 'rxjs'; import { BuildJob } from 'app/entities/programming/build-job.model'; @@ -143,7 +143,7 @@ describe('BuildAgentSummaryComponent', () => { imports: [ArtemisTestModule], declarations: [], providers: [ - { provide: JhiWebsocketService, useValue: mockWebsocketService }, + { provide: WebsocketService, useValue: mockWebsocketService }, { provide: BuildAgentsService, useValue: mockBuildAgentsService }, { provide: DataTableComponent, useClass: DataTableComponent }, MockProvider(AlertService), diff --git a/src/test/javascript/spec/component/modeling-submission/modeling-submission-team.component.spec.ts b/src/test/javascript/spec/component/modeling-submission/modeling-submission-team.component.spec.ts index 51b5296d0d41..1c299659b86e 100644 --- a/src/test/javascript/spec/component/modeling-submission/modeling-submission-team.component.spec.ts +++ b/src/test/javascript/spec/component/modeling-submission/modeling-submission-team.component.spec.ts @@ -23,7 +23,7 @@ import { Result } from 'app/entities/result.model'; import { routes } from 'app/exercises/modeling/participate/modeling-participation.route'; import { AssessmentType } from 'app/entities/assessment-type.model'; import { Feedback, FeedbackType } from 'app/entities/feedback.model'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe'; import { UMLDiagramType, UMLElement, UMLModel } from '@ls1intum/apollon'; import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; @@ -322,7 +322,7 @@ describe('ModelingSubmissionComponent', () => { it('should update submission when new submission comes in from websocket', () => { submission.submitted = false; jest.spyOn(service, 'getLatestSubmissionForModelingEditor').mockReturnValue(of(submission)); - const websocketService = debugElement.injector.get(JhiWebsocketService); + const websocketService = debugElement.injector.get(WebsocketService); jest.spyOn(websocketService, 'subscribe'); const modelSubmission = ({ id: 1, diff --git a/src/test/javascript/spec/component/modeling-submission/modeling-submission.component.spec.ts b/src/test/javascript/spec/component/modeling-submission/modeling-submission.component.spec.ts index 603e093efca5..b665f96043b6 100644 --- a/src/test/javascript/spec/component/modeling-submission/modeling-submission.component.spec.ts +++ b/src/test/javascript/spec/component/modeling-submission/modeling-submission.component.spec.ts @@ -23,7 +23,7 @@ import { Result } from 'app/entities/result.model'; import { routes } from 'app/exercises/modeling/participate/modeling-participation.route'; import { AssessmentType } from 'app/entities/assessment-type.model'; import { Feedback, FeedbackType } from 'app/entities/feedback.model'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe'; import { UMLDiagramType, UMLElement, UMLModel } from '@ls1intum/apollon'; import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; @@ -472,7 +472,7 @@ describe('ModelingSubmissionComponent', () => { submission.submitted = false; jest.spyOn(service, 'getLatestSubmissionForModelingEditor').mockReturnValue(of(submission)); - const websocketService = debugElement.injector.get(JhiWebsocketService); + const websocketService = debugElement.injector.get(WebsocketService); jest.spyOn(websocketService, 'subscribe'); const modelSubmission = ({ id: 1, diff --git a/src/test/javascript/spec/component/overview/course-conversations/services/metis-conversation.service.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/services/metis-conversation.service.spec.ts index d707c727fbe7..287e4deb3d45 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/services/metis-conversation.service.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/services/metis-conversation.service.spec.ts @@ -5,7 +5,7 @@ import { MockProvider } from 'ng-mocks'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { OneToOneChatService } from 'app/shared/metis/conversations/one-to-one-chat.service'; import { AlertService } from 'app/core/util/alert.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { Course } from 'app/entities/course.model'; import { ConversationService } from 'app/shared/metis/conversations/conversation.service'; import { ChannelService } from 'app/shared/metis/conversations/channel.service'; @@ -32,7 +32,7 @@ describe('MetisConversationService', () => { let groupChatService: GroupChatService; let oneToOneChatService: OneToOneChatService; let channelService: ChannelService; - let websocketService: JhiWebsocketService; + let websocketService: WebsocketService; let courseManagementService: CourseManagementService; let alertService: AlertService; let notificationService: NotificationService; @@ -53,7 +53,7 @@ describe('MetisConversationService', () => { MockProvider(ChannelService), MockProvider(OneToOneChatService), MockProvider(ConversationService), - MockProvider(JhiWebsocketService), + MockProvider(WebsocketService), MockProvider(AlertService), { provide: NotificationService, useClass: MockNotificationService }, { provide: AccountService, useClass: MockAccountService }, @@ -71,7 +71,7 @@ describe('MetisConversationService', () => { groupChatService = TestBed.inject(GroupChatService); oneToOneChatService = TestBed.inject(OneToOneChatService); channelService = TestBed.inject(ChannelService); - websocketService = TestBed.inject(JhiWebsocketService); + websocketService = TestBed.inject(WebsocketService); courseManagementService = TestBed.inject(CourseManagementService); conversationService = TestBed.inject(ConversationService); alertService = TestBed.inject(AlertService); 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 1a90ec18ecd3..d529682dca13 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 @@ -15,7 +15,7 @@ import { TextExercise } from 'app/entities/text/text-exercise.model'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { TextPlagiarismResult } from 'app/exercises/shared/plagiarism/types/text/TextPlagiarismResult'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { PlagiarismInspectorService } from 'app/exercises/shared/plagiarism/plagiarism-inspector/plagiarism-inspector.service'; import { PlagiarismComparison } from 'app/exercises/shared/plagiarism/types/PlagiarismComparison'; @@ -116,7 +116,7 @@ describe('Plagiarism Inspector Component', () => { }); it('should register to topic and fetch latest results on init', fakeAsync(() => { - const websocketService = TestBed.inject(JhiWebsocketService); + const websocketService = TestBed.inject(WebsocketService); const websocketServiceSpy = jest.spyOn(websocketService, 'subscribe'); jest.spyOn(websocketService, 'receive').mockReturnValue(of({ state: 'COMPLETED', messages: 'a message' } as PlagiarismCheckState)); jest.spyOn(modelingExerciseService, 'getLatestPlagiarismResult').mockReturnValue(of(modelingPlagiarismResultDTO)); diff --git a/src/test/javascript/spec/component/shared/notification/system-notification.component.spec.ts b/src/test/javascript/spec/component/shared/notification/system-notification.component.spec.ts index 9734bf8279ec..dbadfc93e848 100644 --- a/src/test/javascript/spec/component/shared/notification/system-notification.component.spec.ts +++ b/src/test/javascript/spec/component/shared/notification/system-notification.component.spec.ts @@ -9,14 +9,14 @@ import { ArtemisTestModule } from '../../../test.module'; import { MockSyncStorage } from '../../../helpers/mocks/service/mock-sync-storage.service'; import { MockAccountService } from '../../../helpers/mocks/service/mock-account.service'; import { SystemNotification, SystemNotificationType } from 'app/entities/system-notification.model'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { MockWebsocketService } from '../../../helpers/mocks/service/mock-websocket.service'; describe('System Notification Component', () => { let systemNotificationComponent: SystemNotificationComponent; let systemNotificationComponentFixture: ComponentFixture; let systemNotificationService: SystemNotificationService; - let jhiWebsocketService: JhiWebsocketService; + let jhiWebsocketService: WebsocketService; const createActiveNotification = (type: SystemNotificationType, id: number) => { return { @@ -47,7 +47,7 @@ describe('System Notification Component', () => { { provide: LocalStorageService, useClass: MockSyncStorage }, { provide: SessionStorageService, useClass: MockSyncStorage }, { provide: AccountService, useClass: MockAccountService }, - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, ], }) .compileComponents() @@ -55,7 +55,7 @@ describe('System Notification Component', () => { systemNotificationComponentFixture = TestBed.createComponent(SystemNotificationComponent); systemNotificationComponent = systemNotificationComponentFixture.componentInstance; systemNotificationService = TestBed.inject(SystemNotificationService); - jhiWebsocketService = TestBed.inject(JhiWebsocketService); + jhiWebsocketService = TestBed.inject(WebsocketService); }); }); diff --git a/src/test/javascript/spec/component/shared/user-settings/user-settings.directive.spec.ts b/src/test/javascript/spec/component/shared/user-settings/user-settings.directive.spec.ts index df1c00916686..01cb9563222d 100644 --- a/src/test/javascript/spec/component/shared/user-settings/user-settings.directive.spec.ts +++ b/src/test/javascript/spec/component/shared/user-settings/user-settings.directive.spec.ts @@ -1,7 +1,7 @@ import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AccountService } from 'app/core/auth/account.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { UserSettingsService } from 'app/shared/user-settings/user-settings.service'; import { SettingId, UserSettingsCategory } from 'app/shared/constants/user-settings.constants'; import { MockWebsocketService } from '../../../helpers/mocks/service/mock-websocket.service'; @@ -56,7 +56,7 @@ describe('User Settings Directive', () => { provideHttpClient(), provideHttpClientTesting(), MockProvider(ChangeDetectorRef), - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, { provide: AccountService, useClass: MockAccountService }, { provide: UserSettingsService, useClass: MockUserSettingsService }, { provide: Router, useValue: router }, diff --git a/src/test/javascript/spec/component/shared/user-settings/user-settings.service.spec.ts b/src/test/javascript/spec/component/shared/user-settings/user-settings.service.spec.ts index d52dd09a8b2c..bd8d69bf72b7 100644 --- a/src/test/javascript/spec/component/shared/user-settings/user-settings.service.spec.ts +++ b/src/test/javascript/spec/component/shared/user-settings/user-settings.service.spec.ts @@ -1,7 +1,7 @@ import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { AccountService } from 'app/core/auth/account.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { UserSettingsService } from 'app/shared/user-settings/user-settings.service'; import { SettingId, UserSettingsCategory } from 'app/shared/constants/user-settings.constants'; import { MockWebsocketService } from '../../../helpers/mocks/service/mock-websocket.service'; @@ -128,7 +128,7 @@ describe('User Settings Service', () => { providers: [ provideHttpClient(), provideHttpClientTesting(), - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, { provide: AccountService, useClass: MockAccountService }, ], }) diff --git a/src/test/javascript/spec/component/text-editor/text-editor.component.spec.ts b/src/test/javascript/spec/component/text-editor/text-editor.component.spec.ts index fe249338cccc..de22cddaaf15 100644 --- a/src/test/javascript/spec/component/text-editor/text-editor.component.spec.ts +++ b/src/test/javascript/spec/component/text-editor/text-editor.component.spec.ts @@ -309,6 +309,7 @@ describe('TextEditorComponent', () => { }); it('should receive submission from team', () => { + comp.participation = { id: 1, team: { id: 1 } } as StudentParticipation; comp.textExercise = { id: 1, studentParticipations: [] as StudentParticipation[], diff --git a/src/test/javascript/spec/core/websocket.service.spec.ts b/src/test/javascript/spec/core/websocket.service.spec.ts new file mode 100644 index 000000000000..58b4d6616ec1 --- /dev/null +++ b/src/test/javascript/spec/core/websocket.service.spec.ts @@ -0,0 +1,350 @@ +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { COMPRESSION_HEADER, ConnectionState, WebsocketService } from 'app/core/websocket/websocket.service'; +import { AccountService } from 'app/core/auth/account.service'; +import { MockAccountService } from '../helpers/mocks/service/mock-account.service'; +import { IrisWebsocketService } from 'app/iris/iris-websocket.service'; +import { defer, of } from 'rxjs'; +import { provideHttpClient } from '@angular/common/http'; +import { Message, Subscription as StompSubscription } from 'webstomp-client'; + +jest.mock('sockjs-client'); +jest.mock('webstomp-client', () => ({ + over: jest.fn().mockReturnValue({ + connect: jest.fn(), + subscribe: jest.fn(), + send: jest.fn(), + disconnect: jest.fn(), + connected: false, + }), +})); + +describe('WebsocketService', () => { + let irisWebsocketService: IrisWebsocketService; + let websocketService: WebsocketService; + + const sessionId = 1; + const channel = `/user/topic/iris/${sessionId}`; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [provideHttpClient(), provideHttpClientTesting(), IrisWebsocketService, WebsocketService, { provide: AccountService, useClass: MockAccountService }], + }); + irisWebsocketService = TestBed.inject(IrisWebsocketService); + websocketService = TestBed.inject(WebsocketService); + }); + + afterEach(() => { + websocketService.ngOnDestroy(); + irisWebsocketService.ngOnDestroy(); + jest.restoreAllMocks(); + }); + + const createMockMessage = (message: string) => { + return defer(() => Promise.resolve(message)); + }; + + it('should subscribe to a channel', fakeAsync(() => { + const subscribeSpy = jest.spyOn(websocketService, 'subscribe').mockReturnValue(websocketService); + const receiveSpy = jest.spyOn(websocketService, 'receive').mockReturnValue(of(null)); + + irisWebsocketService.subscribeToSession(sessionId); + + expect(subscribeSpy).toHaveBeenCalledWith(channel); + expect(receiveSpy).toHaveBeenCalledWith(channel); + expect(irisWebsocketService['subscribedChannels'].has(sessionId)).toBeTrue(); + })); + + it('should return an existing channel', fakeAsync(() => { + // Spy on the WebsocketService's subscribe and receive methods + const subscribeSpy = jest.spyOn(websocketService, 'subscribe').mockReturnValue(websocketService); + const receiveSpy = jest.spyOn(websocketService, 'receive').mockReturnValue(of(null)); + + // Call subscribeToSession for the first time + const firstObservable = irisWebsocketService.subscribeToSession(sessionId); + + // Call subscribeToSession for the second time + const secondObservable = irisWebsocketService.subscribeToSession(sessionId); + + // Check that subscribe and receive were called only once + expect(subscribeSpy).toHaveBeenCalledOnce(); + expect(receiveSpy).toHaveBeenCalledOnce(); + + // Check that the same observable was returned both times + expect(firstObservable).toStrictEqual(secondObservable); + })); + + it('should emit a message', fakeAsync(() => { + const testMessage = 'Test message'; + + // Spy on the WebsocketService's subscribe and receive methods + const subscribeSpy = jest.spyOn(websocketService, 'subscribe').mockReturnValue(websocketService); + const receiveSpy = jest.spyOn(websocketService, 'receive').mockReturnValue(defer(() => Promise.resolve(testMessage))); + + // Call subscribeToSession and subscribe to the returned observable + const observable = irisWebsocketService.subscribeToSession(sessionId); + let receivedMessage: any; + observable.subscribe((message) => { + // Store the message emitted by the observable + receivedMessage = message; + }); + tick(); + expect(receivedMessage).toEqual(testMessage); + // Check that subscribe and receive were called with the correct channel + expect(subscribeSpy).toHaveBeenCalledWith(channel); + expect(receiveSpy).toHaveBeenCalledWith(channel); + })); + + it('should emit and decode a message', fakeAsync(() => { + const testMessage = 'Test message'; + const encodedMessage = window.btoa(testMessage); + const subscribeSpy = jest.spyOn(websocketService, 'subscribe').mockReturnValue(websocketService); + const receiveSpy = jest.spyOn(websocketService, 'receive').mockReturnValue(createMockMessage(encodedMessage)); + + const observable = irisWebsocketService.subscribeToSession(sessionId); + let receivedMessage: any; + + observable.subscribe((message) => { + receivedMessage = window.atob(message); // Decode the Base64 message + }); + + tick(); + + expect(receivedMessage).toEqual(testMessage); + expect(subscribeSpy).toHaveBeenCalledWith(channel); + expect(receiveSpy).toHaveBeenCalledWith(channel); + })); + + it('should unsubscribe from a channel', fakeAsync(() => { + jest.spyOn(websocketService, 'subscribe').mockReturnValue(websocketService); + jest.spyOn(websocketService, 'receive').mockReturnValue(of(null)); + const unsubscribeSpy = jest.spyOn(websocketService, 'unsubscribe'); + + irisWebsocketService.subscribeToSession(sessionId); + expect(irisWebsocketService['subscribedChannels'].has(sessionId)).toBeTrue(); + + const result = irisWebsocketService.unsubscribeFromSession(sessionId); + + expect(unsubscribeSpy).toHaveBeenCalledWith(channel); + + // Check that the sessionId was removed from the subscribedChannels map + expect(irisWebsocketService['subscribedChannels'].has(sessionId)).toBeFalse(); + + // Check that the method returned true + expect(result).toBeTrue(); + })); + + it('should handle invalid Base64 messages gracefully', fakeAsync(() => { + const invalidBase64 = 'InvalidMessage$$'; // Not a valid Base64 string + jest.spyOn(websocketService, 'subscribe').mockReturnValue(websocketService); + jest.spyOn(websocketService, 'receive').mockReturnValue(defer(() => Promise.resolve(invalidBase64))); + + const observable = irisWebsocketService.subscribeToSession(sessionId); + let receivedMessage: any; + + observable.subscribe({ + next: (message) => { + try { + // Attempt to decode the invalid Base64 + receivedMessage = window.atob(message); + } catch (error) { + receivedMessage = null; // Handle decoding error + } + }, + }); + + tick(); + + // Ensure the message was handled gracefully + expect(receivedMessage).toBeNull(); // Expect null because decoding should fail + })); + + it('should compress and decompress correctly', () => { + // Arrange + const largePayload = { data: 'x'.repeat(2000) }; // Creates a large JSON payload + const jsonPayload = JSON.stringify(largePayload); + // @ts-ignore + const compressedAndEncodedPayload = WebsocketService.compressAndEncode(jsonPayload); + // @ts-ignore + const originalPayload = WebsocketService.decodeAndDecompress(compressedAndEncodedPayload); + expect(originalPayload).toEqual(jsonPayload); + }); + + it('should handle reconnection with backoff', fakeAsync(() => { + jest.useFakeTimers(); + const timeoutSpy = jest.spyOn(global, 'setTimeout'); + + websocketService.enableReconnect(); + websocketService['consecutiveFailedAttempts'] = 0; + + websocketService.stompFailureCallback(); + expect(websocketService['consecutiveFailedAttempts']).toBe(1); + expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5000); + + websocketService.stompFailureCallback(); + expect(websocketService['consecutiveFailedAttempts']).toBe(2); + expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5000); + + websocketService.stompFailureCallback(); + expect(websocketService['consecutiveFailedAttempts']).toBe(3); + expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 10000); + + websocketService['consecutiveFailedAttempts'] = 4; + websocketService.stompFailureCallback(); + expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 20000); + + websocketService['consecutiveFailedAttempts'] = 8; + websocketService.stompFailureCallback(); + expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 60000); + + websocketService['consecutiveFailedAttempts'] = 12; + websocketService.stompFailureCallback(); + expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 120000); + + websocketService['consecutiveFailedAttempts'] = 17; + websocketService.stompFailureCallback(); + expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 300000); + + websocketService['consecutiveFailedAttempts'] = 20; + websocketService.stompFailureCallback(); + expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 600000); + + jest.useRealTimers(); + })); + + it('should not reconnect when reconnect is disabled', fakeAsync(() => { + jest.useFakeTimers(); + const connectSpy = jest.spyOn(websocketService, 'connect'); + websocketService.disableReconnect(); + websocketService.stompFailureCallback(); + jest.runAllTimers(); + expect(connectSpy).not.toHaveBeenCalled(); + jest.useRealTimers(); + })); + + it('should handle sending message when disconnected', () => { + websocketService.connect(); + const sendSpy = jest.spyOn(websocketService['stompClient']!, 'send'); + websocketService['stompClient']!.connected = false; + + websocketService.send('/test', { data: 'test' }); + expect(sendSpy).not.toHaveBeenCalled(); + }); + + it('should handle large messages with compression', () => { + websocketService.connect(); + websocketService['stompClient']!.connected = true; + const sendSpy = jest.spyOn(websocketService['stompClient']!, 'send'); + const largeData = { data: 'x'.repeat(2000) }; + + websocketService.send('/test', largeData); + + expect(sendSpy).toHaveBeenCalledWith('/test', expect.any(String), { 'X-Compressed': 'true' }); + }); + + it('should handle undefined channel subscription', () => { + // This test case is simply to hit the initial check in the function + const result = websocketService.subscribe(undefined!); + expect(result).toBe(websocketService); + }); + + it('should handle multiple subscriptions to same channel', fakeAsync(() => { + websocketService.connect(); + websocketService['connectionStateInternal'].next(new ConnectionState(true, true, false)); + + const channel = '/test/channel'; + websocketService.subscribe(channel); + websocketService.subscribe(channel); + + tick(); + + expect(websocketService['stompSubscriptions'].size).toBe(1); + websocketService['stompSubscriptions'] = new Map(); + })); + + it('should handle unsubscribe from non-existent channel', () => { + const channel = '/non-existent'; + expect(() => websocketService.unsubscribe(channel)).not.toThrow(); + }); + + it('should handle multiple connect calls', () => { + const connectSpy = jest.spyOn(websocketService, 'connect'); + websocketService.connect(); + websocketService.connect(); + + expect(connectSpy).toHaveBeenCalledTimes(2); + expect(websocketService['connecting']).toBeTruthy(); + }); + + it('should handle JSON parsing errors', () => { + const invalidJson = 'invalid-json'; + // @ts-ignore + const result = WebsocketService.parseJSON(invalidJson); + expect(result).toBe(invalidJson); + }); + + it('should handle incoming message with no compression', () => { + const channel = '/topic/test'; + const message: Message = { + body: JSON.stringify({ data: 'test' }), + headers: {}, + ack: jest.fn(), + nack: jest.fn(), + command: '', + }; + const subscriber = jest.fn(); + // @ts-ignore + websocketService['subscribers'].set(channel, { next: subscriber }); + + websocketService['handleIncomingMessage'](channel)(message); + + expect(subscriber).toHaveBeenCalledWith({ data: 'test' }); + }); + + it('should handle incoming message with compression', () => { + const channel = '/topic/test'; + // @ts-ignore + const messageBody = WebsocketService.compressAndEncode(JSON.stringify({ data: 'test' })); + const message: Message = { + body: messageBody, + headers: COMPRESSION_HEADER, + ack: jest.fn(), + nack: jest.fn(), + command: '', + }; + const subscriber = jest.fn(); + // @ts-ignore + websocketService['subscribers'].set(channel, { next: subscriber }); + + websocketService['handleIncomingMessage'](channel)(message); + + expect(subscriber).toHaveBeenCalledWith({ data: 'test' }); + }); + + it('should update observables when calling receive', () => { + expect(websocketService['observables'].size).toBe(0); + websocketService.receive('/test/topic'); + expect(websocketService['observables'].size).toBe(1); + websocketService.receive('/test/topic'); + expect(websocketService['observables'].size).toBe(1); + websocketService.receive('/test/topictwo'); + expect(websocketService['observables'].size).toBe(2); + }); + + it('should have default value for get session id if unsubscribed', () => { + expect(websocketService['getSessionId']()).toBe('unsubscribed'); + }); + + it('should enable and disable reconnect when functions are called', () => { + let connectSpy = jest.spyOn(websocketService, 'connect'); + websocketService.connect(); + websocketService['stompClient']!.connected = false; + expect(websocketService['shouldReconnect']).toBeFalsy(); + websocketService.enableReconnect(); + expect(websocketService['shouldReconnect']).toBeTruthy(); + expect(connectSpy).toHaveBeenCalledTimes(2); + websocketService.disableReconnect(); + expect(websocketService['shouldReconnect']).toBeFalsy(); + }); +}); diff --git a/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts b/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts index f90fad7ce190..95033800a99b 100644 --- a/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts +++ b/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts @@ -24,7 +24,7 @@ import { ProgrammingSubmissionService, ProgrammingSubmissionState, ProgrammingSu import { MockProgrammingSubmissionService } from '../../helpers/mocks/service/mock-programming-submission.service'; import { GuidedTourService } from 'app/guided-tour/guided-tour.service'; import { GuidedTourMapping } from 'app/guided-tour/guided-tour-setting.model'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { MockWebsocketService } from '../../helpers/mocks/service/mock-websocket.service'; import { Participation } from 'app/entities/participation/participation.model'; import { BuildLogEntryArray } from 'app/entities/programming/build-log.model'; @@ -86,7 +86,7 @@ describe('CodeEditorContainerIntegration', () => { CodeEditorConflictStateService, MockProvider(AlertService), { provide: ActivatedRoute, useClass: MockActivatedRouteWithSubjects }, - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, { provide: ParticipationWebsocketService, useClass: MockParticipationWebsocketService }, { provide: ProgrammingExerciseParticipationService, useClass: MockProgrammingExerciseParticipationService }, { provide: SessionStorageService, useClass: MockSyncStorage }, diff --git a/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts b/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts index 2569e345ff4c..c67a0ed52664 100644 --- a/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts +++ b/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts @@ -39,7 +39,7 @@ import { MockCodeEditorRepositoryFileService } from '../../helpers/mocks/service import { MockParticipationWebsocketService } from '../../helpers/mocks/service/mock-participation-websocket.service'; import { MockParticipationService } from '../../helpers/mocks/service/mock-participation.service'; import { MockProgrammingExerciseService } from '../../helpers/mocks/service/mock-programming-exercise.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { MockWebsocketService } from '../../helpers/mocks/service/mock-websocket.service'; import { MockComponent, MockModule, MockPipe } from 'ng-mocks'; import { CodeEditorContainerComponent } from 'app/exercises/programming/shared/code-editor/container/code-editor-container.component'; @@ -137,7 +137,7 @@ describe('CodeEditorInstructorIntegration', () => { { provide: ParticipationService, useClass: MockParticipationService }, { provide: ProgrammingExerciseParticipationService, useClass: MockProgrammingExerciseParticipationService }, { provide: ProgrammingExerciseService, useClass: MockProgrammingExerciseService }, - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, ], }) .compileComponents() diff --git a/src/test/javascript/spec/integration/code-editor/code-editor-student.integration.spec.ts b/src/test/javascript/spec/integration/code-editor/code-editor-student.integration.spec.ts index 6b3b8441f2e7..79dac1bd740a 100644 --- a/src/test/javascript/spec/integration/code-editor/code-editor-student.integration.spec.ts +++ b/src/test/javascript/spec/integration/code-editor/code-editor-student.integration.spec.ts @@ -15,7 +15,7 @@ import { MockProgrammingExerciseParticipationService } from '../../helpers/mocks import { ProgrammingSubmissionService } from 'app/exercises/programming/participate/programming-submission.service'; import { MockProgrammingSubmissionService } from '../../helpers/mocks/service/mock-programming-submission.service'; import { getElement } from '../../helpers/utils/general.utils'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { MockWebsocketService } from '../../helpers/mocks/service/mock-websocket.service'; import { Participation } from 'app/entities/participation/participation.model'; import { ResultService } from 'app/exercises/shared/result/result.service'; @@ -112,7 +112,7 @@ describe('CodeEditorStudentIntegration', () => { JhiLanguageHelper, { provide: AccountService, useClass: MockAccountService }, { provide: ActivatedRoute, useClass: MockActivatedRouteWithSubjects }, - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, { provide: ParticipationWebsocketService, useClass: MockParticipationWebsocketService }, { provide: ProgrammingExerciseParticipationService, useClass: MockProgrammingExerciseParticipationService }, { provide: SessionStorageService, useClass: MockSyncStorage }, diff --git a/src/test/javascript/spec/service/account.service.spec.ts b/src/test/javascript/spec/service/account.service.spec.ts index b9b36bd7d4b8..f5ddf6b2c5ce 100644 --- a/src/test/javascript/spec/service/account.service.spec.ts +++ b/src/test/javascript/spec/service/account.service.spec.ts @@ -2,7 +2,7 @@ import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { of } from 'rxjs'; import { MockService } from 'ng-mocks'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { FeatureToggleService } from 'app/shared/feature-toggle/feature-toggle.service'; import { MockHttpService } from '../helpers/mocks/service/mock-http.service'; import { User } from 'app/core/user/user.model'; @@ -46,7 +46,7 @@ describe('AccountService', () => { providers: [ { provide: TranslateService, useClass: MockTranslateService }, { provide: SessionStorageService, useClass: MockSyncStorage }, - { provide: JhiWebsocketService, useValue: MockService(JhiWebsocketService) }, + { provide: WebsocketService, useValue: MockService(WebsocketService) }, { provide: FeatureToggleService, useValue: MockService(FeatureToggleService) }, provideHttpClient(), provideHttpClientTesting(), diff --git a/src/test/javascript/spec/service/exam-participation-live-events.service.spec.ts b/src/test/javascript/spec/service/exam-participation-live-events.service.spec.ts index 07b49299c4ec..fbca10610483 100644 --- a/src/test/javascript/spec/service/exam-participation-live-events.service.spec.ts +++ b/src/test/javascript/spec/service/exam-participation-live-events.service.spec.ts @@ -1,7 +1,7 @@ import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { Subject, firstValueFrom } from 'rxjs'; -import { ConnectionState, JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { ConnectionState, WebsocketService } from 'app/core/websocket/websocket.service'; import { ExamParticipationService } from 'app/exam/participate/exam-participation.service'; import { ExamLiveEvent, ExamLiveEventType, ExamParticipationLiveEventsService } from 'app/exam/participate/exam-participation-live-events.service'; import { LocalStorageService } from 'ngx-webstorage'; @@ -12,7 +12,7 @@ import { provideHttpClient } from '@angular/common/http'; describe('ExamParticipationLiveEventsService', () => { let service: ExamParticipationLiveEventsService; let httpMock: HttpTestingController; - let mockWebsocketService: JhiWebsocketService; + let mockWebsocketService: WebsocketService; let mockExamParticipationService: ExamParticipationService; let mockLocalStorageService: LocalStorageService; let websocketConnectionStateSubject: Subject; @@ -32,14 +32,14 @@ describe('ExamParticipationLiveEventsService', () => { const tmpMockWebsocketService = new MockWebsocketService(); tmpMockWebsocketService.state = websocketConnectionStateSubject.asObservable(); - mockWebsocketService = tmpMockWebsocketService as unknown as JhiWebsocketService; + mockWebsocketService = tmpMockWebsocketService as unknown as WebsocketService; TestBed.configureTestingModule({ imports: [], providers: [ provideHttpClient(), provideHttpClientTesting(), - { provide: JhiWebsocketService, useValue: mockWebsocketService }, + { provide: WebsocketService, useValue: mockWebsocketService }, { provide: ExamParticipationService, useValue: mockExamParticipationService }, { provide: LocalStorageService, useValue: mockLocalStorageService }, ], @@ -47,7 +47,7 @@ describe('ExamParticipationLiveEventsService', () => { service = TestBed.inject(ExamParticipationLiveEventsService); httpMock = TestBed.inject(HttpTestingController); - mockWebsocketService = TestBed.inject(JhiWebsocketService); + mockWebsocketService = TestBed.inject(WebsocketService); service['studentExamId'] = 1; service['examId'] = 1; diff --git a/src/test/javascript/spec/service/login.service.spec.ts b/src/test/javascript/spec/service/login.service.spec.ts index 3876da12645a..f9e0e02acf6a 100644 --- a/src/test/javascript/spec/service/login.service.spec.ts +++ b/src/test/javascript/spec/service/login.service.spec.ts @@ -3,7 +3,7 @@ import { MockRouter } from '../helpers/mocks/mock-router'; import { MockAccountService } from '../helpers/mocks/service/mock-account.service'; import { MockAuthServerProviderService } from '../helpers/mocks/service/mock-auth-server-provider.service'; import { AccountService } from 'app/core/auth/account.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { LoginService } from 'app/core/login/login.service'; import { AuthServerProvider } from 'app/core/auth/auth-jwt.service'; import { TestBed } from '@angular/core/testing'; @@ -27,7 +27,7 @@ describe('LoginService', () => { TestBed.configureTestingModule({ providers: [ { provide: AccountService, useClass: MockAccountService }, - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, { provide: AuthServerProvider, useClass: MockAuthServerProviderService }, { provide: Router, useClass: MockRouter }, MockProvider(AlertService), diff --git a/src/test/javascript/spec/service/metis/metis.service.spec.ts b/src/test/javascript/spec/service/metis/metis.service.spec.ts index 021abff70aca..221bb567a92b 100644 --- a/src/test/javascript/spec/service/metis/metis.service.spec.ts +++ b/src/test/javascript/spec/service/metis/metis.service.spec.ts @@ -21,7 +21,7 @@ import { MockRouter } from '../../helpers/mocks/mock-router'; import { MockLocalStorageService } from '../../helpers/mocks/service/mock-local-storage.service'; import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; import { MockProvider } from 'ng-mocks'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { MetisPostDTO } from 'app/entities/metis/metis-post-dto.model'; import { Subject, of } from 'rxjs'; import { @@ -57,7 +57,7 @@ describe('Metis Service', () => { let metisServiceCreateWebsocketSubscriptionSpy: jest.SpyInstance; let websocketServiceSubscribeSpy: jest.SpyInstance; let websocketServiceReceiveStub: jest.SpyInstance; - let websocketService: JhiWebsocketService; + let websocketService: WebsocketService; let reactionService: ReactionService; let postService: PostService; let answerPostService: AnswerPostService; @@ -89,7 +89,7 @@ describe('Metis Service', () => { ], }); metisService = TestBed.inject(MetisService); - websocketService = TestBed.inject(JhiWebsocketService); + websocketService = TestBed.inject(WebsocketService); reactionService = TestBed.inject(ReactionService); postService = TestBed.inject(PostService); answerPostService = TestBed.inject(AnswerPostService); diff --git a/src/test/javascript/spec/service/notification.service.spec.ts b/src/test/javascript/spec/service/notification.service.spec.ts index 2814844440db..896a6c7bb29b 100644 --- a/src/test/javascript/spec/service/notification.service.spec.ts +++ b/src/test/javascript/spec/service/notification.service.spec.ts @@ -20,7 +20,7 @@ import { CourseManagementService } from 'app/course/manage/course-management.ser import { BehaviorSubject, Subject } from 'rxjs'; import { AccountService } from 'app/core/auth/account.service'; import { MockAccountService } from '../helpers/mocks/service/mock-account.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { MockWebsocketService } from '../helpers/mocks/service/mock-websocket.service'; import { Course } from 'app/entities/course.model'; import { QuizExercise } from 'app/entities/quiz/quiz-exercise.model'; @@ -50,7 +50,7 @@ describe('Notification Service', () => { let artemisTranslatePipe: ArtemisTranslatePipe; let accountService: MockAccountService; - let websocketService: JhiWebsocketService; + let websocketService: WebsocketService; let wsSubscribeStub: jest.SpyInstance; let wsUnsubscribeStub: jest.SpyInstance; let wsReceiveNotificationStub: jest.SpyInstance; @@ -171,7 +171,7 @@ describe('Notification Service', () => { { provide: AccountService, useClass: MockAccountService }, { provide: ArtemisTranslatePipe, useClass: ArtemisTranslatePipe }, { provide: ChangeDetectorRef, useValue: {} }, - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, { provide: MetisService, useClass: MockMetisService }, { provide: ActivatedRoute, @@ -191,7 +191,7 @@ describe('Notification Service', () => { httpMock = TestBed.inject(HttpTestingController); router = TestBed.inject(Router) as any; - websocketService = TestBed.inject(JhiWebsocketService); + websocketService = TestBed.inject(WebsocketService); artemisTranslatePipe = TestBed.inject(ArtemisTranslatePipe); accountService = TestBed.inject(AccountService) as any; wsSubscribeStub = jest.spyOn(websocketService, 'subscribe'); diff --git a/src/test/javascript/spec/service/participation-websocket.service.spec.ts b/src/test/javascript/spec/service/participation-websocket.service.spec.ts index b4dfd749c5b3..b2bc6a181a37 100644 --- a/src/test/javascript/spec/service/participation-websocket.service.spec.ts +++ b/src/test/javascript/spec/service/participation-websocket.service.spec.ts @@ -3,13 +3,13 @@ import { BehaviorSubject, Subject } from 'rxjs'; import { ParticipationWebsocketService } from 'app/overview/participation-websocket.service'; import { Participation } from 'app/entities/participation/participation.model'; import { Result } from 'app/entities/result.model'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; import { MockWebsocketService } from '../helpers/mocks/service/mock-websocket.service'; import { MockParticipationService } from '../helpers/mocks/service/mock-participation.service'; describe('ParticipationWebsocketService', () => { - let websocketService: JhiWebsocketService; + let websocketService: WebsocketService; let receiveParticipationSubject: Subject; let receiveParticipation2Subject: Subject; let receiveResultForParticipationSubject: Subject; @@ -42,14 +42,14 @@ describe('ParticipationWebsocketService', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, { provide: ParticipationService, useClass: MockParticipationService }, ], }) .compileComponents() .then(() => { participationWebsocketService = TestBed.inject(ParticipationWebsocketService); - websocketService = TestBed.inject(JhiWebsocketService); + websocketService = TestBed.inject(WebsocketService); subscribeSpy = jest.spyOn(websocketService, 'subscribe'); unsubscribeSpy = jest.spyOn(websocketService, 'unsubscribe'); diff --git a/src/test/javascript/spec/service/programming-exercise-grading.service.spec.ts b/src/test/javascript/spec/service/programming-exercise-grading.service.spec.ts index 6396a81e6dff..8fccddb80867 100644 --- a/src/test/javascript/spec/service/programming-exercise-grading.service.spec.ts +++ b/src/test/javascript/spec/service/programming-exercise-grading.service.spec.ts @@ -2,7 +2,7 @@ import { TestBed } from '@angular/core/testing'; import { Subject, of } from 'rxjs'; import { tap } from 'rxjs/operators'; import { MockWebsocketService } from '../helpers/mocks/service/mock-websocket.service'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { ProgrammingExerciseGradingService } from 'app/exercises/programming/manage/services/programming-exercise-grading.service'; import { MockHttpService } from '../helpers/mocks/service/mock-http.service'; import { ProgrammingExerciseTestCase } from 'app/entities/programming/programming-exercise-test-case.model'; @@ -10,7 +10,7 @@ import { Result } from 'app/entities/result.model'; import { HttpClient } from '@angular/common/http'; describe('ProgrammingExerciseGradingService', () => { - let websocketService: JhiWebsocketService; + let websocketService: WebsocketService; let httpService: HttpClient; let exercise1TestCaseSubject: Subject; let exercise2TestCaseSubject: Subject; @@ -40,12 +40,12 @@ describe('ProgrammingExerciseGradingService', () => { TestBed.configureTestingModule({ providers: [ { provide: HttpClient, useClass: MockHttpService }, - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, ], }) .compileComponents() .then(() => { - websocketService = TestBed.inject(JhiWebsocketService); + websocketService = TestBed.inject(WebsocketService); httpService = TestBed.inject(HttpClient); gradingService = TestBed.inject(ProgrammingExerciseGradingService); diff --git a/src/test/javascript/spec/service/programming-submission.service.spec.ts b/src/test/javascript/spec/service/programming-submission.service.spec.ts index 2cec23b11bbd..8435b9a3c9ff 100644 --- a/src/test/javascript/spec/service/programming-submission.service.spec.ts +++ b/src/test/javascript/spec/service/programming-submission.service.spec.ts @@ -19,14 +19,14 @@ import { ProgrammingExerciseParticipationService } from 'app/exercises/programmi import { MockProgrammingExerciseParticipationService } from '../helpers/mocks/service/mock-programming-exercise-participation.service'; import { HttpClient, provideHttpClient } from '@angular/common/http'; import { TestBed, discardPeriodicTasks, fakeAsync, tick } from '@angular/core/testing'; -import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { WebsocketService } from 'app/core/websocket/websocket.service'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { ProfileService } from '../../../../main/webapp/app/shared/layouts/profiles/profile.service'; import { MockProfileService } from '../helpers/mocks/service/mock-profile.service'; import { SubmissionProcessingDTO } from '../../../../main/webapp/app/entities/programming/submission-processing-dto'; describe('ProgrammingSubmissionService', () => { - let websocketService: JhiWebsocketService; + let websocketService: WebsocketService; let httpService: HttpClient; let participationWebsocketService: ParticipationWebsocketService; let participationService: ProgrammingExerciseParticipationService; @@ -88,7 +88,7 @@ describe('ProgrammingSubmissionService', () => { providers: [ provideHttpClient(), provideHttpClientTesting(), - { provide: JhiWebsocketService, useClass: MockWebsocketService }, + { provide: WebsocketService, useClass: MockWebsocketService }, { provide: ParticipationWebsocketService, useClass: MockParticipationWebsocketService }, { provide: ProgrammingExerciseParticipationService, useClass: MockProgrammingExerciseParticipationService }, { provide: ProfileService, useClass: MockProfileService }, @@ -97,7 +97,7 @@ describe('ProgrammingSubmissionService', () => { .compileComponents() .then(() => { submissionService = TestBed.inject(ProgrammingSubmissionService); - websocketService = TestBed.inject(JhiWebsocketService); + websocketService = TestBed.inject(WebsocketService); httpService = TestBed.inject(HttpClient); participationWebsocketService = TestBed.inject(ParticipationWebsocketService); participationService = TestBed.inject(ProgrammingExerciseParticipationService); From bb65c4e05c6dd4aaf8cfd8fb8664c26e91f67b3b Mon Sep 17 00:00:00 2001 From: Enea Gore <73840596+EneaGore@users.noreply.github.com> Date: Mon, 20 Jan 2025 21:38:19 +0100 Subject: [PATCH 02/14] Development: Set the isGraded flag to false for student athena requests (#10155) --- .../aet/artemis/text/service/TextExerciseFeedbackService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java index d95a76755e6c..0a874e45b8f8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java @@ -121,7 +121,7 @@ public void generateAutomaticNonGradedFeedback(StudentParticipation participatio log.debug("Submission id: {}", textSubmission.getId()); - var athenaResponse = this.athenaFeedbackSuggestionsService.orElseThrow().getTextFeedbackSuggestions(textExercise, textSubmission, true); + var athenaResponse = this.athenaFeedbackSuggestionsService.orElseThrow().getTextFeedbackSuggestions(textExercise, textSubmission, false); Set textBlocks = new HashSet<>(); List feedbacks = new ArrayList<>(); From 302ce50941006575129c8d1b1f910fcc75f7d06a Mon Sep 17 00:00:00 2001 From: Ole Vester <73833780+ole-ve@users.noreply.github.com> Date: Mon, 20 Jan 2025 21:40:23 +0100 Subject: [PATCH 03/14] Development: Define DTO architecture test per module with module-specific thresholds (#9959) --- .../AssessmentCodeStyleArchitectureTest.java | 21 ++++++++ .../AthenaCodeStyleArchitectureTest.java | 16 ++++++ .../AtlasCodeStyleArchitectureTest.java | 16 ++++++ ...ommunicationCodeStyleArchitectureTest.java | 21 ++++++++ .../CoreCodeStyleArchitectureTest.java | 21 ++++++++ .../ExamCodeStyleArchitectureTest.java | 16 ++++++ .../ExerciseCodeStyleArchitectureTest.java | 21 ++++++++ .../FileUploadCodeStyleArchitectureTest.java | 11 ++++ .../IrisCodeStyleArchitectureTest.java | 21 ++++++++ .../LectureCodeStyleArchitectureTest.java | 11 ++++ .../LtiCodeStyleArchitectureTest.java | 16 ++++++ .../ModelingCodeStyleArchitectureTest.java | 11 ++++ .../PlagiarismCodeStyleArchitectureTest.java | 16 ++++++ .../ProgrammingCodeStyleArchitectureTest.java | 21 ++++++++ .../QuizCodeStyleArchitectureTest.java | 11 ++++ .../shared/architecture/ArchitectureTest.java | 20 ------- .../module/AbstractModuleCodeStyleTest.java | 53 +++++++++++++++++++ .../TextCodeStyleArchitectureTest.java | 21 ++++++++ ...utorialGroupCodeStyleArchitectureTest.java | 11 ++++ 19 files changed, 335 insertions(+), 20 deletions(-) create mode 100644 src/test/java/de/tum/cit/aet/artemis/assessment/architecture/AssessmentCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/athena/architecture/AthenaCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/atlas/architecture/AtlasCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/communication/architecture/CommunicationCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/core/architecture/CoreCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/exam/architecture/ExamCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/exercise/architecture/ExerciseCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/fileupload/architecture/FileUploadCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/iris/architecture/IrisCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/lecture/architecture/LectureCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/lti/architecture/LtiCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/modeling/architecture/ModelingCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/plagiarism/architecture/PlagiarismCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/programming/architecture/ProgrammingCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/quiz/architecture/QuizCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleCodeStyleTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/text/architecture/TextCodeStyleArchitectureTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/tutorialgroup/architecture/TutorialGroupCodeStyleArchitectureTest.java diff --git a/src/test/java/de/tum/cit/aet/artemis/assessment/architecture/AssessmentCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/assessment/architecture/AssessmentCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..4fc43324c1af --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/assessment/architecture/AssessmentCodeStyleArchitectureTest.java @@ -0,0 +1,21 @@ +package de.tum.cit.aet.artemis.assessment.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class AssessmentCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".assessment"; + } + + @Override + protected int dtoAsAnnotatedRecordThreshold() { + return 3; + } + + @Override + protected int dtoNameEndingThreshold() { + return 1; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/athena/architecture/AthenaCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/athena/architecture/AthenaCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..7c3d9804dc2b --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/athena/architecture/AthenaCodeStyleArchitectureTest.java @@ -0,0 +1,16 @@ +package de.tum.cit.aet.artemis.athena.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class AthenaCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".athena"; + } + + @Override + protected int dtoNameEndingThreshold() { + return 1; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/architecture/AtlasCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/architecture/AtlasCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..f6289d2cd019 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/architecture/AtlasCodeStyleArchitectureTest.java @@ -0,0 +1,16 @@ +package de.tum.cit.aet.artemis.atlas.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class AtlasCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".atlas"; + } + + @Override + protected int dtoNameEndingThreshold() { + return 4; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/architecture/CommunicationCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/architecture/CommunicationCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..5d0d6e3dc705 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/communication/architecture/CommunicationCodeStyleArchitectureTest.java @@ -0,0 +1,21 @@ +package de.tum.cit.aet.artemis.communication.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class CommunicationCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".communication"; + } + + @Override + protected int dtoAsAnnotatedRecordThreshold() { + return 7; + } + + @Override + protected int dtoNameEndingThreshold() { + return 6; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/core/architecture/CoreCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/core/architecture/CoreCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..3106bcd0e0b0 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/core/architecture/CoreCodeStyleArchitectureTest.java @@ -0,0 +1,21 @@ +package de.tum.cit.aet.artemis.core.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class CoreCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".core"; + } + + @Override + protected int dtoAsAnnotatedRecordThreshold() { + return 10; + } + + @Override + protected int dtoNameEndingThreshold() { + return 10; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/exam/architecture/ExamCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/exam/architecture/ExamCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..e108a581f7e4 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/exam/architecture/ExamCodeStyleArchitectureTest.java @@ -0,0 +1,16 @@ +package de.tum.cit.aet.artemis.exam.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class ExamCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".exam"; + } + + @Override + protected int dtoNameEndingThreshold() { + return 4; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/architecture/ExerciseCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/architecture/ExerciseCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..10d68925cfdd --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/architecture/ExerciseCodeStyleArchitectureTest.java @@ -0,0 +1,21 @@ +package de.tum.cit.aet.artemis.exercise.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class ExerciseCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".exercise"; + } + + @Override + protected int dtoAsAnnotatedRecordThreshold() { + return 2; + } + + @Override + protected int dtoNameEndingThreshold() { + return 7; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/fileupload/architecture/FileUploadCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/fileupload/architecture/FileUploadCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..a4df22af3dca --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/fileupload/architecture/FileUploadCodeStyleArchitectureTest.java @@ -0,0 +1,11 @@ +package de.tum.cit.aet.artemis.fileupload.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class FileUploadCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".fileupload"; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/architecture/IrisCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/architecture/IrisCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..28c42c67d205 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/iris/architecture/IrisCodeStyleArchitectureTest.java @@ -0,0 +1,21 @@ +package de.tum.cit.aet.artemis.iris.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class IrisCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".iris"; + } + + @Override + protected int dtoAsAnnotatedRecordThreshold() { + return 2; + } + + @Override + protected int dtoNameEndingThreshold() { + return 4; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/lecture/architecture/LectureCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/lecture/architecture/LectureCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..f7ab2161169f --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/lecture/architecture/LectureCodeStyleArchitectureTest.java @@ -0,0 +1,11 @@ +package de.tum.cit.aet.artemis.lecture.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class LectureCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".lecture"; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/lti/architecture/LtiCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/lti/architecture/LtiCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..911a388cab3b --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/lti/architecture/LtiCodeStyleArchitectureTest.java @@ -0,0 +1,16 @@ +package de.tum.cit.aet.artemis.lti.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class LtiCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".lti"; + } + + @Override + protected int dtoNameEndingThreshold() { + return 11; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/modeling/architecture/ModelingCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/modeling/architecture/ModelingCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..d6dc2219ab45 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/modeling/architecture/ModelingCodeStyleArchitectureTest.java @@ -0,0 +1,11 @@ +package de.tum.cit.aet.artemis.modeling.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class ModelingCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".modeling"; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/plagiarism/architecture/PlagiarismCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/plagiarism/architecture/PlagiarismCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..7acab6337b89 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/plagiarism/architecture/PlagiarismCodeStyleArchitectureTest.java @@ -0,0 +1,16 @@ +package de.tum.cit.aet.artemis.plagiarism.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class PlagiarismCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".plagiarism"; + } + + @Override + protected int dtoNameEndingThreshold() { + return 1; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/architecture/ProgrammingCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/architecture/ProgrammingCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..35687c7a9667 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/programming/architecture/ProgrammingCodeStyleArchitectureTest.java @@ -0,0 +1,21 @@ +package de.tum.cit.aet.artemis.programming.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class ProgrammingCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".programming"; + } + + @Override + protected int dtoAsAnnotatedRecordThreshold() { + return 1; + } + + @Override + protected int dtoNameEndingThreshold() { + return 17; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/quiz/architecture/QuizCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/quiz/architecture/QuizCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..06d44e70908a --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/quiz/architecture/QuizCodeStyleArchitectureTest.java @@ -0,0 +1,11 @@ +package de.tum.cit.aet.artemis.quiz.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class QuizCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".quiz"; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/ArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/ArchitectureTest.java index 2cbd4da31b86..8a6ae4a94243 100644 --- a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/ArchitectureTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/ArchitectureTest.java @@ -39,7 +39,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -200,25 +199,6 @@ void testJSONImplementations() { .check(allClasses); } - @Disabled // TODO: Enable this test once the restructuring is done - @Test - void testDTOImplementations() { - var dtoRecordRule = classes().that().haveSimpleNameEndingWith("DTO").and().areNotInterfaces().should().beRecords().andShould().beAnnotatedWith(JsonInclude.class) - .because("All DTOs should be records and annotated with @JsonInclude(JsonInclude.Include.NON_EMPTY)"); - var result = dtoRecordRule.evaluate(allClasses); - log.info("Current number of DTO classes: {}", result.getFailureReport().getDetails().size()); - log.info("Current DTO classes: {}", result.getFailureReport().getDetails()); - // TODO: reduce the following number to 0, if the current number is less and the test fails, decrease it - assertThat(result.getFailureReport().getDetails()).hasSize(26); - - var dtoPackageRule = classes().that().resideInAPackage("..dto").should().haveSimpleNameEndingWith("DTO"); - result = dtoPackageRule.evaluate(allClasses); - log.info("Current number of DTOs that do not end with \"DTO\": {}", result.getFailureReport().getDetails().size()); - log.info("Current DTOs that do not end with \"DTO\": {}", result.getFailureReport().getDetails()); - // TODO: reduce the following number to 0, if the current number is less and the test fails, decrease it - assertThat(result.getFailureReport().getDetails()).hasSize(32); - } - @Test void testGsonExclusion() { // TODO: Replace all uses of gson with Jackson and check that gson is not used any more diff --git a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleCodeStyleTest.java b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleCodeStyleTest.java new file mode 100644 index 000000000000..73d517a8f2ce --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleCodeStyleTest.java @@ -0,0 +1,53 @@ +package de.tum.cit.aet.artemis.shared.architecture.module; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.shared.architecture.AbstractArchitectureTest; + +public abstract class AbstractModuleCodeStyleTest extends AbstractArchitectureTest implements ModuleArchitectureTest { + + private static final Logger log = LoggerFactory.getLogger(AbstractModuleCodeStyleTest.class); + + /** + * Threshold for number of classes with name ending DTO, that are no records or not annotated with JsonInclude. + * We should aim to reduce this threshold to 0. + */ + protected int dtoAsAnnotatedRecordThreshold() { + return 0; + } + + /** + * Threshold or number of classes in a 'dto'-package which file name does not end with DTO. + * We should aim to reduce this threshold to 0. + */ + protected int dtoNameEndingThreshold() { + return 0; + } + + @Test + void testDTOImplementations() { + var dtoRecordRule = classes().that().resideInAPackage(getModuleDtoSubpackage()).and().haveSimpleNameEndingWith("DTO").and().areNotInterfaces().should().beRecords() + .andShould().beAnnotatedWith(JsonInclude.class).because("All DTOs should be records and annotated with @JsonInclude(JsonInclude.Include.NON_EMPTY)"); + var result = dtoRecordRule.allowEmptyShould(true).evaluate(allClasses); + log.info("Current number of DTO classes: {}", result.getFailureReport().getDetails().size()); + log.info("Current DTO classes: {}", result.getFailureReport().getDetails()); + assertThat(result.getFailureReport().getDetails()).hasSize(dtoAsAnnotatedRecordThreshold()); + + var dtoPackageRule = classes().that().resideInAPackage(getModuleDtoSubpackage()).should().haveSimpleNameEndingWith("DTO"); + result = dtoPackageRule.allowEmptyShould(true).evaluate(allClasses); + log.info("Current number of DTOs that do not end with \"DTO\": {}", result.getFailureReport().getDetails().size()); + log.info("Current DTOs that do not end with \"DTO\": {}", result.getFailureReport().getDetails()); + assertThat(result.getFailureReport().getDetails()).hasSize(dtoNameEndingThreshold()); + } + + private String getModuleDtoSubpackage() { + return getModulePackage() + "..dto.."; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/text/architecture/TextCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/text/architecture/TextCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..3733922c44da --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/text/architecture/TextCodeStyleArchitectureTest.java @@ -0,0 +1,21 @@ +package de.tum.cit.aet.artemis.text.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class TextCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".text"; + } + + @Override + protected int dtoAsAnnotatedRecordThreshold() { + return 1; + } + + @Override + protected int dtoNameEndingThreshold() { + return 1; + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/tutorialgroup/architecture/TutorialGroupCodeStyleArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/tutorialgroup/architecture/TutorialGroupCodeStyleArchitectureTest.java new file mode 100644 index 000000000000..4bf807b83f89 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/tutorialgroup/architecture/TutorialGroupCodeStyleArchitectureTest.java @@ -0,0 +1,11 @@ +package de.tum.cit.aet.artemis.tutorialgroup.architecture; + +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleCodeStyleTest; + +class TutorialGroupCodeStyleArchitectureTest extends AbstractModuleCodeStyleTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".tutorialgroup"; + } +} From 1543e3ea0d78135a231adb941750fb50616b1663 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Mon, 20 Jan 2025 22:20:05 +0100 Subject: [PATCH 04/14] Development: Update server dependencies --- build.gradle | 8 ++++---- gradle.properties | 6 +++--- gradle/jacoco.gradle | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 1e98e96b2cea..ca407a7fb966 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ plugins { id "io.spring.dependency-management" version "1.1.7" id "nebula.lint" version "20.5.5" id "org.liquibase.gradle" version "${liquibase_plugin_version}" - id "org.owasp.dependencycheck" version "12.0.0" + id "org.owasp.dependencycheck" version "12.0.1" id "org.springframework.boot" version "${spring_boot_version}" } @@ -205,7 +205,7 @@ dependencies { implementation "org.apache.sshd:sshd-sftp:${sshd_version}" // https://mvnrepository.com/artifact/net.sourceforge.plantuml/plantuml - implementation "net.sourceforge.plantuml:plantuml:1.2024.8" + implementation "net.sourceforge.plantuml:plantuml:1.2025.0" implementation "me.xdrop:fuzzywuzzy:1.4.0" implementation("org.yaml:snakeyaml") { version { @@ -410,7 +410,7 @@ dependencies { testImplementation "org.springframework.boot:spring-boot-starter-test:${spring_boot_version}" testImplementation "org.springframework.security:spring-security-test:${spring_security_version}" testImplementation "org.springframework.boot:spring-boot-test:${spring_boot_version}" - testImplementation "org.assertj:assertj-core:3.27.2" + testImplementation "org.assertj:assertj-core:3.27.3" testImplementation "org.mockito:mockito-core:${mockito_version}" testImplementation "org.mockito:mockito-junit-jupiter:${mockito_version}" @@ -497,7 +497,7 @@ tasks.named("dependencyUpdates").configure { // 2c) Run tests for modules: ./gradlew test -DincludeModules=athena,atlas -x webapp (executes all tests in directories ./src/main/test/.../athena and ./src/main/test/.../atlas) + ArchitectureTests // 2d) Execute tests with Postgres container: SPRING_PROFILES_INCLUDE=postgres ./gradlew test -x webapp // 2e) Execute tests with MySQL container: SPRING_PROFILES_INCLUDE=mysql ./gradlew test -x webapp -// 3) Verify code coverage (after tests): ./gradlew jacocoTestCoverageVerification +// 3) Verify code coverage (after tests): ./gradlew jacocoTestCoverageVerification -x webapp // 4) Check Java code format: ./gradlew spotlessCheck -x webapp // 5) Apply Java code formatter: ./gradlew spotlessApply -x webapp // 6) Find dependency updates: ./gradlew dependencyUpdates -Drevision=release diff --git a/gradle.properties b/gradle.properties index 25bb5481099d..252dc4d76d25 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,12 +28,12 @@ jplag_version=5.1.0 # NOTE: we cannot need to use the latest version 9.x or 10.x here as long as Stanford CoreNLP does not reference it lucene_version=8.11.4 slf4j_version=2.0.16 -sentry_version=7.20.0 +sentry_version=7.20.1 liquibase_version=4.31.0 docker_java_version=3.4.1 logback_version=1.5.16 java_parser_version=3.26.2 -byte_buddy_version=1.15.11 +byte_buddy_version=1.16.1 netty_version=4.1.115.Final mysql_version=9.1.0 @@ -48,7 +48,7 @@ testcontainer_version=1.20.4 gradle_node_plugin_version=7.1.0 apt_plugin_version=0.21 liquibase_plugin_version=3.0.1 -modernizer_plugin_version=1.10.0 +modernizer_plugin_version=1.11.0 spotless_plugin_version=7.0.2 org.gradle.jvmargs=-Xmx2g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -Duser.country=US -Duser.language=en \ diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle index ce5919b9187a..e9dc08613dea 100644 --- a/gradle/jacoco.gradle +++ b/gradle/jacoco.gradle @@ -42,8 +42,8 @@ ext { "CLASS": 0 ], "iris" : [ - "INSTRUCTION": 0.796, - "CLASS": 16 + "INSTRUCTION": 0.795, + "CLASS": 17 ], "lecture" : [ "INSTRUCTION": 0.867, From 9cd2d749165d7f6ca03ed2808bb6cb22bfa1238b Mon Sep 17 00:00:00 2001 From: Enea Gore <73840596+EneaGore@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:03:34 +0100 Subject: [PATCH 05/14] Text exercises: Fix an issue where students cannot see automatic unreferenced feedback (#10179) --- .../app/exercises/text/participate/text-editor.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/exercises/text/participate/text-editor.component.ts b/src/main/webapp/app/exercises/text/participate/text-editor.component.ts index fc9698863136..592a961a8f5c 100644 --- a/src/main/webapp/app/exercises/text/participate/text-editor.component.ts +++ b/src/main/webapp/app/exercises/text/participate/text-editor.component.ts @@ -24,7 +24,7 @@ import { TextSubmission } from 'app/entities/text/text-submission.model'; import { StringCountService } from 'app/exercises/text/participate/string-count.service'; import { AccountService } from 'app/core/auth/account.service'; import { getFirstResultWithComplaint, getLatestSubmissionResult, setLatestSubmissionResult } from 'app/entities/submission.model'; -import { getManualUnreferencedFeedback, isAthenaAIResult } from 'app/exercises/shared/result/result.utils'; +import { getUnreferencedFeedback, isAthenaAIResult } from 'app/exercises/shared/result/result.utils'; import { onError } from 'app/shared/util/global.utils'; import { Course } from 'app/entities/course.model'; import { getCourseFromExercise } from 'app/entities/exercise.model'; @@ -356,7 +356,7 @@ export class TextEditorComponent implements OnInit, OnDestroy, ComponentCanDeact * Check whether or not a result exists and if, returns the unreferenced feedback of it */ get unreferencedFeedback(): Feedback[] | undefined { - return this.result ? getManualUnreferencedFeedback(this.result.feedbacks) : undefined; + return this.result ? getUnreferencedFeedback(this.result.feedbacks) : undefined; } get wordCount(): number { From 4c90b41b3b029442770bca839e93afa2854f0765 Mon Sep 17 00:00:00 2001 From: Asli Aykan <56061820+asliayk@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:04:46 +0100 Subject: [PATCH 06/14] Communication: Fix cursor jump issue during list text editing (#10181) --- .../model/actions/list.action.ts | 7 ++---- ...postings-markdown-editor.component.spec.ts | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/list.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/list.action.ts index 5f404a25535e..4bef7abe2135 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/list.action.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/list.action.ts @@ -79,15 +79,12 @@ export abstract class ListAction extends TextEditorAction { const lines = selectedText.split('\n'); // Check if the cursor is at the end of the line to add or remove the prefix - let position = editor.getPosition(); + const position = editor.getPosition(); if (position) { const currentLineText = editor.getLineText(position.getLineNumber()); if (!selectedText && position.getColumn() <= currentLineText.length) { - const endPosition = new TextEditorPosition(position.getLineNumber(), currentLineText.length + 1); - editor.setPosition(endPosition); - editor.focus(); - position = endPosition; + return; } if (position.getColumn() === currentLineText.length + 1) { diff --git a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts index eeae40d0f971..276d3e82fd7d 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts @@ -582,4 +582,26 @@ describe('PostingsMarkdownEditor', () => { (component as any).handleKeyDown(mockModel, mockPosition.lineNumber); expect(handleActionClickSpy).not.toHaveBeenCalled(); }); + + it('should keep the cursor position intact when editing text in a list item', () => { + const bulletedListAction = component.defaultActions.find((action: any) => action instanceof BulletedListAction) as BulletedListAction; + mockEditor.getPosition.mockReturnValue({ + getLineNumber: () => 1, + getColumn: () => 5, + } as TextEditorPosition); + mockEditor.getLineText.mockReturnValue('- First line'); + mockEditor.getTextAtRange.mockReturnValue(''); + + const replaceTextSpy = jest.spyOn(mockEditor, 'replaceTextAtRange'); + bulletedListAction.run(mockEditor); + + expect(replaceTextSpy).not.toHaveBeenCalled(); + + const cursorPosition = mockEditor.getPosition(); + expect(cursorPosition).toEqual({ + getLineNumber: expect.any(Function), + getColumn: expect.any(Function), + }); + expect(cursorPosition?.getColumn()).toBe(5); + }); }); From 881d85976f723bc7b2bbd46698b6fb3ab6d95ddb Mon Sep 17 00:00:00 2001 From: Paul Rangger <48455539+PaRangger@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:21:35 +0100 Subject: [PATCH 07/14] Communication: Keep file names when downloading attachments (#9970) --- .../aet/artemis/core/service/FileService.java | 12 +- .../aet/artemis/core/web/FileResource.java | 25 +++- .../domain/FilePathInformation.java | 7 ++ .../artemis/fileupload/domain/FileUpload.java | 113 +++++++++++++++++ .../domain/FileUploadEntityType.java | 23 ++++ .../repository/FileUploadRepository.java | 58 +++++++++ .../service/FileUploadScheduleService.java | 60 +++++++++ .../fileupload/service/FileUploadService.java | 79 ++++++++++++ .../changelog/20241206114231_changelog.xml | 58 +++++++++ .../resources/config/liquibase/master.xml | 1 + .../FileUploadScheduleServiceTest.java | 62 +++++++++ .../service/FileUploadServiceTest.java | 119 ++++++++++++++++++ 12 files changed, 609 insertions(+), 8 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/fileupload/domain/FilePathInformation.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/fileupload/domain/FileUpload.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/fileupload/domain/FileUploadEntityType.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/fileupload/repository/FileUploadRepository.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadScheduleService.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadService.java create mode 100644 src/main/resources/config/liquibase/changelog/20241206114231_changelog.xml create mode 100644 src/test/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadScheduleServiceTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadServiceTest.java diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/FileService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/FileService.java index 56edc1308149..b70b5515635e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/FileService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/FileService.java @@ -67,6 +67,7 @@ import de.tum.cit.aet.artemis.core.exception.FilePathParsingException; import de.tum.cit.aet.artemis.core.exception.InternalServerErrorException; import de.tum.cit.aet.artemis.core.util.CommonsMultipartFile; +import de.tum.cit.aet.artemis.fileupload.domain.FilePathInformation; @Profile(PROFILE_CORE) @Service @@ -194,22 +195,23 @@ public URI handleSaveFile(MultipartFile file, boolean keepFilename, boolean mark * @param conversationId The ID of the conversation. * @return The URI of the saved file. */ - public URI handleSaveFileInConversation(MultipartFile file, Long courseId, Long conversationId) { + public FilePathInformation handleSaveFileInConversation(MultipartFile file, Long courseId, Long conversationId) { // TODO: Improve the access check. The course is already checked, but the user might not be a member of the conversation. The course may not belong to the conversation - String filename = checkAndSanitizeFilename(file.getOriginalFilename()); + String sanitizedOriginalFilename = checkAndSanitizeFilename(file.getOriginalFilename()); - validateExtension(filename, true); + validateExtension(sanitizedOriginalFilename, true); final String filenamePrefix = "Markdown_"; final Path path = FilePathService.getMarkdownFilePathForConversation(courseId, conversationId); - String fileName = generateFilename(filenamePrefix, filename, false); // TODO: keep? + String fileName = generateFilename(filenamePrefix, sanitizedOriginalFilename, false); Path filePath = path.resolve(fileName); copyFile(file, filePath); String currentFilename = filePath.getFileName().toString(); - return URI.create("/api/files/courses/" + courseId + "/conversations/" + conversationId + "/").resolve(currentFilename); + return new FilePathInformation(filePath, URI.create("/api/files/courses/" + courseId + "/conversations/" + conversationId + "/").resolve(currentFilename), + sanitizedOriginalFilename); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java index 11c5d1278609..f828674ba26c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java @@ -64,9 +64,11 @@ import de.tum.cit.aet.artemis.exam.domain.ExamUser; import de.tum.cit.aet.artemis.exam.repository.ExamUserRepository; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; +import de.tum.cit.aet.artemis.fileupload.domain.FileUploadEntityType; import de.tum.cit.aet.artemis.fileupload.domain.FileUploadExercise; import de.tum.cit.aet.artemis.fileupload.domain.FileUploadSubmission; import de.tum.cit.aet.artemis.fileupload.repository.FileUploadSubmissionRepository; +import de.tum.cit.aet.artemis.fileupload.service.FileUploadService; import de.tum.cit.aet.artemis.lecture.domain.Attachment; import de.tum.cit.aet.artemis.lecture.domain.AttachmentType; import de.tum.cit.aet.artemis.lecture.domain.AttachmentUnit; @@ -126,10 +128,13 @@ public class FileResource { private final LectureUnitService lectureUnitService; + private final FileUploadService fileUploadService; + public FileResource(SlideRepository slideRepository, AuthorizationCheckService authorizationCheckService, FileService fileService, ResourceLoaderService resourceLoaderService, LectureRepository lectureRepository, FileUploadSubmissionRepository fileUploadSubmissionRepository, AttachmentRepository attachmentRepository, AttachmentUnitRepository attachmentUnitRepository, AuthorizationCheckService authCheckService, UserRepository userRepository, ExamUserRepository examUserRepository, - QuizQuestionRepository quizQuestionRepository, DragItemRepository dragItemRepository, CourseRepository courseRepository, LectureUnitService lectureUnitService) { + QuizQuestionRepository quizQuestionRepository, DragItemRepository dragItemRepository, CourseRepository courseRepository, LectureUnitService lectureUnitService, + FileUploadService fileUploadService) { this.fileService = fileService; this.resourceLoaderService = resourceLoaderService; this.lectureRepository = lectureRepository; @@ -145,6 +150,7 @@ public FileResource(SlideRepository slideRepository, AuthorizationCheckService a this.dragItemRepository = dragItemRepository; this.courseRepository = courseRepository; this.lectureUnitService = lectureUnitService; + this.fileUploadService = fileUploadService; } /** @@ -185,7 +191,11 @@ public ResponseEntity saveMarkdownFileForConversation(@RequestParam(valu if (file.getSize() > Constants.MAX_FILE_SIZE_COMMUNICATION) { throw new ResponseStatusException(HttpStatus.PAYLOAD_TOO_LARGE, "The file is too large. Maximum file size is " + Constants.MAX_FILE_SIZE_COMMUNICATION + " bytes."); } - String responsePath = fileService.handleSaveFileInConversation(file, courseId, conversationId).toString(); + var filePathInformation = fileService.handleSaveFileInConversation(file, courseId, conversationId); + String responsePath = filePathInformation.publicPath().toString(); + + fileUploadService.createFileUpload(responsePath, filePathInformation.serverPath().toString(), filePathInformation.filename(), conversationId, + FileUploadEntityType.CONVERSATION); // return path for getting the file String responseBody = "{\"path\":\"" + responsePath + "\"}"; @@ -207,7 +217,16 @@ public ResponseEntity getMarkdownFileForConversation(@PathVariable Long // TODO: Improve the access check log.debug("REST request to get file for markdown in conversation: File {} for conversation {} in course {}", filename, conversationId, courseId); sanitizeFilenameElseThrow(filename); - return buildFileResponse(FilePathService.getMarkdownFilePathForConversation(courseId, conversationId), filename, true); + + var path = FilePathService.getMarkdownFilePathForConversation(courseId, conversationId); + + var fileUpload = fileUploadService.findByPath("/api/files/courses/" + courseId + "/conversations/" + conversationId + "/" + filename); + + if (fileUpload.isPresent()) { + return buildFileResponse(path, filename, Optional.ofNullable(fileUpload.get().getFilename()), true); + } + + return buildFileResponse(path, filename, true); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/fileupload/domain/FilePathInformation.java b/src/main/java/de/tum/cit/aet/artemis/fileupload/domain/FilePathInformation.java new file mode 100644 index 000000000000..3333d5b8ee43 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/fileupload/domain/FilePathInformation.java @@ -0,0 +1,7 @@ +package de.tum.cit.aet.artemis.fileupload.domain; + +import java.net.URI; +import java.nio.file.Path; + +public record FilePathInformation(Path serverPath, URI publicPath, String filename) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/fileupload/domain/FileUpload.java b/src/main/java/de/tum/cit/aet/artemis/fileupload/domain/FileUpload.java new file mode 100644 index 000000000000..3815abe2fde5 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/fileupload/domain/FileUpload.java @@ -0,0 +1,113 @@ +package de.tum.cit.aet.artemis.fileupload.domain; + +import java.time.ZonedDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; + +import de.tum.cit.aet.artemis.core.domain.DomainObject; +import de.tum.cit.aet.artemis.fileupload.service.FileUploadScheduleService; + +@Entity +@Table(name = "file_upload") +public class FileUpload extends DomainObject { + + @Column(name = "path", nullable = false) + private String path; + + @Column(name = "server_file_path", nullable = false) + private String serverFilePath; + + @Column(name = "filename", nullable = false) + private String filename; + + /** + * This represents the id of the entity connected to this FileUpload. + * Nullable in case file uploads happen before the creation of + * the corresponding entity. Warning: If left null, the FileUpload + * will be cleaned up after a timeframe specified in + * {{@link FileUploadScheduleService}}. + */ + @Column(name = "entity_id") + private Long entityId; + + /** + * This represents the type of the entity that is connected to this + * FileUpload. The same as above applies to this value. + */ + @Enumerated + @Column(name = "entity_type") + private FileUploadEntityType entityType; + + @Column(name = "creation_date", nullable = false) + private ZonedDateTime creationDate; + + // Constructors + public FileUpload() { + } + + public FileUpload(String path, String serverFilePath, String filename, Long entityId, FileUploadEntityType entityType) { + this.path = path; + this.filename = filename; + this.serverFilePath = serverFilePath; + this.entityId = entityId; + this.entityType = entityType; + this.creationDate = ZonedDateTime.now(); + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getServerFilePath() { + return serverFilePath; + } + + public void setServerFilePath(String serverFilePath) { + this.serverFilePath = serverFilePath; + } + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public Long getEntityId() { + return entityId; + } + + public void setEntityId(Long entityId) { + this.entityId = entityId; + } + + public FileUploadEntityType getEntityType() { + return entityType; + } + + public void setEntityType(FileUploadEntityType entityType) { + this.entityType = entityType; + } + + public ZonedDateTime getCreationDate() { + return creationDate; + } + + public void setCreationDate(ZonedDateTime createdAt) { + this.creationDate = createdAt; + } + + @Override + public String toString() { + return "FileUpload{" + "id=" + getId() + ", path='" + path + '\'' + ", filename='" + filename + '\'' + ", entityId=" + entityId + ", entityType=" + entityType + + ", createdAt=" + creationDate + '}'; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/fileupload/domain/FileUploadEntityType.java b/src/main/java/de/tum/cit/aet/artemis/fileupload/domain/FileUploadEntityType.java new file mode 100644 index 000000000000..deed833024b0 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/fileupload/domain/FileUploadEntityType.java @@ -0,0 +1,23 @@ +package de.tum.cit.aet.artemis.fileupload.domain; + +import java.util.Arrays; + +public enum FileUploadEntityType { + + CONVERSATION((short) 0); + + private final short databaseKey; + + FileUploadEntityType(short databaseKey) { + this.databaseKey = databaseKey; + } + + public short getDatabaseKey() { + return databaseKey; + } + + public static FileUploadEntityType fromDatabaseKey(short databaseKey) { + return Arrays.stream(FileUploadEntityType.values()).filter(type -> type.getDatabaseKey() == databaseKey).findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown database key: " + databaseKey)); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/fileupload/repository/FileUploadRepository.java b/src/main/java/de/tum/cit/aet/artemis/fileupload/repository/FileUploadRepository.java new file mode 100644 index 000000000000..5022487e6658 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/fileupload/repository/FileUploadRepository.java @@ -0,0 +1,58 @@ +package de.tum.cit.aet.artemis.fileupload.repository; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.time.ZonedDateTime; +import java.util.List; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; +import de.tum.cit.aet.artemis.fileupload.domain.FileUpload; +import de.tum.cit.aet.artemis.fileupload.domain.FileUploadEntityType; + +@Profile(PROFILE_CORE) +@Repository +public interface FileUploadRepository extends ArtemisJpaRepository { + + /*** + * Get a single file upload entity by its path + * + * @param path path of the file upload + * + * @return The file upload if exists, null otherwise. + */ + FileUpload findFileUploadByPath(String path); + + /*** + * The value "f.entityType = 0" represents a conversation, given by the enum {{@link FileUploadEntityType}} + * + * @return List of file uploads that do not have a conversation entity connected to them + */ + @Query(""" + SELECT f + FROM FileUpload f + LEFT JOIN Conversation c ON f.entityId = c.id + WHERE f.entityType = 0 + AND c.id IS NULL + """) + List findOrphanedConversationReferences(); + + /*** + * Finds all file uploads that are not connected to an entity and were created before a given date + * + * @param cutoffDate is the date that defines from when the file uploads should be returned. E.g. to give some time before deleting. + * + * @return List of file uploads that do not have an entity and were created before a given date + */ + @Query(""" + SELECT f + FROM FileUpload f + WHERE (f.entityType IS NULL OR f.entityId IS NULL) + AND f.creationDate < :cutoffDate + """) + List findNullEntityReferences(@Param("cutoffDate") ZonedDateTime cutoffDate); +} diff --git a/src/main/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadScheduleService.java b/src/main/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadScheduleService.java new file mode 100644 index 000000000000..da8980b51950 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadScheduleService.java @@ -0,0 +1,60 @@ +package de.tum.cit.aet.artemis.fileupload.service; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_SCHEDULING; + +import java.time.ZonedDateTime; +import java.util.List; + +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.fileupload.domain.FileUpload; +import de.tum.cit.aet.artemis.fileupload.domain.FileUploadEntityType; +import de.tum.cit.aet.artemis.fileupload.repository.FileUploadRepository; + +@Service +@Profile(PROFILE_SCHEDULING) +public class FileUploadScheduleService { + + public static final int DAYS_UNTIL_NULL_ENTITY_FILES_ARE_DELETED = 3; + + private final FileUploadRepository fileUploadRepository; + + private final FileUploadService fileUploadService; + + public FileUploadScheduleService(FileUploadRepository fileUploadRepository, FileUploadService fileUploadService) { + this.fileUploadRepository = fileUploadRepository; + this.fileUploadService = fileUploadService; + } + + /** + * Cleans up all file uploads where corresponding entity does not exist anymore + */ + @Scheduled(cron = "0 0 0 * * *") + public void cleanupOrphanedFileUploads() { + var entityTypes = FileUploadEntityType.values(); + + for (var entityType : entityTypes) { + var fileUploadsToDelete = findByEntity(entityType); + fileUploadService.deleteFileUploads(fileUploadsToDelete); + } + } + + /** + * Cleans up all file uploads where entity id is null + */ + @Scheduled(cron = "0 5 0 * * *") + public void cleanupNullEntityFileUploads() { + var cutoffDate = ZonedDateTime.now().minusDays(DAYS_UNTIL_NULL_ENTITY_FILES_ARE_DELETED); + var fileUploadsToDelete = fileUploadRepository.findNullEntityReferences(cutoffDate); + fileUploadService.deleteFileUploads(fileUploadsToDelete); + } + + private List findByEntity(FileUploadEntityType entityType) { + if (entityType == FileUploadEntityType.CONVERSATION) { + return fileUploadRepository.findOrphanedConversationReferences(); + } + throw new IllegalArgumentException("Unimplemented entity type: " + entityType); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadService.java b/src/main/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadService.java new file mode 100644 index 000000000000..c1e9cf7ab6c8 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadService.java @@ -0,0 +1,79 @@ +package de.tum.cit.aet.artemis.fileupload.service; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.core.service.FileService; +import de.tum.cit.aet.artemis.fileupload.domain.FileUpload; +import de.tum.cit.aet.artemis.fileupload.domain.FileUploadEntityType; +import de.tum.cit.aet.artemis.fileupload.repository.FileUploadRepository; + +@Service +@Profile(PROFILE_CORE) +public class FileUploadService { + + private static final Logger log = LoggerFactory.getLogger(FileUploadService.class); + + private final FileUploadRepository fileUploadRepository; + + private final FileService fileService; + + public FileUploadService(FileUploadRepository fileUploadRepository, FileService fileService) { + this.fileUploadRepository = fileUploadRepository; + this.fileService = fileService; + } + + /** + * Creates a new file upload entity. + * + * @param path Url with which the upload is accessed + * @param serverFilePath file path on the server, used for deletion + * @param fileName filename that is returned when downloading it + * @param entityId the id of the attached entity + * @param entityType type of the entity, to identify + */ + public void createFileUpload(String path, String serverFilePath, String fileName, Long entityId, FileUploadEntityType entityType) { + var fileUpload = new FileUpload(path, serverFilePath, fileName, entityId, entityType); + + fileUploadRepository.save(fileUpload); + } + + /** + * Creates a new file upload entity. + * + * @param path public path that the file upload is accessed with + * + * @return the file upload if found + */ + public Optional findByPath(String path) { + return Optional.ofNullable(fileUploadRepository.findFileUploadByPath(path)); + } + + /** + * Creates a new file upload entity. + * + * @param fileUploads deletes all these file uploads from the database and from the storage + */ + public void deleteFileUploads(List fileUploads) { + for (var fileUpload : fileUploads) { + try { + var path = Path.of(fileUpload.getServerFilePath()); + fileService.schedulePathForDeletion(path, 1); + } + catch (InvalidPathException e) { + log.error("Deleting the file {} did not work because it does not exist", fileUpload.getPath()); + } + } + + fileUploadRepository.deleteAll(fileUploads); + } +} diff --git a/src/main/resources/config/liquibase/changelog/20241206114231_changelog.xml b/src/main/resources/config/liquibase/changelog/20241206114231_changelog.xml new file mode 100644 index 000000000000..e287ae4613af --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241206114231_changelog.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 9d85d51f1bb1..02ac0209b9d1 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -42,6 +42,7 @@ + diff --git a/src/test/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadScheduleServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadScheduleServiceTest.java new file mode 100644 index 000000000000..acfe73e4d2d3 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadScheduleServiceTest.java @@ -0,0 +1,62 @@ +package de.tum.cit.aet.artemis.fileupload.service; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.ZonedDateTime; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import de.tum.cit.aet.artemis.fileupload.domain.FileUpload; +import de.tum.cit.aet.artemis.fileupload.repository.FileUploadRepository; + +@ExtendWith(MockitoExtension.class) +class FileUploadScheduleServiceTest { + + @Mock + private FileUploadRepository fileUploadRepository; + + @Mock + private FileUploadService fileUploadService; + + @InjectMocks + private FileUploadScheduleService fileUploadScheduleService; + + @Test + void shouldDeleteOrphanedFileUploadsWhenCleanupMethodIsCalled() { + List orphanedConversationUploads = List.of(mock(FileUpload.class), mock(FileUpload.class)); + + when(fileUploadRepository.findOrphanedConversationReferences()).thenReturn(orphanedConversationUploads); + + // If this fails you may have added a new entity type that needs to be added as a case to the function + fileUploadScheduleService.cleanupOrphanedFileUploads(); + + verify(fileUploadRepository).findOrphanedConversationReferences(); + + verify(fileUploadService).deleteFileUploads(orphanedConversationUploads); + } + + @Test + void shouldDeleteNullEntityFileUploadsWhenOlderThanThreeDays() { + List nullEntityUploads = List.of(mock(FileUpload.class), mock(FileUpload.class)); + + ZonedDateTime cutoffDate = ZonedDateTime.now().minusDays(FileUploadScheduleService.DAYS_UNTIL_NULL_ENTITY_FILES_ARE_DELETED); + + when(fileUploadRepository.findNullEntityReferences(any())).thenReturn(nullEntityUploads); + + fileUploadScheduleService.cleanupNullEntityFileUploads(); + + verify(fileUploadRepository).findNullEntityReferences(argThat(date -> date.isAfter(cutoffDate.minusMinutes(1)) && date.isBefore(cutoffDate.plusMinutes(1)))); + + verify(fileUploadService).deleteFileUploads(nullEntityUploads); + } + +} diff --git a/src/test/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadServiceTest.java new file mode 100644 index 000000000000..2c5d6d86de71 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/fileupload/service/FileUploadServiceTest.java @@ -0,0 +1,119 @@ +package de.tum.cit.aet.artemis.fileupload.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import de.tum.cit.aet.artemis.core.service.FileService; +import de.tum.cit.aet.artemis.fileupload.domain.FileUpload; +import de.tum.cit.aet.artemis.fileupload.domain.FileUploadEntityType; +import de.tum.cit.aet.artemis.fileupload.repository.FileUploadRepository; + +@ExtendWith(MockitoExtension.class) +class FileUploadServiceTest { + + @Mock + private FileUploadRepository fileUploadRepository; + + @Mock + private FileService fileService; + + private FileUploadService fileUploadService; + + @BeforeEach + void setUp() { + fileUploadService = new FileUploadService(fileUploadRepository, fileService); + } + + @Test + void shouldCreateFileUploadWhenValidParametersProvided() { + String path = "/test/path"; + String serverFilePath = "/server/file/path"; + String fileName = "test.txt"; + Long entityId = 1L; + FileUploadEntityType entityType = FileUploadEntityType.CONVERSATION; + + fileUploadService.createFileUpload(path, serverFilePath, fileName, entityId, entityType); + + verify(fileUploadRepository).save(argThat(fileUpload -> fileUpload.getPath().equals(path) && fileUpload.getServerFilePath().equals(serverFilePath) + && fileUpload.getFilename().equals(fileName) && fileUpload.getEntityId().equals(entityId) && fileUpload.getEntityType().equals(entityType))); + } + + @Test + void shouldFindFileUploadWhenPathExists() { + String path = "/test/path"; + FileUpload expectedFileUpload = new FileUpload(path, "/server/path", "test.txt", 1L, FileUploadEntityType.CONVERSATION); + + when(fileUploadRepository.findFileUploadByPath(path)).thenReturn(expectedFileUpload); + + Optional result = fileUploadService.findByPath(path); + + assertThat(result).isPresent().containsSame(expectedFileUpload); + } + + @Test + void shouldReturnEmptyOptionalWhenPathDoesNotExist() { + String path = "/non/existent/path"; + when(fileUploadRepository.findFileUploadByPath(path)).thenReturn(null); + + Optional result = fileUploadService.findByPath(path); + + assertThat(result).isEmpty(); + } + + @Test + void shouldDeleteFileUploadsWhenValidFileUploadsProvided() { + FileUpload fileUpload1 = mock(FileUpload.class); + FileUpload fileUpload2 = mock(FileUpload.class); + List fileUploads = Arrays.asList(fileUpload1, fileUpload2); + + when(fileUpload1.getServerFilePath()).thenReturn("/path/to/file1"); + when(fileUpload2.getServerFilePath()).thenReturn("/path/to/file2"); + + fileUploadService.deleteFileUploads(fileUploads); + + verify(fileService).schedulePathForDeletion(Path.of("/path/to/file1"), 1); + verify(fileService).schedulePathForDeletion(Path.of("/path/to/file2"), 1); + verify(fileUploadRepository).deleteAll(fileUploads); + } + + @Test + void shouldHandleInvalidPathWhenDeletingFileUploads() { + FileUpload fileUpload = mock(FileUpload.class); + List fileUploads = Collections.singletonList(fileUpload); + + when(fileUpload.getServerFilePath()).thenThrow(new InvalidPathException("", "")); + when(fileUpload.getPath()).thenReturn("/invalid/path"); + + assertThatCode(() -> fileUploadService.deleteFileUploads(fileUploads)).doesNotThrowAnyException(); + + verify(fileUploadRepository).deleteAll(fileUploads); + } + + @Test + void shouldHandleEmptyFileUploadListWhenDeleting() { + List emptyFileUploads = Collections.emptyList(); + + fileUploadService.deleteFileUploads(emptyFileUploads); + + verify(fileUploadRepository).deleteAll(emptyFileUploads); + verifyNoInteractions(fileService); + } +} From 0610eb00769d8b30bf5c6ef5b4fc1f89c3af2667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20S=C3=B6lch?= Date: Tue, 21 Jan 2025 21:23:00 +0100 Subject: [PATCH 08/14] Programming exercises: Update java exercise docker image to java17-22 (#10172) --- src/main/resources/config/application.yml | 4 ++-- .../java/gradle_gradle/exercise/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../java/gradle_gradle/solution/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../maven_maven/test/projectTemplate/pom.xml | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../test/blackbox/projectTemplate/pom.xml | 2 +- .../test/gradle/projectTemplate/build.gradle | 6 +++--- .../gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../java/test/maven/projectTemplate/pom.xml | 2 +- .../kotlin/test/maven/projectTemplate/pom.xml | 2 +- 18 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index a002fcd338f8..e9aebc1980b1 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -65,11 +65,11 @@ artemis: images: java: # possible overrides: maven, gradle - default: "ls1tum/artemis-maven-template:java17-21" + default: "ls1tum/artemis-maven-template:java17-22" maven_blackbox: "ghcr.io/uni-passau-artemis/artemis-dejagnu:22" kotlin: # possible overrides: maven, gradle - default: "ls1tum/artemis-maven-template:java17-21" + default: "ls1tum/artemis-maven-template:java17-22" empty: default: "ubuntu:24.04" python: diff --git a/src/main/resources/templates/java/gradle_gradle/exercise/build.gradle b/src/main/resources/templates/java/gradle_gradle/exercise/build.gradle index 0fe92568cc44..1c5254857341 100644 --- a/src/main/resources/templates/java/gradle_gradle/exercise/build.gradle +++ b/src/main/resources/templates/java/gradle_gradle/exercise/build.gradle @@ -8,7 +8,7 @@ repositories { } dependencies { - implementation 'org.apache.commons:commons-lang3:3.14.0' + implementation 'org.apache.commons:commons-lang3:3.17.0' } sourceSets { diff --git a/src/main/resources/templates/java/gradle_gradle/exercise/gradle/wrapper/gradle-wrapper.jar b/src/main/resources/templates/java/gradle_gradle/exercise/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 12612 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 HJ4oML2Lo<@i8BwbL^1~GkG`E7C$SEa_ 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 diff --git a/src/main/resources/templates/java/gradle_gradle/exercise/gradle/wrapper/gradle-wrapper.properties b/src/main/resources/templates/java/gradle_gradle/exercise/gradle/wrapper/gradle-wrapper.properties index b82aa23a4f05..cea7a793a84b 100644 --- a/src/main/resources/templates/java/gradle_gradle/exercise/gradle/wrapper/gradle-wrapper.properties +++ b/src/main/resources/templates/java/gradle_gradle/exercise/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/resources/templates/java/gradle_gradle/solution/build.gradle b/src/main/resources/templates/java/gradle_gradle/solution/build.gradle index 0fe92568cc44..1c5254857341 100644 --- a/src/main/resources/templates/java/gradle_gradle/solution/build.gradle +++ b/src/main/resources/templates/java/gradle_gradle/solution/build.gradle @@ -8,7 +8,7 @@ repositories { } dependencies { - implementation 'org.apache.commons:commons-lang3:3.14.0' + implementation 'org.apache.commons:commons-lang3:3.17.0' } sourceSets { diff --git a/src/main/resources/templates/java/gradle_gradle/solution/gradle/wrapper/gradle-wrapper.jar b/src/main/resources/templates/java/gradle_gradle/solution/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 12612 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 HJ4oML2Lo<@i8BwbL^1~GkG`E7C$SEa_ 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 diff --git a/src/main/resources/templates/java/gradle_gradle/solution/gradle/wrapper/gradle-wrapper.properties b/src/main/resources/templates/java/gradle_gradle/solution/gradle/wrapper/gradle-wrapper.properties index b82aa23a4f05..cea7a793a84b 100644 --- a/src/main/resources/templates/java/gradle_gradle/solution/gradle/wrapper/gradle-wrapper.properties +++ b/src/main/resources/templates/java/gradle_gradle/solution/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/resources/templates/java/maven_maven/test/projectTemplate/pom.xml b/src/main/resources/templates/java/maven_maven/test/projectTemplate/pom.xml index 93720b6f3aee..a3f2125ac70a 100644 --- a/src/main/resources/templates/java/maven_maven/test/projectTemplate/pom.xml +++ b/src/main/resources/templates/java/maven_maven/test/projectTemplate/pom.xml @@ -122,7 +122,7 @@ com.puppycrawl.tools checkstyle - 10.15.0 + 10.17.0 diff --git a/src/main/resources/templates/java/plain_gradle/exercise/gradle/wrapper/gradle-wrapper.jar b/src/main/resources/templates/java/plain_gradle/exercise/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 12612 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 HJ4oML2Lo<@i8BwbL^1~GkG`E7C$SEa_ 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 diff --git a/src/main/resources/templates/java/plain_gradle/exercise/gradle/wrapper/gradle-wrapper.properties b/src/main/resources/templates/java/plain_gradle/exercise/gradle/wrapper/gradle-wrapper.properties index b82aa23a4f05..cea7a793a84b 100644 --- a/src/main/resources/templates/java/plain_gradle/exercise/gradle/wrapper/gradle-wrapper.properties +++ b/src/main/resources/templates/java/plain_gradle/exercise/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/resources/templates/java/plain_gradle/solution/gradle/wrapper/gradle-wrapper.jar b/src/main/resources/templates/java/plain_gradle/solution/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 12612 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 HJ4oML2Lo<@i8BwbL^1~GkG`E7C$SEa_ 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 diff --git a/src/main/resources/templates/java/plain_gradle/solution/gradle/wrapper/gradle-wrapper.properties b/src/main/resources/templates/java/plain_gradle/solution/gradle/wrapper/gradle-wrapper.properties index b82aa23a4f05..cea7a793a84b 100644 --- a/src/main/resources/templates/java/plain_gradle/solution/gradle/wrapper/gradle-wrapper.properties +++ b/src/main/resources/templates/java/plain_gradle/solution/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/resources/templates/java/test/blackbox/projectTemplate/pom.xml b/src/main/resources/templates/java/test/blackbox/projectTemplate/pom.xml index 38daabb206c3..0c7c4811835e 100644 --- a/src/main/resources/templates/java/test/blackbox/projectTemplate/pom.xml +++ b/src/main/resources/templates/java/test/blackbox/projectTemplate/pom.xml @@ -54,7 +54,7 @@ com.puppycrawl.tools checkstyle - 10.15.0 + 10.17.0 diff --git a/src/main/resources/templates/java/test/gradle/projectTemplate/build.gradle b/src/main/resources/templates/java/test/gradle/projectTemplate/build.gradle index 611ce8468738..0aa8c3dac9f0 100644 --- a/src/main/resources/templates/java/test/gradle/projectTemplate/build.gradle +++ b/src/main/resources/templates/java/test/gradle/projectTemplate/build.gradle @@ -2,9 +2,9 @@ plugins { // %static-code-analysis-start% id 'checkstyle' id 'pmd' - id 'com.github.spotbugs' version '6.0.9' + id 'com.github.spotbugs' version '6.1.0' // %static-code-analysis-stop% - id 'com.teamscale' version '33.1.2' + id 'com.teamscale' version '34.2.1' } apply plugin: 'java' @@ -20,7 +20,7 @@ repositories { dependencies { testImplementation 'de.tum.in.ase:artemis-java-test-sandbox:1.13.0' - implementation 'org.apache.commons:commons-lang3:3.14.0' + implementation 'org.apache.commons:commons-lang3:3.17.0' // testImplementation(':${exerciseNamePomXml}') // testImplementation(':${exerciseNamePomXml}-Solution') diff --git a/src/main/resources/templates/java/test/gradle/projectTemplate/gradle/wrapper/gradle-wrapper.jar b/src/main/resources/templates/java/test/gradle/projectTemplate/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 12612 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 HJ4oML2Lo<@i8BwbL^1~GkG`E7C$SEa_ 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 diff --git a/src/main/resources/templates/java/test/gradle/projectTemplate/gradle/wrapper/gradle-wrapper.properties b/src/main/resources/templates/java/test/gradle/projectTemplate/gradle/wrapper/gradle-wrapper.properties index b82aa23a4f05..cea7a793a84b 100644 --- a/src/main/resources/templates/java/test/gradle/projectTemplate/gradle/wrapper/gradle-wrapper.properties +++ b/src/main/resources/templates/java/test/gradle/projectTemplate/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/resources/templates/java/test/maven/projectTemplate/pom.xml b/src/main/resources/templates/java/test/maven/projectTemplate/pom.xml index 24f9561bc6e9..68cd90459e5c 100644 --- a/src/main/resources/templates/java/test/maven/projectTemplate/pom.xml +++ b/src/main/resources/templates/java/test/maven/projectTemplate/pom.xml @@ -117,7 +117,7 @@ com.puppycrawl.tools checkstyle - 10.15.0 + 10.17.0 diff --git a/src/main/resources/templates/kotlin/test/maven/projectTemplate/pom.xml b/src/main/resources/templates/kotlin/test/maven/projectTemplate/pom.xml index c1c04e44339e..cc16a769f3d4 100644 --- a/src/main/resources/templates/kotlin/test/maven/projectTemplate/pom.xml +++ b/src/main/resources/templates/kotlin/test/maven/projectTemplate/pom.xml @@ -15,7 +15,7 @@ UTF-8 -Dfile.encoding=UTF-8 1.9.23 - 33.1.2 + 34.2.1 From 1946a8965b075d14b0c1a68ae43668cfb86faef5 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Tue, 21 Jan 2025 21:32:49 +0100 Subject: [PATCH 09/14] Assessment: Fix an issue where instructors accidentally override presentation scores (#10143) --- .../cit/aet/artemis/core/domain/Course.java | 8 ------ .../aet/artemis/core/web/CourseResource.java | 25 ++++--------------- .../course/manage/course-update.component.ts | 11 +++++--- 3 files changed, 12 insertions(+), 32 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java index 8000a24c0b55..527b187c0a71 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/Course.java @@ -591,14 +591,6 @@ public boolean getComplaintsEnabled() { return this.maxComplaintTimeDays > 0; } - public Set getPosts() { - return posts; - } - - public void setPosts(Set posts) { - this.posts = posts; - } - public boolean getRequestMoreFeedbackEnabled() { return maxRequestMoreFeedbackTimeDays > 0; } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java index 495cbff77d7b..cc2589f5321c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java @@ -64,7 +64,6 @@ import de.tum.cit.aet.artemis.assessment.service.AssessmentDashboardService; import de.tum.cit.aet.artemis.assessment.service.ComplaintService; import de.tum.cit.aet.artemis.assessment.service.CourseScoreCalculationService; -import de.tum.cit.aet.artemis.assessment.service.GradingScaleService; import de.tum.cit.aet.artemis.athena.service.AthenaModuleService; import de.tum.cit.aet.artemis.atlas.api.LearnerProfileApi; import de.tum.cit.aet.artemis.atlas.api.LearningPathApi; @@ -170,8 +169,6 @@ public class CourseResource { private final TutorialGroupsConfigurationService tutorialGroupsConfigurationService; - private final GradingScaleService gradingScaleService; - private final CourseScoreCalculationService courseScoreCalculationService; private final GradingScaleRepository gradingScaleRepository; @@ -197,10 +194,10 @@ public CourseResource(UserRepository userRepository, CourseService courseService Optional onlineCourseConfigurationService, AuthorizationCheckService authCheckService, TutorParticipationRepository tutorParticipationRepository, SubmissionService submissionService, Optional optionalVcsUserManagementService, AssessmentDashboardService assessmentDashboardService, ExerciseRepository exerciseRepository, Optional optionalCiUserManagementService, - FileService fileService, TutorialGroupsConfigurationService tutorialGroupsConfigurationService, GradingScaleService gradingScaleService, - CourseScoreCalculationService courseScoreCalculationService, GradingScaleRepository gradingScaleRepository, LearningPathApi learningPathApi, - ConductAgreementService conductAgreementService, Optional athenaModuleService, ExamRepository examRepository, ComplaintService complaintService, - TeamRepository teamRepository, LearnerProfileApi learnerProfileApi) { + FileService fileService, TutorialGroupsConfigurationService tutorialGroupsConfigurationService, CourseScoreCalculationService courseScoreCalculationService, + GradingScaleRepository gradingScaleRepository, LearningPathApi learningPathApi, ConductAgreementService conductAgreementService, + Optional athenaModuleService, ExamRepository examRepository, ComplaintService complaintService, TeamRepository teamRepository, + LearnerProfileApi learnerProfileApi) { this.courseService = courseService; this.courseRepository = courseRepository; this.exerciseService = exerciseService; @@ -215,7 +212,6 @@ public CourseResource(UserRepository userRepository, CourseService courseService this.exerciseRepository = exerciseRepository; this.fileService = fileService; this.tutorialGroupsConfigurationService = tutorialGroupsConfigurationService; - this.gradingScaleService = gradingScaleService; this.courseScoreCalculationService = courseScoreCalculationService; this.gradingScaleRepository = gradingScaleRepository; this.learningPathApi = learningPathApi; @@ -278,17 +274,6 @@ public ResponseEntity updateCourse(@PathVariable Long courseId, @Request } } - if (courseUpdate.getPresentationScore() != null && courseUpdate.getPresentationScore() != 0) { - Optional gradingScale = gradingScaleService.findGradingScaleByCourseId(courseUpdate.getId()); - if (gradingScale.isPresent() && gradingScale.get().getPresentationsNumber() != null) { - throw new BadRequestAlertException("You cannot set a presentation score if the grading scale is already set up for graded presentations", Course.ENTITY_NAME, - "gradedPresentationAlreadySet", true); - } - if (courseUpdate.getPresentationScore() < 0) { - throw new BadRequestAlertException("The presentation score cannot be negative", Course.ENTITY_NAME, "negativePresentationScore", true); - } - } - // Make sure to preserve associations in updated entity courseUpdate.setId(courseId); courseUpdate.setPrerequisites(existingCourse.getPrerequisites()); @@ -1353,7 +1338,7 @@ public ResponseEntity removeUserFromCourseGroup(String userLogin, User ins public ResponseEntity getCourseDTOForDetailView(@PathVariable Long courseId) { Course course = courseRepository.findByIdElseThrow(courseId); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.TEACHING_ASSISTANT, course, null); - GradingScale gradingScale = gradingScaleService.findGradingScaleByCourseId(courseId).orElse(null); + GradingScale gradingScale = gradingScaleRepository.findByCourseId(courseId).orElse(null); CourseManagementDetailViewDTO managementDetailViewDTO = courseService.getStatsForDetailView(course, gradingScale); return ResponseEntity.ok(managementDetailViewDTO); } diff --git a/src/main/webapp/app/course/manage/course-update.component.ts b/src/main/webapp/app/course/manage/course-update.component.ts index 5fcde16d3efb..224d6eceab34 100644 --- a/src/main/webapp/app/course/manage/course-update.component.ts +++ b/src/main/webapp/app/course/manage/course-update.component.ts @@ -326,15 +326,18 @@ export class CourseUpdateComponent implements OnInit { file = base64StringToBlob(base64Data, 'image/*'); } - const course = this.courseForm.getRawValue(); + const course = this.courseForm.getRawValue() as Course; + // NOTE: prevent overriding this value accidentally + // TODO: move presentationScore to gradingScale to avoid this + course.presentationScore = this.course.presentationScore; if (this.communicationEnabled && this.messagingEnabled) { - course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING; + course.courseInformationSharingConfiguration = CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING; } else if (this.communicationEnabled && !this.messagingEnabled) { - course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.COMMUNICATION_ONLY; + course.courseInformationSharingConfiguration = CourseInformationSharingConfiguration.COMMUNICATION_ONLY; } else { this.communicationEnabled = false; - course['courseInformationSharingConfiguration'] = CourseInformationSharingConfiguration.DISABLED; + course.courseInformationSharingConfiguration = CourseInformationSharingConfiguration.DISABLED; } if (!course.enrollmentEnabled) { From 07355ec52047999901543f4d41da7d78065f6e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20S=C3=B6lch?= Date: Tue, 21 Jan 2025 21:43:40 +0100 Subject: [PATCH 10/14] Athena: Improve AI feedback request validation (#10165) --- .../AthenaFeedbackSuggestionsService.java | 40 +++++++++ .../exercise/web/ParticipationResource.java | 5 +- .../ModelingExerciseFeedbackService.java | 86 ++++++------------- ...mingExerciseCodeReviewFeedbackService.java | 18 +--- .../service/TextExerciseFeedbackService.java | 42 ++++----- .../request-feedback-button.component.html | 2 +- src/main/webapp/i18n/de/exercise.json | 3 +- src/main/webapp/i18n/en/exercise.json | 1 + .../ParticipationIntegrationTest.java | 68 +++++++++++++++ 9 files changed, 163 insertions(+), 102 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/athena/service/AthenaFeedbackSuggestionsService.java b/src/main/java/de/tum/cit/aet/artemis/athena/service/AthenaFeedbackSuggestionsService.java index 210b3c7ba859..6a3248c3e86d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/athena/service/AthenaFeedbackSuggestionsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/athena/service/AthenaFeedbackSuggestionsService.java @@ -8,12 +8,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import com.fasterxml.jackson.annotation.JsonInclude; +import de.tum.cit.aet.artemis.assessment.domain.AssessmentType; +import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.athena.dto.ExerciseBaseDTO; import de.tum.cit.aet.artemis.athena.dto.ModelingFeedbackDTO; import de.tum.cit.aet.artemis.athena.dto.ProgrammingFeedbackDTO; @@ -23,6 +26,7 @@ import de.tum.cit.aet.artemis.core.domain.LLMRequest; import de.tum.cit.aet.artemis.core.domain.LLMServiceType; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.exception.ConflictException; import de.tum.cit.aet.artemis.core.exception.NetworkingException; import de.tum.cit.aet.artemis.core.service.LLMTokenUsageService; @@ -58,6 +62,9 @@ public class AthenaFeedbackSuggestionsService { private final LLMTokenUsageService llmTokenUsageService; + @Value("${artemis.athena.allowed-feedback-requests:10}") + private int allowedFeedbackRequests; + /** * Create a new AthenaFeedbackSuggestionsService to receive feedback suggestions from the Athena service. * @@ -185,4 +192,37 @@ private void storeTokenUsage(Exercise exercise, Submission submission, ResponseM llmTokenUsageService.saveLLMTokenUsage(llmRequests, LLMServiceType.ATHENA, (llmTokenUsageBuilder -> llmTokenUsageBuilder.withCourse(courseId).withExercise(exercise.getId()).withUser(userId))); } + + /** + * Checks if the number of Athena results for the given participation exceeds + * the allowed threshold and throws an exception if the limit is reached. + * + * @param participation the student participation to check + * @throws BadRequestAlertException if the maximum number of Athena feedback requests is exceeded + */ + public void checkRateLimitOrThrow(StudentParticipation participation) { + List athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); + + long countOfSuccessfulRequests = athenaResults.stream().filter(result -> result.isSuccessful() == Boolean.TRUE).count(); + + if (countOfSuccessfulRequests >= this.allowedFeedbackRequests) { + throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "maxAthenaResultsReached", true); + } + } + + /** + * Ensures that the submission does not already have an Athena-generated result. + * Throws an exception if Athena result already exists. + * + * @param submission the student's submission to validate + * @throws BadRequestAlertException if an Athena result is already present for the submission + */ + public void checkLatestSubmissionHasNoAthenaResultOrThrow(Submission submission) { + Result latestResult = submission.getLatestResult(); + + if (latestResult != null && latestResult.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA) { + log.debug("Submission ID: {} already has an Athena result. Skipping feedback generation.", submission.getId()); + throw new BadRequestAlertException("Submission already has an Athena result", "submission", "submissionAlreadyHasAthenaResult", true); + } + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java index 14ab3e8da46d..f30b526335cd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java @@ -401,8 +401,9 @@ private ResponseEntity handleExerciseFeedbackRequest(Exerc // Check submission requirements if (exercise instanceof TextExercise || exercise instanceof ModelingExercise) { - if (submissionRepository.findAllByParticipationId(participation.getId()).isEmpty()) { - throw new BadRequestAlertException("You need to submit at least once", "participation", "preconditions not met"); + boolean hasSubmittedOnce = submissionRepository.findAllByParticipationId(participation.getId()).stream().anyMatch(Submission::isSubmitted); + if (!hasSubmittedOnce) { + throw new BadRequestAlertException("You need to submit at least once", "participation", "noSubmissionExists", true); } } else if (exercise instanceof ProgrammingExercise) { diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingExerciseFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingExerciseFeedbackService.java index fc026f73988c..63bd92c81a44 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingExerciseFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/service/ModelingExerciseFeedbackService.java @@ -69,9 +69,24 @@ public ModelingExerciseFeedbackService(Optional this.generateAutomaticNonGradedFeedback(participation, modelingExercise)); + this.athenaFeedbackSuggestionsService.get().checkRateLimitOrThrow(participation); + + Optional submissionOptional = participationService.findExerciseParticipationWithLatestSubmissionAndResultElseThrow(participation.getId()) + .findLatestSubmission(); + + if (submissionOptional.isEmpty()) { + throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmissionExists", true); + } + + ModelingSubmission modelingSubmission = (ModelingSubmission) submissionOptional.get(); + + this.athenaFeedbackSuggestionsService.orElseThrow().checkLatestSubmissionHasNoAthenaResultOrThrow(modelingSubmission); + + if (modelingSubmission.isEmpty()) { + throw new BadRequestAlertException("Submission can not be empty for an AI feedback request", "submission", "noAthenaFeedbackOnEmptySubmission", true); + } + + CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(modelingSubmission, participation, modelingExercise)); } return participation; } @@ -80,29 +95,21 @@ public StudentParticipation handleNonGradedFeedbackRequest(StudentParticipation * Generates automatic non-graded feedback for a modeling exercise submission. * This method leverages the Athena service to generate feedback based on the latest submission. * - * @param participation the student participation associated with the exercise. - * @param modelingExercise the modeling exercise object. + * @param modelingSubmission the modeling submission associated with the student participation. + * @param participation the student participation associated with the exercise. + * @param modelingExercise the modeling exercise object. */ - public void generateAutomaticNonGradedFeedback(StudentParticipation participation, ModelingExercise modelingExercise) { + public void generateAutomaticNonGradedFeedback(ModelingSubmission modelingSubmission, StudentParticipation participation, ModelingExercise modelingExercise) { log.debug("Using athena to generate (modeling exercise) feedback request: {}", modelingExercise.getId()); - Optional submissionOptional = participationService.findExerciseParticipationWithLatestSubmissionAndResultElseThrow(participation.getId()) - .findLatestSubmission(); - - if (submissionOptional.isEmpty()) { - throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission"); - } - - Submission submission = submissionOptional.get(); - - Result automaticResult = createInitialResult(participation, submission); + Result automaticResult = createInitialResult(participation, modelingSubmission); try { this.resultWebsocketService.broadcastNewResult(participation, automaticResult); - log.debug("Submission id: {}", submission.getId()); + log.debug("Submission id: {}", modelingSubmission.getId()); - List feedbacks = getAthenaFeedback(modelingExercise, (ModelingSubmission) submission); + List feedbacks = getAthenaFeedback(modelingExercise, modelingSubmission); double totalFeedbackScore = calculateTotalFeedbackScore(feedbacks, modelingExercise); @@ -112,7 +119,7 @@ public void generateAutomaticNonGradedFeedback(StudentParticipation participatio automaticResult = this.resultRepository.save(automaticResult); resultService.storeFeedbackInResult(automaticResult, feedbacks, true); - submissionService.saveNewResult(submission, automaticResult); + submissionService.saveNewResult(modelingSubmission, automaticResult); this.resultWebsocketService.broadcastNewResult(participation, automaticResult); } catch (Exception e) { @@ -190,45 +197,4 @@ private double calculateTotalFeedbackScore(List feedbacks, ModelingExe return (totalCredits / maxPoints) * 100; } - - /** - * Checks if the number of Athena results for the given participation exceeds - * the allowed threshold and throws an exception if the limit is reached. - * - * @param participation the student participation to check - * @throws BadRequestAlertException if the maximum number of Athena feedback requests is exceeded - */ - private void checkRateLimitOrThrow(StudentParticipation participation) { - List athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); - - if (athenaResults.size() >= 10) { - throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "maxAthenaResultsReached", true); - } - } - - /** - * Ensures that the latest submission associated with the participation does not already - * have an Athena-generated result. Throws an exception if Athena result already exists. - * - * @param participation the student participation to validate - * @throws BadRequestAlertException if no legal submissions exist or if an Athena result is already present - */ - private void checkLatestSubmissionHasAthenaResultOrThrow(StudentParticipation participation) { - Optional submissionOptional = participationService.findExerciseParticipationWithLatestSubmissionAndResultElseThrow(participation.getId()) - .findLatestSubmission(); - - if (submissionOptional.isEmpty()) { - throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission"); - } - - Submission submission = submissionOptional.get(); - - Result latestResult = submission.getLatestResult(); - - if (latestResult != null && latestResult.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA) { - log.debug("Submission ID: {} already has an Athena result. Skipping feedback generation.", submission.getId()); - throw new BadRequestAlertException("Submission already has an Athena result", "submission", "submissionAlreadyHasAthenaResult", true); - } - } - } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java index 935a3412b10e..1f994918ffac 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java @@ -19,7 +19,6 @@ import de.tum.cit.aet.artemis.assessment.domain.AssessmentType; import de.tum.cit.aet.artemis.assessment.domain.Feedback; import de.tum.cit.aet.artemis.assessment.domain.FeedbackType; -import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.assessment.repository.ResultRepository; import de.tum.cit.aet.artemis.assessment.service.ResultService; import de.tum.cit.aet.artemis.athena.service.AthenaFeedbackSuggestionsService; @@ -91,7 +90,7 @@ public ProgrammingExerciseCodeReviewFeedbackService(GroupNotificationService gro public ProgrammingExerciseStudentParticipation handleNonGradedFeedbackRequest(Long exerciseId, ProgrammingExerciseStudentParticipation participation, ProgrammingExercise programmingExercise) { if (this.athenaFeedbackSuggestionsService.isPresent()) { - this.checkRateLimitOrThrow(participation); + this.athenaFeedbackSuggestionsService.get().checkRateLimitOrThrow(participation); CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(participation, programmingExercise)); return participation; } @@ -110,13 +109,13 @@ public ProgrammingExerciseStudentParticipation handleNonGradedFeedbackRequest(Lo * @param programmingExercise the programming exercise object. */ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentParticipation participation, ProgrammingExercise programmingExercise) { - log.debug("Using athena to generate feedback request: {}", programmingExercise.getId()); + log.debug("Using athena to generate (programming exercise) feedback request: {}", programmingExercise.getId()); // athena takes over the control here var submissionOptional = programmingExerciseParticipationService.findProgrammingExerciseParticipationWithLatestSubmissionAndResult(participation.getId()) .findLatestSubmission(); if (submissionOptional.isEmpty()) { - throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmissionExists"); + throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmissionExists", true); } var submission = submissionOptional.get(); @@ -222,15 +221,4 @@ private void unlockRepository(ProgrammingExerciseStudentParticipation participat this.programmingExerciseStudentParticipationRepository.save(participation); } } - - private void checkRateLimitOrThrow(ProgrammingExerciseStudentParticipation participation) { - - List athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); - - long countOfSuccessfulRequests = athenaResults.stream().filter(result -> result.isSuccessful() == Boolean.TRUE).count(); - - if (countOfSuccessfulRequests >= this.allowedFeedbackAttempts) { - throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "maxAthenaResultsReached", true); - } - } } diff --git a/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java index 0a874e45b8f8..eac0960ab698 100644 --- a/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java @@ -63,17 +63,6 @@ public TextExerciseFeedbackService(Optional at this.textBlockService = textBlockService; } - private void checkRateLimitOrThrow(StudentParticipation participation) { - - List athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); - - long countOfAthenaResults = athenaResults.size(); - - if (countOfAthenaResults >= 10) { - throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "maxAthenaResultsReached", true); - } - } - /** * Handles the request for generating feedback for a text exercise. * Unlike programming exercises a tutor is not notified if Athena is not available. @@ -84,8 +73,21 @@ private void checkRateLimitOrThrow(StudentParticipation participation) { */ public StudentParticipation handleNonGradedFeedbackRequest(StudentParticipation participation, TextExercise textExercise) { if (this.athenaFeedbackSuggestionsService.isPresent()) { - this.checkRateLimitOrThrow(participation); - CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(participation, textExercise)); + this.athenaFeedbackSuggestionsService.get().checkRateLimitOrThrow(participation); + + var submissionOptional = participationService.findExerciseParticipationWithLatestSubmissionAndResultElseThrow(participation.getId()).findLatestSubmission(); + if (submissionOptional.isEmpty()) { + throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmissionExists", true); + } + TextSubmission textSubmission = (TextSubmission) submissionOptional.get(); + + this.athenaFeedbackSuggestionsService.orElseThrow().checkLatestSubmissionHasNoAthenaResultOrThrow(textSubmission); + + if (textSubmission.isEmpty()) { + throw new BadRequestAlertException("Submission can not be empty for an AI feedback request", "submission", "noAthenaFeedbackOnEmptySubmission", true); + } + + CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(textSubmission, participation, textExercise)); } return participation; } @@ -94,20 +96,14 @@ public StudentParticipation handleNonGradedFeedbackRequest(StudentParticipation * Generates automatic non-graded feedback for a text exercise submission. * This method leverages the Athena service to generate feedback based on the latest submission. * - * @param participation the student participation associated with the exercise. - * @param textExercise the text exercise object. + * @param textSubmission the text submission associated with the student participation. + * @param participation the student participation associated with the exercise. + * @param textExercise the text exercise object. */ - public void generateAutomaticNonGradedFeedback(StudentParticipation participation, TextExercise textExercise) { + public void generateAutomaticNonGradedFeedback(TextSubmission textSubmission, StudentParticipation participation, TextExercise textExercise) { log.debug("Using athena to generate (text exercise) feedback request: {}", textExercise.getId()); // athena takes over the control here - var submissionOptional = participationService.findExerciseParticipationWithLatestSubmissionAndResultElseThrow(participation.getId()).findLatestSubmission(); - - if (submissionOptional.isEmpty()) { - throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission"); - } - TextSubmission textSubmission = (TextSubmission) submissionOptional.get(); - Result automaticResult = new Result(); automaticResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); automaticResult.setRated(true); diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html index 2dedcfbc6575..8becafe3e2a9 100644 --- a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html @@ -1,6 +1,6 @@ @if (!isExamExercise && requestFeedbackEnabled) { @if (athenaEnabled) { - @if (exercise().type === ExerciseType.TEXT) { + @if (exercise().type === ExerciseType.TEXT || exercise().type === ExerciseType.MODELING) {