diff --git a/.idea/runConfigurations/Artemis__Server__LocalVC___LocalCI_.xml b/.idea/runConfigurations/Artemis__Server__LocalVC___LocalCI_.xml index 9135ec4a2fd2..77a7a62f375d 100644 --- a/.idea/runConfigurations/Artemis__Server__LocalVC___LocalCI_.xml +++ b/.idea/runConfigurations/Artemis__Server__LocalVC___LocalCI_.xml @@ -1,13 +1,12 @@ - - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8447fced1b2b..f3d9bce02838 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,8 @@ -import com.diffplug.spotless.FormatterFunc -import com.diffplug.spotless.FormatterStep -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.gradle.api.tasks.testing.logging.TestLogEvent - -import static com.diffplug.spotless.Formatter.NO_FILE_SENTINEL +buildscript { + dependencies { + classpath "com.diffplug.spotless:spotless-plugin-gradle:6.25.0" + } +} plugins { id "checkstyle" @@ -33,8 +32,19 @@ java { targetCompatibility = JavaVersion.VERSION_21 } -//this enables us to invoke ./gradlew liquibaseDiffChangelog +wrapper { + gradleVersion = "8.12" +} + +node { + download = true + version = "${node_version}" + npmVersion = "${npm_version}" +} + apply from: "gradle/liquibase.gradle" +apply from: "gradle/test.gradle" +apply from: "gradle/spotless.gradle" if (project.hasProperty("prod")) { apply from: "gradle/profile_prod.gradle" @@ -46,65 +56,12 @@ if (project.hasProperty("war")) { apply from: "gradle/war.gradle" } -apply plugin: "jacoco" - idea { module { excludeDirs += files("node_modules") } } -spotless { - - // allows to execute the code formatting commands ./gradlew spotlessApply and ./gradlew spotlessCheck - java { - target { - fileTree(rootDir) { - include ( - "src/main/java/**/*.java", - "src/test/java/**/*.java", - ) - exclude( - "**/src/test/resources/test-data/repository-export/EncodingISO_8559_1.java", - "**/node_modules/**", - "**/out/**", - "**/repos/**", - "**/repos-download/**", - "**/build/**", - "**/src/main/generated/**", - "**/src/main/resources/templates/**", - "/docker/**", - "checked-out-repos/**", - "supporting_scripts/**" - ) - } - } - importOrderFile "artemis-spotless.importorder" - eclipse("4.33").configFile "artemis-spotless-style.xml" - - removeUnusedImports() - trimTrailingWhitespace() - - // Wildcard imports cannot be resolved by spotless itself. - // This will require the developer themselves to adhere to best practices. - addStep(FormatterStep.createNeverUpToDate("Refuse wildcard imports", new FormatterFunc() { - @Override - String apply(String s) throws Exception { - apply(s, NO_FILE_SENTINEL) - } - - @Override - String apply(String s, File file) throws Exception { - if (s =~ /\nimport .*\*;/) { - throw new IllegalArgumentException("Do not use wildcard imports. spotlessApply cannot resolve this issue.\n" + - "The following file violates this rule: " + file.getName()) - } - return s // Ensure a value is returned after processing - } - })) - } -} - defaultTasks "bootRun" springBoot { @@ -118,103 +75,55 @@ modernizer { exclusions = ["java/util/Optional.get:()Ljava/lang/Object;"] } -// Execute the test cases: ./gradlew test -// Execute only architecture tests: ./gradlew test -DincludeTags="ArchitectureTest" -test { - if (System.getProperty("includeTags")) { - useJUnitPlatform { - includeTags System.getProperty("includeTags") - } - } else { - useJUnitPlatform() - exclude "**/*IT*", "**/*IntTest*" - } - - testLogging { - events "FAILED", "SKIPPED" - } - testLogging.showStandardStreams = true - reports.html.required = false - minHeapSize = "2g" // initial heap size - maxHeapSize = "8g" // maximum heap size -} - -tasks.register("testReport", TestReport) { - destinationDirectory = layout.buildDirectory.file("reports/tests").get().asFile - testResults.from(test) -} - -jacoco { - toolVersion = "0.8.12" -} - -jar { - enabled = false -} - -private excludedClassFilesForReport(classDirectories) { - classDirectories.setFrom(files(classDirectories.files.collect { - fileTree(dir: it, - exclude: [ - "**/de/tum/cit/aet/artemis/**/domain/**/*_*", - "**/de/tum/cit/aet/artemis/core/config/migration/entries/**", - "**/gradle-wrapper.jar/**" - ] - ) - })) -} - -jacocoTestReport { - reports { - xml.required = true - } - // we want to ignore some generated files in the domain folders - afterEvaluate { - excludedClassFilesForReport(classDirectories) - } -} - -jacocoTestCoverageVerification { - violationRules { - rule { - limit { - counter = "INSTRUCTION" - value = "COVEREDRATIO" - // TODO: in the future the following value should become higher than 0.92 - minimum = 0.892 - } - limit { - counter = "CLASS" - value = "MISSEDCOUNT" - // TODO: in the future the following value should become less than 10 - maximum = 65 - } - } - } - // we want to ignore some generated files in the domain folders - afterEvaluate { - excludedClassFilesForReport(classDirectories) - } -} -check.dependsOn jacocoTestCoverageVerification - configurations { providedRuntime } repositories { - mavenLocal() mavenCentral() + mavenLocal() // required for org.gradle:gradle-tooling-api dependency maven { - url "https://repo.gradle.org/gradle/libs-releases" + url = "https://repo.gradle.org/gradle/libs-releases" } // required for org.opensaml:* dependencies maven { - url "https://build.shibboleth.net/maven/releases" + url = "https://build.shibboleth.net/maven/releases" } } +configurations.configureEach { +// exclude group: "org.dom4j", module: "dom4j" + exclude group: "org.xmlpull", module: "pull-parser" + exclude group: "jaxen", module: "jaxen" + exclude group: "xmlpull", module: "xpp3" + exclude group: "xsdlib", module: "xsdlib" + exclude group: "javax.xml.stream", module: "stax-api" +// exclude group: "javax.xml.bind", module: "jaxb-api" + + exclude group: "org.junit.vintage", module: "junit-vintage-engine" + exclude group: "com.vaadin.external.google", module: "android-json" + exclude group: "org.xmlunit", module: "xmlunit-core" + + exclude group: "org.testcontainers", module: "mariadb" + exclude group: "org.testcontainers", module: "mssqlserver" + + exclude group: "jakarta.ws.rs", module: "jsr311-api" + + exclude group: "org.springframework.boot", module: "spring-boot-starter-undertow" + + // these modules include security issues and are not needed + exclude group: "commons-jxpath", module: "commons-jxpath" + exclude group: "com.fasterxml.woodstox", module: "woodstox-core" + + // those are transitive dependencies of JPlag Text --> Stanford NLP + // Note: we exclude them because they are not needed and might have security vulnerabilities + exclude group: "org.apache.lucene", module: "lucene-queryparser" + exclude group: "org.apache.lucene", module: "lucene-core" + exclude group: "org.apache.lucene", module: "lucene-analyzers-common" + exclude group: "com.google.protobuf", module: "protobuf-java" +} + dependencies { // Note: jenkins-client is not well maintained and includes dependencies to libraries with critical security issues (e.g. CVE-2020-10683 for dom4j@1.6.1) @@ -253,13 +162,6 @@ dependencies { implementation "de.jplag:text:${jplag_version}" implementation "de.jplag:typescript:${jplag_version}" - // those are transitive dependencies of JPlag Text --> Stanford NLP - // Note: ideally we would exclude them, but for some reason this does not work - implementation "org.apache.lucene:lucene-queryparser:${lucene_version}" - implementation "org.apache.lucene:lucene-core:${lucene_version}" - implementation "org.apache.lucene:lucene-analyzers-common:${lucene_version}" - implementation "com.google.protobuf:protobuf-java:4.29.2" - // we have to override those values to use the latest version implementation "org.slf4j:jcl-over-slf4j:${slf4j_version}" implementation "org.slf4j:jul-to-slf4j:${slf4j_version}" @@ -271,15 +173,11 @@ dependencies { implementation "org.apache.logging.log4j:log4j-to-slf4j:2.24.3" - // Note: spring-security-lti13 does not work with jakarta yet, so we built our own custom version and declare its transitive dependencies below - // implementation "uk.ac.ox.ctl:spring-security-lti13:0.1.11" - implementation files("libs/spring-security-lti13-0.1.13.jar") + implementation "uk.ac.ox.ctl:spring-security-lti13:0.3.0" // https://search.maven.org/artifact/org.eclipse.jgit/org.eclipse.jgit implementation "org.eclipse.jgit:org.eclipse.jgit:${jgit_version}" implementation "org.eclipse.jgit:org.eclipse.jgit.ssh.apache:${jgit_version}" - // Note: jgit.htt.server is not compatible with jakarta yet and neither is there a timeline. Hence, we had to add the source files to our repository. - // Once the compatibility is given, we can switch back to the maven dependency. implementation "org.eclipse.jgit:org.eclipse.jgit.http.server:${jgit_version}" // apache ssh enabled the ssh git operations in LocalVC together with JGit @@ -308,13 +206,13 @@ dependencies { implementation "ch.qos.logback:logback-core:${logback_version}" // required by eureka client implementation "com.thoughtworks.xstream:xstream:1.4.21" - // required by JPlag + // required by JPlag, should NOT be used in other places implementation "xerces:xercesImpl:2.12.2" - // required by JPlag + // required by JPlag, should NOT be used in other places implementation "xalan:xalan:2.7.3" - // required by JPlag + // required by JPlag, should NOT be used in other places implementation "xalan:serializer:2.7.3" - // required by Saml2 + // required by Saml2, should NOT be used in other places implementation "org.apache.santuario:xmlsec:4.0.3" implementation "org.jsoup:jsoup:1.18.3" @@ -381,17 +279,9 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-aop:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-data-jpa:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-security:${spring_boot_version}" - implementation("org.springframework.boot:spring-boot-starter-web:${spring_boot_version}") { - exclude module: "spring-boot-starter-undertow" - } + implementation "org.springframework.boot:spring-boot-starter-web:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-tomcat:${spring_boot_version}" - // Avoid security issues in Tomcat 10.1.33 - implementation "org.apache.tomcat.embed:tomcat-embed-core:${tomcat_version}" - implementation "org.apache.tomcat.embed:tomcat-embed-el:${tomcat_version}" - implementation "org.apache.tomcat.embed:tomcat-embed-websocket:${tomcat_version}" - implementation "org.apache.tomcat:tomcat-annotations-api:${tomcat_version}" - implementation "org.springframework.boot:spring-boot-starter-websocket:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-thymeleaf:${spring_boot_version}" implementation "org.springframework.boot:spring-boot-starter-oauth2-resource-server:${spring_boot_version}" @@ -400,11 +290,7 @@ dependencies { implementation "org.springframework.ldap:spring-ldap-core:3.2.10" implementation "org.springframework.data:spring-data-ldap:3.4.1" - implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:${spring_cloud_version}") { - // NOTE: these modules contain security vulnerabilities and are not needed - exclude module: "commons-jxpath" - exclude module: "woodstox-core" - } + implementation "org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:${spring_cloud_version}" implementation "org.springframework.cloud:spring-cloud-starter-config:${spring_cloud_version}" implementation "org.springframework.cloud:spring-cloud-commons:${spring_cloud_version}" @@ -501,12 +387,8 @@ dependencies { } annotationProcessor "org.hibernate:hibernate-jpamodelgen:${hibernate_version}" - annotationProcessor("org.glassfish.jaxb:jaxb-runtime:${jaxb_runtime_version}") { - exclude group: "jakarta.ws.rs", module: "jsr311-api" - } - annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:${spring_boot_version}") { - exclude group: "com.vaadin.external.google", module: "android-json" - } + annotationProcessor "org.glassfish.jaxb:jaxb-runtime:${jaxb_runtime_version}" + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor:${spring_boot_version}" // ---- CHECKSTYLE DEPENDENCIES ---- @@ -519,11 +401,7 @@ dependencies { // ---- TEST DEPENDENCIES ---- - testImplementation("org.springframework.boot:spring-boot-starter-test:${spring_boot_version}") { - exclude group: "org.junit.vintage", module: "junit-vintage-engine" - exclude group: "com.vaadin.external.google", module: "android-json" - exclude group: "org.xmlunit", module: "xmlunit-core" - } + 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.0" @@ -536,14 +414,10 @@ dependencies { testImplementation "org.gradle:gradle-tooling-api:8.12" testImplementation "org.apache.maven.surefire:surefire-report-parser:3.5.2" testImplementation "com.opencsv:opencsv:5.9" - testImplementation("io.zonky.test:embedded-database-spring-test:2.6.0") { - exclude group: "org.testcontainers", module: "mariadb" - exclude group: "org.testcontainers", module: "mssqlserver" - } + testImplementation "io.zonky.test:embedded-database-spring-test:2.6.0" + testImplementation "com.tngtech.archunit:archunit:1.3.0" - testImplementation("org.skyscreamer:jsonassert:1.5.3") { - exclude module: "android-json" - } + testImplementation "org.skyscreamer:jsonassert:1.5.3" // NOTE: cannot update due to "Syntax error in SQL statement "WITH ids_to_delete" --> should be resolved when we collapse the changelogs again for Artemis 8.0 // testImplementation "com.h2database:h2:2.3.232" @@ -570,64 +444,10 @@ tasks.withType(JavaCompile).configureEach { options.compilerArgs << "-Xlint:deprecation" } -// Taken from here: https://stackoverflow.com/questions/3963708/gradle-how-to-display-test-results-in-the-console-in-real-time -tasks.withType(Test).configureEach { - // a collection to track failedTests - ext.failedTests = [] - - testLogging { - // set options for log level LIFECYCLE - events TestLogEvent.FAILED, - TestLogEvent.PASSED, - TestLogEvent.SKIPPED - exceptionFormat TestExceptionFormat.FULL - showExceptions true - showCauses true - showStackTraces true - - info.events = debug.events - info.exceptionFormat = debug.exceptionFormat - } - - afterTest { descriptor, result -> - if (result.resultType == TestResult.ResultType.FAILURE) { - var failedTest = "${descriptor.className}::${descriptor.name}" - logger.debug("Adding " + failedTest + " to failedTests...") - failedTests << [failedTest] - } - } - - afterSuite { suite, result -> - if (!suite.parent) { // will match the outermost suite - def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)" - def startItem = "| ", endItem = " |" - def repeatLength = startItem.length() + output.length() + endItem.length() - println("\n" + ("-" * repeatLength) + "\n" + startItem + output + endItem + "\n" + ("-" * repeatLength)) - // logs each failed test - if (!failedTests.empty) { - logger.lifecycle("Failed tests:") - failedTests.each { failedTest -> - println("${failedTest}") - } - } - } - } -} - -wrapper { - gradleVersion = "8.12" -} - tasks.register("stage") { dependsOn "bootWar" } -node { - download = true - version = "${node_version}" - npmVersion = "${npm_version}" -} - // Set the npm cache (used in the Dockerfile) tasks.register("npmSetCacheDockerfile", NpmTask) { args = ["set", "cache", "/opt/artemis/.npm"] @@ -635,8 +455,8 @@ tasks.register("npmSetCacheDockerfile", NpmTask) { // Command to execute the JavaDoc checkstyle verification ./gradlew checkstyleMain checkstyle { - toolVersion "${checkstyle_version}" - configFile file("checkstyle.xml") + toolVersion = "${checkstyle_version}" + configFile = file("checkstyle.xml") checkstyleTest.enabled = false maxErrors = 0 } diff --git a/docker/artemis-migration-check-mysql.yml b/docker/artemis-migration-check-mysql.yml index 7438524fa2e2..7947a108d6db 100644 --- a/docker/artemis-migration-check-mysql.yml +++ b/docker/artemis-migration-check-mysql.yml @@ -17,7 +17,7 @@ services: file: ./mysql.yml service: mysql migration-check: - image: alpine + image: docker.io/library/alpine:3.21.0 container_name: migration-check command: /bin/sh -c "exit 0" depends_on: diff --git a/docker/artemis-migration-check-postgres.yml b/docker/artemis-migration-check-postgres.yml index 1ac60ab3ef45..b3afd781a0dd 100644 --- a/docker/artemis-migration-check-postgres.yml +++ b/docker/artemis-migration-check-postgres.yml @@ -20,7 +20,7 @@ services: file: ./postgres.yml service: postgres migration-check: - image: alpine + image: docker.io/library/alpine:3.21.0 container_name: migration-check command: /bin/sh -c "exit 0" depends_on: diff --git a/docker/atlassian/atlassian-setup.sh b/docker/atlassian/atlassian-setup.sh deleted file mode 100755 index 5a5c17c42ded..000000000000 --- a/docker/atlassian/atlassian-setup.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/bash -# for configuring instance over script -jira_external_port=8081 - -set -e -echo In order to work, you need to provide the Username and Password of an admin account for Jira -echo Jira Admin Account -read -p 'Username: ' jira_uservar -read -sp 'Password: ' jira_passvar - -# create groups - -declare -a group_names=("tutors" "instructors" "students" "editors") - -jira_group_url="http://localhost:$jira_external_port/rest/api/latest/group" - -for group_name in "${group_names[@]}"; do - curl -u "$jira_uservar":"$jira_passvar" \ - -s \ - --header "Content-Type: application/json" \ - --request POST \ - --fail \ - --show-error \ - --data "{ - \"name\": \"$group_name\" - }" \ - $jira_group_url -done - -# create users - -jira_user_url="http://localhost:$jira_external_port/rest/api/latest/user" -jira_group_add_url="http://localhost:$jira_external_port/rest/api/2/group/user?groupname=" - - -create_user_and_add_to_group() { - # User 1-5 are students, 6-10 are tutors, 11-15 are editors and 16-20 are instructors - # For the cypress tests students: 100,102,104,(105),106; tutors: 101; instructors: 103 - group="students" - if ((i > 15)) && ((i < 100 || i == 103)); then - group="instructors" - elif ((i > 10)) && ((i < 100)); then - group="editors" - elif ((i > 5)) && ((i < 100 || i == 101)); then - group="tutors" - fi - - # Create user - curl -u "$jira_uservar":"$jira_passvar" \ - -s \ - --header "Content-Type: application/json" \ - --request POST \ - --fail \ - --show-error \ - --data "{ - \"password\": \"artemis_test_user_$i\", - \"emailAddress\": \"artemis_test_user_$i@artemis.local\", - \"displayName\": \"Artemis Test User $i\", - \"name\": \"artemis_test_user_$i\" - - }" \ - $jira_user_url - - # Add user to group - curl -u "$jira_uservar":"$jira_passvar" \ - -s \ - --header "Content-Type: application/json" \ - --request POST \ - --fail \ - --show-error \ - --data "{ - \"name\": \"artemis_test_user_$i\" - }" \ - "$jira_group_add_url$group" -} - -for i in {1..20}; do - create_user_and_add_to_group "$i" -done -for i in {100..106}; do - create_user_and_add_to_group "$i" -done diff --git a/docker/broker-registry.yml b/docker/broker-registry.yml index c984494be465..b26f0ef1b340 100644 --- a/docker/broker-registry.yml +++ b/docker/broker-registry.yml @@ -2,7 +2,7 @@ services: jhipster-registry: container_name: artemis-jhipster-registry image: docker.io/jhipster/jhipster-registry:v7.4.0 - pull_policy: if_not_present + pull_policy: missing volumes: - ./registry:/central-config # When run with the "dev" Spring profile, the JHipster Registry will @@ -28,8 +28,8 @@ services: activemq-broker: container_name: artemis-activemq-broker - image: docker.io/apache/activemq-artemis:2.33.0-alpine - pull_policy: if_not_present + image: docker.io/apache/activemq-artemis:2.39.0-alpine + pull_policy: missing environment: ARTEMIS_USER: ${BROKER_USER:-guest} ARTEMIS_PASSWORD: ${BROKER_PASSWORD:-guest} diff --git a/docker/gitlab-gitlabci.yml b/docker/gitlab-gitlabci.yml index 7560d188f971..3283384ff4a8 100644 --- a/docker/gitlab-gitlabci.yml +++ b/docker/gitlab-gitlabci.yml @@ -13,7 +13,7 @@ services: nginx['listen_port'] = 80 gitlab-runner: image: docker.io/gitlab/gitlab-runner:latest - pull_policy: if_not_present + pull_policy: missing container_name: artemis-gitlab-runner volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/docker/mailhog.yml b/docker/mailhog.yml index fb93d5784b4d..2b95dd0e7497 100644 --- a/docker/mailhog.yml +++ b/docker/mailhog.yml @@ -7,8 +7,8 @@ services: mailhog: container_name: artemis-mailhog - image: docker.io/mailhog/mailhog - pull_policy: if_not_present + image: docker.io/mailhog/mailhog:v1.0.1 + pull_policy: missing ports: - "1025:1025" - "8025:8025" diff --git a/docker/monitoring.yml b/docker/monitoring.yml index 75e763abfc16..5e411733f6b5 100644 --- a/docker/monitoring.yml +++ b/docker/monitoring.yml @@ -9,8 +9,9 @@ services: prometheus: container_name: artemis-prometheus - image: docker.io/prom/prometheus:v2.52.0 - pull_policy: if_not_present + # TODO: check if we can upgrade to 3.x + image: docker.io/prom/prometheus:v2.55.1 + pull_policy: missing volumes: - ./monitoring/prometheus/:/etc/prometheus/ # If you want to expose these ports outside your dev PC, @@ -25,8 +26,9 @@ services: network_mode: 'host' # to test locally running service grafana: container_name: artemis-grafana - image: docker.io/grafana/grafana:10.1.10 - pull_policy: if_not_present + # TODO: check if we can upgrade to 11.x + image: docker.io/grafana/grafana:10.4.14 + pull_policy: missing volumes: - ./monitoring/grafana/provisioning/:/etc/grafana/provisioning/ environment: diff --git a/docker/moodle/moodle.yml b/docker/moodle/moodle.yml index 4fc80e796115..95e58363a9f2 100644 --- a/docker/moodle/moodle.yml +++ b/docker/moodle/moodle.yml @@ -1,15 +1,7 @@ -# Copyright VMware, Inc. -# SPDX-License-Identifier: APACHE-2.0 - -# Disclaimer: The service marks and trademarks mentioned herein belong to their respective owners. -# No commercial licensing for these products is provided through this file. -# This docker-compose configuration is derived from Bitnami's publicly shared files, accessible at https://github.com/bitnami/containers/tree/main/bitnami/moodle, and is distributed under an open-source license. -# The Moodleâ„¢ LMS included in this docker-compose is developed and overseen by Moodle HQ, an independent project from this adaptation and Bitnami. - version: '2' services: mariadb: - image: docker.io/bitnami/mariadb:10.6 + image: docker.io/bitnami/mariadb:11.6.2 environment: # ALLOW_EMPTY_PASSWORD is recommended only for development. - ALLOW_EMPTY_PASSWORD=yes @@ -23,7 +15,7 @@ services: extra_hosts: - "host.docker.internal:host-gateway" moodle: - image: docker.io/bitnami/moodle:4.2 + image: docker.io/bitnami/moodle:4.5.1 ports: - '8085:8080' - '443:8443' diff --git a/docker/mysql.yml b/docker/mysql.yml index ea476dd16287..4af09f97071f 100644 --- a/docker/mysql.yml +++ b/docker/mysql.yml @@ -6,7 +6,7 @@ services: mysql: container_name: artemis-mysql image: docker.io/library/mysql:9.1.0 - pull_policy: if_not_present + pull_policy: missing volumes: - artemis-mysql-data:/var/lib/mysql # DO NOT use this default file for production systems! diff --git a/docker/nginx.yml b/docker/nginx.yml index 4a5988717061..c83d515a50cc 100644 --- a/docker/nginx.yml +++ b/docker/nginx.yml @@ -7,7 +7,7 @@ services: # nginx setup based on artemis prod ansible repository container_name: artemis-nginx image: docker.io/library/nginx:1.27.3 - pull_policy: if_not_present + pull_policy: missing volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/timeouts.conf:/etc/nginx/conf.d/timeouts.conf:ro diff --git a/docker/playwright.yml b/docker/playwright.yml index 82386b6e7b82..2e2a03d8d8e9 100644 --- a/docker/playwright.yml +++ b/docker/playwright.yml @@ -4,8 +4,8 @@ services: artemis-playwright: - image: mcr.microsoft.com/playwright:v1.44.0-jammy - pull_policy: if_not_present + image: mcr.microsoft.com/playwright:v1.49.1 + pull_policy: missing environment: CI: 'true' BASE_URL: 'https://artemis-nginx' diff --git a/docker/postgres.yml b/docker/postgres.yml index 27bbf433e40b..7a6625ee8faf 100644 --- a/docker/postgres.yml +++ b/docker/postgres.yml @@ -6,7 +6,7 @@ services: postgres: container_name: artemis-postgres image: docker.io/library/postgres:17.2-alpine - pull_policy: if_not_present + pull_policy: missing user: postgres command: ["postgres", "-c", "max_connections=10000"] volumes: diff --git a/docker/saml-test.yml b/docker/saml-test.yml index a71d69dee02e..b2126eaf1db1 100644 --- a/docker/saml-test.yml +++ b/docker/saml-test.yml @@ -12,8 +12,8 @@ services: saml-test: container_name: artemis-saml-test - image: docker.io/jamedjo/test-saml-idp - pull_policy: if_not_present + image: docker.io/jamedjo/test-saml-idp:1.18 + pull_policy: missing ports: - "9980:8080" # expose the port to make it reachable docker internally even if the external port mapping changes diff --git a/docs/admin/setup/kubernetes.rst b/docs/admin/setup/kubernetes.rst index 5ac81b798c7c..def90cdff6b0 100644 --- a/docs/admin/setup/kubernetes.rst +++ b/docs/admin/setup/kubernetes.rst @@ -15,7 +15,7 @@ This section describes how to set up an environment deployed in Kubernetes. Follow the links to install the tools which will be needed to proceed with the Kubernetes cluster setup. The setup has been tested with the described versions; newer ones might work, too. -* `Docker `__ - v20.10.7 +* `Docker `__ - v27.4.1 Docker is a platform for developing, shipping and running applications. In our case, we will use it to build the images which we will deploy. It is also needed from k3d to create a cluster. The cluster nodes are deployed on Docker containers. @@ -24,17 +24,17 @@ The setup has been tested with the described versions; newer ones might work, to Docker Hub is a service provided by Docker for finding and sharing container images. Account in DockerHub is needed to push the Artemis image which will be used by the Kubernetes deployment. -* `k3d `__ - v4.4.7 +* `k3d `__ - v5.7.5 k3d is a lightweight wrapper to run k3s which is a lightweight Kubernetes distribution in Docker. k3d makes it very easy to create k3s clusters especially for local deployment on Kubernetes. Windows users can use ``choco`` to install it. More details can be found in the link under ``Other Installation Methods`` -* `kubectl `__ - v1.21 +* `kubectl `__ - v1.32 kubectl is the Kubernetes command-line tool, which allows you to run commands against Kubernetes clusters. It can be used to deploy applications, inspect and manage cluster resources, and view logs. -* `helm `__ - v3.6.3 +* `helm `__ - v3.16.4 Helm is the package manager for Kubernetes. We will use it to install cert-manager and Rancher diff --git a/eslint.config.js b/eslint.config.js index 622299e984e1..6029bf30bcd8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,6 +5,7 @@ const jestPlugin = require('eslint-plugin-jest'); const jestExtendedPlugin = require('eslint-plugin-jest-extended'); const typescriptParser = require('@typescript-eslint/parser'); const angularTemplateParser = require('@angular-eslint/template-parser'); +const customRulesPlugin = require('./rules/custom-rules'); module.exports = [ { @@ -43,11 +44,13 @@ module.exports = [ '@typescript-eslint': tsPlugin, '@angular-eslint': angularPlugin, prettier: prettierPlugin, + 'custom-rules': customRulesPlugin, }, rules: { ...prettierPlugin.configs.recommended.rules, ...tsPlugin.configs.recommended.rules, ...angularPlugin.configs.recommended.rules, + 'custom-rules/enforce-no-http-client-testing-module': 'error', '@angular-eslint/directive-selector': [ 'warn', { diff --git a/gradle.properties b/gradle.properties index 28a4b2335dd7..0277f98d2a3f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ node_version=22.12.0 npm_version=10.9.0 # Dependency versions -jhipster_dependencies_version=8.7.2 +jhipster_dependencies_version=8.8.0 spring_boot_version=3.4.1 spring_framework_version=6.2.1 spring_cloud_version=4.2.0 @@ -35,7 +35,6 @@ logback_version=1.5.15 java_parser_version=3.26.2 byte_buddy_version=1.15.11 netty_version=4.1.115.Final -tomcat_version=10.1.34 # testing # make sure both versions are compatible diff --git a/gradle/liquibase.gradle b/gradle/liquibase.gradle index 56132d15bd6b..8f698a891937 100644 --- a/gradle/liquibase.gradle +++ b/gradle/liquibase.gradle @@ -34,7 +34,7 @@ tasks.register('initPaths', { }) def liquibaseCommand(command) { - javaexec { + tasks.register("runLiquibaseCommand", JavaExec) { if (isWindows) { classpath tasks.named('pathingLiquibaseJar').get().outputs.files } else { @@ -53,8 +53,8 @@ def liquibaseCommand(command) { "--url=jdbc:mysql://localhost:3306/Artemis700?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=UTC", "--driver=com.mysql.cj.jdbc.Driver", command - ] - } + ] + }.get().exec() } tasks.register('liquibaseDiffChangeLog', { diff --git a/gradle/spotless.gradle b/gradle/spotless.gradle new file mode 100644 index 000000000000..136a30f1c0b0 --- /dev/null +++ b/gradle/spotless.gradle @@ -0,0 +1,93 @@ +spotless { + // allows to execute the code formatting commands ./gradlew spotlessApply and ./gradlew spotlessCheck + java { + target { + fileTree(rootDir) { + include ( + "src/main/java/**/*.java", + "src/test/java/**/*.java", + ) + exclude( + "**/src/test/resources/test-data/repository-export/EncodingISO_8559_1.java", + "**/node_modules/**", + "**/out/**", + "**/repos/**", + "**/repos-download/**", + "**/build/**", + "**/src/main/generated/**", + "**/src/main/resources/templates/**", + "/docker/**", + "checked-out-repos/**", + "supporting_scripts/**" + ) + } + } + importOrderFile "artemis-spotless.importorder" + eclipse("4.34").configFile "artemis-spotless-style.xml" + + removeUnusedImports() + + custom 'Remove commented-out import statements', { + it.replaceAll(/\n\/\/ import .*?;.*/, '') + } + + custom 'Refuse wildcard imports', { code -> + // Wildcard imports can't be resolved by spotless itself. + // This will require the developer themselves to adhere to best practices. + if (code =~ /\nimport .*\*;/) { + throw new IllegalArgumentException("Do not use wildcard imports. 'spotlessApply' cannot resolve this issue.") + } + return code // make sure to avoid a warning and always + } + + custom 'Remove unhelpful javadoc stubs', { + // e.g., remove the following lines: + // "* @param paramName" + // "* @throws ExceptionType" + // "* @return returnType"' + // Multiline to allow anchors on newlines + it.replaceAll(/(?m)^ *\* *@(?:param|throws|return) *\w* *\n/, '') + } + + custom 'Remove any empty Javadocs and block comments', { + // Matches any /** [...] */ or /* [...] */ that contains: + // (a) only whitespace + // (b) trivial information, such as "@param paramName" or @throws ExceptionType + // without any additional information. This information is implicit in the signature. + it.replaceAll(/\/\*+\s*\n(\s*\*\s*\n)*\s*\*+\/\s*\n/, '') + } + + // Enforce style modifier order + custom 'Modifier ordering', { + def modifierRanking = [ + "public" : 1, + "protected" : 2, + "private" : 3, + "abstract" : 4, + "default" : 5, + "static" : 6, + "final" : 7, + "transient" : 8, + "volatile" : 9, + "synchronized": 10, + "native" : 11, + "strictfp" : 12] + // Find any instance of multiple modifiers. Lead with a non-word character to avoid + // accidental matching against for instance, "an alternative default value" + it.replaceAll(/\W(?:public |protected |private |abstract |default |static |final |transient |volatile |synchronized |native |strictfp ){2,}/, { + // Do not replace the leading non-word character. Identify the modifiers + it.replaceAll(/(?:public |protected |private |abstract |default |static |final |transient |volatile |synchronized |native |strictfp ){2,}/, { + // Sort the modifiers according to the ranking above + it.split().sort({ modifierRanking[it] }).join(' ') + ' ' + } + ) + } + ) + } + + trimTrailingWhitespace() + endWithNewline() + } +} + + diff --git a/gradle/test.gradle b/gradle/test.gradle new file mode 100644 index 000000000000..d09c3a9c61c4 --- /dev/null +++ b/gradle/test.gradle @@ -0,0 +1,125 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent + +// Taken from here: https://stackoverflow.com/questions/3963708/gradle-how-to-display-test-results-in-the-console-in-real-time +tasks.withType(Test).configureEach { + // a collection to track failedTests + ext.failedTests = [] + + testLogging { + // set options for log level LIFECYCLE + events TestLogEvent.FAILED, + TestLogEvent.PASSED, + TestLogEvent.SKIPPED + exceptionFormat = TestExceptionFormat.FULL + showExceptions = true + showCauses = true + showStackTraces = true + + info.events = debug.events + info.exceptionFormat = debug.exceptionFormat + } + + afterTest { descriptor, result -> + if (result.resultType == TestResult.ResultType.FAILURE) { + var failedTest = "${descriptor.className}::${descriptor.name}" + logger.debug("Adding " + failedTest + " to failedTests...") + failedTests << [failedTest] + } + } + + afterSuite { suite, result -> + if (!suite.parent) { // will match the outermost suite + def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)" + def startItem = "| ", endItem = " |" + def repeatLength = startItem.length() + output.length() + endItem.length() + println("\n" + ("-" * repeatLength) + "\n" + startItem + output + endItem + "\n" + ("-" * repeatLength)) + // logs each failed test + if (!failedTests.empty) { + logger.lifecycle("Failed tests:") + failedTests.each { failedTest -> + println("${failedTest}") + } + } + } + } +} + + +// Execute the test cases: ./gradlew test +// Execute only architecture tests: ./gradlew test -DincludeTags="ArchitectureTest" +test { + if (System.getProperty("includeTags")) { + useJUnitPlatform { + includeTags System.getProperty("includeTags") + } + } else { + useJUnitPlatform() + exclude "**/*IT*", "**/*IntTest*" + } + + testLogging { + events "FAILED", "SKIPPED" + } + testLogging.showStandardStreams = true + reports.html.required = false + minHeapSize = "2g" // initial heap size + maxHeapSize = "6g" // maximum heap size +} + +tasks.register("testReport", TestReport) { + destinationDirectory = layout.buildDirectory.file("reports/tests").get().asFile + testResults.from(test) +} + +apply plugin: "jacoco" + +jacoco { + toolVersion = "0.8.12" +} + +private excludedClassFilesForReport(classDirectories) { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, + exclude: [ + "**/de/tum/cit/aet/artemis/**/domain/**/*_*", + "**/de/tum/cit/aet/artemis/core/config/migration/entries/**", + "**/gradle-wrapper.jar/**" + ] + ) + })) +} + +jacocoTestReport { + reports { + xml.required = true + } + // we want to ignore some generated files in the domain folders + afterEvaluate { + excludedClassFilesForReport(classDirectories) + } +} + +jacocoTestCoverageVerification { + violationRules { + rule { + limit { + counter = "INSTRUCTION" + value = "COVEREDRATIO" + // TODO: in the future the following value should become higher than 0.92 + minimum = 0.895 + } + limit { + counter = "CLASS" + value = "MISSEDCOUNT" + // TODO: in the future the following value should become less than 10 + maximum = 56 + } + } + } + // we want to ignore some generated files in the domain folders + afterEvaluate { + excludedClassFilesForReport(classDirectories) + } +} +check.dependsOn jacocoTestCoverageVerification diff --git a/gradle/war.gradle b/gradle/war.gradle index fe9e9cd85aa4..7daba4b40242 100644 --- a/gradle/war.gradle +++ b/gradle/war.gradle @@ -19,3 +19,7 @@ war { enabled = true archiveExtension = "war.original" } + +jar { + enabled = false +} diff --git a/jest.config.js b/jest.config.js index 6c0c7102c751..a5ce2254e1a5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -105,10 +105,10 @@ module.exports = { coverageThreshold: { global: { // TODO: in the future, the following values should increase to at least 90% - statements: 87.73, + statements: 87.71, branches: 73.85, - functions: 82.31, - lines: 87.78, + functions: 82.28, + lines: 87.77, }, }, coverageReporters: ['clover', 'json', 'lcov', 'text-summary'], diff --git a/libs/spring-security-lti13-0.1.13.jar b/libs/spring-security-lti13-0.1.13.jar deleted file mode 100644 index fc86bfd877d7..000000000000 Binary files a/libs/spring-security-lti13-0.1.13.jar and /dev/null differ diff --git a/package-lock.json b/package-lock.json index cee87b1beaf0..df82b26a8c98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,6 +80,7 @@ "zone.js": "0.14.10" }, "devDependencies": { + "@analogjs/vite-plugin-angular": "1.10.3", "@angular-builders/jest": "18.0.0", "@angular-devkit/build-angular": "18.2.12", "@angular-eslint/builder": "18.4.1", @@ -91,6 +92,7 @@ "@angular/compiler-cli": "18.2.13", "@angular/language-service": "18.2.13", "@sentry/types": "8.47.0", + "@testing-library/angular": "17.3.5", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", @@ -129,6 +131,8 @@ "sass": "1.83.0", "ts-jest": "29.2.5", "typescript": "5.5.4", + "vite-tsconfig-paths": "5.1.4", + "vitest": "2.1.8", "weak-napi": "2.0.2" }, "engines": { @@ -148,6 +152,80 @@ "node": ">=6.0.0" } }, + "node_modules/@analogjs/vite-plugin-angular": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@analogjs/vite-plugin-angular/-/vite-plugin-angular-1.10.3.tgz", + "integrity": "sha512-3EWappJ5K6YopJpq2QRVim8qZgaTQJD0RB4G/DXo+Fg0s27BjDORiaeixqLHIwhUzzZ5FR2d1S7dgIi9zOg4sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ts-morph": "^21.0.0", + "vfile": "^6.0.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/brandonroberts" + }, + "peerDependencies": { + "@angular-devkit/build-angular": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "@angular/build": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@angular-devkit/build-angular": { + "optional": true + }, + "@angular/build": { + "optional": true + } + } + }, + "node_modules/@analogjs/vite-plugin-angular/node_modules/@ts-morph/common": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.22.0.tgz", + "integrity": "sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "minimatch": "^9.0.3", + "mkdirp": "^3.0.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@analogjs/vite-plugin-angular/node_modules/code-block-writer": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@analogjs/vite-plugin-angular/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@analogjs/vite-plugin-angular/node_modules/ts-morph": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-21.0.1.tgz", + "integrity": "sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.22.0", + "code-block-writer": "^12.0.0" + } + }, "node_modules/@angular-builders/common": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@angular-builders/common/-/common-2.0.0.tgz", @@ -6742,6 +6820,93 @@ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", "license": "ISC" }, + "node_modules/@testing-library/angular": { + "version": "17.3.5", + "resolved": "https://registry.npmjs.org/@testing-library/angular/-/angular-17.3.5.tgz", + "integrity": "sha512-B79mN7kNDY9/+0Kc6cVGnVptQ03oHVL1at1tMu44wvr7Ksk10g11lZw7BA0c7rHAJQoXgiWrXmvtJxkPA1R2uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.3.1" + }, + "peerDependencies": { + "@angular/common": ">= 17.0.0", + "@angular/core": ">= 17.0.0", + "@angular/platform-browser": ">= 17.0.0", + "@angular/router": ">= 17.0.0", + "@testing-library/dom": "^10.0.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@ts-morph/common": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.23.0.tgz", @@ -6833,6 +6998,14 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -7321,6 +7494,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", @@ -7584,6 +7764,139 @@ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" } }, + "node_modules/@vitest/expect": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", + "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", + "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", + "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.8", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", + "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.8", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", + "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", + "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.8", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vscode/codicons": { "version": "0.0.36", "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.36.tgz", @@ -8176,6 +8489,16 @@ "node": ">=8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -8780,6 +9103,16 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cacache": { "version": "18.0.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", @@ -8901,6 +9234,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8935,6 +9285,16 @@ "dev": true, "license": "MIT" }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -10026,6 +10386,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -10137,6 +10507,17 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -10237,6 +10618,14 @@ "node": ">=6" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -11322,6 +11711,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -11425,6 +11824,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", @@ -12232,6 +12641,13 @@ "node": ">= 4" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, "node_modules/gopd": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.1.0.tgz", @@ -15095,6 +15511,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -15104,6 +15527,17 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.11", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", @@ -16989,6 +17423,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pdf-lib": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", @@ -18815,6 +19266,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -19200,6 +19658,13 @@ "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -19210,6 +19675,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -19800,12 +20272,26 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", + "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/tinygradient": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-1.1.5.tgz", @@ -19816,6 +20302,36 @@ "tinycolor2": "^1.0.0" } }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tldts": { "version": "6.1.65", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.65.tgz", @@ -20077,6 +20593,27 @@ } } }, + "node_modules/tsconfck": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.4.tgz", + "integrity": "sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -20369,6 +20906,20 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -20527,6 +21078,36 @@ "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "5.4.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", @@ -20587,6 +21168,49 @@ } } }, + "node_modules/vite-node": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", + "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -21017,6 +21641,82 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/vitest": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", + "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.8", + "@vitest/mocker": "2.1.8", + "@vitest/pretty-format": "^2.1.8", + "@vitest/runner": "2.1.8", + "@vitest/snapshot": "2.1.8", + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.8", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.8", + "@vitest/ui": "2.1.8", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -21533,6 +22233,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", diff --git a/package.json b/package.json index cc14577f03c2..7c7e3e405edd 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "yargs-parser": "21.1.1" }, "devDependencies": { + "@analogjs/vite-plugin-angular": "1.10.3", "@angular-builders/jest": "18.0.0", "@angular-devkit/build-angular": "18.2.12", "@angular-eslint/builder": "18.4.1", @@ -125,6 +126,7 @@ "@angular/compiler-cli": "18.2.13", "@angular/language-service": "18.2.13", "@sentry/types": "8.47.0", + "@testing-library/angular": "17.3.5", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", @@ -156,13 +158,15 @@ "jest-junit": "16.0.0", "jest-preset-angular": "14.4.2", "lint-staged": "15.2.11", - "ngxtension": "4.2.0", "ng-mocks": "14.13.1", + "ngxtension": "4.2.0", "prettier": "3.4.2", "rimraf": "6.0.1", "sass": "1.83.0", "ts-jest": "29.2.5", "typescript": "5.5.4", + "vite-tsconfig-paths": "5.1.4", + "vitest": "2.1.8", "weak-napi": "2.0.2" }, "engines": { @@ -187,6 +191,7 @@ "test:leaks": "npm run prebuild && ng test --log-heap-usage --detect-leaks", "test:open-handles": "npm run prebuild && ng test --detect-open-handles", "test": "npm run prebuild && ng test --coverage --log-heap-usage -w=4", + "vitest": "npm run prebuild && npx vitest --run", "testw8": "npm run prebuild && ng test --coverage --log-heap-usage -w=8", "update": "ncu -i --format group", "webapp:build": "npm run clean-www && npm run prebuild -- --develop && ng build --configuration development", diff --git a/rules/custom-rules.js b/rules/custom-rules.js new file mode 100644 index 000000000000..521a031582ab --- /dev/null +++ b/rules/custom-rules.js @@ -0,0 +1,5 @@ +const noHttpClientTestingModuleRule = require("./no-http-client-testing-module"); +const plugin = { + rules: { "enforce-no-http-client-testing-module": noHttpClientTestingModuleRule } +}; +module.exports = plugin; diff --git a/rules/no-http-client-testing-module.js b/rules/no-http-client-testing-module.js new file mode 100644 index 000000000000..2585d6ed81d4 --- /dev/null +++ b/rules/no-http-client-testing-module.js @@ -0,0 +1,38 @@ +/** + * @fileoverview Custom ESLint rule to disallow the use of deprecated HttpClientTestingModule. + */ + +"use strict"; + +module.exports = { + meta: { + type: "problem", // Indicates this rule relates to a possible error + docs: { + description: "Disallow the use of deprecated HttpClientTestingModule", + category: "Best Practices", + recommended: true, // Whether this rule is recommended in ESLint configurations + }, + messages: { + avoidHttpClientTestingModule: "'HttpClientTestingModule' is deprecated. Avoid using it.", + }, + schema: [], // No options for this rule + }, + + create(context) { + return { + ImportDeclaration(node) { + if ( + node.source.value === "@angular/common/http/testing" && + node.specifiers.some( + (specifier) => specifier.imported && specifier.imported.name === "HttpClientTestingModule" + ) + ) { + context.report({ + node, + messageId: "avoidHttpClientTestingModule", + }); + } + }, + }; + }, +}; diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/GradingCriterionDTO.java b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/GradingCriterionDTO.java index ebfc35775657..3a54b15e2d4a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/GradingCriterionDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/GradingCriterionDTO.java @@ -16,7 +16,7 @@ public record GradingCriterionDTO(long id, String title, Set authorities = Arrays.stream(authorityClaim.toString().split(",")).map(SimpleGrantedAuthority::new).toList(); User principal = new User(claims.getSubject(), "", authorities); - return new UsernamePasswordAuthenticationToken(principal, token, authorities); } 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 2545a6ffb192..3523bcb48608 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 @@ -884,7 +884,7 @@ public ResponseEntity> getExerciseSt * zip file. It immediately returns and runs this task asynchronously. When the task is done, the course is marked as archived, which means the zip file can be downloaded. * * @param courseId the id of the course - * @return empty + * @return the ResponseEntity with status 200 (OK) when no exception occurred */ @PutMapping("courses/{courseId}/archive") @EnforceAtLeastInstructor diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/UserResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/UserResource.java index 7e8adfbd022c..60612c7ec6b2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/UserResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/UserResource.java @@ -119,7 +119,7 @@ public ResponseEntity updateUserNotificationDate() { * Updates the HideNotificationsUntil property that indicates which notifications to show (based on their creation date) * * @param showAllNotifications is true if all notifications should be displayed in the sidebar else depending on the HideNotificationsUntil property - * @return void + * @return the ResponseEntity with status 200 (OK) that the update was successful */ @PutMapping("users/notification-visibility") @EnforceAtLeastStudent diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java b/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java index 22941fff85e7..8d0e16e4a5c7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java @@ -1206,7 +1206,7 @@ public ResponseEntity> getLockedSubmissionsForExam(@PathVariabl * * @param courseId the id of the course * @param examId the id of the exam to archive - * @return empty + * @return the ResponseEntity with status 200 (OK) if the archiving process has been started successfully */ @PutMapping("courses/{courseId}/exams/{examId}/archive") @EnforceAtLeastInstructor diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java index 9fa635e491eb..3d03030d1bfb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java @@ -751,7 +751,7 @@ public void setNumberOfOpenMoreFeedbackRequests(Long numberOfOpenMoreFeedbackReq /** * Checks whether the exercise is released * - * @return boolean + * @return true if the exercise is released (i.e. now is after the release date or the release date is null), false otherwise */ @JsonIgnore public boolean isReleased() { diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/service/team/TeamService.java b/src/main/java/de/tum/cit/aet/artemis/exercise/service/team/TeamService.java index 027ae3ead69c..6f67611ce8dc 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/service/team/TeamService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/service/team/TeamService.java @@ -304,7 +304,7 @@ else if (student.getVisibleRegistrationNumber() != null) { * Returns an instance of TeamImportStrategy based on the given import strategy type (enum) * * @param importStrategyType Type for which to instantiate a strategy - * @return TeamImportStrategy + * @return an instance of TeamImportStrategy based on the given import strategy type */ private TeamImportStrategy getTeamImportStrategy(TeamImportStrategyType importStrategyType) { return switch (importStrategyType) { diff --git a/src/main/java/de/tum/cit/aet/artemis/lti/config/Lti13LaunchFilter.java b/src/main/java/de/tum/cit/aet/artemis/lti/config/Lti13LaunchFilter.java index 669e309566ef..6929eee2d537 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lti/config/Lti13LaunchFilter.java +++ b/src/main/java/de/tum/cit/aet/artemis/lti/config/Lti13LaunchFilter.java @@ -110,7 +110,7 @@ private OidcAuthenticationToken finishOidcFlow(HttpServletRequest request, HttpS throw new IllegalStateException("No authentication was returned"); } } - catch (OAuth2AuthenticationException | IllegalStateException ex) { + catch (OAuth2AuthenticationException | IllegalStateException | IOException | ServletException ex) { throw new IllegalStateException("Failed to attempt LTI 1.3 login authentication: " + ex.getMessage(), ex); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java index 22e9dd1b0233..1264ba9b643f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java @@ -394,7 +394,7 @@ public ContinuousIntegrationService.BuildStatus getBuildStatusOfPlan(String proj } try { - var uri = UriComponentsBuilder.fromHttpUrl(serverUrl.toString()).pathSegment("job", projectKey, "job", planKey, "lastBuild", "api", "json").build().toUri(); + var uri = UriComponentsBuilder.fromUriString(serverUrl.toString()).pathSegment("job", projectKey, "job", planKey, "lastBuild", "api", "json").build().toUri(); var response = restTemplate.getForObject(uri, JsonNode.class); var isJobBuilding = response.get("building").asBoolean(); return isJobBuilding ? ContinuousIntegrationService.BuildStatus.BUILDING : ContinuousIntegrationService.BuildStatus.INACTIVE; @@ -475,7 +475,7 @@ public void givePlanPermissions(ProgrammingExercise programmingExercise, String */ public void enablePlan(String projectKey, String planKey) { try { - var uri = UriComponentsBuilder.fromHttpUrl(serverUrl.toString()).pathSegment("job", projectKey, "job", planKey, "enable").build(true).toUri(); + var uri = UriComponentsBuilder.fromUriString(serverUrl.toString()).pathSegment("job", projectKey, "job", planKey, "enable").build(true).toUri(); restTemplate.postForEntity(uri, null, String.class); } catch (HttpClientErrorException e) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCPostPushHook.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCPostPushHook.java index fb925717a387..024c0fa77546 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCPostPushHook.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCPostPushHook.java @@ -25,11 +25,8 @@ public LocalVCPostPushHook(LocalVCServletService localVCServletService) { /** * Called by JGit after a push has been received (i.e. after the pushed files were successfully written to disk). * - * @param receivePack - * the process handling the current receive. Hooks may obtain - * details about the destination repository through this handle. - * @param commands - * unmodifiable set of successfully completed commands. + * @param receivePack the process handling the current receive. Hooks may obtain details about the destination repository through this handle. + * @param commands unmodifiable set of successfully completed commands. */ @Override public void onPostReceive(ReceivePack receivePack, Collection commands) { diff --git a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/dto/TutorialGroupFreePeriodDTO.java b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/dto/TutorialGroupFreePeriodDTO.java index 25086f7b7154..19ac2761f938 100644 --- a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/dto/TutorialGroupFreePeriodDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/dto/TutorialGroupFreePeriodDTO.java @@ -9,9 +9,6 @@ /** * Used because we want to interpret the date in the time zone of the tutorial groups configuration * - * @param startDate - * @param endDate - * @param reason */ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record TutorialGroupFreePeriodDTO(@NotNull LocalDateTime startDate, @NotNull LocalDateTime endDate, String reason) { diff --git a/src/main/kubernetes/artemis/deployments/artemis-deployment.yml b/src/main/kubernetes/artemis/deployments/artemis-deployment.yml index 168d56f4fa54..8c4597a339ef 100644 --- a/src/main/kubernetes/artemis/deployments/artemis-deployment.yml +++ b/src/main/kubernetes/artemis/deployments/artemis-deployment.yml @@ -59,7 +59,7 @@ spec: terminationGracePeriodSeconds: 30 initContainers: - name: init-ds - image: busybox:latest + image: docker.io/library/busybox:1.37.0 command: - '/bin/sh' - '-c' diff --git a/src/main/kubernetes/artemis/deployments/kafka-deployment.yml b/src/main/kubernetes/artemis/deployments/kafka-deployment.yml index 5c08d6ba750c..039fc5b82401 100644 --- a/src/main/kubernetes/artemis/deployments/kafka-deployment.yml +++ b/src/main/kubernetes/artemis/deployments/kafka-deployment.yml @@ -27,7 +27,7 @@ spec: - configMapRef: name: kafka optional: false - image: confluentinc/cp-kafka:5.5.3 + image: docker.io/confluentinc/cp-kafka:7.8.0 imagePullPolicy: Always name: kafka resources: {} diff --git a/src/main/kubernetes/artemis/deployments/zookeeper-deployment.yml b/src/main/kubernetes/artemis/deployments/zookeeper-deployment.yml index 4d5db7d64d9f..dd9383f1af4d 100644 --- a/src/main/kubernetes/artemis/deployments/zookeeper-deployment.yml +++ b/src/main/kubernetes/artemis/deployments/zookeeper-deployment.yml @@ -29,7 +29,7 @@ spec: - configMapRef: name: zookeeper optional: false - image: confluentinc/cp-zookeeper:5.5.3 + image: docker.io/confluentinc/cp-zookeeper:7.8.0 imagePullPolicy: Always name: zookeeper ports: @@ -51,4 +51,4 @@ spec: restartPolicy: Always schedulerName: default-scheduler securityContext: {} - terminationGracePeriodSeconds: 30 \ No newline at end of file + terminationGracePeriodSeconds: 30 diff --git a/src/main/kubernetes/artemis/statefulsets/artemis-mysql.yml b/src/main/kubernetes/artemis/statefulsets/artemis-mysql.yml index 38d92869a3c3..172e8641343a 100644 --- a/src/main/kubernetes/artemis/statefulsets/artemis-mysql.yml +++ b/src/main/kubernetes/artemis/statefulsets/artemis-mysql.yml @@ -31,7 +31,7 @@ spec: envFrom: - configMapRef: name: artemis-mysql - image: mysql:9.1.0 + image: docker.io/library/mysql:9.1.0 imagePullPolicy: IfNotPresent name: artemis-mysql ports: diff --git a/src/main/kubernetes/artemis/statefulsets/jhipster-registry.yml b/src/main/kubernetes/artemis/statefulsets/jhipster-registry.yml index bf81fe40df68..e888a6c2f7cf 100644 --- a/src/main/kubernetes/artemis/statefulsets/jhipster-registry.yml +++ b/src/main/kubernetes/artemis/statefulsets/jhipster-registry.yml @@ -34,7 +34,7 @@ spec: terminationGracePeriodSeconds: 10 containers: - name: jhipster-registry - image: jhipster/jhipster-registry:v6.7.1 + image: docker.io/jhipster/jhipster-registry:v7.4.0 ports: - containerPort: 8761 envFrom: diff --git a/src/main/webapp/app/admin/admin.module.ts b/src/main/webapp/app/admin/admin.module.ts index 1d5d5e8314ed..0a236665c96d 100644 --- a/src/main/webapp/app/admin/admin.module.ts +++ b/src/main/webapp/app/admin/admin.module.ts @@ -75,6 +75,7 @@ const ENTITY_STATES = [...adminState]; StandardizedCompetencyDetailComponent, DeleteUsersButtonComponent, ProfilePictureComponent, + AdminImportStandardizedCompetenciesComponent, BuildAgentSummaryComponent, BuildAgentDetailsComponent, ], @@ -102,7 +103,6 @@ const ENTITY_STATES = [...adminState]; StandardizedCompetencyEditComponent, KnowledgeAreaEditComponent, StandardizedCompetencyManagementComponent, - AdminImportStandardizedCompetenciesComponent, ], }) export class ArtemisAdminModule {} diff --git a/src/main/webapp/app/admin/standardized-competencies/import/admin-import-standardized-competencies.component.ts b/src/main/webapp/app/admin/standardized-competencies/import/admin-import-standardized-competencies.component.ts index a61113395a00..bd63f1306106 100644 --- a/src/main/webapp/app/admin/standardized-competencies/import/admin-import-standardized-competencies.component.ts +++ b/src/main/webapp/app/admin/standardized-competencies/import/admin-import-standardized-competencies.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { faBan, faChevronRight, faFileImport, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { KnowledgeAreaDTO, @@ -18,6 +18,12 @@ import { ActivatedRoute, Router } from '@angular/router'; import { MatTreeNestedDataSource } from '@angular/material/tree'; import { NestedTreeControl } from '@angular/cdk/tree'; import { getIcon } from 'app/entities/competency.model'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { StandardizedCompetencyDetailComponent } from 'app/shared/standardized-competencies/standardized-competency-detail.component'; +import { KnowledgeAreaTreeComponent } from 'app/shared/standardized-competencies/knowledge-area-tree.component'; +import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; interface ImportCount { knowledgeAreas: number; @@ -26,7 +32,9 @@ interface ImportCount { @Component({ selector: 'jhi-admin-import-standardized-competencies', + standalone: true, templateUrl: './admin-import-standardized-competencies.component.html', + imports: [ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisMarkdownModule, FontAwesomeModule, StandardizedCompetencyDetailComponent, KnowledgeAreaTreeComponent], }) export class AdminImportStandardizedCompetenciesComponent { protected isLoading = false; @@ -76,12 +84,10 @@ export class AdminImportStandardizedCompetenciesComponent { } \`\`\``; - public constructor( - private alertService: AlertService, - private adminStandardizedCompetencyService: AdminStandardizedCompetencyService, - private activatedRoute: ActivatedRoute, - private router: Router, - ) {} + private alertService = inject(AlertService); + private adminStandardizedCompetencyService = inject(AdminStandardizedCompetencyService); + private activatedRoute = inject(ActivatedRoute); + private router = inject(Router); /** * Verifies the file (only .json, smaller than 20 MB) and then tries to read the importData from it diff --git a/src/main/webapp/app/shared/layouts/main/main.component.ts b/src/main/webapp/app/shared/layouts/main/main.component.ts index 6563c998cdfa..299eebd09926 100644 --- a/src/main/webapp/app/shared/layouts/main/main.component.ts +++ b/src/main/webapp/app/shared/layouts/main/main.component.ts @@ -27,12 +27,12 @@ export class JhiMainComponent implements OnInit, OnDestroy { courseOverviewSubscription: Subscription; testRunSubscription: Subscription; ltiSubscription: Subscription; - isProduction: boolean = true; - isTestServer: boolean = false; - isExamStarted: boolean = false; - isTestRunExam: boolean = false; - isCourseOverview: boolean = false; - isShownViaLti: boolean = false; + isProduction = true; + isTestServer = false; + isExamStarted = false; + isTestRunExam = false; + isCourseOverview = false; + isShownViaLti = false; constructor( private jhiLanguageHelper: JhiLanguageHelper, diff --git a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java index 3b5e71768282..3f4606f09e63 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java @@ -992,7 +992,6 @@ private LinkedMultiValueMap createParamsForPagingRequest(String * * @param userNumbers the user creation matrix * @return String of the user authority with the most users - * @throws Exception */ private String getMainUserAuthority(Integer[] userNumbers) throws Exception { List userNumbersList = Arrays.asList(userNumbers); diff --git a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/AbstractModuleTestArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/AbstractModuleTestArchitectureTest.java index cc4f8c80357c..a9191dcf0a76 100644 --- a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/AbstractModuleTestArchitectureTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/AbstractModuleTestArchitectureTest.java @@ -8,9 +8,9 @@ public abstract class AbstractModuleTestArchitectureTest extends AbstractArchitectureTest { - abstract protected String getModulePackageName(); + protected abstract String getModulePackageName(); - abstract protected Class getAbstractModuleIntegrationTestClass(); + protected abstract Class getAbstractModuleIntegrationTestClass(); @Test void integrationTestsShouldExtendAbstractModuleIntegrationTest() { diff --git a/src/test/javascript/spec/component/course/course-update.component.spec.ts b/src/test/javascript/spec/component/course/course-update.component.spec.ts index 9a8d29a1b1fd..eb9819114476 100644 --- a/src/test/javascript/spec/component/course/course-update.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-update.component.spec.ts @@ -112,6 +112,8 @@ describe('Course Management Update Component', () => { { provide: NgbModal, useClass: MockNgbModalService }, MockProvider(TranslateService), MockProvider(LoadImageService), + provideHttpClient(), + provideHttpClientTesting(), ], declarations: [ CourseUpdateComponent, diff --git a/src/test/javascript/spec/component/iris/settings/iris-settings-update-component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-settings-update-component.spec.ts index bfea843b7c8b..e35074c2942d 100644 --- a/src/test/javascript/spec/component/iris/settings/iris-settings-update-component.spec.ts +++ b/src/test/javascript/spec/component/iris/settings/iris-settings-update-component.spec.ts @@ -14,6 +14,8 @@ import { IrisCourseSettingsUpdateComponent } from 'app/iris/settings/iris-course import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { NgbTooltipMockDirective } from '../../../helpers/mocks/directive/ngbTooltipMocks.module'; import { MockJhiTranslateDirective } from '../../../helpers/mocks/directive/mock-jhi-translate-directive.directive'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; describe('IrisSettingsUpdateComponent', () => { let component: IrisSettingsUpdateComponent; @@ -37,6 +39,8 @@ describe('IrisSettingsUpdateComponent', () => { getUncombinedCourseSettings: () => of(mockSettings()), getUncombinedExerciseSettings: () => of(mockSettings()), }), + provideHttpClient(), + provideHttpClientTesting(), ], }) .compileComponents() diff --git a/src/test/javascript/spec/component/quiz-exercise/quiz-exercise-update.component.spec.ts b/src/test/javascript/spec/component/quiz-exercise/quiz-exercise-update.component.spec.ts index c271a8deb04e..47d8dabd2185 100644 --- a/src/test/javascript/spec/component/quiz-exercise/quiz-exercise-update.component.spec.ts +++ b/src/test/javascript/spec/component/quiz-exercise/quiz-exercise-update.component.spec.ts @@ -1,4 +1,5 @@ -import { HttpResponse } from '@angular/common/http'; +import { HttpResponse, provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ChangeDetectorRef, SimpleChange } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'; @@ -158,6 +159,8 @@ describe('QuizExerciseUpdateComponent', () => { { provide: SessionStorageService, useClass: MockSyncStorage }, { provide: TranslateService, useClass: MockTranslateService }, { provide: Router, useClass: MockRouter }, + provideHttpClient(), + provideHttpClientTesting(), ], }) .overrideTemplate(QuizExerciseUpdateComponent, '') diff --git a/src/test/javascript/spec/component/standardized-competencies/admin-import-standardized-competencies.spec.ts b/src/test/javascript/spec/component/standardized-competencies/admin-import-standardized-competencies.spec.ts index 15ce0871d27a..cea5d75e4e6e 100644 --- a/src/test/javascript/spec/component/standardized-competencies/admin-import-standardized-competencies.spec.ts +++ b/src/test/javascript/spec/component/standardized-competencies/admin-import-standardized-competencies.spec.ts @@ -1,6 +1,6 @@ import { ArtemisTestModule } from '../../test.module'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks'; import { AdminImportStandardizedCompetenciesComponent } from 'app/admin/standardized-competencies/import/admin-import-standardized-competencies.component'; import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe'; import { KnowledgeAreaTreeStubComponent } from './knowledge-area-tree-stub.component'; @@ -15,6 +15,11 @@ import { HttpResponse } from '@angular/common/http'; import { of } from 'rxjs'; import { KnowledgeAreasForImportDTO } from 'app/entities/competency/standardized-competency.model'; import { StandardizedCompetencyDetailComponent } from 'app/shared/standardized-competencies/standardized-competency-detail.component'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { ArtemisSharedModule } from '../../../../../main/webapp/app/shared/shared.module'; +import { ArtemisSharedComponentModule } from '../../../../../main/webapp/app/shared/components/shared-component.module'; +import { KnowledgeAreaTreeComponent } from '../../../../../main/webapp/app/shared/standardized-competencies/knowledge-area-tree.component'; +import { ArtemisMarkdownModule } from '../../../../../main/webapp/app/shared/markdown.module'; describe('AdminImportStandardizedCompetenciesComponent', () => { let componentFixture: ComponentFixture; @@ -29,6 +34,12 @@ describe('AdminImportStandardizedCompetenciesComponent', () => { KnowledgeAreaTreeStubComponent, MockComponent(ButtonComponent), MockComponent(StandardizedCompetencyDetailComponent), + MockModule(ArtemisSharedModule), + MockModule(ArtemisSharedComponentModule), + MockModule(ArtemisMarkdownModule), + MockModule(FontAwesomeModule), + MockComponent(StandardizedCompetencyDetailComponent), + MockComponent(KnowledgeAreaTreeComponent), ], providers: [{ provide: Router, useClass: MockRouter }, MockProvider(AlertService)], }) diff --git a/src/test/javascript/spec/component/team/team.component.spec.ts b/src/test/javascript/spec/component/team/team.component.spec.ts index 0acd872ac1c4..2b35c859c567 100644 --- a/src/test/javascript/spec/component/team/team.component.spec.ts +++ b/src/test/javascript/spec/component/team/team.component.spec.ts @@ -1,6 +1,5 @@ -import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { Router, RouterModule } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; import { NgxDatatableModule } from '@siemens/ngx-datatable'; @@ -19,10 +18,13 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks'; import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; import { of, throwError } from 'rxjs'; -import { TeamRequestInterceptorMock, mockExercise, mockTeam, mockTeams } from '../../helpers/mocks/service/mock-team.service'; +import { Exercise } from '../../../../../main/webapp/app/entities/exercise.model'; +import { Team } from '../../../../../main/webapp/app/entities/team.model'; +import { mockExercise, mockTeam, mockTeams } from '../../helpers/mocks/service/mock-team.service'; import { ArtemisTestModule } from '../../test.module'; import { AssessmentWarningComponent } from 'app/assessment/assessment-warning/assessment-warning.component'; import { AlertService } from 'app/core/util/alert.service'; +import { HttpResponse } from '@angular/common/http'; describe('TeamComponent', () => { let comp: TeamComponent; @@ -55,18 +57,6 @@ describe('TeamComponent', () => { TeamService, MockProvider(TranslateService), ExerciseService, - { - provide: HTTP_INTERCEPTORS, - useClass: TeamRequestInterceptorMock, - multi: true, - }, - MockProvider(Router), - { - provide: ActivatedRoute, - useValue: { - params: of({ teamId: mockTeam.id, exerciseId: mockExercise.id }), - }, - }, ], }) .compileComponents() @@ -94,7 +84,8 @@ describe('TeamComponent', () => { }); it('should set team and exercise from services and call find on exerciseService to retreive exercise', () => { - jest.spyOn(exerciseService, 'find'); + jest.spyOn(exerciseService, 'find').mockReturnValue(of(new HttpResponse({ body: mockExercise }))); + jest.spyOn(teamService, 'find').mockReturnValue(of(new HttpResponse({ body: mockTeam }))); comp.ngOnInit(); expect(comp.exercise).toEqual(mockExercise); expect(comp.team).toEqual(mockTeam); @@ -146,6 +137,8 @@ describe('TeamComponent', () => { describe('onTeamDelete', () => { it('should go to teams overview on delete', () => { + jest.spyOn(exerciseService, 'find').mockReturnValue(of(new HttpResponse({ body: mockExercise }))); + jest.spyOn(teamService, 'find').mockReturnValue(of(new HttpResponse({ body: mockTeam }))); comp.ngOnInit(); jest.spyOn(router, 'navigate'); comp.onTeamDelete(); diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-team.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-team.service.ts index 5f830bdf013d..b6509a12c977 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-team.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-team.service.ts @@ -172,18 +172,3 @@ export class MockTeamService implements ITeamService { return of({ body: entity }) as Observable>; } } - -@Injectable() -export class TeamRequestInterceptorMock implements HttpInterceptor { - constructor() {} - - intercept(request: HttpRequest, next: HttpHandler): Observable> { - if (request.url && request.url.indexOf(`${TeamService.resourceUrl(mockExercise.id!)}/${mockTeamFromServer.id}`) > -1) { - return of(new HttpResponse({ status: 200, body: mockTeamFromServer })); - } - if (request.url === `api/exercises/${mockExercise.id}`) { - return of(new HttpResponse({ status: 200, body: mockExercise })); - } - return next.handle(request); - } -} diff --git a/src/test/javascript/spec/test.module.ts b/src/test/javascript/spec/test.module.ts index 5d01f73cb5d2..66a0cfab9693 100644 --- a/src/test/javascript/spec/test.module.ts +++ b/src/test/javascript/spec/test.module.ts @@ -1,7 +1,6 @@ import { DatePipe, registerLocaleData } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { ElementRef, NgModule, Renderer2 } from '@angular/core'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; import { NgbActiveModal, NgbDatepickerConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { AccountService } from 'app/core/auth/account.service'; import { MockAccountService } from './helpers/mocks/service/mock-account.service'; @@ -20,9 +19,11 @@ import { MockThemeService } from './helpers/mocks/service/mock-theme.service'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { MockProfileService } from './helpers/mocks/service/mock-profile.service'; import { FontAwesomeTestingModule } from '@fortawesome/angular-fontawesome/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; @NgModule({ - imports: [HttpClientTestingModule], + imports: [], providers: [ DatePipe, ParseLinks, @@ -65,6 +66,8 @@ import { FontAwesomeTestingModule } from '@fortawesome/angular-fontawesome/testi provide: ProfileService, useClass: MockProfileService, }, + provideHttpClient(), + provideHttpClientTesting(), ], exports: [FontAwesomeTestingModule], }) diff --git a/src/test/javascript/vitest/main.component.spec.ts b/src/test/javascript/vitest/main.component.spec.ts new file mode 100644 index 000000000000..ead43e7ec9fe --- /dev/null +++ b/src/test/javascript/vitest/main.component.spec.ts @@ -0,0 +1,79 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { render } from '@testing-library/angular'; +import { JhiMainComponent } from 'app/shared/layouts/main/main.component'; +import { AlertOverlayComponent } from 'app/shared/alert/alert-overlay.component'; +import { PageRibbonComponent } from 'app/shared/layouts/profiles/page-ribbon.component'; +import { NotificationPopupComponent } from 'app/shared/notification/notification-popup/notification-popup.component'; +import { TranslateService } from '@ngx-translate/core'; +import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; +import { ThemeService } from 'app/core/theme/theme.service'; +import { ArtemisTranslatePipe } from '../../../main/webapp/app/shared/pipes/artemis-translate.pipe'; +import { MockSyncStorage } from '../spec/helpers/mocks/service/mock-sync-storage.service'; +import { MockTranslateService } from '../spec/helpers/mocks/service/mock-translate.service'; +import { ArtemisTestModule } from '../spec/test.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +// Mock the initialize method +class MockThemeService { + // Required because the real ThemeService uses this method, even though it's not called in the tests + initialize(): void {} +} + +describe('JhiMainComponent', () => { + let component: JhiMainComponent; + let componentFixture: ComponentFixture; + let container: Element; + + beforeEach(async () => { + const { fixture, container: renderedContainer } = await render(JhiMainComponent, { + imports: [ArtemisTestModule], + declarations: [AlertOverlayComponent, PageRibbonComponent, NotificationPopupComponent], + providers: [ + { provide: LocalStorageService, useClass: MockSyncStorage }, + { provide: SessionStorageService, useClass: MockSyncStorage }, + { provide: TranslateService, useClass: MockTranslateService }, + { provide: ThemeService, useClass: MockThemeService }, + ArtemisTranslatePipe, + ], + }); + + componentFixture = fixture; + component = fixture.componentInstance; + container = renderedContainer; // Save the container for querying elements + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should use the initialize method of ThemeService', () => { + const themeService = TestBed.inject(ThemeService) as MockThemeService; + themeService.initialize(); + }); + + it('should display footer if there is no exam', async () => { + component.isExamStarted = false; + component.showSkeleton = true; + + const footerElement = container.querySelector('jhi-footer'); + const notificationPopup = container.querySelector('jhi-notification-popup'); + + expect(footerElement).not.toBeNull(); + expect(notificationPopup).not.toBeNull(); + }); + + it('should not display footer during an exam', async () => { + component.isExamStarted = true; + component.showSkeleton = true; + component.isTestRunExam = false; + component.isShownViaLti = false; + + componentFixture.detectChanges(); // Trigger change detection + + const notificationPopup = container.querySelector('jhi-notification-popup'); + const footerElement = container.querySelector('jhi-footer'); + + expect(notificationPopup).not.toBeNull(); + expect(footerElement).toBeNull(); + }); +}); diff --git a/src/test/javascript/vitest/vitest-setup.ts b/src/test/javascript/vitest/vitest-setup.ts new file mode 100644 index 000000000000..7010c5a45b12 --- /dev/null +++ b/src/test/javascript/vitest/vitest-setup.ts @@ -0,0 +1,6 @@ +import '@analogjs/vite-plugin-angular/setup-vitest'; + +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; +import { getTestBed } from '@angular/core/testing'; + +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index 8fce605203fa..f730d6102f9c 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -194,7 +194,7 @@ zonky: type: H2 # Alternatives: H2 / MYSQL / POSTGRES postgres: docker: - image: "postgres:17.2-alpine" + image: docker.io/library/postgres:17.2-alpine tmpfs: enabled: true server: @@ -217,7 +217,7 @@ zonky: max_parallel_maintenance_workers: 4 mysql: docker: - image: "mysql:9.1.0" + image: docker.io/library/mysql:9.1.0 tmpfs: enabled: true diff --git a/tsconfig.json b/tsconfig.json index 8ce7380a6beb..d6cc333517c6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -43,5 +43,5 @@ } }, "include": ["src/main/webapp/app"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "src/main/resources"] } diff --git a/vite.config.mts b/vite.config.mts new file mode 100644 index 000000000000..40d740042449 --- /dev/null +++ b/vite.config.mts @@ -0,0 +1,37 @@ +/// + +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +// https://vitejs.dev/config/ +export default defineConfig(({ mode }) => { + return { + plugins: [angular(), tsconfigPaths()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['src/test/javascript/vitest/vitest-setup.ts'], + include: ['src/test/javascript/vitest/**/*.spec.ts'], + exclude: [ + 'src/main/resources/**', + 'build/resources/**', // Exclude problematic directories + ], + reporters: ['default'] + }, + resolve: { + alias: { + // Manually resolve the "app/*" alias if needed + app: '/src/main/webapp/app', + }, + }, + build: { + rollupOptions: { + external: [/build\/resources\/.*/, /src\/main\/resources\/.*/], + }, + }, + define: { + 'import.meta.vitest': mode !== 'production', + }, + }; +});