diff --git a/.env-template b/.env-template new file mode 100644 index 00000000..1b18f9ec --- /dev/null +++ b/.env-template @@ -0,0 +1,13 @@ +client-secret= +client-id= +tenant-id= +PGU= +PGP= +POSTGRES_USER=${PGU} +URI= +EXPRESS_SESSION_SECRET= +PORT= +ENVIRONMENT= +DB_HOST= +DB_PORT= +DB_NAME= \ No newline at end of file diff --git a/.github/workflows/testing.yml b/.github/workflows/backend_testing.yaml similarity index 75% rename from .github/workflows/testing.yml rename to .github/workflows/backend_testing.yaml index 7f5cac91..e936f0e5 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/backend_testing.yaml @@ -1,4 +1,4 @@ -name: running test with github actions +name: Backend Testing on: pull_request jobs: @@ -11,4 +11,4 @@ jobs: uses: actions/setup-java@v1 with: java-version: 17 - - run: gradle test -p backend/app/ + - run: gradle unitTests -p backend/app/ diff --git a/.gitignore b/.gitignore index dcf2de13..cdae00ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +backend/web-bff/App/.env.dev + HELP.md .gradle build/ @@ -36,7 +38,17 @@ out/ .vscode/ backend/app/data/* backend/data/* +backend/tmp/* +backend/app/tmp/* +data/* ### Secrets ### backend/app/src/main/resources/application-secrets.properties docker.env + + +./startBackend.sh +startBackend.sh + +/.env +backend/web-bff/App/.env diff --git a/README.md b/README.md index eb843e8c..fe1d656e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,57 @@ -# UGent-6 +![badge](https://github.com/SELab-2/UGent-6/actions/workflows/backend_testing.yaml/badge.svg) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + + + + + +| Student | E-mailadres | +----------------------------------------------------|-----------------------------| +| [Matthias Vaneyck](https://github.com/Matthias-VE) | matthias.vaneyck@ugent.be | +| [Inti Danschutter](https://github.com/Aqua-sc) | inti.danschutter@ugent.be | +| [Arthur Werbrouck](https://github.com/AWerbrouck) | Arthur.Werbrouck@ugent.be | +| [Arne Dierick](https://github.com/arnedierick) | arne.dierick@ugent.be | +| [Wout Verdyck](https://github.com/usserwoutV2) | wout.verdyck@ugent.be | +| [Floris Kornelis Van Dijken](https://github.com/badduck32) | floris.kornelisvandijken@ugent.be | +| [Tristan Verbeken](https://github.com/TR1VER) | tristan.verbeken@ugent.be| + + +[wiki documentation](https://github.com/SELab-2/UGent-6/wiki) + +[api documentation](https://apidog.com/apidoc/project-467959) +## Tree view of the project + +- backend + - app + - src + - main/java/com/ugent/pidgeon + - auth (Authentication related logic) + - config (Configuration settings and beans) + - controllers (Web controllers for handling requests) + - model (models for testing submissions) + - json (request/response bodies) + - postgre (Database models and repositories) + - util (Utility classes and helpers) + - resources (Configuration files, property files etc.) + - test/java/com/ugent/pidgeon (Unit and integration tests) + - database (Database schemas and scripts) + - db (Database related scripts) + - web-bff (Express webserver that manages user authentication with cookie sessions) + +- frontend + - public (Static files like images, fonts, and `index.html`) + - src + - @types (TypeScript type definitions) + - assets (Static assets like images and logos used in the app) + - components (Reusable UI components) + - common (Commonly used components across the application) + - forms (Form components, including project form tabs) + - layout (Components related to layout such as navbars and sidebars) + - hooks (Custom React hooks) + - i18n (Internationalization setup, including language files) + - pages (Component structure for each page) + - providers (Context providers for state management) + - router (Routing and path management) + - theme (Styling themes and fonts) + - util (Utility functions and helpers such as our `apiFetch`) -https://github.com/SELab-2/UGent-6/wiki diff --git a/app.conf b/app.conf deleted file mode 100644 index e7c31d29..00000000 --- a/app.conf +++ /dev/null @@ -1,34 +0,0 @@ -server { - listen 80; - server_name sel2-6.ugent.be; - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - - #location / { - # return 301 https://$host$request_uri; - #} -} - -server { - listen 443 ssl; - server_name sel2-6.ugent.be; - - ssl_certificate /etc/letsencrypt/live/sel2-6.ugent.be/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/sel2-6.ugent.be/privkey.pem; - - location / { - root /usr/share/nginx/html; - try_files $uri /index.html; - } - - location /api { - #proxy_set_header X-Real-IP $remote_addr; - #proxy_set_header Host $host; - #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - #proxy_set_header X-Forwarded-Proto $scheme; - proxy_pass http://backend:8080/; - } -} - diff --git a/backend/app/Dockerfile b/backend/app/Dockerfile index 65cb465b..f5621616 100644 --- a/backend/app/Dockerfile +++ b/backend/app/Dockerfile @@ -11,3 +11,5 @@ RUN mkdir /app COPY --from=build /home/gradle/src/build/libs/*T.jar /app/spring-boot-application.jar ENTRYPOINT ["java", "-jar","/app/spring-boot-application.jar"] + +#@Relevant \ No newline at end of file diff --git a/backend/app/artifactPath b/backend/app/artifactPath new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/build.gradle b/backend/app/build.gradle index 32285bec..8c4995ce 100644 --- a/backend/app/build.gradle +++ b/backend/app/build.gradle @@ -21,7 +21,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'org.postgresql:postgresql' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.security:spring-security-config' implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' @@ -44,13 +43,43 @@ dependencies { implementation "org.springframework.boot:spring-boot-devtools" testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.mockito:mockito-junit-jupiter:4.0.0' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + testImplementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' +} + +// tasks.named('test',Test) { +// useJUnitPlatform() +// maxHeapSize = '1G' + +// testLogging { +// events "passed" + +// } +// } + +task unitTests (type: Test){ + + exclude '**/docker' + + useJUnitPlatform() + maxHeapSize = '1G' + + testLogging { + events "passed" + } + } -tasks.named('test',Test) { +task allTest (type: Test) { + + include '**' useJUnitPlatform() - maxHeapSize = '1G' + maxHeapSize = '1G' + - testLogging { - events "passed" - } + testLogging { + events "passed" + } } + diff --git a/backend/app/src/main/java/com/ugent/pidgeon/ApiErrorReponse.java b/backend/app/src/main/java/com/ugent/pidgeon/ApiErrorReponse.java new file mode 100644 index 00000000..56a0cb87 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/ApiErrorReponse.java @@ -0,0 +1,6 @@ +package com.ugent.pidgeon; + +import java.time.OffsetDateTime; + +public record ApiErrorReponse(OffsetDateTime timestamp, int status, String error, String message, String path) { +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/GlobalErrorHandler.java b/backend/app/src/main/java/com/ugent/pidgeon/GlobalErrorHandler.java new file mode 100644 index 00000000..5f540fa1 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/GlobalErrorHandler.java @@ -0,0 +1,88 @@ +package com.ugent.pidgeon; + + +import jakarta.servlet.http.HttpServletRequest; +import java.time.OffsetDateTime; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +@ControllerAdvice +public class GlobalErrorHandler { + + + + private void logError(Exception ex) { + Logger logger = Logger.getGlobal(); + // Log the error with the logger + logger.log(Level.SEVERE, ex.getMessage(), ex); + } + + /* Gets thrown when a invalid json is sent */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadableException(HttpServletRequest request, Exception ex) { + logError(ex); + String path = request.getRequestURI(); + HttpStatus status = HttpStatus.BAD_REQUEST; + return ResponseEntity.status(status).body(new ApiErrorReponse(OffsetDateTime.now(), status.value(),status.getReasonPhrase(), + "Unable to process the request due to invalid or missing data. Please ensure the request body is properly formatted and all required fields are provided.", path)); + } + + /* Gets thrown when endpoint doesn't exist */ + @ExceptionHandler(NoHandlerFoundException.class) + public ResponseEntity handleNoHandlerFoundException(HttpServletRequest request, Exception ex) { + logError(ex); + String path = request.getRequestURI(); + HttpStatus status = HttpStatus.NOT_FOUND; + return ResponseEntity.status(status).body(new ApiErrorReponse(OffsetDateTime.now(), status.value(), status.getReasonPhrase(), + "Resource/endpoint doesn't exist", path)); + } + + @ExceptionHandler(NoResourceFoundException.class) + public ResponseEntity handleNoResourceFoundException(HttpServletRequest request, Exception ex) { + logError(ex); + String path = request.getRequestURI(); + HttpStatus status = HttpStatus.NOT_FOUND; + return ResponseEntity.status(status).body(new ApiErrorReponse(OffsetDateTime.now(), status.value(), status.getReasonPhrase(), + "Resource/endpoint doesn't exist", path)); + } + + /* Gets thrown when the method is not allowed */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity handleMethodNotSupportedException(HttpServletRequest request, Exception ex) { + logError(ex); + String path = request.getRequestURI(); + HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED; + return ResponseEntity.status(status).body(new ApiErrorReponse(OffsetDateTime.now(), status.value(), status.getReasonPhrase(), + "Method not supported", path)); + } + + /* Gets thrown when u path variable is of the wrong type */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handleMethodArgumentTypeMismatchException(HttpServletRequest request, Exception ex) { + logError(ex); + String path = request.getRequestURI(); + HttpStatus status = HttpStatus.BAD_REQUEST; + return ResponseEntity.status(status).body(new ApiErrorReponse(OffsetDateTime.now(), status.value(), status.getReasonPhrase(), + "Invalid url argument type", path)); + } + + /* Gets thrown when an unexpected error occurs */ + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(HttpServletRequest request, Exception ex) { + logError(ex); + String path = request.getRequestURI(); + HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; + return ResponseEntity.status(status).body(new ApiErrorReponse(OffsetDateTime.now(), status.value(), status.getReasonPhrase(), + "An unexpected error occurred", path)); + } + +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/PidgeonApplication.java b/backend/app/src/main/java/com/ugent/pidgeon/PidgeonApplication.java index c600e2c2..fd32894b 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/PidgeonApplication.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/PidgeonApplication.java @@ -3,7 +3,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController diff --git a/backend/app/src/main/java/com/ugent/pidgeon/auth/JwtAuthenticationFilter.java b/backend/app/src/main/java/com/ugent/pidgeon/auth/JwtAuthenticationFilter.java index a4ba6cfc..37dcbefc 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/auth/JwtAuthenticationFilter.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/auth/JwtAuthenticationFilter.java @@ -14,17 +14,15 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.util.StringUtils; -import org.springframework.web.filter.OncePerRequestFilter; - import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.security.interfaces.RSAPublicKey; import java.util.ArrayList; -import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; /** * This class extends OncePerRequestFilter to provide a filter that decodes and verifies JWT tokens. @@ -83,6 +81,7 @@ public void doFilterInternal(HttpServletRequest request, HttpServletResponse res String lastName; String email; String oid; + String studentnumber; String version = jwt.getClaim("ver").asString(); @@ -92,21 +91,21 @@ public void doFilterInternal(HttpServletRequest request, HttpServletResponse res lastName = jwt.getClaim("family_name").asString(); email = jwt.getClaim("unique_name").asString(); oid = jwt.getClaim("oid").asString(); + studentnumber = jwt.getClaim("ugentStudentID").asString(); } else if (version.startsWith("2.0")) { displayName = jwt.getClaim("name").asString(); lastName = jwt.getClaim("surname").asString(); firstName = displayName.replace(lastName, "").strip(); email = jwt.getClaim("mail").asString(); oid = jwt.getClaim("oid").asString(); + studentnumber = jwt.getClaim("ugentStudentID").asString(); } else { throw new JwkException("Invalid OAuth version"); } // print full object - // logger.info(jwt.getClaims()); - - + logger.info(jwt.getClaims()); - User user = new User(displayName, firstName,lastName, email, oid); + User user = new User(displayName, firstName,lastName, email, oid, studentnumber); Auth authUser = new Auth(user, new ArrayList<>()); SecurityContextHolder.getContext().setAuthentication(authUser); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/auth/Roles.java b/backend/app/src/main/java/com/ugent/pidgeon/auth/Roles.java index 6beea441..0a7a574d 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/auth/Roles.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/auth/Roles.java @@ -1,7 +1,6 @@ package com.ugent.pidgeon.auth; import com.ugent.pidgeon.postgre.models.types.UserRole; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/auth/RolesInterceptor.java b/backend/app/src/main/java/com/ugent/pidgeon/auth/RolesInterceptor.java index f87246fe..bbb35d18 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/auth/RolesInterceptor.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/auth/RolesInterceptor.java @@ -7,17 +7,15 @@ import com.ugent.pidgeon.postgre.repository.UserRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; -import java.time.OffsetDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.logging.Logger; - /** * This class is a Spring component that implements the HandlerInterceptor interface. @@ -61,10 +59,10 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons if(userEntity == null) { System.out.println("User does not exist, creating new one. user_id: " + auth.getOid()); - userEntity = new UserEntity(auth.getUser().firstName,auth.getUser().lastName, auth.getEmail(), UserRole.student, auth.getOid()); + userEntity = new UserEntity(auth.getUser().firstName,auth.getUser().lastName, auth.getEmail(), UserRole.student, auth.getOid(), auth.getStudentNumber()); OffsetDateTime now = OffsetDateTime.now(); userEntity.setCreatedAt(now); - userRepository.save(userEntity); + userEntity = userRepository.save(userEntity); System.out.println("User created with id: " + userEntity.getId()); } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/config/AuthConfig.java b/backend/app/src/main/java/com/ugent/pidgeon/config/AuthConfig.java index 34d8eb81..4ed0c4f2 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/config/AuthConfig.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/config/AuthConfig.java @@ -6,14 +6,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; -import org.springframework.core.convert.converter.Converter; -import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; @Configuration diff --git a/backend/app/src/main/java/com/ugent/pidgeon/config/SecretsFileCheckConfiguration.java b/backend/app/src/main/java/com/ugent/pidgeon/config/SecretsFileCheckConfiguration.java index 70ee8209..4f6c7e27 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/config/SecretsFileCheckConfiguration.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/config/SecretsFileCheckConfiguration.java @@ -1,13 +1,11 @@ package com.ugent.pidgeon.config; +import javax.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; import org.springframework.core.io.ClassPathResource; -import javax.annotation.PostConstruct; - /** * We check if the application-secrets.properties file exists. If it does not exist, we throw an exception. */ diff --git a/backend/app/src/main/java/com/ugent/pidgeon/config/WebConfig.java b/backend/app/src/main/java/com/ugent/pidgeon/config/WebConfig.java index 0009ac5d..ea2dc330 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/config/WebConfig.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/config/WebConfig.java @@ -23,7 +23,9 @@ public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedMethods("*") .allowedOrigins("*") + .exposedHeaders("Content-Disposition") .allowedHeaders("*"); + } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ApiRoutes.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ApiRoutes.java index d28e6003..ab5886cc 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ApiRoutes.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ApiRoutes.java @@ -1,12 +1,11 @@ package com.ugent.pidgeon.controllers; public final class ApiRoutes { - public static final String USER_BASE_PATH = "/api/users"; + public static final String USERS_BASE_PATH = "/api/users"; public static final String COURSE_BASE_PATH = "/api/courses"; - public static final String DEADLINE_BASE_PATH = "/api/deadlines"; public static final String PROJECT_BASE_PATH = "/api/projects"; - + public static final String LOGGEDIN_USER_PATH = "/api/user"; public static final String SUBMISSION_BASE_PATH = "/api/submissions"; public static final String TEST_BASE_PATH = "/api/tests"; @@ -14,10 +13,4 @@ public final class ApiRoutes { public static final String GROUP_MEMBER_BASE_PATH = GROUP_BASE_PATH + "/{groupid}/members"; public static final String GROUP_FEEDBACK_PATH = PROJECT_BASE_PATH + "/{projectid}/groups/{groupid}/score"; public static final String CLUSTER_BASE_PATH = "/api/clusters"; - - public static final String USER_AUTH_PATH = "/api/auth"; - - public static final String GROUP_SCORE_PATH = GROUP_BASE_PATH + "/{groupid}/score"; - - } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ClusterController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ClusterController.java index 40692f85..ebfe8172 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ClusterController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ClusterController.java @@ -2,23 +2,43 @@ import com.ugent.pidgeon.auth.Roles; +import com.ugent.pidgeon.json.ClusterFillJson; +import com.ugent.pidgeon.json.GroupClusterCreateJson; +import com.ugent.pidgeon.json.GroupClusterJson; +import com.ugent.pidgeon.json.GroupClusterUpdateJson; +import com.ugent.pidgeon.json.GroupCreateJson; import com.ugent.pidgeon.model.Auth; -import com.ugent.pidgeon.model.json.*; import com.ugent.pidgeon.postgre.models.CourseEntity; import com.ugent.pidgeon.postgre.models.GroupClusterEntity; import com.ugent.pidgeon.postgre.models.GroupEntity; import com.ugent.pidgeon.postgre.models.types.CourseRelation; import com.ugent.pidgeon.postgre.models.types.UserRole; -import com.ugent.pidgeon.postgre.repository.*; -import com.ugent.pidgeon.util.*; +import com.ugent.pidgeon.postgre.repository.CourseUserRepository; +import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; +import com.ugent.pidgeon.postgre.repository.GroupMemberRepository; +import com.ugent.pidgeon.postgre.repository.GroupRepository; +import com.ugent.pidgeon.util.CheckResult; +import com.ugent.pidgeon.util.ClusterUtil; +import com.ugent.pidgeon.util.CommonDatabaseActions; +import com.ugent.pidgeon.util.CourseUtil; +import com.ugent.pidgeon.util.EntityToJsonConverter; +import com.ugent.pidgeon.util.Pair; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; - -import java.time.OffsetDateTime; -import java.util.List; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; @RestController public class ClusterController { @@ -28,7 +48,9 @@ public class ClusterController { @Autowired GroupRepository groupRepository; @Autowired - GroupUserRepository groupUserRepository; + GroupMemberRepository groupMemberRepository; + @Autowired + CourseUserRepository courseUserRepository; @Autowired @@ -58,10 +80,15 @@ public ResponseEntity getClustersForCourse(@PathVariable("courseid") Long cou if (checkResult.getStatus() != HttpStatus.OK) { return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } + + CourseRelation courseRelation = checkResult.getData().getSecond(); + boolean hideStudentNumber = courseRelation.equals(CourseRelation.enrolled); + // Get the clusters for the course List clusters = groupClusterRepository.findClustersWithoutInvidualByCourseId(courseid); List clusterJsons = clusters.stream().map( - entityToJsonConverter::clusterEntityToClusterJson).toList(); + g -> entityToJsonConverter.clusterEntityToClusterJson(g, hideStudentNumber) + ).toList(); // Return the clusters return ResponseEntity.ok(clusterJsons); } @@ -100,20 +127,21 @@ public ResponseEntity createClusterForCourse(@PathVariable("courseid") Long c clusterJson.groupCount() ); cluster.setCreatedAt(OffsetDateTime.now()); + cluster.setLockGroupsAfter(clusterJson.lockGroupsAfter()); GroupClusterEntity clusterEntity = groupClusterRepository.save(cluster); for (int i = 0; i < clusterJson.groupCount(); i++) { groupRepository.save(new GroupEntity("Group " + (i + 1), cluster.getId())); } - GroupClusterJson clusterJsonResponse = entityToJsonConverter.clusterEntityToClusterJson(clusterEntity); + GroupClusterJson clusterJsonResponse = entityToJsonConverter.clusterEntityToClusterJson(clusterEntity, false); // Return the cluster return ResponseEntity.status(HttpStatus.CREATED).body(clusterJsonResponse); } /** - * Returns all groups for a cluster + * Get cluster by ID * * @param clusterid identifier of a cluster * @param auth authentication object of the requesting user @@ -131,8 +159,11 @@ public ResponseEntity getCluster(@PathVariable("clusterid") Long clusterid, A return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } GroupClusterEntity cluster = checkResult.getData(); + + CheckResult courseAdmin = courseUtil.getCourseIfAdmin(cluster.getCourseId(), auth.getUserEntity()); + boolean hideStudentNumber = !courseAdmin.getStatus().equals(HttpStatus.OK); // Return the cluster - return ResponseEntity.ok(entityToJsonConverter.clusterEntityToClusterJson(cluster)); + return ResponseEntity.ok(entityToJsonConverter.clusterEntityToClusterJson(cluster, hideStudentNumber)); } @@ -167,10 +198,78 @@ public ResponseEntity doGroupClusterUpdate(GroupClusterEntity clusterEntity, } clusterEntity.setMaxSize(clusterJson.getCapacity()); clusterEntity.setName(clusterJson.getName()); - groupClusterRepository.save(clusterEntity); - return ResponseEntity.ok(entityToJsonConverter.clusterEntityToClusterJson(clusterEntity)); + clusterEntity.setLockGroupsAfter(clusterJson.getLockGroupsAfter()); + clusterEntity = groupClusterRepository.save(clusterEntity); + return ResponseEntity.ok(entityToJsonConverter.clusterEntityToClusterJson(clusterEntity, false)); } + /** + * Fills up the groups in a cluster by providing a map of groupids with lists of userids + * + * @param clusterid identifier of a cluster + * @param auth authentication object of the requesting user + * @param clusterFillMap Map object containing a map of all groups and their members of that cluster + * @return ResponseEntity + * @ApiDog apiDog documentation + * @HttpMethod PUT + * @ApiPath /api/clusters/{clusterid}/fill + * @AllowedRoles student, teacher + */ + @PutMapping(ApiRoutes.CLUSTER_BASE_PATH + "/{clusterid}/fill") + @Transactional + @Roles({UserRole.teacher, UserRole.student}) + public ResponseEntity fillCluster(@PathVariable("clusterid") Long clusterid, Auth auth, @RequestBody Map clusterFillMap) { + ClusterFillJson clusterFillJson = new ClusterFillJson(clusterFillMap); + try{ + CheckResult checkResult = clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(clusterid, auth.getUserEntity()); + + if (checkResult.getStatus() != HttpStatus.OK) { + return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); + } + + GroupClusterEntity groupCluster = checkResult.getData(); + + List groups = groupRepository.findAllByClusterId(clusterid); + + CheckResult jsonCheckRes = clusterUtil.checkFillClusterJson(clusterFillJson, groupCluster); + if (jsonCheckRes.getStatus() != HttpStatus.OK) { + return ResponseEntity.status(jsonCheckRes.getStatus()).body(jsonCheckRes.getMessage()); + } + + for(GroupEntity group: groups){ + commonDatabaseActions.removeGroup(group.getId()); + } + + for(String groupName: clusterFillJson.getClusterGroupMembers().keySet()){ + Long[] users = clusterFillJson.getClusterGroupMembers().get(groupName); + GroupEntity groupEntity = new GroupEntity(groupName, clusterid); + groupEntity = groupRepository.save(groupEntity); + for(Long userid: users){ + groupMemberRepository.addMemberToGroup(groupEntity.getId(), userid); + } + } + + groupCluster.setGroupAmount(clusterFillJson.getClusterGroupMembers().size()); + groupClusterRepository.save(groupCluster); + return ResponseEntity.status(HttpStatus.OK).body(entityToJsonConverter.clusterEntityToClusterJson(groupCluster, false)); + } catch (Exception e) { + Logger.getGlobal().severe(e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Something went wrong"); + } + } + + /** + * Updates a cluster + * + * @param clusterid identifier of a cluster + * @param auth authentication object of the requesting user + * @param clusterJson ClusterJson object containing the cluster data + * @return ResponseEntity + * @ApiDog apiDog documentation + * @HttpMethod PATCH + * @ApiPath /api/clusters/{clusterid} + * @AllowedRoles student, teacher + */ @PatchMapping(ApiRoutes.CLUSTER_BASE_PATH + "/{clusterid}") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity patchCluster(@PathVariable("clusterid") Long clusterid, Auth auth, @RequestBody GroupClusterUpdateJson clusterJson) { @@ -190,6 +289,10 @@ public ResponseEntity patchCluster(@PathVariable("clusterid") Long clusterid, clusterJson.setName(cluster.getName()); } + if (clusterJson.getLockGroupsAfter() == null) { + clusterJson.setLockGroupsAfter(cluster.getLockGroupsAfter()); + } + return doGroupClusterUpdate(cluster, clusterJson); } @@ -254,6 +357,6 @@ public ResponseEntity createGroupForCluster(@PathVariable("clusterid") Long c cluster.setGroupAmount(cluster.getGroupAmount() + 1); groupClusterRepository.save(cluster); - return ResponseEntity.status(HttpStatus.CREATED).body(entityToJsonConverter.groupEntityToJson(group)); + return ResponseEntity.status(HttpStatus.CREATED).body(entityToJsonConverter.groupEntityToJson(group, false)); } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java index 215d24e9..afe6fc63 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/CourseController.java @@ -1,24 +1,54 @@ package com.ugent.pidgeon.controllers; import com.ugent.pidgeon.auth.Roles; +import com.ugent.pidgeon.json.CourseJoinInformationJson; +import com.ugent.pidgeon.json.CourseJson; +import com.ugent.pidgeon.json.CourseMemberRequestJson; +import com.ugent.pidgeon.json.CourseWithRelationJson; +import com.ugent.pidgeon.json.RelationRequest; +import com.ugent.pidgeon.json.UserReferenceWithRelation; import com.ugent.pidgeon.model.Auth; import com.ugent.pidgeon.model.ProjectResponseJson; -import com.ugent.pidgeon.model.json.*; -import com.ugent.pidgeon.postgre.models.*; +import com.ugent.pidgeon.postgre.models.CourseEntity; +import com.ugent.pidgeon.postgre.models.CourseUserEntity; +import com.ugent.pidgeon.postgre.models.CourseUserId; +import com.ugent.pidgeon.postgre.models.GroupClusterEntity; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.CourseRelation; import com.ugent.pidgeon.postgre.models.types.UserRole; -import com.ugent.pidgeon.postgre.repository.*; -import com.ugent.pidgeon.util.*; +import com.ugent.pidgeon.postgre.repository.CourseRepository; +import com.ugent.pidgeon.postgre.repository.CourseUserRepository; +import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; +import com.ugent.pidgeon.postgre.repository.ProjectRepository; +import com.ugent.pidgeon.postgre.repository.UserRepository; +import com.ugent.pidgeon.util.CheckResult; +import com.ugent.pidgeon.util.CommonDatabaseActions; +import com.ugent.pidgeon.util.CourseUtil; +import com.ugent.pidgeon.util.EntityToJsonConverter; +import com.ugent.pidgeon.util.Pair; +import com.ugent.pidgeon.util.UserUtil; +import jakarta.validation.constraints.NotNull; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; - -import java.util.*; -import java.util.logging.Logger; -import java.time.OffsetDateTime; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController public class CourseController { @@ -56,10 +86,16 @@ public class CourseController { */ @GetMapping(ApiRoutes.COURSE_BASE_PATH) @Roles({UserRole.teacher, UserRole.student}) - public ResponseEntity getUserCourses(Auth auth) { + public ResponseEntity getUserCourses(Auth auth, @RequestParam(value = "archived", required = false) Boolean archived) { long userID = auth.getUserEntity().getId(); try { - List userCourses = userRepository.findCourseIdsByUserId(userID); + List userCourses = new ArrayList<>(); + if (archived == null || !archived) { + userCourses.addAll(userRepository.findCourseIdsByUserId(userID)); + } + if (archived == null || archived) { + userCourses.addAll(userRepository.findArchivedCoursesByUserId(userID)); + } // Retrieve course entities based on user courses List courseJSONObjects = userCourses.stream() @@ -73,7 +109,10 @@ public ResponseEntity getUserCourses(Auth auth) { ) .filter(Objects::nonNull) .toList(); + for (CourseWithRelationJson courseJson: courseJSONObjects) { + Logger.getGlobal().info("UserCourses: " + courseJson); + } // Return the JSON string in ResponseEntity return ResponseEntity.ok(courseJSONObjects); } catch (Exception e) { @@ -81,7 +120,6 @@ public ResponseEntity getUserCourses(Auth auth) { } } - /** * Function to create a new course * @@ -100,18 +138,19 @@ public ResponseEntity createCourse(@RequestBody CourseJson courseJson, Auth a UserEntity user = auth.getUserEntity(); long userId = user.getId(); - CheckResult courseJsonCheck = courseUtil.checkCourseJson(courseJson); + CheckResult courseJsonCheck = courseUtil.checkCourseJson(courseJson, user, null); if (courseJsonCheck.getStatus() != HttpStatus.OK) { return ResponseEntity.status(courseJsonCheck.getStatus()).body(courseJsonCheck.getMessage()); } // Create new course - CourseEntity courseEntity = new CourseEntity(courseJson.getName(), courseJson.getDescription()); + CourseEntity courseEntity = new CourseEntity(courseJson.getName(), courseJson.getDescription(), courseJson.getYear()); // Get current time and convert to SQL Timestamp OffsetDateTime currentTimestamp = OffsetDateTime.now(); courseEntity.setCreatedAt(currentTimestamp); + courseEntity.setJoinKey(UUID.randomUUID().toString()); // Save course - courseRepository.save(courseEntity); + courseEntity = courseRepository.save(courseEntity); // Add user as course creator CourseUserEntity courseUserEntity = new CourseUserEntity(courseEntity.getId(), userId, CourseRelation.creator); @@ -122,7 +161,7 @@ public ResponseEntity createCourse(@RequestBody CourseJson courseJson, Auth a groupClusterEntity.setCreatedAt(currentTimestamp); groupClusterRepository.save(groupClusterEntity); - return ResponseEntity.ok(entityToJsonConverter.courseEntityToCourseWithInfo(courseEntity, courseUtil.getJoinLink(courseEntity.getJoinKey(), "" + courseEntity.getId()))); + return ResponseEntity.ok(entityToJsonConverter.courseEntityToCourseWithInfo(courseEntity, courseUtil.getJoinLink(courseEntity.getJoinKey(), "" + courseEntity.getId()), false)); } catch (Exception e) { Logger.getLogger("CourseController").severe("Error while creating course: " + e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); @@ -130,15 +169,19 @@ public ResponseEntity createCourse(@RequestBody CourseJson courseJson, Auth a } - private ResponseEntity doCourseUpdate(CourseEntity courseEntity, CourseJson courseJson) { - CheckResult courseJsonCheck = courseUtil.checkCourseJson(courseJson); + private ResponseEntity doCourseUpdate(CourseEntity courseEntity, CourseJson courseJson, UserEntity user) { + CheckResult courseJsonCheck = courseUtil.checkCourseJson(courseJson, user, courseEntity.getId()); if (courseJsonCheck.getStatus() != HttpStatus.OK) { return ResponseEntity.status(courseJsonCheck.getStatus()).body(courseJsonCheck.getMessage()); } courseEntity.setName(courseJson.getName()); courseEntity.setDescription(courseJson.getDescription()); - courseRepository.save(courseEntity); - return ResponseEntity.ok(entityToJsonConverter.courseEntityToCourseWithInfo(courseEntity, courseUtil.getJoinLink(courseEntity.getJoinKey(), "" + courseEntity.getId()))); + courseEntity.setCourseYear(courseJson.getYear()); + if (courseJson.getArchived() != null) { + courseEntity.setArchivedAt(courseJson.getArchived() ? OffsetDateTime.now() : null); + } + courseEntity = courseRepository.save(courseEntity); + return ResponseEntity.ok(entityToJsonConverter.courseEntityToCourseWithInfo(courseEntity, courseUtil.getJoinLink(courseEntity.getJoinKey(), "" + courseEntity.getId()), false)); } /** @@ -165,12 +208,24 @@ public ResponseEntity updateCourse(@RequestBody CourseJson courseJson, @PathV } CourseEntity course = checkResult.getData(); - return doCourseUpdate(course, courseJson); + return doCourseUpdate(course, courseJson, user); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } + /** + * Function to update a course + * + * @param courseJson JSON object containing the course name and description + * @param courseId ID of the course to update + * @param auth authentication object of the requesting user + * @return ResponseEntity with the updated course entity + * @ApiDog apiDog documentation + * @HttpMethod PATCH + * @AllowedRoles teacher, student + * @ApiPath /api/courses/{courseId} + */ @PatchMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity patchCourse(@RequestBody CourseJson courseJson, @PathVariable long courseId, Auth auth) { @@ -182,10 +237,6 @@ public ResponseEntity patchCourse(@RequestBody CourseJson courseJson, @PathVa return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } - if (courseJson.getName() == null && courseJson.getDescription() == null) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Name or description is required"); - } - CourseEntity courseEntity = checkResult.getData(); if (courseJson.getName() == null) { courseJson.setName(courseEntity.getName()); @@ -193,8 +244,11 @@ public ResponseEntity patchCourse(@RequestBody CourseJson courseJson, @PathVa if (courseJson.getDescription() == null) { courseJson.setDescription(courseEntity.getDescription()); } + if (courseJson.getYear() == null) { + courseJson.setYear(courseEntity.getCourseYear()); + } - return doCourseUpdate(courseEntity, courseJson); + return doCourseUpdate(courseEntity, courseJson, user); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } @@ -219,12 +273,12 @@ public ResponseEntity getCourseByCourseId(@PathVariable long courseId, Auth a return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } CourseEntity course = checkResult.getData().getFirst(); + CourseRelation relation = checkResult.getData().getSecond(); - return ResponseEntity.ok(entityToJsonConverter.courseEntityToCourseWithInfo(course, courseUtil.getJoinLink(course.getJoinKey(), "" + course.getId()))); + return ResponseEntity.ok(entityToJsonConverter.courseEntityToCourseWithInfo(course, courseUtil.getJoinLink(course.getJoinKey(), "" + course.getId()), relation.equals(CourseRelation.enrolled))); } - /** * Function to delete a course by its ID * @@ -257,8 +311,10 @@ public ResponseEntity deleteCourse(@PathVariable long courseId, Auth auth) { } } + List clusters = groupClusterRepository.findByCourseId(courseId); + // Delete all groupclusters linked to the course - for (GroupClusterEntity groupCluster : groupClusterRepository.findByCourseId(courseId)) { + for (GroupClusterEntity groupCluster : clusters) { // We don't delete groupfeedback as these have been deleted with the projects CheckResult deleteResult = commonDatabaseActions.deleteClusterById(groupCluster.getId()); if (deleteResult.getStatus() != HttpStatus.OK) { @@ -266,8 +322,9 @@ public ResponseEntity deleteCourse(@PathVariable long courseId, Auth auth) { } } + Iterable courseUsers = courseUserRepository.findAllUsersByCourseId(courseId); // Delete all courseusers linked to the course - courseUserRepository.deleteAll(courseUserRepository.findAllUsersByCourseId(courseId)); + courseUserRepository.deleteAll(courseUsers); // Delete the course courseRepository.deleteById(courseId); @@ -304,7 +361,7 @@ public ResponseEntity getProjectsByCourseId(@PathVariable Long courseId, Auth if (relation.equals(CourseRelation.enrolled)) { projects = projects.stream().filter(ProjectEntity::isVisible).toList(); } - List projectResponseJsons = projects.stream().map(projectEntity -> + List projectResponseJsons = projects.stream().map(projectEntity -> entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, course, user) ).toList(); @@ -318,11 +375,14 @@ private ResponseEntity getJoinLinkPostResponseEntity(long courseId, String co return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } CourseEntity course = checkResult.getData(); - if (!commonDatabaseActions.createNewIndividualClusterGroup(courseId, user.getId())) { + if (course.getArchivedAt() != null) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Course is archived"); + } + if (!commonDatabaseActions.createNewIndividualClusterGroup(courseId, user)) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to add user to individual group, contact admin."); } courseUserRepository.save(new CourseUserEntity(courseId, user.getId(), CourseRelation.enrolled)); - return ResponseEntity.ok(entityToJsonConverter.courseEntityToCourseWithInfo(course, courseUtil.getJoinLink(course.getJoinKey(),"" + course.getId()))); + return ResponseEntity.ok(entityToJsonConverter.courseEntityToCourseWithInfo(course, courseUtil.getJoinLink(course.getJoinKey(), "" + course.getId()), false)); } private ResponseEntity getJoinLinkGetResponseEntity(long courseId, String courseKey, UserEntity user) { @@ -332,15 +392,18 @@ private ResponseEntity getJoinLinkGetResponseEntity(long courseId, String cou } CourseEntity course = checkResult.getData(); - CourseJson courseJson = new CourseJson(course.getName(), course.getDescription()); + if (course.getArchivedAt() != null) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Course is archived"); + } + CourseJoinInformationJson courseJson = new CourseJoinInformationJson(course.getName(), course.getDescription()); return ResponseEntity.ok(courseJson); } /** * Function to join course with key * - * @param auth authentication object of the requesting user - * @param courseId ID of the course to join + * @param auth authentication object of the requesting user + * @param courseId ID of the course to join * @param courseKey key of the course to join * @return ResponseEntity with a statuscode and no body * @ApiDog apiDog documentation @@ -357,8 +420,8 @@ public ResponseEntity joinCourse(Auth auth, @PathVariable Long courseId, @Pat /** * Function to get course information for joining course with key * - * @param auth authentication object of the requesting user - * @param courseId ID of the course to get the join key from + * @param auth authentication object of the requesting user + * @param courseId ID of the course to get the join key from * @param courseKey key of the course to get the join key from * @return ResponseEntity with a statuscode and a JSON object containing the course information * @ApiDog apiDog documentation @@ -368,14 +431,14 @@ public ResponseEntity joinCourse(Auth auth, @PathVariable Long courseId, @Pat */ @GetMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}/join/{courseKey}") @Roles({UserRole.student, UserRole.teacher}) - public ResponseEntity getCourseJoinKey(Auth auth, @PathVariable Long courseId, @PathVariable String courseKey) { + public ResponseEntity getCourseJoinInformation(Auth auth, @PathVariable Long courseId, @PathVariable String courseKey) { return getJoinLinkGetResponseEntity(courseId, courseKey, auth.getUserEntity()); } /** * Function to join course without key * - * @param auth authentication object of the requesting user + * @param auth authentication object of the requesting user * @param courseId ID of the course to join * @return ResponseEntity with a statuscode and no body * @ApiDog apiDog documentation @@ -392,17 +455,17 @@ public ResponseEntity joinCourse(Auth auth, @PathVariable Long courseId) { /** * Function to get course information for joining course without key * - * @param auth authentication object of the requesting user + * @param auth authentication object of the requesting user * @param courseId ID of the course to get the join key from * @return ResponseEntity with a statuscode and a JSON object containing the course information - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @HttpMethod GET * @AllowedRoles teacher, student * @ApiPath /api/courses/{courseId}/join */ @GetMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}/join") @Roles({UserRole.student, UserRole.teacher}) - public ResponseEntity getCourseJoinKey(Auth auth, @PathVariable Long courseId) { + public ResponseEntity getCourseJoinInformation(Auth auth, @PathVariable Long courseId) { return getJoinLinkGetResponseEntity(courseId, null, auth.getUserEntity()); } @@ -410,7 +473,7 @@ public ResponseEntity getCourseJoinKey(Auth auth, @PathVariable Long courseId * Function to leave a course * * @param courseId ID of the course to leave - * @param auth authentication object of the requesting user + * @param auth authentication object of the requesting user * @return ResponseEntity with a statuscode and no body * @ApiDog apiDog documentation * @HttpMethod DELETE @@ -423,20 +486,7 @@ public ResponseEntity leaveCourse(@PathVariable long courseId, Auth auth) { try { long userId = auth.getUserEntity().getId(); CheckResult checkResult = courseUtil.canLeaveCourse(courseId, auth.getUserEntity()); - if (!checkResult.getStatus().equals(HttpStatus.OK)) { - return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); - } - CourseRelation userRelation = checkResult.getData(); - - // Delete the user from the course - courseUserRepository.deleteById(new CourseUserId(courseId, userId)); - if (userRelation.equals(CourseRelation.enrolled)) { - if (!commonDatabaseActions.removeIndividualClusterGroup(courseId, userId)) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to remove user from individual group, contact admin."); - } - } - - return ResponseEntity.ok().build(); + return doRemoveFromCourse(courseId, userId, checkResult); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } @@ -445,9 +495,9 @@ public ResponseEntity leaveCourse(@PathVariable long courseId, Auth auth) { /** * Function to remove a different user from a course * - * @param auth authentication object of the requesting user + * @param auth authentication object of the requesting user * @param courseId ID of the course to leave - * @param userId JSON object containing the user id + * @param userId JSON object containing the user id * @return ResponseEntity with a statuscode and no body * @ApiDog apiDog documentation * @HttpMethod DELETE @@ -458,6 +508,14 @@ public ResponseEntity leaveCourse(@PathVariable long courseId, Auth auth) { @Roles({UserRole.teacher, UserRole.admin, UserRole.student}) public ResponseEntity removeCourseMember(Auth auth, @PathVariable Long courseId, @PathVariable Long userId) { CheckResult checkResult = courseUtil.canDeleteUser(courseId, userId, auth.getUserEntity()); + return doRemoveFromCourse(courseId, userId, checkResult); + } + + @NotNull + private ResponseEntity doRemoveFromCourse( + Long courseId, + Long userId, + CheckResult checkResult) { if (!checkResult.getStatus().equals(HttpStatus.OK)) { return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } @@ -475,11 +533,11 @@ public ResponseEntity removeCourseMember(Auth auth, @PathVariable Long course /** * Function to add a different user to a course * - * @param auth authentication object of the requesting user + * @param auth authentication object of the requesting user * @param courseId ID of the course to add the user to - * @param request JSON object containing the user id and relation + * @param request JSON object containing the user id and relation * @return ResponseEntity with a statuscode and no body - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @HttpMethod POST * @AllowedRoles teacher, admin, student * @ApiPath /api/courses/{courseId}/members @@ -495,7 +553,11 @@ public ResponseEntity addCourseMember(Auth auth, @PathVariable Long courseId, courseUserRepository.save(new CourseUserEntity(courseId, request.getUserId(), request.getRelationAsEnum())); if (request.getRelationAsEnum().equals(CourseRelation.enrolled)) { - boolean succesful = commonDatabaseActions.createNewIndividualClusterGroup(courseId, request.getUserId()); + UserEntity user = userUtil.getUserIfExists(request.getUserId()); + if (user == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found"); + } + boolean succesful = commonDatabaseActions.createNewIndividualClusterGroup(courseId, user); if (!succesful) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to add user to individual group, contact admin."); } @@ -506,9 +568,9 @@ public ResponseEntity addCourseMember(Auth auth, @PathVariable Long courseId, /** * Function to update the relation of a user in a course * - * @param auth authentication object of the requesting user + * @param auth authentication object of the requesting user * @param courseId ID of the course to update the user in - * @param request JSON object containing the user id and relation + * @param request JSON object containing the user id and relation * @return ResponseEntity with a statuscode and no body * @ApiDog apiDog documentation * @HttpMethod PATCH @@ -527,22 +589,34 @@ public ResponseEntity updateCourseMember(Auth auth, @PathVariable Long course return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } CourseUserEntity courseUserEntity = checkResult.getData(); - courseUserEntity.setRelation(request.getRelationAsEnum()); - courseUserRepository.save(courseUserEntity); + if (courseUserEntity.getRelation().equals(request.getRelationAsEnum())) { + return ResponseEntity.ok().build(); + } + if (request.getRelationAsEnum().equals(CourseRelation.enrolled)) { - commonDatabaseActions.createNewIndividualClusterGroup(courseId, requestwithid.getUserId()); - } else { + UserEntity user = userUtil.getUserIfExists(requestwithid.getUserId()); + if (user == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found"); + } + if (!commonDatabaseActions.createNewIndividualClusterGroup(courseId, user)) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to add user to individual group, contact admin."); + } + } else if (courseUserEntity.getRelation().equals(CourseRelation.enrolled)){ if (!commonDatabaseActions.removeIndividualClusterGroup(courseId, requestwithid.getUserId())) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to remove user from individual group, contact admin."); } } + + courseUserEntity.setRelation(request.getRelationAsEnum()); + courseUserRepository.save(courseUserEntity); + return ResponseEntity.ok().build(); } /** * Function to get all members of a course * - * @param auth authentication object of the requesting user + * @param auth authentication object of the requesting user * @param courseId ID of the course to get the members from * @return ResponseEntity with a JSON object containing the members of the course * @ApiDog apiDog documentation @@ -558,6 +632,8 @@ public ResponseEntity getCourseMembers(Auth auth, @PathVariable Long courseId return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } + boolean hideStudentNumber = checkResult.getData().getSecond().equals(CourseRelation.enrolled); + List members = courseUserRepository.findAllMembers(courseId); List memberJson = members.stream(). map(cue -> { @@ -565,7 +641,7 @@ public ResponseEntity getCourseMembers(Auth auth, @PathVariable Long courseId if (user == null) { return null; } - return entityToJsonConverter.userEntityToUserReferenceWithRelation(user, cue.getRelation()); + return entityToJsonConverter.userEntityToUserReferenceWithRelation(user, cue.getRelation(), hideStudentNumber); }). filter(Objects::nonNull).toList(); @@ -575,7 +651,7 @@ public ResponseEntity getCourseMembers(Auth auth, @PathVariable Long courseId /** * Function to get the join link of a course * - * @param auth authentication object of the requesting user + * @param auth authentication object of the requesting user * @param courseId ID of the course to get the join link from * @return ResponseEntity with the join link of the course * @ApiDog apiDog documentation @@ -584,20 +660,21 @@ public ResponseEntity getCourseMembers(Auth auth, @PathVariable Long courseId * @ApiPath /api/courses/{courseId}/joinLink */ @Roles({UserRole.teacher, UserRole.student}) - @GetMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}/joinLink") + @GetMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}/joinKey") public ResponseEntity getCourseKey(Auth auth, @PathVariable Long courseId) { CheckResult checkResult = courseUtil.getCourseIfAdmin(courseId, auth.getUserEntity()); if (checkResult.getStatus() != HttpStatus.OK) { return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } - return ResponseEntity.ok(courseUtil.getJoinLink(checkResult.getData().getJoinKey(), courseId.toString())); + return ResponseEntity.ok(checkResult.getData().getJoinKey()); } // Function for invalidating the previous key and generating a new one, can be useful when starting a new year. + /** * Function to generate a new join link for a course * - * @param auth authentication object of the requesting user + * @param auth authentication object of the requesting user * @param courseId ID of the course to generate the join link for * @return ResponseEntity with the new join link of the course * @ApiDog apiDog documentation @@ -606,7 +683,7 @@ public ResponseEntity getCourseKey(Auth auth, @PathVariable Long courseI * @ApiPath /api/courses/{courseId}/joinLink */ @Roles({UserRole.teacher, UserRole.student}) - @PutMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}/joinLink") + @PutMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}/joinKey") public ResponseEntity getAndGenerateCourseKey(Auth auth, @PathVariable Long courseId) { CheckResult checkResult = courseUtil.getCourseIfAdmin(courseId, auth.getUserEntity()); if (checkResult.getStatus() != HttpStatus.OK) { @@ -617,13 +694,13 @@ public ResponseEntity getAndGenerateCourseKey(Auth auth, @PathVariable Long c String key = UUID.randomUUID().toString(); course.setJoinKey(key); courseRepository.save(course); - return ResponseEntity.ok(courseUtil.getJoinLink(key, courseId.toString())); + return ResponseEntity.ok(key); } /** * Function to remove the joinKey from the joinLink of a course * - * @param auth authentication object of the requesting user + * @param auth authentication object of the requesting user * @param courseId ID of the course to remove the join link from * @return ResponseEntity with the new join link of the course (without the key) * @ApiDog apiDog documentation @@ -632,7 +709,7 @@ public ResponseEntity getAndGenerateCourseKey(Auth auth, @PathVariable Long c * @ApiPath /api/courses/{courseId}/joinLink */ @Roles({UserRole.teacher, UserRole.student}) - @DeleteMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}/joinLink") + @DeleteMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}/joinKey") public ResponseEntity deleteCourseKey(Auth auth, @PathVariable Long courseId) { CheckResult checkResult = courseUtil.getCourseIfAdmin(courseId, auth.getUserEntity()); if (checkResult.getStatus() != HttpStatus.OK) { @@ -640,7 +717,48 @@ public ResponseEntity deleteCourseKey(Auth auth, @PathVariable Long cour } CourseEntity course = checkResult.getData(); course.setJoinKey(null); - return ResponseEntity.ok(courseUtil.getJoinLink(null, courseId.toString())); + courseRepository.save(course); + return ResponseEntity.ok(""); } -} \ No newline at end of file + /** + * Function to copy a course + * + * @param courseId ID of the course to copy + * @param auth authentication object of the requesting user + * @return ResponseEntity with the copied course entity + * @ApiDog apiDog documentation + * @HttpMethod POST + * @AllowedRoles teacher + * @ApiPath /api/courses/{courseId}/copy + */ + @PostMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}/copy") + @Roles({UserRole.teacher}) + @Transactional + public ResponseEntity copyCourse(@PathVariable long courseId, Auth auth) { + try { + CheckResult> checkResult = courseUtil.getCourseIfUserInCourse(courseId, auth.getUserEntity()); + if (checkResult.getStatus() != HttpStatus.OK) { + return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); + } + if (!checkResult.getData().getSecond().equals(CourseRelation.creator)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Only the creator of a course can copy it"); + } + + CourseEntity course = checkResult.getData().getFirst(); + + CheckResult copyCheckRes = commonDatabaseActions.copyCourse(course, auth.getUserEntity().getId()); + if (copyCheckRes.getStatus() != HttpStatus.OK) { + return ResponseEntity.status(copyCheckRes.getStatus()).body(copyCheckRes.getMessage()); + } + CourseEntity newCourse = copyCheckRes.getData(); + + return ResponseEntity.status(HttpStatus.CREATED).body(entityToJsonConverter.courseEntityToCourseWithInfo(newCourse, courseUtil.getJoinLink(newCourse.getJoinKey(), "" + newCourse.getId()), false)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + + +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupController.java index 14bd4b7e..d017c0df 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupController.java @@ -1,21 +1,29 @@ package com.ugent.pidgeon.controllers; import com.ugent.pidgeon.auth.Roles; +import com.ugent.pidgeon.json.GroupJson; +import com.ugent.pidgeon.json.NameRequest; import com.ugent.pidgeon.model.Auth; -import com.ugent.pidgeon.model.json.GroupJson; -import com.ugent.pidgeon.model.json.NameRequest; import com.ugent.pidgeon.postgre.models.GroupEntity; import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.GroupRepository; import com.ugent.pidgeon.util.CheckResult; +import com.ugent.pidgeon.util.ClusterUtil; import com.ugent.pidgeon.util.CommonDatabaseActions; +import com.ugent.pidgeon.util.CourseUtil; import com.ugent.pidgeon.util.EntityToJsonConverter; import com.ugent.pidgeon.util.GroupUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; @RestController @@ -28,13 +36,21 @@ public class GroupController { private EntityToJsonConverter entityToJsonConverter; @Autowired private CommonDatabaseActions commonDatabaseActions; + @Autowired + private ClusterUtil clusterUtil; + @Autowired + private CourseUtil courseUtil; /** * Function to get a group by its identifier - * @param groupid - * @param auth - * @return + * @param groupid identifier of a group + * @param auth authentication object of the requesting user + * @return ResponseEntity + * @ApiDog apiDog documentation + * @HttpMethod GET + * @AllowedRoles student, teacher + * @ApiPath /api/groups/{groupid} */ @GetMapping(ApiRoutes.GROUP_BASE_PATH + "/{groupid}") @Roles({UserRole.student, UserRole.teacher}) @@ -50,8 +66,14 @@ public ResponseEntity getGroupById(@PathVariable("groupid") Long groupid, Aut return ResponseEntity.status(checkResult1.getStatus()).body(checkResult1.getMessage()); } + boolean hideStudentNumber = true; + CheckResult adminCheck = groupUtil.isAdminOfGroup(groupid, auth.getUserEntity()); + if (adminCheck.getStatus().equals(HttpStatus.OK)) { + hideStudentNumber = false; + } + // Return the group - GroupJson groupJson = entityToJsonConverter.groupEntityToJson(group); + GroupJson groupJson = entityToJsonConverter.groupEntityToJson(group, hideStudentNumber); return ResponseEntity.ok(groupJson); } @@ -63,7 +85,7 @@ public ResponseEntity getGroupById(@PathVariable("groupid") Long groupid, Aut * @param auth authentication object of the requesting user * @return ResponseEntity * @ApiDog apiDog documentation - * @HttpMethod Put + * @HttpMethod PUT * @AllowedRoles teacher * @ApiPath /api/groups/{groupid} */ @@ -81,7 +103,7 @@ public ResponseEntity updateGroupName(@PathVariable("groupid") Long groupid, * @param auth authentication object of the requesting user * @return ResponseEntity * @ApiDog apiDog documentation - * @HttpMethod Patch + * @HttpMethod PATCH * @AllowedRoles teacher * @ApiPath /api/groups/{groupid} */ @@ -113,7 +135,7 @@ private ResponseEntity doGroupNameUpdate(Long groupid, NameRequest nameReques groupRepository.save(group); // Return the updated group - GroupJson groupJson = entityToJsonConverter.groupEntityToJson(group); + GroupJson groupJson = entityToJsonConverter.groupEntityToJson(group, false); return ResponseEntity.ok(groupJson); } @@ -124,7 +146,7 @@ private ResponseEntity doGroupNameUpdate(Long groupid, NameRequest nameReques * @param auth authentication object of the requesting user * @return ResponseEntity * @ApiDog apiDog documentation - * @HttpMethod Delete + * @HttpMethod DELETE * @AllowedRoles teacher, student * @ApiPath /api/groups/{groupid} */ @@ -136,7 +158,9 @@ public ResponseEntity deleteGroup(@PathVariable("groupid") Long groupid, Auth return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } - commonDatabaseActions.removeGroup(groupid); + if (!commonDatabaseActions.removeGroup(groupid)) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error deleting group"); + } // Return 204 return ResponseEntity.status(HttpStatus.NO_CONTENT).body("Group deleted"); } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupFeedbackController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupFeedbackController.java index 8d459179..ab22a527 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupFeedbackController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupFeedbackController.java @@ -1,14 +1,18 @@ package com.ugent.pidgeon.controllers; import com.ugent.pidgeon.auth.Roles; -import com.ugent.pidgeon.model.json.GroupFeedbackJsonWithProject; -import com.ugent.pidgeon.model.json.UpdateGroupScoreRequest; +import com.ugent.pidgeon.json.GroupFeedbackJsonWithProject; +import com.ugent.pidgeon.json.UpdateGroupScoreRequest; import com.ugent.pidgeon.model.Auth; -import com.ugent.pidgeon.model.json.GroupFeedbackJson; -import com.ugent.pidgeon.postgre.models.*; +import com.ugent.pidgeon.postgre.models.CourseEntity; +import com.ugent.pidgeon.postgre.models.GroupFeedbackEntity; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.CourseRelation; import com.ugent.pidgeon.postgre.models.types.UserRole; -import com.ugent.pidgeon.postgre.repository.*; +import com.ugent.pidgeon.postgre.repository.GroupFeedbackRepository; +import com.ugent.pidgeon.postgre.repository.GroupRepository; +import com.ugent.pidgeon.postgre.repository.ProjectRepository; import com.ugent.pidgeon.util.CheckResult; import com.ugent.pidgeon.util.CourseUtil; import com.ugent.pidgeon.util.EntityToJsonConverter; @@ -16,13 +20,19 @@ import com.ugent.pidgeon.util.GroupUtil; import com.ugent.pidgeon.util.Pair; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; @RestController @@ -36,12 +46,12 @@ public class GroupFeedbackController { private GroupUtil groupUtil; @Autowired private EntityToJsonConverter entityToJsonConverter; - @Autowired - private ProjectRepository projectRepository; - @Autowired - private GroupRepository groupRepository; - @Autowired - private CourseUtil courseUtil; + @Autowired + private ProjectRepository projectRepository; + @Autowired + private GroupRepository groupRepository; + @Autowired + private CourseUtil courseUtil; /** * Function to update the score of a group @@ -52,7 +62,7 @@ public class GroupFeedbackController { * @param auth authentication object of the requesting user * @return ResponseEntity * @ApiDog apiDog documentation - * @HttpMethod Patch + * @HttpMethod PATCH * @AllowedRoles teacher, student * @ApiPath /api/projects/{projectid}/groups/{groupid}/score */ @@ -83,6 +93,18 @@ public ResponseEntity updateGroupScore(@PathVariable("groupid") long groupId, return doGroupFeedbackUpdate(groupFeedbackEntity, request); } + /** + * Function to delete the score of a group + * + * @param groupId identifier of a group + * @param projectId identifier of a project + * @param auth authentication object of the requesting user + * @return ResponseEntity + * @ApiDog apiDog documentation + * @HttpMethod Delete + * @AllowedRoles teacher, student + * @ApiPath /api/projects/{projectid}/groups/{groupid}/score + */ @DeleteMapping(ApiRoutes.GROUP_FEEDBACK_PATH) @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity deleteGroupScore(@PathVariable("groupid") long groupId, @PathVariable("projectid") long projectId, Auth auth) { @@ -99,6 +121,19 @@ public ResponseEntity deleteGroupScore(@PathVariable("groupid") long groupId, } } + /** + * Function to update the score of a group + * + * @param groupId identifier of a group + * @param projectId identifier of a project + * @param request request object containing the new score + * @param auth authentication object of the requesting user + * @return ResponseEntity + * @ApiDog apiDog documentation + * @HttpMethod PUT + * @AllowedRoles teacher, student + * @ApiPath /api/projects/{projectid}/groups/{groupid}/score + */ @PutMapping(ApiRoutes.GROUP_FEEDBACK_PATH) @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity updateGroupScorePut(@PathVariable("groupid") long groupId, @PathVariable("projectid") long projectId, @RequestBody UpdateGroupScoreRequest request, Auth auth) { @@ -136,8 +171,8 @@ public ResponseEntity doGroupFeedbackUpdate(GroupFeedbackEntity groupFeedback * @param request request object containing the new score * @param auth authentication object of the requesting user * @return ResponseEntity - * @ApiDog apiDog documentation - * @HttpMethod Post + * @ApiDog apiDog documentation + * @HttpMethod POST * @AllowedRoles teacher, student * @ApiPath /api/groups/{groupid}/projects/{projectid}/feedback */ @@ -174,7 +209,7 @@ public ResponseEntity addGroupScore(@PathVariable("groupid") long groupId, @P * @param projectId identifier of a project * @param auth authentication object of the requesting user * @return ResponseEntity - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @HttpMethod Get * @AllowedRoles teacher, student * @ApiPath /api/projects/{projectid}/groups/{groupid}/score @@ -203,6 +238,17 @@ public ResponseEntity getGroupScore(@PathVariable("groupid") long groupI return ResponseEntity.ok(entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity)); } + /** + * Function to get the grades of a course + * + * @param courseId identifier of a course + * @param auth authentication object of the requesting user + * @return ResponseEntity + * @ApiDog apiDog documentation + * @HttpMethod Get + * @AllowedRoles teacher, student + * @ApiPath /api/courses/{courseId}/grades + */ @GetMapping(ApiRoutes.COURSE_BASE_PATH + "/{courseId}/grades") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity getCourseGrades(@PathVariable("courseId") long courseId, Auth auth) { @@ -222,19 +268,19 @@ public ResponseEntity getCourseGrades(@PathVariable("courseId") long courseId List grades = new ArrayList<>(); for (ProjectEntity project : projects) { - Long GroupId = groupRepository.groupIdByProjectAndUser(project.getId(), user.getId()); - if (GroupId == null) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("You are not part of this course"); - } - CheckResult checkResult = groupFeedbackUtil.getGroupFeedbackIfExists(GroupId, project.getId()); - if (checkResult.getStatus() != HttpStatus.OK) { - grades.add(entityToJsonConverter.groupFeedbackEntityToJsonWithProject(null, project)); + Long groupId = groupRepository.groupIdByProjectAndUser(project.getId(), user.getId()); + if (groupId == null) { // Student not yet in a group for this project + grades.add(entityToJsonConverter.groupFeedbackEntityToJsonWithProject(null, project)); } else { + CheckResult checkResult = groupFeedbackUtil.getGroupFeedbackIfExists(groupId, project.getId()); + if (checkResult.getStatus() != HttpStatus.OK) { + grades.add(entityToJsonConverter.groupFeedbackEntityToJsonWithProject(null, project)); + } else { GroupFeedbackEntity groupFeedbackEntity = checkResult.getData(); grades.add(entityToJsonConverter.groupFeedbackEntityToJsonWithProject(groupFeedbackEntity, project)); + } } } - return ResponseEntity.ok(grades); } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupMemberController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupMemberController.java index a56578ce..99c28ace 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupMemberController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/GroupMemberController.java @@ -1,24 +1,25 @@ package com.ugent.pidgeon.controllers; import com.ugent.pidgeon.auth.Roles; +import com.ugent.pidgeon.json.UserReferenceJson; import com.ugent.pidgeon.model.Auth; -import com.ugent.pidgeon.model.json.UserJson; -import com.ugent.pidgeon.model.json.UserReferenceJson; -import com.ugent.pidgeon.postgre.models.GroupEntity; import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.UserRole; -import com.ugent.pidgeon.postgre.repository.*; +import com.ugent.pidgeon.postgre.repository.GroupMemberRepository; import com.ugent.pidgeon.util.CheckResult; import com.ugent.pidgeon.util.EntityToJsonConverter; import com.ugent.pidgeon.util.GroupUtil; -import com.ugent.pidgeon.util.UserUtil; +import java.util.List; +import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.logging.Logger; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; @RestController public class GroupMemberController { @@ -45,6 +46,7 @@ public class GroupMemberController { */ @DeleteMapping(ApiRoutes.GROUP_MEMBER_BASE_PATH + "/{memberid}") @Roles({UserRole.teacher, UserRole.student}) + @Transactional public ResponseEntity removeMemberFromGroup(@PathVariable("groupid") long groupId, @PathVariable("memberid") long memberid, Auth auth) { UserEntity user = auth.getUserEntity(); CheckResult check = groupUtil.canRemoveUserFromGroup(groupId, memberid, user); @@ -64,7 +66,7 @@ public ResponseEntity removeMemberFromGroup(@PathVariable("groupid") lon * @param groupId ID of the group to remove the member from * @param auth authentication object of the requesting user * @return ResponseEntity with a string message about the operation result - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @HttpMethod DELETE * @AllowedRoles teacher, student * @ApiPath /api/groups/{groupid}/members @@ -109,7 +111,9 @@ public ResponseEntity addMemberToGroup(@PathVariable("groupid") long gro try { groupMemberRepository.addMemberToGroup(groupId, memberid); List members = groupMemberRepository.findAllMembersByGroupId(groupId); - List response = members.stream().map(entityToJsonConverter::userEntityToUserReference).toList(); + List response = members.stream().map( + u -> entityToJsonConverter.userEntityToUserReference(u, false) + ).toList(); return ResponseEntity.ok(response); } catch (Exception e) { Logger.getGlobal().severe(e.getMessage()); @@ -124,7 +128,7 @@ public ResponseEntity addMemberToGroup(@PathVariable("groupid") long gro * @param groupId ID of the group to add the member to * @param auth authentication object of the requesting user * @return ResponseEntity with a list of UserJson objects containing the members of the group - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @HttpMethod POST * @AllowedRoles teacher, student * @ApiPath /api/groups/{groupid}/members @@ -141,7 +145,9 @@ public ResponseEntity addMemberToGroupInferred(@PathVariable("groupid") try { groupMemberRepository.addMemberToGroup(groupId,user.getId()); List members = groupMemberRepository.findAllMembersByGroupId(groupId); - List response = members.stream().map(entityToJsonConverter::userEntityToUserReference).toList(); + List response = members.stream().map( + u -> entityToJsonConverter.userEntityToUserReference(u, true) + ).toList(); return ResponseEntity.ok(response); } catch (Exception e) { Logger.getGlobal().severe(e.getMessage()); @@ -169,8 +175,12 @@ public ResponseEntity findAllMembersByGroupId(@PathVariable("groupid") l return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } + boolean hideStudentnumber = !groupUtil.isAdminOfGroup(groupId, user).getStatus().equals(HttpStatus.OK); + List members = groupMemberRepository.findAllMembersByGroupId(groupId); - List response = members.stream().map((UserEntity e) -> entityToJsonConverter.userEntityToUserReference(e)).toList(); + List response = members.stream().map( + (UserEntity e) -> entityToJsonConverter.userEntityToUserReference(e, hideStudentnumber)) + .toList(); return ResponseEntity.ok(response); } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java index 53058ab6..fe3d258e 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java @@ -1,22 +1,46 @@ package com.ugent.pidgeon.controllers; import com.ugent.pidgeon.auth.Roles; +import com.ugent.pidgeon.json.GroupJson; +import com.ugent.pidgeon.json.ProjectJson; +import com.ugent.pidgeon.json.ProjectResponseJsonWithStatus; +import com.ugent.pidgeon.json.UserProjectsJson; import com.ugent.pidgeon.model.Auth; import com.ugent.pidgeon.model.ProjectResponseJson; -import com.ugent.pidgeon.model.json.*; - -import com.ugent.pidgeon.postgre.models.*; +import com.ugent.pidgeon.postgre.models.CourseEntity; +import com.ugent.pidgeon.postgre.models.GroupClusterEntity; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.CourseRelation; import com.ugent.pidgeon.postgre.models.types.UserRole; -import com.ugent.pidgeon.postgre.repository.*; -import com.ugent.pidgeon.util.*; +import com.ugent.pidgeon.postgre.repository.CourseRepository; +import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; +import com.ugent.pidgeon.postgre.repository.GroupRepository; +import com.ugent.pidgeon.postgre.repository.ProjectRepository; +import com.ugent.pidgeon.util.CheckResult; +import com.ugent.pidgeon.util.ClusterUtil; +import com.ugent.pidgeon.util.CommonDatabaseActions; +import com.ugent.pidgeon.util.CourseUtil; +import com.ugent.pidgeon.util.EntityToJsonConverter; +import com.ugent.pidgeon.util.Pair; +import com.ugent.pidgeon.util.ProjectUtil; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.*; -import java.util.logging.Logger; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; @@ -49,7 +73,7 @@ public class ProjectController { /** * Function to get all projects of a user * @param auth authentication object of the requesting user - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @HttpMethod GET * @AllowedRoles teacher, student * @ApiPath /api/projects @@ -74,6 +98,10 @@ public ResponseEntity getProjects(Auth auth) { CourseRelation relation = courseCheck.getData().getSecond(); if (relation.equals(CourseRelation.enrolled)) { + if (project.getVisibleAfter() != null && project.getVisibleAfter().isBefore(OffsetDateTime.now())) { + project.setVisible(true); + projectRepository.save(project); + } if (project.isVisible()) { enrolledProjects.add(entityToJsonConverter.projectEntityToProjectResponseJsonWithStatus(project, course, user)); } @@ -82,7 +110,7 @@ public ResponseEntity getProjects(Auth auth) { } } - return ResponseEntity.ok().body(new userProjectsJson(enrolledProjects, adminProjects)); + return ResponseEntity.ok().body(new UserProjectsJson(enrolledProjects, adminProjects)); } @@ -113,6 +141,11 @@ public ResponseEntity getProjectById(@PathVariable Long projectId, Auth auth) } CourseEntity course = courseCheck.getData().getFirst(); CourseRelation relation = courseCheck.getData().getSecond(); + + if (project.getVisibleAfter() != null && project.getVisibleAfter().isBefore(OffsetDateTime.now())) { + project.setVisible(true); + projectRepository.save(project); + } if (!project.isVisible() && relation.equals(CourseRelation.enrolled)) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Project not found"); } @@ -161,6 +194,8 @@ public ResponseEntity createProject( projectJson.getGroupClusterId(), null, projectJson.isVisible(), projectJson.getMaxScore(), projectJson.getDeadline()); + project.setVisibleAfter(projectJson.getVisibleAfter()); + // Save the project entity ProjectEntity savedProject = projectRepository.save(project); CourseEntity courseEntity = checkAcces.getData(); @@ -180,6 +215,10 @@ private ResponseEntity doProjectUpdate(ProjectEntity project, ProjectJson pro project.setDeadline(projectJson.getDeadline()); project.setMaxScore(projectJson.getMaxScore()); project.setVisible(projectJson.isVisible()); + project.setVisibleAfter(projectJson.getVisibleAfter()); + if (project.getVisibleAfter() != null && project.getVisibleAfter().isBefore(OffsetDateTime.now())) { + project.setVisible(true); + } projectRepository.save(project); return ResponseEntity.ok(entityToJsonConverter.projectEntityToProjectResponseJson(project, courseRepository.findById(project.getCourseId()).get(), user)); } @@ -190,7 +229,7 @@ private ResponseEntity doProjectUpdate(ProjectEntity project, ProjectJson pro * @param projectJson ProjectUpdateDTO object containing the new project's information * @param auth authentication object of the requesting user * @ApiDog apiDog documentation - * @HttpMethod Put + * @HttpMethod PUT * @AllowedRoles teacher * @ApiPath /api/projects/{projectId} * @return ResponseEntity with the created project @@ -227,7 +266,7 @@ public ResponseEntity putProjectById(@PathVariable Long projectId, @RequestBo * @param projectJson ProjectUpdateDTO object containing the new project's information * @param auth authentication object of the requesting user * @ApiDog apiDog documentation - * @HttpMethod Patch + * @HttpMethod PATCH * @AllowedRoles teacher * @ApiPath /api/projects/{projectId} * @return ResponseEntity with the created project @@ -261,6 +300,10 @@ public ResponseEntity patchProjectById(@PathVariable Long projectId, @Request projectJson.setVisible(project.isVisible()); } + if (projectJson.getVisibleAfter() == null) { + projectJson.setVisibleAfter(project.getVisibleAfter()); + } + CheckResult checkProject = projectUtil.checkProjectJson(projectJson, project.getCourseId()); if (checkProject.getStatus() != HttpStatus.OK) { return ResponseEntity.status(checkProject.getStatus()).body(checkProject.getMessage()); @@ -292,11 +335,15 @@ public ResponseEntity getGroupsOfProject(@PathVariable Long projectId, Auth a "No groups for this project: use " + memberUrl + " to get the members of the course"); } + boolean hideStudentNumber; + CheckResult adminCheck = projectUtil.isProjectAdmin(projectId, auth.getUserEntity()); + hideStudentNumber = !adminCheck.getStatus().equals(HttpStatus.OK); + List groups = projectRepository.findGroupIdsByProjectId(projectId); List groupjsons = groups.stream() - .map((Long id) -> { - return groupRepository.findById(id).orElse(null); - }).filter(Objects::nonNull).map(entityToJsonConverter::groupEntityToJson).toList(); + .map((Long id) -> groupRepository.findById(id).orElse(null)).filter(Objects::nonNull).map( + g -> entityToJsonConverter.groupEntityToJson(g, hideStudentNumber)) + .toList(); return ResponseEntity.ok(groupjsons); } @@ -311,7 +358,7 @@ public ResponseEntity getGroupsOfProject(@PathVariable Long projectId, Auth a * @return ResponseEntity with the status, no content */ @DeleteMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectId}") - @Roles({UserRole.teacher}) + @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity deleteProjectById(@PathVariable long projectId, Auth auth) { CheckResult projectCheck = projectUtil.getProjectIfAdmin(projectId, auth.getUserEntity()); if (projectCheck.getStatus() != HttpStatus.OK) { diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java index a943ce41..00d2f997 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java @@ -1,36 +1,67 @@ package com.ugent.pidgeon.controllers; import com.ugent.pidgeon.auth.Roles; +import com.ugent.pidgeon.json.GroupFeedbackJson; +import com.ugent.pidgeon.json.GroupJson; +import com.ugent.pidgeon.json.LastGroupSubmissionJson; +import com.ugent.pidgeon.json.SubmissionJson; import com.ugent.pidgeon.model.Auth; -import com.ugent.pidgeon.model.json.GroupFeedbackJson; -import com.ugent.pidgeon.model.json.GroupJson; -import com.ugent.pidgeon.model.json.LastGroupSubmissionJson; -import com.ugent.pidgeon.model.json.SubmissionJson; +import com.ugent.pidgeon.model.submissionTesting.DockerOutput; +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; import com.ugent.pidgeon.model.submissionTesting.SubmissionTemplateModel; -import com.ugent.pidgeon.postgre.models.*; +import com.ugent.pidgeon.postgre.models.FileEntity; +import com.ugent.pidgeon.postgre.models.GroupEntity; +import com.ugent.pidgeon.postgre.models.GroupFeedbackEntity; +import com.ugent.pidgeon.postgre.models.SubmissionEntity; +import com.ugent.pidgeon.postgre.models.TestEntity; +import com.ugent.pidgeon.postgre.models.types.DockerTestState; +import com.ugent.pidgeon.postgre.models.types.DockerTestType; import com.ugent.pidgeon.postgre.models.types.UserRole; -import com.ugent.pidgeon.postgre.repository.*; -import com.ugent.pidgeon.util.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - +import com.ugent.pidgeon.postgre.repository.FileRepository; +import com.ugent.pidgeon.postgre.repository.GroupFeedbackRepository; +import com.ugent.pidgeon.postgre.repository.GroupRepository; +import com.ugent.pidgeon.postgre.repository.ProjectRepository; +import com.ugent.pidgeon.postgre.repository.SubmissionRepository; +import com.ugent.pidgeon.postgre.repository.TestRepository; +import com.ugent.pidgeon.util.CheckResult; +import com.ugent.pidgeon.util.CommonDatabaseActions; +import com.ugent.pidgeon.util.EntityToJsonConverter; +import com.ugent.pidgeon.util.Filehandler; +import com.ugent.pidgeon.util.GroupUtil; +import com.ugent.pidgeon.util.ProjectUtil; +import com.ugent.pidgeon.util.SubmissionUtil; +import com.ugent.pidgeon.util.TestRunner; +import com.ugent.pidgeon.util.TestUtil; import java.io.File; -import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.time.OffsetDateTime; +import java.util.ArrayList; import java.util.List; -import java.util.function.Function; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; @RestController -public class SubmissionController { +public class SubmissionController { @Autowired private GroupRepository groupRepository; @@ -55,22 +86,12 @@ public class SubmissionController { private EntityToJsonConverter entityToJsonConverter; @Autowired private CommonDatabaseActions commonDatabaseActions; + @Autowired + private TestUtil testUtil; + @Autowired + private TestRunner testRunner; - private SubmissionTemplateModel.SubmissionResult runStructureTest(ZipFile file, TestEntity testEntity) throws IOException { - // Get the test file from the server - FileEntity testfileEntity = fileRepository.findById(testEntity.getStructureTestId()).orElse(null); - if (testfileEntity == null) { - return null; - } - String testfile = Filehandler.getStructureTestString(Path.of(testfileEntity.getPath())); - - // Parse the file - SubmissionTemplateModel model = new SubmissionTemplateModel(); - model.parseSubmissionTemplate(testfile); - - return model.checkSubmission(file); - } /** * Function to get a submission by its ID @@ -93,8 +114,17 @@ public ResponseEntity getSubmission(@PathVariable("submissionid") long submis SubmissionEntity submission = checkResult.getData(); SubmissionJson submissionJson = entityToJsonConverter.getSubmissionJson(submission); - return ResponseEntity.ok(submissionJson); - } + return ResponseEntity.ok(submissionJson); + } + + private Map> getLatestSubmissionsForProject(long projectId) { + List groupIds = projectRepository.findGroupIdsByProjectId(projectId); + return groupIds.stream() + .collect(Collectors.toMap( + groupId -> groupId, + groupId -> submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(projectId, groupId) + )); + } /** * Function to get all submissions @@ -110,41 +140,37 @@ public ResponseEntity getSubmission(@PathVariable("submissionid") long submis @GetMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/submissions") //Route to get all submissions for a project @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity getSubmissions(@PathVariable("projectid") long projectid, Auth auth) { - try { CheckResult checkResult = projectUtil.isProjectAdmin(projectid, auth.getUserEntity()); if (!checkResult.getStatus().equals(HttpStatus.OK)) { return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } - List projectGroupIds = projectRepository.findGroupIdsByProjectId(projectid); - List res = projectGroupIds.stream().map(groupId -> { - GroupEntity group = groupRepository.findById(groupId).orElse(null); + Map> submissions = getLatestSubmissionsForProject(projectid); + List res = new ArrayList<>(); + for (Map.Entry> entry : submissions.entrySet()) { + GroupEntity group = groupRepository.findById(entry.getKey()).orElse(null); if (group == null) { throw new RuntimeException("Group not found"); } - GroupJson groupjson = entityToJsonConverter.groupEntityToJson(group); - GroupFeedbackEntity groupFeedbackEntity = groupFeedbackRepository.getGroupFeedback(groupId, projectid); + GroupJson groupjson = entityToJsonConverter.groupEntityToJson(group, false); + GroupFeedbackEntity groupFeedbackEntity = groupFeedbackRepository.getGroupFeedback(entry.getKey(), projectid); GroupFeedbackJson groupFeedbackJson; if (groupFeedbackEntity == null) { groupFeedbackJson = null; } else { - groupFeedbackJson = new GroupFeedbackJson(groupFeedbackEntity.getScore(), groupFeedbackEntity.getFeedback(), groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId()); + groupFeedbackJson = entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity); } - SubmissionEntity submission = submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(projectid, groupId).orElse(null); + SubmissionEntity submission = entry.getValue().orElse(null); if (submission == null) { - return new LastGroupSubmissionJson(null, groupjson, groupFeedbackJson); + res.add(new LastGroupSubmissionJson(null, groupjson, groupFeedbackJson)); + continue; } + res.add(new LastGroupSubmissionJson(entityToJsonConverter.getSubmissionJson(submission), groupjson, groupFeedbackJson)); + } - return new LastGroupSubmissionJson(entityToJsonConverter.getSubmissionJson(submission), groupjson, groupFeedbackJson); - - }).toList(); return ResponseEntity.ok(res); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); - } } - /** * Function to submit a file * @@ -159,6 +185,7 @@ public ResponseEntity getSubmissions(@PathVariable("projectid") long projecti */ @PostMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/submit") //Route to submit a file, it accepts a multiform with the file and submissionTime + @Transactional @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity submitFile(@RequestParam("file") MultipartFile file, @PathVariable("projectid") long projectid, Auth auth) { long userId = auth.getUserEntity().getId(); @@ -168,13 +195,13 @@ public ResponseEntity submitFile(@RequestParam("file") MultipartFile file, @P return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } - long groupId = checkResult.getData(); + Long groupId = checkResult.getData(); - //TODO: execute the docker tests onces these are implemented try { //Save the file entry in the database to get the id FileEntity fileEntity = new FileEntity("", "", userId); - long fileid = fileRepository.save(fileEntity).getId(); + fileEntity = fileRepository.save(fileEntity); + long fileid = fileEntity.getId(); OffsetDateTime now = OffsetDateTime.now(); SubmissionEntity submissionEntity = new SubmissionEntity( @@ -185,6 +212,8 @@ public ResponseEntity submitFile(@RequestParam("file") MultipartFile file, @P false, false ); + submissionEntity.setDockerTestState(DockerTestState.finished); + submissionEntity.setDockerType(DockerTestType.NONE); //Save the submission in the database SubmissionEntity submission = submissionRepository.save(submissionEntity); @@ -192,7 +221,7 @@ public ResponseEntity submitFile(@RequestParam("file") MultipartFile file, @P //Save the file on the server String filename = file.getOriginalFilename(); Path path = Filehandler.getSubmissionPath(projectid, groupId, submission.getId()); - File savedFile = Filehandler.saveSubmission(path, file); + File savedFile = Filehandler.saveFile(path, file, Filehandler.SUBMISSION_FILENAME); String pathname = path.resolve(Filehandler.SUBMISSION_FILENAME).toString(); //Update name and path for the file entry @@ -200,35 +229,84 @@ public ResponseEntity submitFile(@RequestParam("file") MultipartFile file, @P fileEntity.setPath(pathname); fileRepository.save(fileEntity); + // Run structure tests + TestEntity testEntity = testRepository.findByProjectId(projectid).orElse(null); + SubmissionTemplateModel.SubmissionResult structureTestResult; + if (testEntity == null) { + Logger.getLogger("SubmissionController").info("no tests"); + submission.setStructureFeedback("No specific structure requested for this project."); + submission.setStructureAccepted(true); + submission.setDockerAccepted(true); + submissionRepository.save(submission); + } else { + + // Check file structure + SubmissionTemplateModel model = new SubmissionTemplateModel(); + structureTestResult = testRunner.runStructureTest(new ZipFile(savedFile), testEntity, model); + if (structureTestResult == null) { + submission.setStructureFeedback( + "No specific structure requested for this project."); + submission.setStructureAccepted(true); + } else { + submission.setStructureAccepted(structureTestResult.passed); + submission.setStructureFeedback(structureTestResult.feedback); + } - // Run structure tests - TestEntity testEntity = testRepository.findByProjectId(projectid).orElse(null); - SubmissionTemplateModel.SubmissionResult testresult; - if (testEntity == null) { - Logger.getLogger("SubmissionController").info("no test"); - testresult = new SubmissionTemplateModel.SubmissionResult(true, "No structure requirements for this project."); - } else { - testresult = runStructureTest(new ZipFile(savedFile), testEntity); - } - if (testresult == null) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error while running tests: test files not found"); - } - submissionRepository.save(submissionEntity); - // Update the submission with the test resultsetAccepted - submission.setStructureAccepted(testresult.passed); - submission = submissionRepository.save(submission); + if (testEntity.getDockerTestTemplate() != null) { + submission.setDockerType(DockerTestType.TEMPLATE); + } else if (testEntity.getDockerTestScript() != null) { + submission.setDockerType(DockerTestType.SIMPLE); + } else { + submission.setDockerType(DockerTestType.NONE); + } - // Update the submission with the test feedbackfiles - submission.setDockerFeedback("TEMP DOCKER FEEDBACK"); - submission.setStructureFeedback(testresult.feedback); - submissionRepository.save(submission); + // save the first feedback, without docker feedback + submissionRepository.save(submission); + + if (testEntity.getDockerTestScript() != null) { + // Define docker test as running + submission.setDockerTestState(DockerTestState.running); + // run docker tests in background + File finalSavedFile = savedFile; + Path artifactPath = Filehandler.getSubmissionArtifactPath(projectid, groupId, submission.getId()); + + CompletableFuture.runAsync(() -> { + try { + // Check if docker tests succeed + DockerSubmissionTestModel dockerModel = new DockerSubmissionTestModel(testEntity.getDockerImage()); + DockerOutput dockerOutput = testRunner.runDockerTest(new ZipFile(finalSavedFile), testEntity, artifactPath, dockerModel, projectid); + if (dockerOutput == null) { + throw new RuntimeException("Error while running docker tests."); + } + // Representation of dockerOutput, this will be a json(easily displayable in frontend) if it is a template test + // or a string if it is a simple test + submission.setDockerFeedback(dockerOutput.getFeedbackAsString()); + submission.setDockerAccepted(dockerOutput.isAllowed()); + + submission.setDockerTestState(DockerTestState.finished); + submissionRepository.save(submission); + } catch (Exception e) { + /* Log error */ + Logger.getLogger("SubmissionController").log(Level.SEVERE, e.getMessage(), e); + + submission.setDockerFeedback(""); + submission.setDockerAccepted(false); + + submission.setDockerTestState(DockerTestState.aborted); + submissionRepository.save(submission); - return ResponseEntity.ok(entityToJsonConverter.getSubmissionJson(submissionEntity)); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error while saving file: " + e.getMessage()); + } + }); } + } + return ResponseEntity.ok(entityToJsonConverter.getSubmissionJson(submission)); + } catch (Exception e) { + Logger.getLogger("SubmissionController").log(Level.SEVERE, e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to save submissions on file server."); } + } /** * Function to get a submission file @@ -257,70 +335,87 @@ public ResponseEntity getSubmissionFile(@PathVariable("submissionid") long su } // Get the file from the server - try { - Resource zipFile = Filehandler.getSubmissionAsResource(Path.of(file.getPath())); + return Filehandler.getZipFileAsResponse(Path.of(file.getPath()), file.getName()); + } - // Set headers for the response - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + file.getName()); - headers.add(HttpHeaders.CONTENT_TYPE, "application/zip"); + @GetMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/submissions/files") + @Roles({UserRole.teacher, UserRole.student}) + public ResponseEntity getSubmissionsFiles(@PathVariable("projectid") long projectid, @RequestParam(value = "artifacts", required = false) Boolean artifacts, Auth auth) { + try { + CheckResult checkResult = projectUtil.isProjectAdmin(projectid, auth.getUserEntity()); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); + } + + Path tempDir = Files.createTempDirectory("SELAB6CANDELETEallsubmissions"); + Path mainZipPath = tempDir.resolve("main.zip"); + try (ZipOutputStream mainZipOut = new ZipOutputStream(Files.newOutputStream(mainZipPath))) { + Map> submissions = getLatestSubmissionsForProject(projectid); + for (Map.Entry> entry : submissions.entrySet()) { + SubmissionEntity submission = entry.getValue().orElse(null); + if (submission == null) { + continue; + } + FileEntity file = fileRepository.findById(submission.getFileId()).orElse(null); + if (file == null) { + continue; + } + + // Create the group-specific zip file in a temporary location + Path groupZipPath = tempDir.resolve("group-" + submission.getGroupId() + ".zip"); + try (ZipOutputStream groupZipOut = new ZipOutputStream(Files.newOutputStream(groupZipPath))) { + File submissionZip = Path.of(file.getPath()).toFile(); + Filehandler.addExistingZip(groupZipOut, "files.zip", submissionZip); + + if (artifacts != null && artifacts) { + Path artifactPath = Filehandler.getSubmissionArtifactPath(projectid, submission.getGroupId(), submission.getId()); + File artifactZip = artifactPath.toFile(); + if (artifactZip.exists()) { + Filehandler.addExistingZip(groupZipOut, "artifacts.zip", artifactZip); + } + } - return ResponseEntity.ok() - .headers(headers) - .body(zipFile); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); - } - } + } + Filehandler.addExistingZip(mainZipOut, "group-" + submission.getGroupId() + ".zip", groupZipPath.toFile()); + } + } - public ResponseEntity getFeedbackReponseEntity(long submissionid, Auth auth, Function feedbackGetter) { + return Filehandler.getZipFileAsResponse(mainZipPath, "allsubmissions.zip"); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } + } + @GetMapping(ApiRoutes.SUBMISSION_BASE_PATH + "/{submissionid}/artifacts") //Route to get a submission + @Roles({UserRole.teacher, UserRole.student}) + public ResponseEntity getSubmissionArtifacts(@PathVariable("submissionid") long submissionid, Auth auth) { CheckResult checkResult = submissionUtil.canGetSubmission(submissionid, auth.getUserEntity()); if (!checkResult.getStatus().equals(HttpStatus.OK)) { return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } SubmissionEntity submission = checkResult.getData(); - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_TYPE, String.valueOf(MediaType.TEXT_PLAIN)); - return ResponseEntity.ok().headers(headers).body(feedbackGetter.apply(submission)); - } + // Get the file from the server + try { + Resource zipFile = Filehandler.getFileAsResource(Filehandler.getSubmissionArtifactPath(submission.getProjectId(), submission.getGroupId(), submission.getId())); + if (zipFile == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("No artifacts found for this submission."); + } + // Set headers for the response + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=artifacts.zip" ); + headers.add(HttpHeaders.CONTENT_TYPE, "application/zip"); - /** - * Function to get the structure feedback of a submission - * - * @param submissionid ID of the submission to get the feedback from - * @param auth authentication object of the requesting user - * @return ResponseEntity with the feedback - * @ApiDog apiDog documentation - * @HttpMethod GET - * @AllowedRoles teacher, student - * @ApiPath /api/submissions/{submissionid}/structurefeedback - */ - @GetMapping(ApiRoutes.SUBMISSION_BASE_PATH + "/{submissionid}/structurefeedback") - //Route to get the structure feedback - @Roles({UserRole.teacher, UserRole.student}) - public ResponseEntity getStructureFeedback(@PathVariable("submissionid") long submissionid, Auth auth) { - return getFeedbackReponseEntity(submissionid, auth, SubmissionEntity::getStructureFeedback); + return ResponseEntity.ok() + .headers(headers) + .body(zipFile); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } } - /** - * Function to get the docker feedback of a submission - * - * @param submissionid ID of the submission to get the feedback from - * @param auth authentication object of the requesting user - * @return ResponseEntity with the feedback - * @ApiDog apiDog documentation - * @HttpMethod GET - * @AllowedRoles teacher, student - * @ApiPath /api/submissions/{submissionid}/dockerfeedback - */ - @GetMapping(ApiRoutes.SUBMISSION_BASE_PATH + "/{submissionid}/dockerfeedback") //Route to get the docker feedback - @Roles({UserRole.teacher, UserRole.student}) - public ResponseEntity getDockerFeedback(@PathVariable("submissionid") long submissionid, Auth auth) { - return getFeedbackReponseEntity(submissionid, auth, SubmissionEntity::getDockerFeedback); - } + /** @@ -372,4 +467,17 @@ public ResponseEntity getSubmissionsForGroup(@PathVariable("projectid") long List res = submissions.stream().map(entityToJsonConverter::getSubmissionJson).toList(); return ResponseEntity.ok(res); } + + @GetMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/adminsubmissions") + @Roles({UserRole.teacher, UserRole.student}) + public ResponseEntity getAdminSubmissions(@PathVariable("projectid") long projectid, Auth auth) { + CheckResult checkResult = projectUtil.isProjectAdmin(projectid, auth.getUserEntity()); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); + } + + List submissions = submissionRepository.findAdminSubmissionsByProjectId(projectid); + List res = submissions.stream().map(entityToJsonConverter::getSubmissionJson).toList(); + return ResponseEntity.ok(res); + } } \ No newline at end of file diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java index 39bc514c..21a1222e 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java @@ -1,22 +1,42 @@ package com.ugent.pidgeon.controllers; import com.ugent.pidgeon.auth.Roles; +import com.ugent.pidgeon.json.TestJson; +import com.ugent.pidgeon.json.TestUpdateJson; import com.ugent.pidgeon.model.Auth; -import com.ugent.pidgeon.model.json.TestJson; -import com.ugent.pidgeon.postgre.models.*; +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; +import com.ugent.pidgeon.postgre.models.FileEntity; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.TestEntity; +import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.UserRole; -import com.ugent.pidgeon.postgre.repository.*; -import com.ugent.pidgeon.util.*; +import com.ugent.pidgeon.postgre.repository.FileRepository; +import com.ugent.pidgeon.postgre.repository.ProjectRepository; +import com.ugent.pidgeon.postgre.repository.TestRepository; +import com.ugent.pidgeon.util.CheckResult; +import com.ugent.pidgeon.util.CommonDatabaseActions; +import com.ugent.pidgeon.util.EntityToJsonConverter; +import com.ugent.pidgeon.util.FileUtil; +import com.ugent.pidgeon.util.Filehandler; +import com.ugent.pidgeon.util.Pair; +import com.ugent.pidgeon.util.ProjectUtil; +import com.ugent.pidgeon.util.TestUtil; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.Resource; -import org.springframework.http.*; -import org.springframework.web.bind.annotation.*; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.io.*; -import java.nio.file.Path; - -import java.util.Optional; -import java.util.function.Function; @RestController public class TestController { @@ -36,16 +56,15 @@ public class TestController { private CommonDatabaseActions commonDatabaseActions; @Autowired private EntityToJsonConverter entityToJsonConverter; + @Autowired + private ProjectUtil projectUtil; /** * Function to update the tests of a project - * @param dockerImage the docker image to use for the tests - * @param dockerTest the docker test file - * @param structureTest the structure test file * @param projectId the id of the project to update the tests for * @param auth the authentication object of the requesting user * @HttpMethod POST - * @ApiDog apiDog documentation + * @ApiDog apiDog documentation * @AllowedRoles teacher * @ApiPath /api/projects/{projectid}/tests * @return ResponseEntity with the updated tests @@ -53,34 +72,54 @@ public class TestController { @PostMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity updateTests( - @RequestParam(name = "dockerimage", required = false) String dockerImage, - @RequestParam(name = "dockertest", required = false) MultipartFile dockerTest, - @RequestParam(name = "structuretest", required = false) MultipartFile structureTest, - @PathVariable("projectid") long projectId, - Auth auth) { - return alterTests(projectId, auth.getUserEntity(), dockerImage, dockerTest, structureTest, HttpMethod.POST); + @RequestBody TestUpdateJson testJson, + + @PathVariable("projectid") long projectId, + Auth auth) { + return alterTests(projectId, auth.getUserEntity(), testJson.getDockerImage(), testJson.getDockerScript(), + testJson.getDockerTemplate(), testJson.getStructureTest(), HttpMethod.POST); } + /** + * Function to update the tests of a project + * @param projectId the id of the project to update the tests for + * @param auth the authentication object of the requesting user + * @HttpMethod PATCH + * @ApiDog apiDog documentation + * @AllowedRoles teacher + * @ApiPath /api/projects/{projectid}/tests + * @return ResponseEntity with the updated tests + */ @PatchMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity patchTests( - @RequestParam(name = "dockerimage", required = false) String dockerImage, - @RequestParam(name = "dockertest", required = false) MultipartFile dockerTest, - @RequestParam(name = "structuretest", required = false) MultipartFile structureTest, - @PathVariable("projectid") long projectId, - Auth auth) { - return alterTests(projectId, auth.getUserEntity(), dockerImage, dockerTest, structureTest, HttpMethod.PATCH); + @RequestBody TestUpdateJson testJson, + + @PathVariable("projectid") long projectId, + Auth auth) { + return alterTests(projectId, auth.getUserEntity(), testJson.getDockerImage(), testJson.getDockerScript(), + testJson.getDockerTemplate(), testJson.getStructureTest(), HttpMethod.PATCH); } + /** + * Function to update the tests of a project + * @param projectId the id of the project to update the tests for + * @param auth the authentication object of the requesting user + * @HttpMethod PUT + * @ApiDog apiDog documentation + * @AllowedRoles teacher + * @ApiPath /api/projects/{projectid}/tests + * @return ResponseEntity with the updated tests + */ @PutMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity putTests( - @RequestParam(name = "dockerimage", required = false) String dockerImage, - @RequestParam(name = "dockertest", required = false) MultipartFile dockerTest, - @RequestParam(name = "structuretest", required = false) MultipartFile structureTest, + @RequestBody TestUpdateJson testJson, + @PathVariable("projectid") long projectId, Auth auth) { - return alterTests(projectId, auth.getUserEntity(), dockerImage, dockerTest, structureTest, HttpMethod.PUT); + return alterTests(projectId, auth.getUserEntity(), testJson.getDockerImage(), testJson.getDockerScript(), + testJson.getDockerTemplate(), testJson.getStructureTest(), HttpMethod.PUT); } @@ -88,48 +127,96 @@ private ResponseEntity alterTests( long projectId, UserEntity user, String dockerImage, - MultipartFile dockerTest, - MultipartFile structureTest, + String dockerScript, + String dockerTemplate, + String structureTemplate, HttpMethod httpMethod ) { - CheckResult> checkResult = testUtil.checkForTestUpdate(projectId, user, dockerImage, dockerTest, structureTest, httpMethod); - if (!checkResult.getStatus().equals(HttpStatus.OK)) { - return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); + + + if (dockerImage != null && dockerImage.isBlank()) { + dockerImage = null; + } + if (dockerScript != null && dockerScript.isBlank()) { + dockerScript = null; + } + if (dockerTemplate != null && dockerTemplate.isBlank()) { + dockerTemplate = null; + } + if (structureTemplate != null && structureTemplate.isBlank()) { + structureTemplate = null; } - TestEntity testEntity = checkResult.getData().getFirst(); - ProjectEntity projectEntity = checkResult.getData().getSecond(); - try { + CheckResult> updateCheckResult = testUtil.checkForTestUpdate(projectId, user, dockerImage, dockerScript, dockerTemplate, structureTemplate, httpMethod); + + + if (!updateCheckResult.getStatus().equals(HttpStatus.OK)) { + return ResponseEntity.status(updateCheckResult.getStatus()).body(updateCheckResult.getMessage()); + } + + TestEntity testEntity = updateCheckResult.getData().getFirst(); + ProjectEntity projectEntity = updateCheckResult.getData().getSecond(); + + // Creating a test entry + if(httpMethod.equals(HttpMethod.POST)){ + testEntity = new TestEntity(); + } + + // Docker test + if(dockerImage != null) { + + // update/install image if possible, do so in a seperate thread to reduce wait time. + String finalDockerImage = dockerImage; + CompletableFuture.runAsync(() -> { + DockerSubmissionTestModel.installImage(finalDockerImage); + }); + } + + String oldDockerImage = testEntity.getDockerImage(); + + //Update fields + if (dockerImage != null || !httpMethod.equals(HttpMethod.PATCH)) { + testEntity.setDockerImage(dockerImage); + if (!testRepository.imageIsUsed(dockerImage)) { + // Do it on a different thread + String finalDockerImage1 = dockerImage; + CompletableFuture.runAsync(() -> { + DockerSubmissionTestModel.removeDockerImage( + finalDockerImage1); + }); + } + } - // Save the files on server - long dockertestFileEntityId; - long structuretestFileEntityId; - if (dockerTest != null) { - Path dockerTestPath = Filehandler.saveTest(dockerTest, projectId); - FileEntity dockertestFileEntity = fileUtil.saveFileEntity(dockerTestPath, projectId, user.getId()); - dockertestFileEntityId = dockertestFileEntity.getId(); - } else { - dockertestFileEntityId = testEntity.getDockerTestId(); - } - - if (structureTest != null) { - Path structureTestPath = Filehandler.saveTest(structureTest, projectId); - FileEntity structuretestFileEntity = fileUtil.saveFileEntity(structureTestPath, projectId, user.getId()); - structuretestFileEntityId = structuretestFileEntity.getId(); - } else { - structuretestFileEntityId = testEntity.getStructureTestId(); - } - - // Create/update test entity - TestEntity test = new TestEntity(dockerImage, dockertestFileEntityId, structuretestFileEntityId); - test = testRepository.save(test); - projectEntity.setTestId(test.getId()); - projectRepository.save(projectEntity); - return ResponseEntity.ok(entityToJsonConverter.testEntityToTestJson(test, projectId)); - } catch (IOException e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error while saving files: " + e.getMessage()); + if (dockerScript != null || !httpMethod.equals(HttpMethod.PATCH)) { + testEntity.setDockerTestScript(dockerScript); } + if (dockerTemplate != null || !httpMethod.equals(HttpMethod.PATCH)) { + testEntity.setDockerTestTemplate(dockerTemplate); + } + + if (structureTemplate != null || !httpMethod.equals(HttpMethod.PATCH)) { + testEntity.setStructureTemplate(structureTemplate); + } + // save test entity + testEntity = testRepository.save(testEntity); + projectEntity.setTestId(testEntity.getId()); + projectRepository.save(projectEntity); // make sure to update test id in project + + // Uninstall dockerimage if necessary + if (oldDockerImage != null) { + if (!testRepository.imageIsUsed(oldDockerImage)) { + // Do it on a different thread + String finalDockerImage1 = oldDockerImage; + CompletableFuture.runAsync(() -> { + DockerSubmissionTestModel.removeDockerImage( + finalDockerImage1); + }); + } + } + + return ResponseEntity.ok(entityToJsonConverter.testEntityToTestJson(testEntity, projectId)); + } @@ -147,92 +234,168 @@ private ResponseEntity alterTests( @GetMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity getTests(@PathVariable("projectid") long projectId, Auth auth) { - CheckResult projectCheck = testUtil.getTestIfAdmin(projectId, auth.getUserEntity()); + CheckResult> projectCheck = testUtil.getTestWithAdminStatus(projectId, auth.getUserEntity()); if (!projectCheck.getStatus().equals(HttpStatus.OK)) { return ResponseEntity.status(projectCheck.getStatus()).body(projectCheck.getMessage()); } - TestEntity test = projectCheck.getData(); + TestEntity test = projectCheck.getData().getFirst(); + if (!projectCheck.getData().getSecond()) { // user is not an admin, hide script and image + test.setDockerTestScript(null); + test.setDockerImage(null); + } TestJson res = entityToJsonConverter.testEntityToTestJson(test, projectId); return ResponseEntity.ok(res); } /** - * Function to get the structure test file of a project - * @param projectId the id of the project to get the structure test file for + * Function to delete the tests of a project + * @param projectId the id of the test to delete * @param auth the authentication object of the requesting user - * @HttpMethod GET - * @ApiDog apiDog documentation - * @AllowedRoles teacher, student - * @ApiPath /api/projects/{projectid}/tests/structuretest - * @return ResponseEntity with the structure test file + * @HttpMethod DELETE + * @ApiDog apiDog documentation + * @AllowedRoles teacher + * @ApiPath /api/projects/{projectid}/tests + * @return ResponseEntity */ - @GetMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests/structuretest") + @DeleteMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests") @Roles({UserRole.teacher, UserRole.student}) - public ResponseEntity getStructureTestFile(@PathVariable("projectid") long projectId, Auth auth) { - return getTestFileResponseEnity(projectId, auth, TestEntity::getStructureTestId); + public ResponseEntity deleteTestById(@PathVariable("projectid") long projectId, Auth auth) { + CheckResult> updateCheckResult = testUtil.checkForTestUpdate(projectId, auth.getUserEntity(), null, null, null, null, HttpMethod.DELETE); + if (!updateCheckResult.getStatus().equals(HttpStatus.OK)) { + return ResponseEntity.status(updateCheckResult.getStatus()).body(updateCheckResult.getMessage()); + } + + ProjectEntity projectEntity = updateCheckResult.getData().getSecond(); + TestEntity testEntity = updateCheckResult.getData().getFirst(); + + CheckResult deleteResult = commonDatabaseActions.deleteTestById(projectEntity, testEntity); + if (!deleteResult.getStatus().equals(HttpStatus.OK)) { + return ResponseEntity.status(deleteResult.getStatus()).body(deleteResult.getMessage()); + } + return ResponseEntity.ok().build(); } /** - * Function to get the docker test file of a project - * @param projectId the id of the project to get the docker test file for + * Function to upload extra files for a test + * @param projectId the id of the project to upload the files for + * @param file the file to upload * @param auth the authentication object of the requesting user - * @HttpMethod GET - * @ApiDog apiDog documentation + * @HttpMethod PUT + * @ApiDog apiDog documentation * @AllowedRoles teacher, student - * @ApiPath /api/projects/{projectid}/tests/dockertest - * @return ResponseEntity with the docker test file + * @ApiPath /api/projects/{projectid}/tests/extrafiles + * @return ResponseEntity with the updated tests */ - @GetMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests/dockertest") + @PutMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests/extrafiles") @Roles({UserRole.teacher, UserRole.student}) - public ResponseEntity getDockerTestFile(@PathVariable("projectid") long projectId, Auth auth) { - return getTestFileResponseEnity(projectId, auth, TestEntity::getDockerTestId); + public ResponseEntity uploadExtraTestFiles( + @PathVariable("projectid") long projectId, + @RequestParam("file") MultipartFile file, + Auth auth + ) { + CheckResult checkResult = testUtil.getTestIfAdmin(projectId, auth.getUserEntity()); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); + } + + TestEntity testEntity = checkResult.getData(); + + try { + Path path = Filehandler.getTestExtraFilesPath(projectId); + Filehandler.saveFile(path, file, Filehandler.EXTRA_TESTFILES_FILENAME); + + FileEntity fileEntity = new FileEntity(); + fileEntity.setName(file.getOriginalFilename()); + fileEntity.setPath(path.resolve(Filehandler.EXTRA_TESTFILES_FILENAME).toString()); + fileEntity.setUploadedBy(auth.getUserEntity().getId()); + fileEntity = fileRepository.save(fileEntity); + + testEntity.setExtraFilesId(fileEntity.getId()); + testEntity = testRepository.save(testEntity); + + return ResponseEntity.ok(entityToJsonConverter.testEntityToTestJson(testEntity, projectId)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error while saving files"); + } } - public ResponseEntity getTestFileResponseEnity(long projectId, Auth auth, Function testFileIdGetter) { - CheckResult projectCheck = testUtil.getTestIfAdmin(projectId, auth.getUserEntity()); - if (!projectCheck.getStatus().equals(HttpStatus.OK)) { - return ResponseEntity.status(projectCheck.getStatus()).body(projectCheck.getMessage()); + /** + * Function to delete extra files for a test + * @param projectId the id of the project to delete the files for + * @param auth the authentication object of the requesting user + * @HttpMethod DELETE + * @ApiDog apiDog documentation + * @AllowedRoles teacher, student + * @ApiPath /api/projects/{projectid}/tests/extrafiles + * @return ResponseEntity with the updated tests + */ + @DeleteMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests/extrafiles") + @Roles({UserRole.teacher, UserRole.student}) + public ResponseEntity deleteExtraTestFiles( + @PathVariable("projectid") long projectId, + Auth auth + ) { + CheckResult checkResult = testUtil.getTestIfAdmin(projectId, auth.getUserEntity()); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } - TestEntity testEntity = projectCheck.getData(); - long testFileId = testFileIdGetter.apply(testEntity); - Optional fileEntity = fileRepository.findById(testFileId); - if (fileEntity.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("No file found for test with id: " + testFileId); + TestEntity testEntity = checkResult.getData(); + + try { + + FileEntity fileEntity = testEntity.getExtraFilesId() == null ? + null : fileRepository.findById(testEntity.getExtraFilesId()).orElse(null); + if (fileEntity == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("No extra files found"); + } + + testEntity.setExtraFilesId(null); + testEntity = testRepository.save(testEntity); + + CheckResult delResult = fileUtil.deleteFileById(fileEntity.getId()); + if (!delResult.getStatus().equals(HttpStatus.OK)) { + return ResponseEntity.status(delResult.getStatus()).body(delResult.getMessage()); + } + + return ResponseEntity.ok(entityToJsonConverter.testEntityToTestJson(testEntity, projectId)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error while deleting files"); } - Resource file = Filehandler.getFileAsResource(Path.of(fileEntity.get().getPath())); - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileEntity.get().getName()); - headers.add(HttpHeaders.CONTENT_TYPE, String.valueOf(MediaType.TEXT_PLAIN)); - return ResponseEntity.ok().headers(headers).body(file); } /** - * Function to delete the tests of a project - * @param projectId the id of the test to delete + * Function to get extra files for a test + * @param projectId the id of the project to get the files for * @param auth the authentication object of the requesting user - * @HttpMethod DELETE - * @ApiDog apiDog documentation - * @AllowedRoles teacher - * @ApiPath /api/projects/{projectid}/tests - * @return ResponseEntity + * @HttpMethod GET + * @ApiDog apiDog documentation + * @AllowedRoles teacher, student + * @ApiPath /api/projects/{projectid}/tests/extrafiles + * @return ResponseEntity with the updated tests */ - @DeleteMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests") + @GetMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests/extrafiles") @Roles({UserRole.teacher, UserRole.student}) - public ResponseEntity deleteTestById(@PathVariable("projectid") long projectId, Auth auth) { - CheckResult> updateCheckResult = testUtil.checkForTestUpdate(projectId, auth.getUserEntity(), null, null, null, HttpMethod.DELETE); - if (!updateCheckResult.getStatus().equals(HttpStatus.OK)) { - return ResponseEntity.status(updateCheckResult.getStatus()).body(updateCheckResult.getMessage()); + public ResponseEntity getExtraTestFiles( + @PathVariable("projectid") long projectId, + Auth auth + ) { + CheckResult checkResult = testUtil.getTestIfAdmin(projectId, auth.getUserEntity()); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } - ProjectEntity projectEntity = updateCheckResult.getData().getSecond(); - TestEntity testEntity = updateCheckResult.getData().getFirst(); - CheckResult deleteResult = commonDatabaseActions.deleteTestById(projectEntity, testEntity); - if (!deleteResult.getStatus().equals(HttpStatus.OK)) { - return ResponseEntity.status(deleteResult.getStatus()).body(deleteResult.getMessage()); + TestEntity testEntity = checkResult.getData(); + if (testEntity.getExtraFilesId() == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("No extra files found"); } - return ResponseEntity.ok().build(); + FileEntity fileEntity = fileRepository.findById(testEntity.getExtraFilesId()).orElse(null); + if (fileEntity == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("No extra files found"); + } + + return Filehandler.getZipFileAsResponse(Path.of(fileEntity.getPath()), fileEntity.getName()); } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/UserController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/UserController.java index 3c210da4..4abbf741 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/UserController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/UserController.java @@ -1,20 +1,26 @@ package com.ugent.pidgeon.controllers; import com.ugent.pidgeon.auth.Roles; +import com.ugent.pidgeon.json.UserJson; +import com.ugent.pidgeon.json.UserUpdateJson; import com.ugent.pidgeon.model.Auth; -import com.ugent.pidgeon.model.json.UserJson; -import com.ugent.pidgeon.model.json.UserUpdateJson; import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.UserRepository; import com.ugent.pidgeon.util.CheckResult; import com.ugent.pidgeon.util.UserUtil; +import java.util.ArrayList; +import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.logging.Logger; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { @@ -35,11 +41,11 @@ public class UserController { * @ApiDog apiDog documentation * @return user object */ - @GetMapping(ApiRoutes.USER_BASE_PATH + "/{userid}") - @Roles({UserRole.student}) + @GetMapping(ApiRoutes.USERS_BASE_PATH + "/{userid}") + @Roles({UserRole.student, UserRole.teacher}) public ResponseEntity getUserById(@PathVariable("userid") Long userid,Auth auth) { UserEntity requester = auth.getUserEntity(); - if (requester.getId() != userid) { + if (requester.getId() != userid && requester.getRole() != UserRole.admin) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("You do not have access to this user"); } @@ -53,11 +59,68 @@ public ResponseEntity getUserById(@PathVariable("userid") Long userid,Au return ResponseEntity.ok().body(res); } + /** + * Function to search users by email, name and surname + * + * @param email email of a user + * @param name name of a user + * @param surname surname of a user + * @HttpMethod GET + * @ApiPath /api/user + * @AllowedRoles admin + * @ApiDog apiDog documentation + * @return user object + */ + @GetMapping(ApiRoutes.USERS_BASE_PATH) + @Roles({UserRole.admin}) + public ResponseEntity getUsersByNameOrSurname( + @RequestParam(value="email", required = false) String email, + @RequestParam(value = "name", required = false) String name, + @RequestParam(value = "surname", required = false) String surname + ) { + if (email != null) { + + UserEntity user = userRepository.findByEmail(email); + if (user == null) { + return ResponseEntity.status(HttpStatus.OK).body(new ArrayList<>()); + } + if (name != null && !user.getName().toLowerCase().contains(name.toLowerCase())) { + return ResponseEntity.ok().body(new ArrayList<>()); + } else if (surname != null && !user.getSurname().toLowerCase().contains(surname.toLowerCase())) { + return ResponseEntity.ok().body(new ArrayList<>()); + } + + return ResponseEntity.ok().body(List.of(new UserJson(user))); + } + + if ((name == null || name.length() < 3) && (surname == null || surname.length() < 3)) { + return ResponseEntity.status(HttpStatus.OK).body(new ArrayList<>()); + } + + if (name == null) name = ""; + if (surname == null) surname = ""; + + List usersByName = userRepository.findByName(name, surname); + - @GetMapping(ApiRoutes.USER_AUTH_PATH) + return ResponseEntity.ok().body(usersByName.stream().map(UserJson::new).toList()); + } + + /** + * Function to get the logged in user + * + * @param auth authentication object + * @HttpMethod GET + * @ApiPath /api/user + * @AllowedRoles student + * @ApiDog apiDog documentation + * @return user object + */ + @GetMapping(ApiRoutes.LOGGEDIN_USER_PATH) @Roles({UserRole.student, UserRole.teacher}) - public ResponseEntity getUserByAzureId(Auth auth) { + public ResponseEntity getLoggedInUser(Auth auth) { UserEntity res = auth.getUserEntity(); + UserJson userJson = new UserJson(res); return ResponseEntity.ok().body(userJson); } @@ -83,7 +146,7 @@ private ResponseEntity doUserUpdate(UserEntity user, UserUpdateJson json) { * @ApiDog apiDog documentation * @return string */ - @PutMapping(ApiRoutes.USER_BASE_PATH + "/{userid}") + @PutMapping(ApiRoutes.USERS_BASE_PATH + "/{userid}") @Roles({UserRole.admin}) public ResponseEntity updateUserById(@PathVariable("userid") Long userid, @RequestBody UserUpdateJson userUpdateJson, Auth auth) { @@ -106,7 +169,7 @@ public ResponseEntity updateUserById(@PathVariable("userid") Long userid, @Re * @ApiDog apiDog documentation * @return string */ - @PatchMapping(ApiRoutes.USER_BASE_PATH + "/{userid}") + @PatchMapping(ApiRoutes.USERS_BASE_PATH + "/{userid}") @Roles({UserRole.admin}) public ResponseEntity patchUserById(@PathVariable("userid") Long userid, @RequestBody UserUpdateJson userUpdateJson, Auth auth) { UserEntity user = userUtil.getUserIfExists(userid); @@ -124,7 +187,6 @@ public ResponseEntity patchUserById(@PathVariable("userid") Long userid, @Req userUpdateJson.setEmail(user.getEmail()); } - Logger.getGlobal().info(userUpdateJson.getRole()); if (userUpdateJson.getRole() == null) { userUpdateJson.setRole(user.getRole().toString()); } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/json/ClusterFillJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/ClusterFillJson.java new file mode 100644 index 00000000..f1172bb4 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/ClusterFillJson.java @@ -0,0 +1,25 @@ +package com.ugent.pidgeon.json; + +import java.util.HashMap; +import java.util.Map; + +public class ClusterFillJson { + private final Map clusterGroupMembers; + + public ClusterFillJson() { + this.clusterGroupMembers = new HashMap<>(); + } + + public ClusterFillJson(Map clusterGroupMembers) { + this.clusterGroupMembers = clusterGroupMembers; + } + + public Map getClusterGroupMembers() { + return clusterGroupMembers; + } + + public void addClusterGroupMembers(String clusterId, Long[] groupIds) { + clusterGroupMembers.put(clusterId, groupIds); + } + +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/json/CourseJoinInformationJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/CourseJoinInformationJson.java new file mode 100644 index 00000000..bfd55ff7 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/CourseJoinInformationJson.java @@ -0,0 +1,28 @@ +package com.ugent.pidgeon.json; + +public class CourseJoinInformationJson { + private String name; + private String description; + + public CourseJoinInformationJson(String name, String description) { + this.name = name; + this.description = description; + } + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/json/CourseJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/CourseJson.java new file mode 100644 index 00000000..23655114 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/CourseJson.java @@ -0,0 +1,52 @@ +package com.ugent.pidgeon.json; + +// Hulpklasse die gebruikt wordt in Requestbodies. +public class CourseJson{ + private String name; + + private String description; + + private Boolean isArchived; + + private Integer year; + + public CourseJson(String name, String description, Boolean isArchived, Integer courseYear) { + this.name = name; + this.description = description; + this.isArchived = isArchived; + this.year = courseYear; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getArchived() { + return isArchived; + } + + public void setArchived(Boolean isArchived) { + this.isArchived = isArchived; + } + + public Integer getYear() { + return year; + } + + public void setYear(Integer courseYear) { + this.year = courseYear; + } +} + diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseMemberRequestJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/CourseMemberRequestJson.java similarity index 95% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseMemberRequestJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/CourseMemberRequestJson.java index 7c7046b0..b1a89607 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseMemberRequestJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/CourseMemberRequestJson.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; import com.ugent.pidgeon.postgre.models.types.CourseRelation; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseReferenceJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/CourseReferenceJson.java similarity index 64% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseReferenceJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/CourseReferenceJson.java index 5608c1f1..c6e9fde9 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseReferenceJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/CourseReferenceJson.java @@ -1,14 +1,19 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; + +import java.time.OffsetDateTime; public class CourseReferenceJson { private String name; private String url; private Long courseId; + private OffsetDateTime archivedAt; - public CourseReferenceJson(String name, String url, Long courseId) { + public CourseReferenceJson(String name, String url, Long courseId, + OffsetDateTime archived) { this.name = name; this.url = url; this.courseId = courseId; + this.archivedAt = archived; } public String getName() { @@ -34,4 +39,12 @@ public Long getCourseId() { public void setCourseId(Long courseId) { this.courseId = courseId; } + + public OffsetDateTime getArchivedAt() { + return archivedAt; + } + + public void setArchivedAt(OffsetDateTime archivedAt) { + this.archivedAt = archivedAt; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseWithInfoJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/CourseWithInfoJson.java similarity index 54% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseWithInfoJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/CourseWithInfoJson.java index a8012aaf..ad7abb0f 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseWithInfoJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/CourseWithInfoJson.java @@ -1,5 +1,6 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; +import java.time.OffsetDateTime; import java.util.List; public record CourseWithInfoJson ( @@ -9,6 +10,10 @@ public record CourseWithInfoJson ( UserReferenceJson teacher, List assistants, String memberUrl, - String joinUrl + String joinUrl, + String joinKey, + OffsetDateTime archivedAt, + OffsetDateTime createdAt, + Integer year ) {} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/json/CourseWithRelationJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/CourseWithRelationJson.java new file mode 100644 index 00000000..890de40b --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/CourseWithRelationJson.java @@ -0,0 +1,8 @@ +package com.ugent.pidgeon.json; + +import com.ugent.pidgeon.postgre.models.types.CourseRelation; +import java.time.OffsetDateTime; + +public record CourseWithRelationJson (String url, CourseRelation relation, String name, Long courseId, + OffsetDateTime archivedAt, Integer memberCount, OffsetDateTime createdAt, Integer year) { } + diff --git a/backend/app/src/main/java/com/ugent/pidgeon/json/DockerTestFeedbackJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/DockerTestFeedbackJson.java new file mode 100644 index 00000000..f2d23be8 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/DockerTestFeedbackJson.java @@ -0,0 +1,14 @@ +package com.ugent.pidgeon.json; + + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.ugent.pidgeon.postgre.models.types.DockerTestType; + +@JsonSerialize(using = DockerTestFeedbackJsonSerializer.class) +public record DockerTestFeedbackJson( + DockerTestType type, + String feedback, + boolean allowed +) { + +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/json/DockerTestFeedbackJsonSerializer.java b/backend/app/src/main/java/com/ugent/pidgeon/json/DockerTestFeedbackJsonSerializer.java new file mode 100644 index 00000000..d7bd1aff --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/DockerTestFeedbackJsonSerializer.java @@ -0,0 +1,24 @@ +package com.ugent.pidgeon.json; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.ugent.pidgeon.postgre.models.types.DockerTestType; +import java.io.IOException; + +public class DockerTestFeedbackJsonSerializer extends JsonSerializer { + + @Override + public void serialize(DockerTestFeedbackJson value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeStringField("type", value.type().toString()); + if (value.type() == DockerTestType.TEMPLATE) { + gen.writeFieldName("feedback"); + gen.writeRawValue(value.feedback().replace("\n", "\\n")); + } else { + gen.writeStringField("feedback", value.feedback()); + } + gen.writeBooleanField("allowed", value.allowed()); + gen.writeEndObject(); + } +} + diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupClusterCreateJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupClusterCreateJson.java similarity index 51% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupClusterCreateJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/GroupClusterCreateJson.java index 8e461631..9d065504 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupClusterCreateJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupClusterCreateJson.java @@ -1,9 +1,12 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; + +import java.time.OffsetDateTime; public record GroupClusterCreateJson( String name, Integer capacity, - Integer groupCount + Integer groupCount, + OffsetDateTime lockGroupsAfter ) { public GroupClusterCreateJson { } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupClusterJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupClusterJson.java similarity index 79% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupClusterJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/GroupClusterJson.java index b78b0d66..5cb7994c 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupClusterJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupClusterJson.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; import java.time.OffsetDateTime; import java.util.List; @@ -10,10 +10,10 @@ public record GroupClusterJson( int groupCount, OffsetDateTime createdAt, List groups, + OffsetDateTime lockGroupsAfter, String courseUrl ) { - public GroupClusterJson { - } + } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupClusterReferenceJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupClusterReferenceJson.java similarity index 83% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupClusterReferenceJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/GroupClusterReferenceJson.java index be1286fb..8dfa92ee 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupClusterReferenceJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupClusterReferenceJson.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public record GroupClusterReferenceJson ( long clusterId, diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupClusterUpdateJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupClusterUpdateJson.java similarity index 58% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupClusterUpdateJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/GroupClusterUpdateJson.java index 3d8b3a2a..43138a66 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupClusterUpdateJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupClusterUpdateJson.java @@ -1,8 +1,11 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; + +import java.time.OffsetDateTime; public class GroupClusterUpdateJson { private String name; private Integer capacity; + private OffsetDateTime lockGroupsAfter; public GroupClusterUpdateJson() { } @@ -16,6 +19,10 @@ public Integer getCapacity() { return capacity; } + public OffsetDateTime getLockGroupsAfter() { + return lockGroupsAfter; + } + // Setters public void setName(String name) { this.name = name; @@ -24,4 +31,8 @@ public void setName(String name) { public void setCapacity(Integer capacity) { this.capacity = capacity; } + + public void setLockGroupsAfter(OffsetDateTime lockGroupsAfter) { + this.lockGroupsAfter = lockGroupsAfter; + } } \ No newline at end of file diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupCreateJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupCreateJson.java similarity index 55% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupCreateJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/GroupCreateJson.java index d7373886..81c1e7b7 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupCreateJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupCreateJson.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public record GroupCreateJson(String name) { } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupFeedbackJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupFeedbackJson.java similarity index 84% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupFeedbackJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/GroupFeedbackJson.java index e9a9b58a..f0d8a797 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupFeedbackJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupFeedbackJson.java @@ -1,8 +1,8 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public class GroupFeedbackJson { - private float score; + private Float score; private String feedback; private long groupId; @@ -11,7 +11,7 @@ public class GroupFeedbackJson { public GroupFeedbackJson() { } - public GroupFeedbackJson(float score, String feedback, long groupId, long projectId) { + public GroupFeedbackJson(Float score, String feedback, long groupId, long projectId) { this.score = score; this.feedback = feedback; this.groupId = groupId; @@ -19,7 +19,7 @@ public GroupFeedbackJson(float score, String feedback, long groupId, long projec } - public float getScore() { + public Float getScore() { return score; } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupFeedbackJsonWithProject.java b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupFeedbackJsonWithProject.java similarity index 85% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupFeedbackJsonWithProject.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/GroupFeedbackJsonWithProject.java index ed812e9d..006f0e00 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupFeedbackJsonWithProject.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupFeedbackJsonWithProject.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public class GroupFeedbackJsonWithProject { @@ -6,12 +6,12 @@ public class GroupFeedbackJsonWithProject { private String projectUrl; private GroupFeedbackJson groupFeedback; - private float maxScore; + private Integer maxScore; private Long projectId; public GroupFeedbackJsonWithProject(String projectName, String projectUrl, Long projectId, - GroupFeedbackJson groupFeedback, float maxScore) { + GroupFeedbackJson groupFeedback, Integer maxScore) { this.projectName = projectName; this.projectUrl = projectUrl; this.groupFeedback = groupFeedback; @@ -43,11 +43,11 @@ public void setGroupFeedback(GroupFeedbackJson groupFeedback) { this.groupFeedback = groupFeedback; } - public float getMaxScore() { + public Integer getMaxScore() { return maxScore; } - public void setMaxScore(float maxScore) { + public void setMaxScore(Integer maxScore) { this.maxScore = maxScore; } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupJson.java similarity index 97% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/GroupJson.java index 690732a0..ba0c2a2b 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupJson.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; import java.util.List; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupReferenceJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupReferenceJson.java similarity index 72% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupReferenceJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/GroupReferenceJson.java index 888dc854..221f6d80 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupReferenceJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/GroupReferenceJson.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public record GroupReferenceJson(String name, String url) { public GroupReferenceJson { diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/LastGroupSubmissionJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/LastGroupSubmissionJson.java similarity index 96% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/LastGroupSubmissionJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/LastGroupSubmissionJson.java index b81c45ef..0be4d9ed 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/LastGroupSubmissionJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/LastGroupSubmissionJson.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public class LastGroupSubmissionJson { diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/MemberIdRequest.java b/backend/app/src/main/java/com/ugent/pidgeon/json/MemberIdRequest.java similarity index 86% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/MemberIdRequest.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/MemberIdRequest.java index 9e0c68ef..19c0cc36 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/MemberIdRequest.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/MemberIdRequest.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public class MemberIdRequest { private Long memberId; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/NameRequest.java b/backend/app/src/main/java/com/ugent/pidgeon/json/NameRequest.java similarity index 82% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/NameRequest.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/NameRequest.java index 28d0e264..4c51eb2c 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/NameRequest.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/NameRequest.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public class NameRequest { private String name; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/ProjectJson.java similarity index 83% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/ProjectJson.java index ffff3f2e..c1eebdad 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/ProjectJson.java @@ -1,9 +1,7 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.ugent.pidgeon.postgre.models.OffsetDateTimeSerializer; - - import java.time.OffsetDateTime; @@ -15,11 +13,12 @@ public class ProjectJson { private Long groupClusterId; private Boolean visible; private Integer maxScore; + private OffsetDateTime visibleAfter; @JsonSerialize(using = OffsetDateTimeSerializer.class) private OffsetDateTime deadline; - public ProjectJson(String name, String description, Long groupClusterId, Long testId, Boolean visible, Integer maxScore, OffsetDateTime deadline) { + public ProjectJson(String name, String description, Long groupClusterId, Boolean visible, Integer maxScore, OffsetDateTime deadline) { this.name = name; this.description = description; this.groupClusterId = groupClusterId; @@ -79,4 +78,12 @@ public Integer getMaxScore() { public void setMaxScore(Integer maxScore) { this.maxScore = maxScore; } + + public OffsetDateTime getVisibleAfter() { + return visibleAfter; + } + + public void setVisibleAfter(OffsetDateTime visibleAfter) { + this.visibleAfter = visibleAfter; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectProgressJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/ProjectProgressJson.java similarity index 65% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectProgressJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/ProjectProgressJson.java index 18722929..8a35580e 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectProgressJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/ProjectProgressJson.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public record ProjectProgressJson(Integer completed, Integer total) { } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectResponseJsonWithStatus.java b/backend/app/src/main/java/com/ugent/pidgeon/json/ProjectResponseJsonWithStatus.java similarity index 79% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectResponseJsonWithStatus.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/ProjectResponseJsonWithStatus.java index 4f988e4f..e599e548 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectResponseJsonWithStatus.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/ProjectResponseJsonWithStatus.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; import com.ugent.pidgeon.model.ProjectResponseJson; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/json/ProjectStatus.java b/backend/app/src/main/java/com/ugent/pidgeon/json/ProjectStatus.java new file mode 100644 index 00000000..45dabbf6 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/ProjectStatus.java @@ -0,0 +1,18 @@ +package com.ugent.pidgeon.json; + +public enum ProjectStatus { + not_started, + correct, + incorrect, + no_group; + + @Override + public String toString() { + if (this == ProjectStatus.not_started) { + return "not started"; + } else if (this == ProjectStatus.no_group) { + return "no group"; + } + return super.toString(); + } +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectUpdateDTO.java b/backend/app/src/main/java/com/ugent/pidgeon/json/ProjectUpdateDTO.java similarity index 94% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectUpdateDTO.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/ProjectUpdateDTO.java index e186776a..531354d3 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/ProjectUpdateDTO.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/ProjectUpdateDTO.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; import java.time.OffsetDateTime; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/PublicUserDTO.java b/backend/app/src/main/java/com/ugent/pidgeon/json/PublicUserDTO.java similarity index 95% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/PublicUserDTO.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/PublicUserDTO.java index f1df3ca1..7e23d5b1 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/PublicUserDTO.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/PublicUserDTO.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public class PublicUserDTO { private String firstName; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/RelationRequest.java b/backend/app/src/main/java/com/ugent/pidgeon/json/RelationRequest.java similarity index 93% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/RelationRequest.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/RelationRequest.java index fc8dd16c..e1a75293 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/RelationRequest.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/RelationRequest.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; import com.ugent.pidgeon.postgre.models.types.CourseRelation; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/SubmissionJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/SubmissionJson.java similarity index 66% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/SubmissionJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/SubmissionJson.java index 49096d42..84099a24 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/SubmissionJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/SubmissionJson.java @@ -1,8 +1,7 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.ugent.pidgeon.postgre.models.OffsetDateTimeSerializer; - import java.time.OffsetDateTime; public class SubmissionJson { @@ -14,29 +13,26 @@ public class SubmissionJson { private String fileUrl; private Boolean structureAccepted; - private Boolean dockerAccepted; + private String dockerStatus; @JsonSerialize(using = OffsetDateTimeSerializer.class) private OffsetDateTime submissionTime; - private String structureFeedbackUrl; + private String structureFeedback; - public String getDockerFeedbackUrl() { - return dockerFeedbackUrl; - } - public void setDockerFeedbackUrl(String dockerFeedbackUrl) { - this.dockerFeedbackUrl = dockerFeedbackUrl; - } + private DockerTestFeedbackJson dockerFeedback; + private String artifactUrl; + - private String dockerFeedbackUrl; public SubmissionJson() { } public SubmissionJson( long id, String projectUrl, String groupUrl, Long projectId, Long groupId, String fileUrl, - Boolean structureAccepted, OffsetDateTime submissionTime, Boolean dockerAccepted, String structureFeedbackUrl, String dockerFeedbackUrl) { + Boolean structureAccepted, OffsetDateTime submissionTime, String structureFeedback, DockerTestFeedbackJson dockerFeedback, String dockerStatus, + String artifactUrl) { this.submissionId = id; this.projectUrl = projectUrl; this.groupUrl = groupUrl; @@ -45,9 +41,10 @@ public SubmissionJson( this.fileUrl = fileUrl; this.structureAccepted = structureAccepted; this.submissionTime = submissionTime; - this.dockerAccepted = dockerAccepted; - this.structureFeedbackUrl = structureFeedbackUrl; - this.dockerFeedbackUrl = dockerFeedbackUrl; + this.dockerFeedback = dockerFeedback; + this.structureFeedback = structureFeedback; + this.dockerStatus = dockerStatus; + this.artifactUrl = artifactUrl; } public long getSubmissionId() { @@ -98,20 +95,14 @@ public void setSubmissionTime(OffsetDateTime submissionTime) { this.submissionTime = submissionTime; } - public Boolean getDockerAccepted() { - return dockerAccepted; - } - public void setDockerAccepted(Boolean dockerAccepted) { - this.dockerAccepted = dockerAccepted; - } - public String getStructureFeedbackUrl() { - return structureFeedbackUrl; + public String getStructureFeedback() { + return structureFeedback; } - public void setStructureFeedbackUrl(String structureFeedbackUrl) { - this.structureFeedbackUrl = structureFeedbackUrl; + public void setStructureFeedback(String structureFeedback) { + this.structureFeedback = structureFeedback; } public Long getProjectId() { @@ -129,4 +120,28 @@ public Long getGroupId() { public void setGroupId(Long groupId) { this.groupId = groupId; } + + public String getDockerStatus() { + return dockerStatus; + } + + public void setDockerStatus(String dockerStatus) { + this.dockerStatus = dockerStatus; + } + + public DockerTestFeedbackJson getDockerFeedback() { + return dockerFeedback; + } + + public void setDockerFeedback(DockerTestFeedbackJson dockerFeedback) { + this.dockerFeedback = dockerFeedback; + } + + public String getArtifactUrl() { + return artifactUrl; + } + + public void setArtifactUrl(String artifactUrl) { + this.artifactUrl = artifactUrl; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/json/TestJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/TestJson.java new file mode 100644 index 00000000..1e206d19 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/TestJson.java @@ -0,0 +1,83 @@ +package com.ugent.pidgeon.json; + +public class TestJson { + + private String projectUrl; + private String dockerImage; + private String dockerScript; + private String dockerTemplate; + private String structureTest; + private String extraFilesUrl; + private String extraFilesName; + + + public TestJson() { + } + + public TestJson(String projectUrl, String dockerImage, String dockerScript, + String dockerTemplate, String structureTest, String extraFilesUrl, String extraFilesName) { + this.projectUrl = projectUrl; + this.dockerImage = dockerImage; + this.dockerScript = dockerScript; + this.dockerTemplate = dockerTemplate; + this.structureTest = structureTest; + this.extraFilesUrl = extraFilesUrl; + this.extraFilesName = extraFilesName; + } + + public String getProjectUrl() { + return projectUrl; + } + + public void setProjectUrl(String projectUrl) { + this.projectUrl = projectUrl; + } + + public String getDockerImage() { + return dockerImage; + } + + public void setDockerImage(String dockerImage) { + this.dockerImage = dockerImage; + } + + public String getDockerScript() { + return dockerScript; + } + + public void setDockerScript(String dockerScript) { + this.dockerScript = dockerScript; + } + + public String getStructureTest() { + return structureTest; + } + + public void setStructureTest(String structureTest) { + this.structureTest = structureTest; + } + + public String getDockerTemplate() { + return dockerTemplate; + } + + public void setDockerTemplate(String dockerTemplate) { + this.dockerTemplate = dockerTemplate; + } + + public String getExtraFilesUrl() { + return extraFilesUrl; + } + + public void setExtraFilesUrl(String extraFilesUrl) { + this.extraFilesUrl = extraFilesUrl; + } + + public String getExtraFilesName() { + return extraFilesName; + } + + public void setExtraFilesName(String extraFilesName) { + this.extraFilesName = extraFilesName; + } +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/json/TestUpdateJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/TestUpdateJson.java new file mode 100644 index 00000000..6f488905 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/TestUpdateJson.java @@ -0,0 +1,49 @@ +package com.ugent.pidgeon.json; + +public class TestUpdateJson { + private String dockerImage; + private String dockerScript; + private String dockerTemplate; + private String structureTest; + + public TestUpdateJson(String dockerImage, String dockerScript, String dockerTemplate, String structureTest) { + this.dockerImage = dockerImage; + this.dockerScript = dockerScript; + this.dockerTemplate = dockerTemplate; + this.structureTest = structureTest; + } + + public String getDockerImage() { + return dockerImage; + } + + public void setDockerImage(String dockerImage) { + this.dockerImage = dockerImage; + } + + public String getDockerScript() { + return dockerScript; + } + + public void setDockerScript(String dockerScript) { + this.dockerScript = dockerScript; + } + + public String getDockerTemplate() { + return dockerTemplate; + } + + public void setDockerTemplate(String dockerTemplate) { + this.dockerTemplate = dockerTemplate; + } + + public String getStructureTest() { + return structureTest; + } + + public void setStructureTest(String structureTest) { + this.structureTest = structureTest; + } + +} + diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UpdateGroupScoreRequest.java b/backend/app/src/main/java/com/ugent/pidgeon/json/UpdateGroupScoreRequest.java similarity index 91% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/UpdateGroupScoreRequest.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/UpdateGroupScoreRequest.java index 0a62762c..83f38b97 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UpdateGroupScoreRequest.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/UpdateGroupScoreRequest.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public class UpdateGroupScoreRequest { private Float score; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserIdJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/UserIdJson.java similarity index 85% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/UserIdJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/UserIdJson.java index b64793ce..fa92f794 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserIdJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/UserIdJson.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public class UserIdJson { private Long userId; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/UserJson.java similarity index 82% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/UserJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/UserJson.java index 77441994..ecf60604 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/UserJson.java @@ -1,10 +1,8 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; import com.ugent.pidgeon.controllers.ApiRoutes; - import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.UserRole; - import java.time.OffsetDateTime; public class UserJson { @@ -14,6 +12,7 @@ public class UserJson { private String surname; private String email; private UserRole role; + private String studentNumber; private OffsetDateTime createdAt; @@ -29,6 +28,7 @@ public UserJson(UserEntity entity) { this.email = entity.getEmail(); this.role = entity.getRole(); this.createdAt = entity.getCreatedAt(); + this.studentNumber = entity.getStudentNumber(); // this.courses = new ArrayList<>(); } @@ -81,13 +81,13 @@ public void setCreatedAt(OffsetDateTime createdAt) { } public String getUrl() { - return ApiRoutes.USER_BASE_PATH + "/" + id; + return ApiRoutes.USERS_BASE_PATH + "/" + id; } public void setUrl(String s){} public String getCourseUrl() { - return ApiRoutes.USER_BASE_PATH + "/" + id+"/courses"; + return ApiRoutes.COURSE_BASE_PATH; } public void setCourseUrl(String s){} @@ -96,4 +96,11 @@ public String getProjectUrl() { } public void setProjectUrl(String s){} + public String getStudentNumber() { + return studentNumber; + } + + public void setStudentNumber(String studentNumber) { + this.studentNumber = studentNumber; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/userProjectsJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/UserProjectsJson.java similarity index 58% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/userProjectsJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/UserProjectsJson.java index 85b1c909..ce6fb6b6 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/userProjectsJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/UserProjectsJson.java @@ -1,8 +1,8 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; import com.ugent.pidgeon.model.ProjectResponseJson; import java.util.List; -public record userProjectsJson(List enrolledProjects, List adminProjects) { +public record UserProjectsJson(List enrolledProjects, List adminProjects) { } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserReferenceJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/UserReferenceJson.java similarity index 68% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/UserReferenceJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/UserReferenceJson.java index b14d4068..c636b1fe 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserReferenceJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/UserReferenceJson.java @@ -1,14 +1,16 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public class UserReferenceJson { private String name; private String email; private Long userId; + private String studentNumber; - public UserReferenceJson(String name, String email, Long userId) { + public UserReferenceJson(String name, String email, Long userId, String studentNumber) { this.name = name; this.email = email; this.userId = userId; + this.studentNumber = studentNumber; } public String getEmail() { @@ -39,4 +41,11 @@ public void setName(String name) { } + public String getStudentNumber() { + return studentNumber; + } + + public void setStudentNumber(String studentNumber) { + this.studentNumber = studentNumber; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserReferenceWithRelation.java b/backend/app/src/main/java/com/ugent/pidgeon/json/UserReferenceWithRelation.java similarity index 93% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/UserReferenceWithRelation.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/UserReferenceWithRelation.java index cd158c13..66c41d78 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserReferenceWithRelation.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/UserReferenceWithRelation.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; public class UserReferenceWithRelation { private UserReferenceJson user; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserUpdateJson.java b/backend/app/src/main/java/com/ugent/pidgeon/json/UserUpdateJson.java similarity index 96% rename from backend/app/src/main/java/com/ugent/pidgeon/model/json/UserUpdateJson.java rename to backend/app/src/main/java/com/ugent/pidgeon/json/UserUpdateJson.java index 1bb1d558..11fab0f2 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/UserUpdateJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/json/UserUpdateJson.java @@ -1,4 +1,4 @@ -package com.ugent.pidgeon.model.json; +package com.ugent.pidgeon.json; import com.ugent.pidgeon.postgre.models.types.UserRole; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/Auth.java b/backend/app/src/main/java/com/ugent/pidgeon/model/Auth.java index 4add2b68..aec34038 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/Auth.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/Auth.java @@ -1,13 +1,12 @@ package com.ugent.pidgeon.model; import com.ugent.pidgeon.postgre.models.UserEntity; +import java.util.Collection; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.util.Assert; -import java.util.Collection; -import com.ugent.pidgeon.model.User; public class Auth extends AbstractAuthenticationToken { private static final long serialVersionUID = 620L; @@ -29,6 +28,7 @@ public String getName(){ public String getEmail(){ return user.email; } + public String getStudentNumber() { return user.studentnumber; } public String getOid(){ return user.oid; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/ProjectResponseJson.java b/backend/app/src/main/java/com/ugent/pidgeon/model/ProjectResponseJson.java index ade5f0e1..ca764d0c 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/ProjectResponseJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/ProjectResponseJson.java @@ -1,10 +1,9 @@ package com.ugent.pidgeon.model; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.ugent.pidgeon.model.json.CourseReferenceJson; -import com.ugent.pidgeon.model.json.ProjectProgressJson; +import com.ugent.pidgeon.json.CourseReferenceJson; +import com.ugent.pidgeon.json.ProjectProgressJson; import com.ugent.pidgeon.postgre.models.OffsetDateTimeSerializer; - import java.time.OffsetDateTime; public record ProjectResponseJson( @@ -20,5 +19,7 @@ public record ProjectResponseJson( Integer maxScore, boolean visible, ProjectProgressJson progress, - Long groupId + Long groupId, + Long clusterId, + OffsetDateTime visibleAfter ) {} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/User.java b/backend/app/src/main/java/com/ugent/pidgeon/model/User.java index 330c74e7..dda517c1 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/User.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/User.java @@ -1,7 +1,5 @@ package com.ugent.pidgeon.model; -import java.util.List; - public class User { public String name; @@ -9,12 +7,14 @@ public class User { public String lastName; public String email; public String oid; + public String studentnumber; - public User (String name, String firstName, String lastName, String email, String oid) { + public User (String name, String firstName, String lastName, String email, String oid, String studentnumber) { this.name = name; this.email = email; this.oid = oid; this.firstName = firstName; this.lastName = lastName; + this.studentnumber = studentnumber; } } \ No newline at end of file diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseJson.java b/backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseJson.java deleted file mode 100644 index 42a3b57b..00000000 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseJson.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.ugent.pidgeon.model.json; - -// Hulpklasse die gebruikt wordt in Requestbodies. -public class CourseJson{ - private String name; - - private String description; - - public CourseJson(String name, String description) { - this.name = name; - this.description = description; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } -} - diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseWithRelationJson.java b/backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseWithRelationJson.java deleted file mode 100644 index 4a9ffc73..00000000 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/CourseWithRelationJson.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.ugent.pidgeon.model.json; - -import com.ugent.pidgeon.postgre.models.types.CourseRelation; - -public class CourseWithRelationJson { - private Long courseId; - private String url; - private String name; - - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public CourseWithRelationJson() { - } - - public CourseWithRelationJson(String url, CourseRelation relation, String name, Long courseId) { - this.url = url; - this.relation = relation; - this.name = name; - this.courseId = courseId; - } - - private CourseRelation relation; - - public CourseRelation getRelation() { - return relation; - } - - public void setRelation(CourseRelation relation) { - this.relation = relation; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getCourseId() { - return courseId; - } - - public void setCourseId(Long id) { - this.courseId = id; - } -} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/TestJson.java b/backend/app/src/main/java/com/ugent/pidgeon/model/json/TestJson.java deleted file mode 100644 index 35c5e039..00000000 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/TestJson.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.ugent.pidgeon.model.json; - -public class TestJson { - private String projectUrl; - private String dockerImage; - private String dockerTestUrl; - private String structureTestUrl; - - public TestJson() { - } - - public TestJson(String projectUrl, String dockerImage, String dockerTestUrl, String structureTestUrl) { - this.projectUrl = projectUrl; - this.dockerImage = dockerImage; - this.dockerTestUrl = dockerTestUrl; - this.structureTestUrl = structureTestUrl; - } - - public String getProjectUrl() { - return projectUrl; - } - - public void setProjectUrl(String projectUrl) { - this.projectUrl = projectUrl; - } - - public String getDockerImage() { - return dockerImage; - } - - public void setDockerImage(String dockerImage) { - this.dockerImage = dockerImage; - } - - public String getDockerTestUrl() { - return dockerTestUrl; - } - - public void setDockerTestUrl(String dockerTestUrl) { - this.dockerTestUrl = dockerTestUrl; - } - - public String getStructureTestUrl() { - return structureTestUrl; - } - - public void setStructureTestUrl(String structureTestUrl) { - this.structureTestUrl = structureTestUrl; - } -} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/AddDockerModel.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/AddDockerModel.java index b0dea7cf..c5eac7ba 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/AddDockerModel.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/AddDockerModel.java @@ -1,9 +1,5 @@ package com.ugent.pidgeon.model.submissionTesting; -import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.core.command.PullImageResultCallback; -import com.ugent.pidgeon.util.DockerClientInstance; - public class AddDockerModel { } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerOutput.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerOutput.java new file mode 100644 index 00000000..9024b788 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerOutput.java @@ -0,0 +1,6 @@ +package com.ugent.pidgeon.model.submissionTesting; + +public interface DockerOutput { + boolean isAllowed(); + String getFeedbackAsString(); +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java index 872e9afa..fdddad2c 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java @@ -14,9 +14,15 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Enumeration; import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import org.apache.commons.io.FileUtils; public class DockerSubmissionTestModel { @@ -48,6 +54,12 @@ public DockerSubmissionTestModel(String dockerImage) { createFolder(); // Create the folder after we// generate tmp folder of project // Configure container with volume bindings container.withHostConfig(new HostConfig().withBinds(new Bind(localMountFolder, sharedVolume))); + + // Init directories in the shared folder + new File(localMountFolder + "input/").mkdirs(); + new File(localMountFolder + "output/").mkdirs(); + new File(localMountFolder + "artifacts/").mkdirs(); + new File(localMountFolder + "extra/").mkdirs(); } @@ -63,31 +75,77 @@ private void removeFolder() { // clear shared folder } } - public DockerTestOutput runSubmission(String script) throws InterruptedException { - return runSubmission(script, new File[0]); + // function for deleting shared docker files, only use after catching the artifacts + public void cleanUp() { + removeFolder(); } - private void runContainer(String script, File[] inputFiles, ResultCallback.Adapter callback) { - - // Init directories in the shared folder - new File(localMountFolder + "input/").mkdirs(); - new File(localMountFolder + "output/").mkdirs(); - - // Copy input files to the shared folder - for (File file : inputFiles) { + public void addInputFiles(File[] files) { + for (File file : files) { try { FileUtils.copyFileToDirectory(file, new File(localMountFolder + "input/")); } catch (IOException e) { e.printStackTrace(); } } + } + + public void addUtilFiles(Path pathToZip){ + // first unzip files to the utils folder + try { + ZipFile zipFile = new ZipFile(pathToZip.toFile()); + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + File entryDestination = new File(localMountFolder + "extra/", entry.getName()); + if (entry.isDirectory()) { + entryDestination.mkdirs(); + } else { + File parent = entryDestination.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + try { + FileUtils.copyInputStreamToFile(zipFile.getInputStream(entry), entryDestination); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void addZipInputFiles(ZipFile zipFile) { + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + File entryDestination = new File(localMountFolder + "input/", entry.getName()); + if (entry.isDirectory()) { + entryDestination.mkdirs(); + } else { + File parent = entryDestination.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + try { + FileUtils.copyInputStreamToFile(zipFile.getInputStream(entry), entryDestination); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + private void runContainer(String script, ResultCallback.Adapter callback) { // Configure and start the container container.withCmd("/bin/sh", "-c", script); CreateContainerResponse responseContainer = container.exec(); String executionContainerID = responseContainer.getId(); // Use correct ID for operations dockerClient.startContainerCmd(executionContainerID).exec(); - try{ + try { dockerClient.logContainerCmd(executionContainerID) .withStdOut(true) .withStdErr(true) @@ -95,7 +153,7 @@ private void runContainer(String script, File[] inputFiles, ResultCallback.Adapt .withTailAll() .exec(callback) .awaitCompletion(); - }catch (InterruptedException e){ + } catch (InterruptedException e) { System.err.println("Failed to read output file. Push is denied."); } @@ -107,8 +165,7 @@ private void runContainer(String script, File[] inputFiles, ResultCallback.Adapt } - public DockerTestOutput runSubmission(String script, File[] inputFiles) - { + public DockerTestOutput runSubmission(String script) { List consoleLogs = new ArrayList<>(); ResultCallback.Adapter callback = new ResultCallback.Adapter<>() { @@ -117,7 +174,7 @@ public void onNext(Frame item) { consoleLogs.add(new String(item.getPayload())); } }; - runContainer(script, inputFiles, callback); + runContainer(script, callback); boolean allowPush; @@ -133,15 +190,12 @@ public void onNext(Frame item) { allowPush = false; } - // Cleanup - removeFolder(); return new DockerTestOutput(consoleLogs, allowPush); } - public DockerTemplateTestResult runSubmissionWithTemplate(String script, String template, - File[] inputFiles) throws InterruptedException { + public DockerTemplateTestOutput runSubmissionWithTemplate(String script, String template) { - runContainer(script, inputFiles, new Adapter<>()); + runContainer(script, new Adapter<>()); // execute dockerClient and await @@ -171,9 +225,6 @@ public DockerTemplateTestResult runSubmissionWithTemplate(String script, String } } - // Cleanup - removeFolder(); - // Check if allowed boolean allowed = true; for (DockerSubtestResult result : results) { @@ -183,7 +234,7 @@ public DockerTemplateTestResult runSubmissionWithTemplate(String script, String } } - return new DockerTemplateTestResult(results, allowed); + return new DockerTemplateTestOutput(results, allowed); } private static DockerSubtestResult getDockerSubtestResult(String entry) { @@ -217,15 +268,29 @@ private static DockerSubtestResult getDockerSubtestResult(String entry) { templateEntry.setRequired(true); } else if (currentOption.equalsIgnoreCase(">Optional")) { templateEntry.setRequired(false); - } else if (currentOption.substring(0, 12).equalsIgnoreCase(">Description")) { + } else if (currentOption.length() >=13 && currentOption.substring(0, 13).equalsIgnoreCase(">Description=")) { templateEntry.setTestDescription(currentOption.split("=\"")[1].split("\"")[0]); } } - templateEntry.setCorrect(entry.substring(lineIterator)); + String substring = entry.substring(lineIterator); + if (substring.endsWith("\n")) { + substring = substring.substring(0, substring.length() - 1); + } + templateEntry.setCorrect(substring); return templateEntry; } - public static void addDocker(String imageName) { + public List getArtifacts() { + List files = new ArrayList<>(); + File[] filesInFolder = new File(localMountFolder + "artifacts/").listFiles(); + if (filesInFolder != null) { + files.addAll(Arrays.asList(filesInFolder)); + } + return files; + } + + + public static void installImage(String imageName) { DockerClient dockerClient = DockerClientInstance.getInstance(); // Pull the Docker image (if not already present) @@ -247,4 +312,72 @@ public static void removeDockerImage(String imageName) { System.out.println("Failed removing docker image: " + e.getMessage()); } } + + public static boolean imageExists(String image) { + try { + // Split the image into repository and tag + String[] parts = image.split(":"); + String repository = parts[0]; + String tag = parts.length > 1 ? parts[1] : "latest"; + + // Construct the URL for the Docker Hub API + String apiUrl = "https://hub.docker.com/v2/repositories/library/" + repository + "/tags/" + tag; + URL url = new URL(apiUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.connect(); + int responseCode = connection.getResponseCode(); + + return (responseCode == 200); + } catch (IOException e) { + return false; + } + } + + public static void tryTemplate(String template) { + // lines with @ should be the first of a string + // @ is always the first character + // ">" options under the template should be "required, optional or description="..." + boolean atLeastOne = false; // Template should not be empty + String[] lines = template.split("\n"); + if (lines[0].charAt(0) != '@') { + throw new IllegalArgumentException("Template should start with a '@'"); + } + boolean isConfigurationLine = false; + for (String line : lines) { + if(line.length() == 0){ // skip line if empty + continue; + } + if (line.charAt(0) == '@') { + atLeastOne = true; + isConfigurationLine = true; + continue; + } + if (isConfigurationLine) { + if (line.charAt(0) == '>') { + boolean isDescription = line.length() >= 13 && line.substring(0, 13).equalsIgnoreCase(">Description="); + // option lines + if (!line.equalsIgnoreCase(">Required") && !line.equalsIgnoreCase(">Optional") + && !isDescription) { + throw new IllegalArgumentException("Invalid option in template"); + } + } else { + isConfigurationLine = false; + } + } + } + if(! atLeastOne){ + throw new IllegalArgumentException("Template should not be empty"); + } + } + + public static boolean isValidTemplate(String template){ + try{ + tryTemplate(template); + return true; + }catch (Exception e){ + return false; + } + } + } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubtestResult.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubtestResult.java index 43869cc9..c0c70fcd 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubtestResult.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubtestResult.java @@ -1,6 +1,6 @@ package com.ugent.pidgeon.model.submissionTesting; -public class DockerSubtestResult { +public class DockerSubtestResult implements DockerOutput { private String correct; private String output; private String testName; @@ -59,4 +59,17 @@ public boolean isRequired() { public void setRequired(boolean required) { this.required = required; } + + @Override + public boolean isAllowed() { + return correct.equals(output); + } + + @Override + public String getFeedbackAsString() { + // Display feedback as a json, only display testName and testDescription if they are not empty + String testDescription = this.testDescription.isEmpty() ? "" : "\",\"testDescription\":\"" + this.testDescription; + //TODO add allowed to json + return "{\"testName\":\"" + testName + testDescription + "\",\"correct\":\"" + correct + "\",\"output\":\"" + output + "\", \"required\":" + required + ", \"succes\": " + isAllowed() + "}"; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTemplateTestOutput.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTemplateTestOutput.java new file mode 100644 index 00000000..04465cdf --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTemplateTestOutput.java @@ -0,0 +1,37 @@ +package com.ugent.pidgeon.model.submissionTesting; + +import java.util.List; +import java.util.logging.Logger; +import org.hibernate.usertype.LoggableUserType; + +public class DockerTemplateTestOutput implements DockerOutput{ + private final List subtestResults; + private final boolean allowed; + + public List getSubtestResults() { + return subtestResults; + } + + @Override + public boolean isAllowed() { + return allowed; + } + + public DockerTemplateTestOutput(List subtestResults, boolean allowed) { + this.subtestResults = subtestResults; + this.allowed = allowed; + } + @Override + public String getFeedbackAsString(){ + //json representation of the tests + StringBuilder feedback = new StringBuilder("{\"subtests\": ["); + for (DockerSubtestResult subtestResult : subtestResults) { + feedback.append(subtestResult.getFeedbackAsString()) + .append(","); + } + feedback.deleteCharAt(feedback.length() - 1); // remove last comma , + feedback.append("]}"); + Logger.getGlobal().info(feedback.toString()); + return feedback.toString(); + } +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTemplateTestResult.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTemplateTestResult.java deleted file mode 100644 index 1ece985d..00000000 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTemplateTestResult.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.ugent.pidgeon.model.submissionTesting; - -import java.util.List; - -public class DockerTemplateTestResult { - private List subtestResults; - private boolean allowed; - - public List getSubtestResults() { - return subtestResults; - } - - public boolean isAllowed() { - return allowed; - } - - public DockerTemplateTestResult(List subtestResults, boolean allowed) { - this.subtestResults = subtestResults; - this.allowed = allowed; - } -} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTestOutput.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTestOutput.java index 25e27dc5..6b9e520a 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTestOutput.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTestOutput.java @@ -2,7 +2,7 @@ import java.util.List; -public class DockerTestOutput { +public class DockerTestOutput implements DockerOutput { public List logs; public Boolean allowed; @@ -11,4 +11,13 @@ public DockerTestOutput(List logs, Boolean allowed) { this.allowed = allowed; } + @Override + public boolean isAllowed() { + return allowed; + } + + @Override + public String getFeedbackAsString() { + return String.join("", logs); + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java index 13bdd40b..c33db35c 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java @@ -1,7 +1,13 @@ package com.ugent.pidgeon.model.submissionTesting; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -13,10 +19,39 @@ public class SubmissionTemplateModel { private static class FileEntry { public String name; Pattern pattern; + private String invert(String name){ + // invert . with \. for simpler filenames + List dotLocations = new ArrayList<>(); + List escapedDotLocations = new ArrayList<>(); + for (int i = 0; i < name.length(); i++) { + if(i > 0 && name.charAt(i - 1) == '\\' && name.charAt(i) == '.'){ + escapedDotLocations.add(i); + }else if (name.charAt(i) == '.') { + dotLocations.add(i); + } + } + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < name.length(); i++) { + if(escapedDotLocations.contains(i + 1)){ + // skip the break + continue; + }else if(escapedDotLocations.contains(i)){ + sb.append("."); + }else if(dotLocations.contains(i)){ + sb.append("\\."); + }else{ + sb.append(name.charAt(i)); + } + } + return sb.toString(); + + } public FileEntry(String name) { - this.name = name; - pattern = Pattern.compile("^" + name + "$"); // hat for defining start of the string, $ defines the end + + this.name = invert(name); + + pattern = Pattern.compile("^" + this.name + "$"); // hat for defining start of the string, $ defines the end } public boolean matches(String fileName) { @@ -52,7 +87,6 @@ public void parseSubmissionTemplate(String templateString) { mostSpaces = spaceAmount; } lines[i] = "\t".repeat(tabsPerSpaces.get(spaceAmount)) + line.replaceAll(" ", ""); - ; } // Create folder stack for keeping track of all the folders while exploring the insides @@ -142,7 +176,7 @@ public SubmissionResult checkSubmission(ZipFile file) throws IOException { } // return true if all items in template are in the zip for (int i = 0; i < requiredItemsContained.size(); i++) { - if (requiredItemsContained.get(i) == false) { + if (!requiredItemsContained.get(i)) { filesMissing.add(requiredFiles.get(i).name); } } @@ -151,16 +185,16 @@ public SubmissionResult checkSubmission(ZipFile file) throws IOException { boolean passed = (filesMissing.size() + filesUnrequested.size() + filesDenied.size()) == 0; String feedback = passed ? "File structure is correct" : "File structure failed to pass the template, because: \n "; if (!filesMissing.isEmpty()) { - feedback += " -The following files are required from the template and are not found in the project: \n -"; - feedback += String.join("\n -", filesMissing); + feedback += "- The following files are required from the template and are not found in the project: \n - "; + feedback += String.join("\n - ", filesMissing); } if (!filesUnrequested.isEmpty()) { - feedback += "\n -The following files are not requested in the template: \n -"; - feedback += String.join("\n -", filesUnrequested); + feedback += "\n - The following files are not requested in the template: \n - "; + feedback += String.join("\n - ", filesUnrequested); } if (!filesDenied.isEmpty()) { - feedback += "\n -The following files are not allowed in the project: \n -"; - feedback += String.join("\n -", filesDenied); + feedback += "\n - The following files are not allowed in the project: \n - "; + feedback += String.join("\n - ", filesDenied); } return new SubmissionResult(passed, feedback); @@ -175,4 +209,66 @@ public SubmissionResult checkSubmission(String file) throws IOException { return checkSubmission(new ZipFile(file)); } + // will throw error if there are errors in the template + public static void tryTemplate(String template) throws IllegalArgumentException { + List lines = List.of(template.split("\n")); + // check if the template is valid, control if every line contains a file parsable string + // check if the file is in a valid folder location (indentation is correct) + // check if the first file has indentation 0 + List indentionAmounts = new ArrayList<>(); + indentionAmounts.add(0); + if(getIndentation(lines.get(0)) != 0){ + throw new IllegalArgumentException("First file should not have any spaces or tabs."); + } + boolean newFolder = false; + for(int line_index = 0; line_index < lines.size(); line_index++){ + String line = lines.get(line_index); + int indentation = getIndentation(line); + if(line.isEmpty()){ + throw new IllegalArgumentException("Empty file name in template, remove blank lines"); + } + if(newFolder && indentation > indentionAmounts.get(indentionAmounts.size() - 1)){ + // since the indentation is larger than the previous, we are dealing with the first file in a new folder + indentionAmounts.add(indentation); + newFolder = false; + }else{ + // we are dealing with a file in a folder, thus the indentation should be equal to one of the previous folders + for(int i = indentionAmounts.size() - 1; i >= 0; i--){ + if(indentionAmounts.get(i) == indentation){ + break; + } + if(i == 0){ + throw new IllegalArgumentException("File at line "+ line_index + " is not in a valid folder location (indentation is incorrect)"); + } + } + // check if file is correct, since location is correct + + // first check if file contains valid file names + if(line.substring(0,line.length() - 1).contains("/")){ + throw new IllegalArgumentException("File/folder at line "+ (line_index+1) + " contains invalid characters"); + } + // check if file is a folder + if(line.charAt(line.length() - 1) == '/') { + newFolder = true; + } + } + if(line.charAt(line.length() - 1) == '/'){ + // new folder start! + newFolder = true; + } + } + } + + + private static int getIndentation(String line){ + int length = line.length(); + // one space is equal to a tab + for(int i = 0; i < length; i++){ + if(line.charAt(i) != ' ' && line.charAt(i) != '\t'){ + return i; + } + } + return length - 1; + } + } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/CourseEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/CourseEntity.java index dce6a31e..35aaac64 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/CourseEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/CourseEntity.java @@ -1,7 +1,11 @@ package com.ugent.pidgeon.postgre.models; -import jakarta.persistence.*; - +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.time.OffsetDateTime; @@ -18,9 +22,16 @@ public class CourseEntity { @Column(name = "description", nullable=false) private String description; + @Column(name = "course_year", nullable = true) + private Integer courseYear; + + @Column(name = "created_at") private OffsetDateTime createdAt; + @Column(name = "archived_at") + private OffsetDateTime archivedAt; + public String getJoinKey() { return joinKey; } @@ -32,9 +43,10 @@ public void setJoinKey(String joinKey) { @Column(name = "join_key", nullable=true) private String joinKey; - public CourseEntity(String name, String description) { + public CourseEntity(String name, String description,Integer courseYear) { this.name = name; this.description = description; + this.courseYear = courseYear; } public CourseEntity() { @@ -68,6 +80,7 @@ public void setDescription(String description) { } + public OffsetDateTime getCreatedAt() { return createdAt; } @@ -75,4 +88,22 @@ public OffsetDateTime getCreatedAt() { public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } + + public OffsetDateTime getArchivedAt() { + return archivedAt; + } + + public void setArchivedAt(OffsetDateTime archivedAt) { + this.archivedAt = archivedAt; + } + + public int getCourseYear() { + return courseYear; + } + public void setCourseYear(int courseYear){ + this.courseYear = courseYear; + } + + + } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/CourseUserEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/CourseUserEntity.java index b3231662..4309efe1 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/CourseUserEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/CourseUserEntity.java @@ -1,7 +1,11 @@ package com.ugent.pidgeon.postgre.models; import com.ugent.pidgeon.postgre.models.types.CourseRelation; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; @Entity @IdClass(CourseUserId.class) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/FileEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/FileEntity.java index f7e0acef..284fd6e8 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/FileEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/FileEntity.java @@ -1,6 +1,11 @@ package com.ugent.pidgeon.postgre.models; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; @Entity @Table(name = "files") diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupClusterEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupClusterEntity.java index ea2818bb..e23331cb 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupClusterEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupClusterEntity.java @@ -1,7 +1,11 @@ package com.ugent.pidgeon.postgre.models; -import jakarta.persistence.*; - +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.time.OffsetDateTime; @Entity @@ -26,6 +30,9 @@ public class GroupClusterEntity { @Column(name="group_amount", nullable=false) private int groupAmount; + @Column(name = "lock_groups_after") + private OffsetDateTime lockGroupsAfter; + @Column(name = "created_at") private OffsetDateTime createdAt; @@ -87,4 +94,12 @@ public OffsetDateTime getCreatedAt() { public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } + + public OffsetDateTime getLockGroupsAfter() { + return lockGroupsAfter; + } + + public void setLockGroupsAfter(OffsetDateTime lockGroupsAfter) { + this.lockGroupsAfter = lockGroupsAfter; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupEntity.java index 25fc4d99..42a9af04 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupEntity.java @@ -1,6 +1,11 @@ package com.ugent.pidgeon.postgre.models; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; @Entity @Table(name="groups") diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupFeedbackEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupFeedbackEntity.java index 05d30eea..ce770054 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupFeedbackEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupFeedbackEntity.java @@ -1,6 +1,10 @@ package com.ugent.pidgeon.postgre.models; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; @Entity @IdClass(GroupFeedbackId.class) @@ -52,11 +56,11 @@ public String getFeedback() { } - public float getScore() { + public Float getScore() { return grade; } - public void setScore(float score) { + public void setScore(Float score) { this.grade = score; } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupUserEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupUserEntity.java index 449e4a71..6ef6ef15 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupUserEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupUserEntity.java @@ -1,8 +1,10 @@ package com.ugent.pidgeon.postgre.models; -import jakarta.persistence.*; - -import java.io.Serializable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; @Entity @IdClass(GroupUserId.class) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/OffsetDateTimeSerializer.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/OffsetDateTimeSerializer.java index c0d40644..a21ea48c 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/OffsetDateTimeSerializer.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/OffsetDateTimeSerializer.java @@ -3,9 +3,8 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; - -import java.time.OffsetDateTime; import java.io.IOException; +import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.logging.Logger; diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/ProjectEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/ProjectEntity.java index 67dc778a..a388353e 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/ProjectEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/ProjectEntity.java @@ -1,7 +1,11 @@ package com.ugent.pidgeon.postgre.models; -import jakarta.persistence.*; - +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.time.OffsetDateTime; @@ -38,6 +42,10 @@ public class ProjectEntity { @Column(name="max_score") private Integer maxScore; + @Column(name = "visible_after") + private OffsetDateTime visibleAfter; + + public ProjectEntity(long courseId, String name, String description, long groupClusterId, Long testId, Boolean visible, Integer maxScore, OffsetDateTime deadline) { this.courseId = courseId; this.name = name; @@ -124,4 +132,12 @@ public OffsetDateTime getDeadline() { public void setDeadline(OffsetDateTime deadline) { this.deadline = deadline; } + + public OffsetDateTime getVisibleAfter() { + return visibleAfter; + } + + public void setVisibleAfter(OffsetDateTime visibleAfter) { + this.visibleAfter = visibleAfter; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java index 65803ff0..23395c33 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java @@ -1,7 +1,13 @@ package com.ugent.pidgeon.postgre.models; -import jakarta.persistence.*; - +import com.ugent.pidgeon.postgre.models.types.DockerTestState; +import com.ugent.pidgeon.postgre.models.types.DockerTestType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.time.OffsetDateTime; @Entity @@ -16,7 +22,7 @@ public class SubmissionEntity { private long projectId; @Column(name="group_id", nullable=false) - private long groupId; + private Long groupId; @Column(name="file_id", nullable=false) private long fileId; @@ -36,10 +42,16 @@ public class SubmissionEntity { @Column(name="docker_feedback") private String dockerFeedback; + @Column(name="docker_test_state") + private String dockerTestState; + + @Column(name="docker_type") + private String dockerType; + public SubmissionEntity() { } - public SubmissionEntity(long projectId, long groupId, Long fileId, OffsetDateTime submissionTime, Boolean structureAccepted, Boolean dockerAccepted) { + public SubmissionEntity(long projectId, Long groupId, Long fileId, OffsetDateTime submissionTime, Boolean structureAccepted, Boolean dockerAccepted) { this.projectId = projectId; this.groupId = groupId; this.fileId = fileId; @@ -48,10 +60,14 @@ public SubmissionEntity(long projectId, long groupId, Long fileId, OffsetDateTim this.dockerAccepted = dockerAccepted; } - public long getGroupId() { + public Long getGroupId() { return groupId; } + public void setGroupId(Long groupId) { + this.groupId = groupId; + } + public long getFileId() { return fileId; } @@ -116,4 +132,37 @@ public String getDockerFeedback() { public void setDockerFeedback(String dockerFeedbackFileId) { this.dockerFeedback = dockerFeedbackFileId; } + public DockerTestState getDockerTestState() { + if(dockerTestState == null) { + return DockerTestState.no_test; + } + return switch (dockerTestState) { + case "running" -> DockerTestState.running; + case "finished" -> DockerTestState.finished; + case "aborted" -> DockerTestState.aborted; + default -> null; + }; + } + + public void setDockerTestState(DockerTestState dockerTestState) { + this.dockerTestState = dockerTestState.toString(); + } + + public DockerTestType getDockerTestType() { + if (dockerType == null) { + return DockerTestType.NONE; + } + return switch (dockerType) { + case "SIMPLE" -> DockerTestType.SIMPLE; + case "TEMPLATE" -> DockerTestType.TEMPLATE; + case "NONE" -> DockerTestType.NONE; + default -> null; + }; + } + + public void setDockerType(DockerTestType dockerType) { + this.dockerType = dockerType.toString(); + } + + } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/TestEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/TestEntity.java index f0aac3e0..85d1d0ce 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/TestEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/TestEntity.java @@ -1,6 +1,11 @@ package com.ugent.pidgeon.postgre.models; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; @Entity @Table(name = "tests") @@ -15,30 +20,47 @@ public class TestEntity { @Column(name = "docker_image") private String dockerImage; - @Column(name = "docker_test") - private long dockerTestId; + @Column(name = "docker_test_script") + private String dockerTestScript; - @Column(name = "structure_test_id") - private long structureTestId; + @Column(name = "docker_test_template") + private String dockerTestTemplate; - public TestEntity() { - } + @Column(name = "structure_template") + private String structureTemplate; - public TestEntity(String dockerImage, long dockerTestId, long structureTestId) { + @Column(name = "extra_files") + private Long extraFilesId; + + public TestEntity(String dockerImage, String docker_test_script, + String dockerTestTemplate, + String structureTemplate) { this.dockerImage = dockerImage; - this.dockerTestId = dockerTestId; - this.structureTestId = structureTestId; + this.dockerTestScript = docker_test_script; + this.dockerTestTemplate = dockerTestTemplate; + this.structureTemplate = structureTemplate; } + public TestEntity() { + + } - public void setId(Long id) { - this.id = id; + public String getDockerTestScript() { + return dockerTestScript; + } + + public void setDockerTestScript(String docker_test_script) { + this.dockerTestScript = docker_test_script; } - public Long getId() { + public long getId() { return id; } + public void setId(long id) { + this.id = id; + } + public String getDockerImage() { return dockerImage; } @@ -47,19 +69,27 @@ public void setDockerImage(String dockerImage) { this.dockerImage = dockerImage; } - public long getDockerTestId() { - return dockerTestId; + public String getDockerTestTemplate() { + return dockerTestTemplate; + } + + public void setDockerTestTemplate(String dockerTestTemplate) { + this.dockerTestTemplate = dockerTestTemplate; + } + + public String getStructureTemplate() { + return structureTemplate; } - public void setDockerTestId(long dockerTest) { - this.dockerTestId = dockerTest; + public void setStructureTemplate(String structureTemplate) { + this.structureTemplate = structureTemplate; } - public long getStructureTestId() { - return structureTestId; + public Long getExtraFilesId() { + return extraFilesId; } - public void setStructureTestId(long structureTestId) { - this.structureTestId = structureTestId; + public void setExtraFilesId(Long extraFilesId) { + this.extraFilesId = extraFilesId; } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/UserEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/UserEntity.java index d39ac2cc..ed990b7f 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/UserEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/UserEntity.java @@ -2,8 +2,12 @@ import com.ugent.pidgeon.postgre.models.types.UserRole; -import jakarta.persistence.*; - +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.time.OffsetDateTime; @@ -34,12 +38,17 @@ public class UserEntity { @Column(name = "created_at") private OffsetDateTime createdAt; - public UserEntity(String name, String surname, String email, UserRole role, String azureId) { + @Column(name = "studentnumber") + private String studentNumber; + + public UserEntity(String name, String surname, String email, UserRole role, String azureId, + String studentNumber) { this.name = name; this.surname = surname; this.email = email; this.role = role.toString(); this.azureId = azureId; + this.studentNumber = studentNumber; } public UserEntity() { @@ -110,5 +119,13 @@ public OffsetDateTime getCreatedAt() { public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } + + public String getStudentNumber() { + return studentNumber; + } + + public void setStudentNumber(String studentNumber) { + this.studentNumber = studentNumber; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/types/DockerTestState.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/types/DockerTestState.java new file mode 100644 index 00000000..e1a84616 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/types/DockerTestState.java @@ -0,0 +1,9 @@ +package com.ugent.pidgeon.postgre.models.types; + +public enum DockerTestState { + running, + finished, + aborted, + + no_test +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/types/DockerTestType.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/types/DockerTestType.java new file mode 100644 index 00000000..eefd122c --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/types/DockerTestType.java @@ -0,0 +1,7 @@ +package com.ugent.pidgeon.postgre.models.types; + +public enum DockerTestType { + SIMPLE, + TEMPLATE, + NONE +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/CourseRepository.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/CourseRepository.java index d44a21e5..9c1ca516 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/CourseRepository.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/CourseRepository.java @@ -1,15 +1,13 @@ package com.ugent.pidgeon.postgre.repository; -import com.ugent.pidgeon.model.json.UserReferenceJson; import com.ugent.pidgeon.postgre.models.CourseEntity; import com.ugent.pidgeon.postgre.models.ProjectEntity; import com.ugent.pidgeon.postgre.models.UserEntity; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository public interface CourseRepository extends JpaRepository { @@ -34,7 +32,7 @@ public interface CourseRepository extends JpaRepository { """) List findAssistantsByCourseId(long id); - public interface UserWithRelation { + interface UserWithRelation { UserEntity getUser(); String getRelation(); } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/CourseUserRepository.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/CourseUserRepository.java index 1f20fb0b..5d8bd2aa 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/CourseUserRepository.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/CourseUserRepository.java @@ -1,14 +1,12 @@ package com.ugent.pidgeon.postgre.repository; -import com.fasterxml.jackson.annotation.OptBoolean; import com.ugent.pidgeon.postgre.models.CourseUserEntity; import com.ugent.pidgeon.postgre.models.CourseUserId; import com.ugent.pidgeon.postgre.models.types.CourseRelation; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface CourseUserRepository extends JpaRepository { @@ -47,4 +45,11 @@ SELECT CASE WHEN EXISTS ( Optional findByCourseIdAndUserId(long courseId, long userId); + @Query(value = """ + SELECT COUNT(*) AS entry_count + FROM CourseUserEntity cu + WHERE cu.courseId = :courseId + """) + int countUsersInCourse(long courseId); + } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupClusterRepository.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupClusterRepository.java index 1e10fd83..2cd15c31 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupClusterRepository.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupClusterRepository.java @@ -1,11 +1,10 @@ package com.ugent.pidgeon.postgre.repository; import com.ugent.pidgeon.postgre.models.GroupClusterEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface GroupClusterRepository extends JpaRepository { List findByCourseId(long courseId); @@ -29,6 +28,15 @@ SELECT CASE WHEN EXISTS ( """) Boolean usedInProject(long clusterId); + @Query(value = """ + SELECT CASE WHEN EXISTS ( + SELECT gc.id FROM GroupClusterEntity gc + JOIN CourseEntity c ON gc.courseId = c.id + WHERE gc.id = ?1 AND c.archivedAt IS NOT NULL + ) THEN true ELSE false END + """) + Boolean inArchivedCourse(long clusterId); + @Query(value = """ SELECT CASE WHEN EXISTS ( SELECT g.id FROM GroupEntity g diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupFeedbackRepository.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupFeedbackRepository.java index e94c358f..21347c86 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupFeedbackRepository.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupFeedbackRepository.java @@ -2,14 +2,12 @@ import com.ugent.pidgeon.postgre.models.GroupFeedbackEntity; import com.ugent.pidgeon.postgre.models.GroupFeedbackId; -import com.ugent.pidgeon.postgre.models.SubmissionEntity; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - public interface GroupFeedbackRepository extends JpaRepository { @Query(value = "SELECT * FROM group_feedback WHERE group_id = ?1 AND project_id = ?2", nativeQuery = true) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupMemberRepository.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupMemberRepository.java index 084fc7f6..11c2d6fe 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupMemberRepository.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupMemberRepository.java @@ -2,13 +2,12 @@ import com.ugent.pidgeon.postgre.models.GroupFeedbackEntity; import com.ugent.pidgeon.postgre.models.UserEntity; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - public interface GroupMemberRepository extends JpaRepository { @Modifying diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupRepository.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupRepository.java index 1a3e1d07..ff0c75d2 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupRepository.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/GroupRepository.java @@ -3,13 +3,12 @@ import com.ugent.pidgeon.postgre.models.GroupEntity; import com.ugent.pidgeon.postgre.models.UserEntity; import jakarta.transaction.Transactional; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; -import java.util.List; -import java.util.Optional; - public interface GroupRepository extends JpaRepository{ @Transactional @Modifying @@ -32,13 +31,14 @@ public interface GroupRepository extends JpaRepository{ - public interface UserReference { + interface UserReference { Long getUserId(); String getName(); String getEmail(); + String getStudentNumber(); } @Query(value= """ - SELECT gu.userId as userId, u.name, CONCAT(u.name, ' ', u.surname) as name, u.email as email + SELECT gu.userId as userId, u.name, CONCAT(u.name, ' ', u.surname) as name, u.email as email, u.studentNumber as studentNumber FROM GroupUserEntity gu JOIN UserEntity u ON u.id = gu.userId WHERE gu.groupId = ?1""") List findGroupUsersReferencesByGroupId(long id); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/ProjectRepository.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/ProjectRepository.java index 6a4039b1..fe9c64d3 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/ProjectRepository.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/ProjectRepository.java @@ -1,11 +1,10 @@ package com.ugent.pidgeon.postgre.repository; import com.ugent.pidgeon.postgre.models.ProjectEntity; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import java.util.List; - public interface ProjectRepository extends JpaRepository { List findByCourseId(long courseId); @@ -20,12 +19,9 @@ public interface ProjectRepository extends JpaRepository { @Query(value = """ SELECT CASE WHEN EXISTS ( - SELECT gu - FROM GroupUserEntity gu - INNER JOIN GroupEntity g ON gu.groupId = g.id - INNER JOIN GroupClusterEntity gc ON g.clusterId = gc.id - INNER JOIN ProjectEntity p ON p.groupClusterId = gc.id - WHERE gu.userId = ?2 and p.id = ?1 + SELECT p FROM CourseUserEntity cu + INNER JOIN ProjectEntity p ON p.courseId = cu.courseId + WHERE cu.userId = ?2 and p.id = ?1 ) THEN true ELSE false END""") Boolean userPartOfProject(long projectId, long userId); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/SubmissionRepository.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/SubmissionRepository.java index 0f93ae96..74a22bae 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/SubmissionRepository.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/SubmissionRepository.java @@ -1,12 +1,11 @@ package com.ugent.pidgeon.postgre.repository; import com.ugent.pidgeon.postgre.models.SubmissionEntity; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import java.util.List; - public interface SubmissionRepository extends JpaRepository { List findByGroupIdAndProjectId(long groupId, long projectId); @@ -27,9 +26,17 @@ SELECT MAX(s2.submissionTime) FROM SubmissionEntity s2 WHERE s2.groupId = :groupId AND s2.projectId = :projectId + AND s2.dockerTestState != :#{T(com.ugent.pidgeon.postgre.models.types.DockerTestState).running.toString()} ) ORDER BY s.id DESC LIMIT 1 """) Optional findLatestsSubmissionIdsByProjectAndGroupId(long projectId, long groupId); + @Query(value = """ + SELECT s FROM SubmissionEntity s + WHERE s.projectId = :projectId + AND s.groupId IS NULL + """) + List findAdminSubmissionsByProjectId(long projectId); + List findByProjectIdAndGroupId(long projectid, long groupid); } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/TestRepository.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/TestRepository.java index 7a0c5fb6..e81bd330 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/TestRepository.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/TestRepository.java @@ -1,12 +1,19 @@ package com.ugent.pidgeon.postgre.repository; import com.ugent.pidgeon.postgre.models.TestEntity; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import java.util.Optional; - public interface TestRepository extends JpaRepository { + @Query(value = """ + SELECT CASE WHEN EXISTS (SELECT t FROM TestEntity t WHERE t.dockerImage = ?1) + THEN true + ELSE false + END + """) + boolean imageIsUsed(String image); + @Query(value ="SELECT t FROM ProjectEntity p JOIN TestEntity t ON p.testId = t.id WHERE p.id = ?1") Optional findByProjectId(long projectId); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/UserRepository.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/UserRepository.java index 86405a5e..d16b8142 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/UserRepository.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/UserRepository.java @@ -1,41 +1,59 @@ package com.ugent.pidgeon.postgre.repository; import com.ugent.pidgeon.postgre.models.CourseEntity; -import com.ugent.pidgeon.postgre.models.types.CourseRelation; import com.ugent.pidgeon.postgre.models.UserEntity; +import com.ugent.pidgeon.postgre.models.types.CourseRelation; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Optional; @Repository public interface UserRepository extends JpaRepository { + @Query(value = "SELECT u FROM UserEntity u WHERE lower(u.email) = lower(?1)") + UserEntity findByEmail(String email); - public interface CourseWithRelation { + @Query(value = "SELECT u FROM UserEntity u WHERE lower(u.name) LIKE concat('%', lower(?1), '%') AND lower(u.surname) LIKE concat('%', lower(?2), '%')") + List findByName(String name, String surname); + + + + + interface CourseWithRelation { CourseEntity getCourse(); CourseRelation getRelation(); } - public interface CourseIdWithRelation { + interface CourseIdWithRelation { Long getCourseId(); CourseRelation getRelation(); String getName(); } - @Query(value = "SELECT c.id as courseId, cu.relation as relation, c.name as name FROM CourseEntity c JOIN CourseUserEntity cu ON c.id = cu.courseId WHERE cu.userId = ?1") + @Query(value = """ + SELECT c.id as courseId, cu.relation as relation, c.name as name + FROM CourseEntity c JOIN CourseUserEntity cu ON c.id = cu.courseId + WHERE cu.userId = ?1 AND c.archivedAt IS NULL + """) List findCourseIdsByUserId(long id); + @Query(value = """ + SELECT c.id as courseId, cu.relation as relation, c.name as name + FROM CourseEntity c JOIN CourseUserEntity cu ON c.id = cu.courseId + WHERE cu.userId = ?1 AND c.archivedAt IS NOT NULL + """) + List findArchivedCoursesByUserId(long id); + /* The 'as' is important here, as it is used to map the result to the CourseWithRelation interface */ @Query(value = "SELECT c as course, cu.relation as relation FROM CourseEntity c JOIN CourseUserEntity cu ON c.id = cu.courseId WHERE cu.userId = ?1") List findCoursesByUserId(long id); - @Query(value = "SELECT * FROM users WHERE azure_id = ?1", nativeQuery = true) - public Optional findUserByAzureId(String id); + @Query(value = "SELECT u FROM UserEntity u WHERE u.azureId = ?1") + Optional findUserByAzureId(String id); + + } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/ClusterUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/ClusterUtil.java index b76f2791..2febf335 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/ClusterUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/ClusterUtil.java @@ -1,12 +1,19 @@ package com.ugent.pidgeon.util; -import com.ugent.pidgeon.model.json.GroupClusterCreateJson; -import com.ugent.pidgeon.model.json.GroupClusterUpdateJson; +import com.ugent.pidgeon.json.ClusterFillJson; +import com.ugent.pidgeon.json.GroupClusterCreateJson; +import com.ugent.pidgeon.json.GroupClusterUpdateJson; import com.ugent.pidgeon.postgre.models.CourseEntity; +import com.ugent.pidgeon.postgre.models.CourseUserEntity; +import com.ugent.pidgeon.postgre.models.CourseUserId; import com.ugent.pidgeon.postgre.models.GroupClusterEntity; import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.CourseRelation; +import com.ugent.pidgeon.postgre.repository.CourseUserRepository; import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; @@ -17,6 +24,8 @@ public class ClusterUtil { private GroupClusterRepository groupClusterRepository; @Autowired private CourseUtil courseUtil; + @Autowired + private CourseUserRepository courseUserRepository; /** * Check if a cluster is an individual cluster. This means that it only contains one group @@ -24,7 +33,7 @@ public class ClusterUtil { * @return true if the cluster is an individual cluster */ public boolean isIndividualCluster(GroupClusterEntity cluster) { - return cluster != null && cluster.getGroupAmount() <= 1; + return (cluster != null && cluster.getMaxSize() <= 1); } /** @@ -172,4 +181,24 @@ public CheckResult checkGroupClusterCreateJson(GroupClusterCreateJson clus return new CheckResult<>(HttpStatus.OK, "", null); } + + public CheckResult checkFillClusterJson(ClusterFillJson fillJson, GroupClusterEntity cluster) { + Collection members = fillJson.getClusterGroupMembers().values(); + + Set seen = new HashSet<>(); + for (Long[] member : members) { + for (Long userId : member) { + CourseUserEntity courseUser = courseUserRepository.findById(new CourseUserId(cluster.getCourseId(), userId)).orElse(null); + if (courseUser == null || !courseUser.getRelation().equals(CourseRelation.enrolled)) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "User with id " + userId + " is not enrolled in the course", null); + } + if (seen.contains(userId)) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "Can't add a user to 2 different groups", null); + } + seen.add(userId); + } + } + + return new CheckResult<>(HttpStatus.OK, "", null); + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java b/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java index 94e5fdaf..1ba82c4c 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java @@ -1,15 +1,35 @@ package com.ugent.pidgeon.util; -import com.ugent.pidgeon.postgre.models.*; -import com.ugent.pidgeon.postgre.repository.*; +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; +import com.ugent.pidgeon.postgre.models.CourseEntity; +import com.ugent.pidgeon.postgre.models.CourseUserEntity; +import com.ugent.pidgeon.postgre.models.GroupClusterEntity; +import com.ugent.pidgeon.postgre.models.GroupEntity; +import com.ugent.pidgeon.postgre.models.GroupUserEntity; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.SubmissionEntity; +import com.ugent.pidgeon.postgre.models.TestEntity; +import com.ugent.pidgeon.postgre.models.UserEntity; +import com.ugent.pidgeon.postgre.models.types.CourseRelation; +import com.ugent.pidgeon.postgre.repository.CourseRepository; +import com.ugent.pidgeon.postgre.repository.CourseUserRepository; +import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; +import com.ugent.pidgeon.postgre.repository.GroupFeedbackRepository; +import com.ugent.pidgeon.postgre.repository.GroupRepository; +import com.ugent.pidgeon.postgre.repository.GroupUserRepository; +import com.ugent.pidgeon.postgre.repository.ProjectRepository; +import com.ugent.pidgeon.postgre.repository.SubmissionRepository; +import com.ugent.pidgeon.postgre.repository.TestRepository; +import java.time.OffsetDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; @Component public class CommonDatabaseActions { @@ -31,6 +51,11 @@ public class CommonDatabaseActions { @Autowired private FileUtil fileUtil; + @Autowired + private CourseRepository courseRepository; + @Autowired + private CourseUserRepository courseUserRepository; + /** * Remove a group from the database @@ -39,17 +64,20 @@ public class CommonDatabaseActions { */ public boolean removeGroup(long groupId) { try { - // Delete the group - groupRepository.deleteGroupUsersByGroupId(groupId); - groupRepository.deleteSubmissionsByGroupId(groupId); - groupRepository.deleteGroupFeedbacksByGroupId(groupId); - groupRepository.deleteById(groupId); - - // update groupcount in cluster - groupClusterRepository.findById(groupId).ifPresent(cluster -> { - cluster.setGroupAmount(cluster.getGroupAmount() - 1); - groupClusterRepository.save(cluster); - }); + GroupEntity group = groupRepository.findById(groupId).orElse(null); + if (group != null) { + // Delete the group + groupRepository.deleteGroupUsersByGroupId(groupId); + groupRepository.deleteSubmissionsByGroupId(groupId); + groupRepository.deleteGroupFeedbacksByGroupId(groupId); + groupRepository.deleteById(groupId); + + // update groupcount in cluster + groupClusterRepository.findById(group.getClusterId()).ifPresent(cluster -> { + cluster.setGroupAmount(cluster.getGroupAmount() - 1); + groupClusterRepository.save(cluster); + }); + } return true; } catch (Exception e) { return false; @@ -60,22 +88,22 @@ public boolean removeGroup(long groupId) { /** * Create a new individual cluster group for course * @param courseId id of the course - * @param userId id of the user + * @param user user to add to the group * @return true if the group was created successfully */ - public boolean createNewIndividualClusterGroup(long courseId, long userId) { + public boolean createNewIndividualClusterGroup(long courseId, UserEntity user) { GroupClusterEntity groupClusterEntity = groupClusterRepository.findIndividualClusterByCourseId(courseId).orElse(null); if (groupClusterEntity == null) { return false; } // Create new group for the cluster - GroupEntity groupEntity = new GroupEntity("", groupClusterEntity.getId()); + GroupEntity groupEntity = new GroupEntity(user.getName() + " " + user.getSurname(), groupClusterEntity.getId()); groupClusterEntity.setGroupAmount(groupClusterEntity.getGroupAmount() + 1); groupClusterRepository.save(groupClusterEntity); groupEntity = groupRepository.save(groupEntity); // Add user to the group - GroupUserEntity groupUserEntity = new GroupUserEntity(groupEntity.getId(), userId); + GroupUserEntity groupUserEntity = new GroupUserEntity(groupEntity.getId(), user.getId()); groupUserRepository.save(groupUserEntity); return true; } @@ -93,7 +121,16 @@ public boolean removeIndividualClusterGroup(long courseId, long userId) { } // Find the group of the user Optional groupEntityOptional = groupRepository.groupByClusterAndUser(groupClusterEntity.getId(), userId); - return groupEntityOptional.filter(groupEntity -> removeGroup(groupEntity.getId())).isPresent(); + if (!groupEntityOptional.isPresent()) { + return false; + } + GroupEntity groupEntity = groupEntityOptional.get(); + // Delete the group + removeGroup(groupEntity.getId()); + + groupClusterEntity.setGroupAmount(groupClusterEntity.getGroupAmount() - 1); + groupClusterRepository.save(groupClusterEntity); + return true; } /** @@ -117,13 +154,20 @@ public CheckResult deleteProject(long projectId) { } } + if (projectEntity.getTestId() != null) { + TestEntity testEntity = testRepository.findById(projectEntity.getTestId()).orElse(null); + if (testEntity == null) { + return new CheckResult<>(HttpStatus.NOT_FOUND, "Test not found", null); + } + CheckResult delRes = deleteTestById(projectEntity, testEntity); + if (!delRes.getStatus().equals(HttpStatus.OK)) { + return delRes; + } + } + projectRepository.delete(projectEntity); - TestEntity testEntity = testRepository.findById(projectEntity.getTestId()).orElse(null); - if (testEntity == null) { - return new CheckResult<>(HttpStatus.NOT_FOUND, "Test not found", null); - } - return deleteTestById(projectEntity, testEntity); + return new CheckResult<>(HttpStatus.OK, "", null); } catch (Exception e) { System.out.println(e.getMessage()); return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while deleting project", null); @@ -159,15 +203,15 @@ public CheckResult deleteTestById(ProjectEntity projectEntity, TestEntity try { projectEntity.setTestId(null); projectRepository.save(projectEntity); - testRepository.deleteById(testEntity.getId()) ; - CheckResult checkAndDeleteRes = fileUtil.deleteFileById(testEntity.getStructureTestId()); - if (!checkAndDeleteRes.getStatus().equals(HttpStatus.OK)) { - return checkAndDeleteRes; + testRepository.deleteById(testEntity.getId()); + if(!testRepository.imageIsUsed(testEntity.getDockerImage())){ + DockerSubmissionTestModel.removeDockerImage(testEntity.getDockerImage()); } - return fileUtil.deleteFileById(testEntity.getDockerTestId()); + return new CheckResult<>(HttpStatus.OK, "", null); } catch (Exception e) { return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while deleting test", null); } + } /** @@ -178,9 +222,10 @@ public CheckResult deleteTestById(ProjectEntity projectEntity, TestEntity public CheckResult deleteClusterById(long clusterId) { try { for (GroupEntity group : groupRepository.findAllByClusterId(clusterId)) { - // Delete all groupUsers - groupUserRepository.deleteAllByGroupId(group.getId()); - groupRepository.deleteById(group.getId()); + boolean res = removeGroup(group.getId()); + if (!res) { + return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while deleting cluster", null); + } } groupClusterRepository.deleteById(clusterId); return new CheckResult<>(HttpStatus.OK, "", null); @@ -188,4 +233,147 @@ public CheckResult deleteClusterById(long clusterId) { return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while deleting cluster", null); } } + + + /** + * Copy a course and all its related data. Assumes that permissions are already checked + * @param course course to copy + * @return CheckResult with the status of the copy and the new course + */ + public CheckResult copyCourse(CourseEntity course, long userId) { + // Copy the course + CourseEntity newCourse = new CourseEntity(course.getName(), course.getDescription(), course.getCourseYear()); + // Change the createdAt, archivedAt and joinKey + newCourse.setCreatedAt(OffsetDateTime.now()); + newCourse.setArchivedAt(null); + newCourse.setJoinKey(UUID.randomUUID().toString()); + + newCourse = courseRepository.save(newCourse); + + Map groupClusterMap = new HashMap<>(); + // Copy the group(clusters) linked to the course + GroupClusterEntity groupCluster = groupClusterRepository.findIndividualClusterByCourseId( + course.getId()).orElse(null); + if (groupCluster != null) { + CheckResult checkResult = copyGroupCluster(groupCluster, newCourse.getId(), false); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return new CheckResult<>(checkResult.getStatus(), checkResult.getMessage(), null); + } + groupClusterMap.put(groupCluster.getId(), checkResult.getData().getId()); + } else { + return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while copying course", null); + } + + List groupClusters = groupClusterRepository.findClustersWithoutInvidualByCourseId(course.getId()); + for (GroupClusterEntity cluster : groupClusters) { + CheckResult checkResult = copyGroupCluster(cluster, + newCourse.getId(), true); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return new CheckResult<>(checkResult.getStatus(), checkResult.getMessage(), null); + } + groupClusterMap.put(cluster.getId(), checkResult.getData().getId()); + } + + // Copy the projects linked to the course + List projects = projectRepository.findByCourseId(course.getId()); + for (ProjectEntity project : projects) { + CheckResult checkResult = copyProject(project, newCourse.getId(), groupClusterMap.get(project.getGroupClusterId())); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return new CheckResult<>(checkResult.getStatus(), checkResult.getMessage(), null); + } + } + + // Add user to course + CourseUserEntity courseUserEntity = new CourseUserEntity(newCourse.getId(), userId, CourseRelation.creator); + courseUserRepository.save(courseUserEntity); + + return new CheckResult<>(HttpStatus.OK, "", newCourse); + } + + /** + * Copy a group cluster and all its related data. Assumes that permissions are already checked + * @param groupCluster group cluster that needs to be copied + * @return CheckResult with the status of the copy and the new group cluster + */ + public CheckResult copyGroupCluster(GroupClusterEntity groupCluster, long courseId, boolean copyGroups) { + GroupClusterEntity newGroupCluster = new GroupClusterEntity( + courseId, + groupCluster.getMaxSize(), + groupCluster.getName(), + copyGroups ? groupCluster.getGroupAmount() : 0 + ); + newGroupCluster.setCreatedAt(OffsetDateTime.now()); + + newGroupCluster = groupClusterRepository.save(newGroupCluster); + if (copyGroups) { + List groups = groupRepository.findAllByClusterId(groupCluster.getId()); + for (GroupEntity group : groups) { + GroupEntity newGroup = new GroupEntity(group.getName(), newGroupCluster.getId()); + groupRepository.save(newGroup); + } + } + + return new CheckResult<>(HttpStatus.OK, "", newGroupCluster); + } + + + + /** + * Copy a project and all its related data. Assumes that permissions are already checked + * @param project project that needs to be copied + * @param courseId id of the course the project is linked to + * @param clusterId id of the cluster the project is linked to + * @return CheckResult with the status of the copy and the new project + */ + public CheckResult copyProject(ProjectEntity project, long courseId, long clusterId) { + // Copy the project + ProjectEntity newProject = new ProjectEntity( + courseId, + project.getName(), + project.getDescription(), + clusterId, + null, + project.isVisible(), + project.getMaxScore(), + project.getDeadline()); + + newProject = projectRepository.save(newProject); + + + // Copy the test linked to the project + if (project.getTestId() != null) { + TestEntity test = testRepository.findById(project.getTestId()).orElse(null); + if (test != null) { + CheckResult checkResult = copyTest(test); + if (!checkResult.getStatus().equals(HttpStatus.OK)) { + return new CheckResult<>(checkResult.getStatus(), checkResult.getMessage(), null); + } + newProject.setTestId(checkResult.getData().getId()); + newProject = projectRepository.save(newProject); + } else { + return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while copying project", null); + } + } + + + return new CheckResult<>(HttpStatus.OK, "", newProject); + } + + /** + * Copy a test and all its related data. Assumes that permissions are already checked and that the parameters are valid. + * @param test test that needs to be copied + * @return CheckResult with the status of the copy and the new test + */ + public CheckResult copyTest(TestEntity test) { + // Copy the test + TestEntity newTest = new TestEntity( + test.getDockerImage(), + test.getDockerTestScript(), + test.getDockerTestTemplate(), + test.getStructureTemplate() + ); + + newTest = testRepository.save(newTest); + return new CheckResult<>(HttpStatus.OK, "", newTest); + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/CourseUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/CourseUtil.java index a24db9c0..163d988c 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/CourseUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/CourseUtil.java @@ -1,11 +1,17 @@ package com.ugent.pidgeon.util; import com.ugent.pidgeon.controllers.ApiRoutes; -import com.ugent.pidgeon.model.json.*; -import com.ugent.pidgeon.postgre.models.*; +import com.ugent.pidgeon.json.CourseJson; +import com.ugent.pidgeon.json.CourseMemberRequestJson; +import com.ugent.pidgeon.postgre.models.CourseEntity; +import com.ugent.pidgeon.postgre.models.CourseUserEntity; +import com.ugent.pidgeon.postgre.models.CourseUserId; +import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.CourseRelation; import com.ugent.pidgeon.postgre.models.types.UserRole; -import com.ugent.pidgeon.postgre.repository.*; +import com.ugent.pidgeon.postgre.repository.CourseRepository; +import com.ugent.pidgeon.postgre.repository.CourseUserRepository; +import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -60,7 +66,8 @@ public CheckResult> getCourseIfUserInCourse(l if (courseUserEntity == null && !user.getRole().equals(UserRole.admin)) { return new CheckResult<>(HttpStatus.FORBIDDEN, "User is not part of the course", null); } - return new CheckResult<>(HttpStatus.OK, "", new Pair<>(courseEntity, courseUserEntity.getRelation())); + CourseRelation relation = courseUserEntity != null ? courseUserEntity.getRelation() : CourseRelation.creator; + return new CheckResult<>(HttpStatus.OK, "", new Pair<>(courseEntity, relation)); } @@ -112,7 +119,7 @@ public CheckResult canUpdateUserInCourse(long courseId, Course return new CheckResult<>(HttpStatus.BAD_REQUEST, "User is already part of the course", null); } if (!userUtil.userExists(request.getUserId())) { - return new CheckResult<>(HttpStatus.BAD_REQUEST, "User does not exist", null); + return new CheckResult<>(HttpStatus.NOT_FOUND, "User does not exist", null); } } else { if (!courseMember) { @@ -120,17 +127,20 @@ public CheckResult canUpdateUserInCourse(long courseId, Course } } - if (user.getId() == request.getUserId()) { - return new CheckResult<>(HttpStatus.BAD_REQUEST, "Cannot change your own relation with this course", null); + boolean isAdmin = user.getRole().equals(UserRole.admin); + + if (user.getId() == request.getUserId() && !isAdmin) { + return new CheckResult<>(HttpStatus.FORBIDDEN, "Cannot change your own relation with this course", null); } - if (request.getRelationAsEnum().equals(CourseRelation.creator)) { - return new CheckResult<>(HttpStatus.BAD_REQUEST, "Cannot change the creator of the course", null); + + if (request.getRelationAsEnum().equals(CourseRelation.creator) && !isAdmin) { + return new CheckResult<>(HttpStatus.FORBIDDEN, "Cannot change the creator of the course", null); } - boolean isAdmin = user.getRole().equals(UserRole.admin); boolean isCreator = userRelation.equals(CourseRelation.creator); boolean creatingAdmin = request.getRelationAsEnum().equals(CourseRelation.course_admin); - if (creatingAdmin && !isAdmin && !isCreator) { + boolean downgradingAdmin = courseMember && courseUserEntity.getRelation().equals(CourseRelation.course_admin) && !creatingAdmin; + if ((creatingAdmin || downgradingAdmin) && !isAdmin && !isCreator) { return new CheckResult<>(HttpStatus.FORBIDDEN, "Only the course creator can create course admins", null); } @@ -148,9 +158,13 @@ public CheckResult canLeaveCourse(long courseId, UserEntity user if (courseCheck.getStatus() != HttpStatus.OK) { return new CheckResult<>(courseCheck.getStatus(), courseCheck.getMessage(), null); } + CourseEntity course = courseCheck.getData().getFirst(); CourseRelation relation = courseCheck.getData().getSecond(); if (relation.equals(CourseRelation.creator)) { - return new CheckResult<>(HttpStatus.BAD_REQUEST, "Cannot leave a course you created", null); + return new CheckResult<>(HttpStatus.FORBIDDEN, "Cannot leave a course you created", null); + } + if (course.getArchivedAt() != null) { + return new CheckResult<>(HttpStatus.FORBIDDEN, "Cannot leave an archived course", null); } return new CheckResult<>(HttpStatus.OK, "", relation); } @@ -176,15 +190,20 @@ public CheckResult canDeleteUser(long courseId, long userId, Use CourseUserEntity courseUserEntity = courseUserRepository.findById(new CourseUserId(courseId, userId)).orElse(null); if (courseUserEntity == null) { - return new CheckResult<>(HttpStatus.BAD_REQUEST, "User is not part of the course", null); + return new CheckResult<>(HttpStatus.NOT_FOUND, "User is not part of the course", null); } if (user.getId() == userId) { - return new CheckResult<>(HttpStatus.BAD_REQUEST, "Cannot delete yourself from the course", null); + return new CheckResult<>(HttpStatus.FORBIDDEN, "Cannot delete yourself from the course", null); } if (courseUserEntity.getRelation().equals(CourseRelation.creator)) { - return new CheckResult<>(HttpStatus.BAD_REQUEST, "Cannot delete the creator of the course", null); + return new CheckResult<>(HttpStatus.FORBIDDEN, "Cannot delete the creator of the course", null); + } + + boolean isAdmin = user.getRole().equals(UserRole.admin); + if (courseUserEntity.getRelation().equals(CourseRelation.course_admin) && !userRelation.equals(CourseRelation.creator) && !isAdmin){ + return new CheckResult<>(HttpStatus.FORBIDDEN, "Only the creator can delete course admins", null); } return new CheckResult<>(HttpStatus.OK, "", courseUserEntity.getRelation()); @@ -242,9 +261,22 @@ public CheckResult checkJoinLink(long courseId, String courseKey, * @param courseJson json with the course data * @return CheckResult with the status of the check */ - public CheckResult checkCourseJson(CourseJson courseJson) { - if (courseJson.getName() == null || courseJson.getDescription() == null) { - return new CheckResult<>(HttpStatus.BAD_REQUEST, "name and description are required", null); + public CheckResult checkCourseJson(CourseJson courseJson, UserEntity user, Long courseId) { + // If the courseId is null we are creating a course + if (courseId != null) { + CourseUserEntity courseUserEntity = courseUserRepository.findById(new CourseUserId(courseId, user.getId())).orElse(null); + if (courseUserEntity == null) { + return new CheckResult<>(HttpStatus.FORBIDDEN, "User is not part of the course", null); + } + + if (courseJson.getArchived() != null && !courseUserEntity.getRelation().equals(CourseRelation.creator)) { + return new CheckResult<>(HttpStatus.FORBIDDEN, "Only the course creator can (un)archive the course", null); + } + } + + if (courseJson.getName() == null || courseJson.getDescription() == null || courseJson.getYear() == null) { + Logger.getGlobal().info(""+ courseJson.getYear()); + return new CheckResult<>(HttpStatus.BAD_REQUEST, "name, description and year are required", null); } if (courseJson.getName().isBlank()) { diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/DockerClientInstance.java b/backend/app/src/main/java/com/ugent/pidgeon/util/DockerClientInstance.java index 09ed299e..5980ebed 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/DockerClientInstance.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/DockerClientInstance.java @@ -6,31 +6,32 @@ import com.github.dockerjava.core.DockerClientImpl; import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient; - import java.time.Duration; public class DockerClientInstance { - private static DockerClient dockerClient; + private static DockerClient dockerClient; - private DockerClientInstance() { - // Private constructor to prevent instantiation - } + private DockerClientInstance() { + // Private constructor to prevent instantiation + } + + // @Relevant + public static synchronized DockerClient getInstance() { + + if (dockerClient == null) { - public static synchronized DockerClient getInstance() { - - if (dockerClient == null) { - DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); - DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder() - .dockerHost(config.getDockerHost()) - .sslConfig(config.getSSLConfig()) - .maxConnections(100) - .connectionTimeout(Duration.ofSeconds(30)) - .responseTimeout(Duration.ofSeconds(45)) - .build(); - dockerClient = DockerClientImpl.getInstance(config, httpClient); - } - return dockerClient; + DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); + DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder() + .dockerHost(config.getDockerHost()) + .sslConfig(config.getSSLConfig()) + .maxConnections(100) + .connectionTimeout(Duration.ofSeconds(30)) + .responseTimeout(Duration.ofSeconds(45)) + .build(); + dockerClient = DockerClientImpl.getInstance(config, httpClient); } + return dockerClient; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java index 9df57656..da729a3d 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java @@ -2,18 +2,50 @@ import com.ugent.pidgeon.controllers.ApiRoutes; +import com.ugent.pidgeon.json.CourseReferenceJson; +import com.ugent.pidgeon.json.CourseWithInfoJson; +import com.ugent.pidgeon.json.CourseWithRelationJson; +import com.ugent.pidgeon.json.DockerTestFeedbackJson; +import com.ugent.pidgeon.json.GroupClusterJson; +import com.ugent.pidgeon.json.GroupFeedbackJson; +import com.ugent.pidgeon.json.GroupFeedbackJsonWithProject; +import com.ugent.pidgeon.json.GroupJson; +import com.ugent.pidgeon.json.ProjectProgressJson; +import com.ugent.pidgeon.json.ProjectResponseJsonWithStatus; +import com.ugent.pidgeon.json.ProjectStatus; +import com.ugent.pidgeon.json.SubmissionJson; +import com.ugent.pidgeon.json.TestJson; +import com.ugent.pidgeon.json.UserReferenceJson; +import com.ugent.pidgeon.json.UserReferenceWithRelation; import com.ugent.pidgeon.model.ProjectResponseJson; -import com.ugent.pidgeon.model.json.*; -import com.ugent.pidgeon.postgre.models.*; +import com.ugent.pidgeon.postgre.models.CourseEntity; +import com.ugent.pidgeon.postgre.models.CourseUserEntity; +import com.ugent.pidgeon.postgre.models.CourseUserId; +import com.ugent.pidgeon.postgre.models.FileEntity; +import com.ugent.pidgeon.postgre.models.GroupClusterEntity; +import com.ugent.pidgeon.postgre.models.GroupEntity; +import com.ugent.pidgeon.postgre.models.GroupFeedbackEntity; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.SubmissionEntity; +import com.ugent.pidgeon.postgre.models.TestEntity; +import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.CourseRelation; -import com.ugent.pidgeon.postgre.repository.*; +import com.ugent.pidgeon.postgre.models.types.DockerTestState; +import com.ugent.pidgeon.postgre.models.types.DockerTestType; +import com.ugent.pidgeon.postgre.repository.CourseRepository; +import com.ugent.pidgeon.postgre.repository.CourseUserRepository; +import com.ugent.pidgeon.postgre.repository.FileRepository; +import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; +import com.ugent.pidgeon.postgre.repository.GroupRepository; +import com.ugent.pidgeon.postgre.repository.ProjectRepository; +import com.ugent.pidgeon.postgre.repository.SubmissionRepository; +import com.ugent.pidgeon.postgre.repository.TestRepository; +import java.io.File; +import java.nio.file.Path; +import java.util.List; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; -import java.util.List; - /** * Class that converts entities to json objects @@ -33,19 +65,30 @@ public class EntityToJsonConverter { private CourseUserRepository courseUserRepository; @Autowired private SubmissionRepository submissionRepository; + @Autowired + private ClusterUtil clusterUtil; + @Autowired + private TestUtil testUtil; + @Autowired + private TestRepository testRepository; + @Autowired + private FileRepository fileRepository; - public GroupJson groupEntityToJson(GroupEntity groupEntity) { + public GroupJson groupEntityToJson(GroupEntity groupEntity, boolean hideStudentNumber) { GroupClusterEntity cluster = groupClusterRepository.findById(groupEntity.getClusterId()).orElse(null); + if (cluster == null) { + throw new RuntimeException("Cluster not found"); + } GroupJson group = new GroupJson(cluster.getMaxSize(), groupEntity.getId(), groupEntity.getName(), ApiRoutes.CLUSTER_BASE_PATH + "/" + groupEntity.getClusterId()); - if (cluster != null && cluster.getGroupAmount() > 1){ + if (cluster.getMaxSize() > 1){ group.setGroupClusterUrl(ApiRoutes.CLUSTER_BASE_PATH + "/" + cluster.getId()); } else { group.setGroupClusterUrl(null); } // Get the members of the group List members = groupRepository.findGroupUsersReferencesByGroupId(groupEntity.getId()).stream().map(user -> - new UserReferenceJson(user.getName(), user.getEmail(), user.getUserId()) + new UserReferenceJson(user.getName(), user.getEmail(), user.getUserId(), hideStudentNumber ? null : user.getStudentNumber()) ).toList(); // Return the group with its members @@ -54,9 +97,9 @@ public GroupJson groupEntityToJson(GroupEntity groupEntity) { } - public GroupClusterJson clusterEntityToClusterJson(GroupClusterEntity cluster) { + public GroupClusterJson clusterEntityToClusterJson(GroupClusterEntity cluster, boolean hideStudentNumber) { List groups = groupRepository.findAllByClusterId(cluster.getId()).stream().map( - this::groupEntityToJson + g -> groupEntityToJson(g, hideStudentNumber) ).toList(); return new GroupClusterJson( cluster.getId(), @@ -65,24 +108,31 @@ public GroupClusterJson clusterEntityToClusterJson(GroupClusterEntity cluster) { cluster.getGroupAmount(), cluster.getCreatedAt(), groups, + cluster.getLockGroupsAfter(), ApiRoutes.COURSE_BASE_PATH + "/" + cluster.getCourseId() ); } - public UserReferenceJson userEntityToUserReference(UserEntity user) { - return new UserReferenceJson(user.getName() + " " + user.getSurname(), user.getEmail(), user.getId()); + public UserReferenceJson userEntityToUserReference(UserEntity user, boolean hideStudentNumber) { + return new UserReferenceJson( + user.getName() + " " + user.getSurname(), + user.getEmail(), user.getId(), + hideStudentNumber ? null : user.getStudentNumber() + ); } - public UserReferenceWithRelation userEntityToUserReferenceWithRelation(UserEntity user, CourseRelation relation) { - return new UserReferenceWithRelation(userEntityToUserReference(user), relation.toString()); + public UserReferenceWithRelation userEntityToUserReferenceWithRelation(UserEntity user, CourseRelation relation, boolean hideStudentNumber) { + return new UserReferenceWithRelation(userEntityToUserReference(user, hideStudentNumber), relation.toString()); } - public CourseWithInfoJson courseEntityToCourseWithInfo(CourseEntity course, String joinLink) { + public CourseWithInfoJson courseEntityToCourseWithInfo(CourseEntity course, String joinLink, boolean hideKey) { UserEntity teacher = courseRepository.findTeacherByCourseId(course.getId()); - UserReferenceJson teacherJson = userEntityToUserReference(teacher); + UserReferenceJson teacherJson = userEntityToUserReference(teacher, true); List assistants = courseRepository.findAssistantsByCourseId(course.getId()); - List assistantsJson = assistants.stream().map(this::userEntityToUserReference).toList(); + List assistantsJson = assistants.stream().map( + u -> userEntityToUserReference(u, true) + ).toList(); return new CourseWithInfoJson( course.getId(), @@ -91,16 +141,25 @@ public CourseWithInfoJson courseEntityToCourseWithInfo(CourseEntity course, Stri teacherJson, assistantsJson, ApiRoutes.COURSE_BASE_PATH + "/" + course.getId() + "/members", - joinLink + hideKey ? null : joinLink, + hideKey ? null : course.getJoinKey(), + course.getArchivedAt(), + course.getCreatedAt(), + course.getCourseYear() ); } public CourseWithRelationJson courseEntityToCourseWithRelation(CourseEntity course, CourseRelation relation) { + int memberCount = courseUserRepository.countUsersInCourse(course.getId()); return new CourseWithRelationJson( ApiRoutes.COURSE_BASE_PATH + "/" + course.getId(), relation, course.getName(), - course.getId() + course.getId(), + course.getArchivedAt(), + memberCount, + course.getCreatedAt(), + course.getCourseYear() ); } @@ -126,20 +185,26 @@ public GroupFeedbackJsonWithProject groupFeedbackEntityToJsonWithProject(GroupFe public ProjectResponseJsonWithStatus projectEntityToProjectResponseJsonWithStatus(ProjectEntity project, CourseEntity course, UserEntity user) { // Get status Long groupId = groupRepository.groupIdByProjectAndUser(project.getId(), user.getId()); + if (groupId == null) { + return new ProjectResponseJsonWithStatus( + projectEntityToProjectResponseJson(project, course, user), + ProjectStatus.no_group.toString() + ); + } SubmissionEntity sub = submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(project.getId(), groupId).orElse(null); - String status; + ProjectStatus status; if (sub == null) { - status = "not started"; - } else if (sub.getStructureAccepted() && sub.getStructureAccepted()) { - status = "correct"; + status = ProjectStatus.not_started; + } else if (sub.getStructureAccepted() && sub.getDockerAccepted()) { + status = ProjectStatus.correct; } else { - status = "incorrect"; + status = ProjectStatus.incorrect; } return new ProjectResponseJsonWithStatus( projectEntityToProjectResponseJson(project, course, user), - status + status.toString() ); } @@ -161,55 +226,98 @@ public ProjectResponseJson projectEntityToProjectResponseJson(ProjectEntity proj String submissionUrl = ApiRoutes.PROJECT_BASE_PATH + "/" + project.getId() + "/submissions"; CourseUserEntity courseUserEntity = courseUserRepository.findById(new CourseUserId(course.getId(), user.getId())).orElse(null); if (courseUserEntity == null) { - return null; + throw new RuntimeException("User not found in course"); } + + // GroupId is null if the user is a course_admin/creator + Long groupId = groupRepository.groupIdByProjectAndUser(project.getId(), user.getId()); + if (courseUserEntity.getRelation() == CourseRelation.enrolled) { - Long groupId = groupRepository.groupIdByProjectAndUser(project.getId(), user.getId()); if (groupId == null) { - return null; + submissionUrl = null; + } else { + submissionUrl += "/" + groupId; } - submissionUrl += "/" + groupId; } - // GroupId is null if the user is a course_admin/creator - Long groupId = groupRepository.groupIdByProjectAndUser(project.getId(), user.getId()); + Long clusterId = project.getGroupClusterId(); + if (clusterUtil.isIndividualCluster(clusterId)) { + clusterId = null; + } return new ProjectResponseJson( - new CourseReferenceJson(course.getName(), ApiRoutes.COURSE_BASE_PATH + "/" + course.getId(), course.getId()), + courseEntityToCourseReference(course), project.getDeadline(), project.getDescription(), project.getId(), project.getName(), submissionUrl, - ApiRoutes.TEST_BASE_PATH + "/" + project.getTestId(), + project.getTestId() == null ? null : ApiRoutes.TEST_BASE_PATH + "/" + project.getTestId(), project.getMaxScore(), project.isVisible(), new ProjectProgressJson(completed, total), - groupId + groupId, + clusterId, + project.getVisibleAfter() ); } + public CourseReferenceJson courseEntityToCourseReference(CourseEntity course) { + return new CourseReferenceJson( + course.getName(), + ApiRoutes.COURSE_BASE_PATH + "/" + course.getId(), + course.getId(), + course.getArchivedAt() + ); + } + + + public SubmissionJson getSubmissionJson(SubmissionEntity submission) { + DockerTestFeedbackJson feedback; + if (submission.getDockerTestState().equals(DockerTestState.running)) { + feedback = null; + } else if (submission.getDockerTestType().equals(DockerTestType.NONE)) { + feedback = new DockerTestFeedbackJson(DockerTestType.NONE, "", true); + } + else if (submission.getDockerTestType().equals(DockerTestType.SIMPLE)) { + feedback = new DockerTestFeedbackJson(DockerTestType.SIMPLE, submission.getDockerFeedback(), submission.getDockerAccepted()); + } else { + feedback = new DockerTestFeedbackJson(DockerTestType.TEMPLATE, submission.getDockerFeedback(), submission.getDockerAccepted()); + } + + boolean artifactsExist; + if (submission.getGroupId() != null) { + Path artifactPath = Filehandler.getSubmissionArtifactPath(submission.getProjectId(), submission.getGroupId(), submission.getId()); + artifactsExist = new File(artifactPath.toString()).exists(); + } else { + artifactsExist = false; + } return new SubmissionJson( submission.getId(), ApiRoutes.PROJECT_BASE_PATH + "/" + submission.getProjectId(), - ApiRoutes.GROUP_BASE_PATH + "/" + submission.getGroupId(), + submission.getGroupId() == null ? null : ApiRoutes.GROUP_BASE_PATH + "/" + submission.getGroupId(), submission.getProjectId(), submission.getGroupId(), ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId() + "/file", submission.getStructureAccepted(), submission.getSubmissionTime(), - submission.getDockerAccepted(), - ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId() + "/structurefeedback", - ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId() + "/dockerfeedback" + submission.getStructureFeedback(), + feedback, + submission.getDockerTestState().toString(), + artifactsExist ? ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId() + "/artifacts" : null ); } public TestJson testEntityToTestJson(TestEntity testEntity, long projectId) { + FileEntity extrafiles = testEntity.getExtraFilesId() == null ? null : fileRepository.findById(testEntity.getExtraFilesId()).orElse(null); return new TestJson( ApiRoutes.PROJECT_BASE_PATH + "/" + projectId, - testEntity.getDockerImage(), - ApiRoutes.PROJECT_BASE_PATH + "/" + projectId + "/tests/dockertest", - ApiRoutes.PROJECT_BASE_PATH + "/" + projectId + "/tests/structuretest" + testEntity.getDockerImage(), + testEntity.getDockerTestScript(), + testEntity.getDockerTestTemplate(), + testEntity.getStructureTemplate(), + testEntity.getExtraFilesId() == null ? null : ApiRoutes.PROJECT_BASE_PATH + "/" + projectId + "/tests/extrafiles", + extrafiles == null ? null : extrafiles.getName() ); } } \ No newline at end of file diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/FileUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/FileUtil.java index 9ae43466..f8486835 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/FileUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/FileUtil.java @@ -2,36 +2,18 @@ import com.ugent.pidgeon.postgre.models.FileEntity; import com.ugent.pidgeon.postgre.repository.FileRepository; +import java.io.File; +import java.nio.file.Path; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; -import java.io.IOException; -import java.nio.file.Path; -import java.util.logging.Logger; - @Component public class FileUtil { @Autowired private FileRepository fileRepository; - /** - * Save the file entity to the database - * @param filePath path of the file - * @param projectId id of the project - * @param userId id of the user - * @return the saved file entity - * @throws IOException if an error occurs while saving the file - */ - public FileEntity saveFileEntity(Path filePath, long projectId, long userId) throws IOException { - // Save the file entity to the database - Logger.getGlobal().info("file path: " + filePath.toString()); - Logger.getGlobal().info("file name: " + filePath.getFileName().toString()); - FileEntity fileEntity = new FileEntity(filePath.getFileName().toString(), filePath.toString(), userId); - return fileRepository.save(fileEntity); - } - /** * Delete a file by id from the database and server * @param fileId id of the file @@ -43,8 +25,9 @@ public CheckResult deleteFileById(long fileId) { return new CheckResult<>(HttpStatus.NOT_FOUND, "File not found", null); } try { - Filehandler.deleteLocation(Path.of(fileEntity.getPath())); - } catch (IOException e) { + Path path = Path.of(fileEntity.getPath()); + Filehandler.deleteLocation(new File(path.toString())); + } catch (Exception e) { return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error deleting file", null); } fileRepository.delete(fileEntity); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java b/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java index 415ab5e7..86c2c3f6 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java @@ -1,26 +1,31 @@ package com.ugent.pidgeon.util; -import com.ugent.pidgeon.postgre.models.FileEntity; -import com.ugent.pidgeon.postgre.repository.FileRepository; -import org.apache.tika.Tika; -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.InputStreamResource; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.core.io.Resource; - -import java.io.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.util.Objects; +import java.util.List; import java.util.logging.Logger; -import java.util.zip.ZipFile; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.apache.tika.Tika; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.multipart.MultipartFile; public class Filehandler { static String BASEPATH = "data"; public static String SUBMISSION_FILENAME = "files.zip"; + public static String EXTRA_TESTFILES_FILENAME = "testfiles.zip"; + public static String ADMIN_SUBMISSION_FOLDER = "adminsubmissions"; /** * Save a submission to the server @@ -29,15 +34,15 @@ public class Filehandler { * @return the saved file * @throws IOException if an error occurs while saving the file */ - public static File saveSubmission(Path directory, MultipartFile file) throws IOException { + public static File saveFile(Path directory, MultipartFile file, String filename) throws IOException { // Check if the file is empty - if (file.isEmpty()) { + if (file == null || file.isEmpty()) { throw new IOException("File is empty"); } try { // Create a temporary file and save the uploaded file to it - File tempFile = File.createTempFile("uploaded-zip-", ".zip"); + File tempFile = File.createTempFile("SELAB6CANDELETEuploaded-zip-", ".zip"); file.transferTo(tempFile); // Check if the file is a ZIP file @@ -53,36 +58,27 @@ public static File saveSubmission(Path directory, MultipartFile file) throws IOE } // Save the file to the server - Path filePath = directory.resolve(SUBMISSION_FILENAME); + Path filePath = directory.resolve(filename); try(InputStream stream = new FileInputStream(tempFile)) { Files.copy(stream, filePath, StandardCopyOption.REPLACE_EXISTING); } - return tempFile; + return filePath.toFile(); } catch (IOException e) { throw new IOException(e.getMessage()); } } - /** - * Delete a submission from the server - * @param directory directory of the submission to delete - * @throws IOException if an error occurs while deleting the submission - */ - public static void deleteSubmission(Path directory) throws IOException { - deleteLocation(directory); - } /** - * Delete a directory and all its contents - * @param directory directory to delete + * Delete a directory and all its contents, eg: deleteLocation(new File(path.toString()) + * @param uploadDirectory File representing directory to delete * @throws IOException if an error occurs while deleting the directory */ - public static void deleteLocation(Path directory) throws IOException { + public static void deleteLocation(File uploadDirectory) throws IOException { try { - File uploadDirectory = new File(directory.toString()); if (uploadDirectory.exists()) { if(!uploadDirectory.delete()) { throw new IOException("Error while deleting directory"); @@ -94,16 +90,18 @@ public static void deleteLocation(Path directory) throws IOException { } } + + /** * Delete empty parent directories of a directory * @param directory directory to delete */ - private static void deleteEmptyParentDirectories(File directory) { + private static void deleteEmptyParentDirectories(File directory) throws IOException { if (directory != null && directory.isDirectory()) { File[] files = directory.listFiles(); if (files != null && files.length == 0) { if (!directory.delete()) { - System.err.println("Error while deleting empty directory: " + directory.getAbsolutePath()); + throw new IOException("Error while deleting empty directory: " + directory.getAbsolutePath()); } else { deleteEmptyParentDirectories(directory.getParentFile()); } @@ -119,17 +117,19 @@ private static void deleteEmptyParentDirectories(File directory) { * @param submissionid id of the submission * @return the path of the submission */ - static public Path getSubmissionPath(long projectid, long groupid, long submissionid) { + static public Path getSubmissionPath(long projectid, Long groupid, long submissionid) { + if (groupid == null) { + return Path.of(BASEPATH,"projects", String.valueOf(projectid), ADMIN_SUBMISSION_FOLDER, String.valueOf(submissionid)); + } return Path.of(BASEPATH,"projects", String.valueOf(projectid), String.valueOf(groupid), String.valueOf(submissionid)); } - /** - * Get the path were a test is stored - * @param projectid id of the project - * @return the path of the test - */ - static public Path getTestPath(long projectid) { - return Path.of(BASEPATH,"projects", String.valueOf(projectid), "tests"); + static public Path getSubmissionArtifactPath(long projectid, Long groupid, long submissionid) { + return getSubmissionPath(projectid, groupid, submissionid).resolve("artifacts.zip"); + } + + static public Path getTestExtraFilesPath(long projectid) { + return Path.of(BASEPATH,"projects", String.valueOf(projectid)); } /** @@ -138,6 +138,9 @@ static public Path getTestPath(long projectid) { * @return the file as a resource */ public static Resource getFileAsResource(Path path) { + if (!Files.exists(path)) { + return null; + } File file = path.toFile(); return new FileSystemResource(file); } @@ -161,53 +164,67 @@ public static boolean isZipFile(File file) throws IOException { } - /** - * Get a submission as a resource - * @param path path of the submission - * @return the submission as a resource - * @throws IOException if an error occurs while getting the submission - */ - public static Resource getSubmissionAsResource(Path path) throws IOException { - return new InputStreamResource(new FileInputStream(path.toFile())); - } /** - * Save a file to the server - * @param file file to save - * @param projectId id of the project - * @return the path of the saved file - * @throws IOException if an error occurs while saving the file + * A function for copying internally made lists of files, to a required path. + * @param files list of files to copy + * @param path path to copy the files to + * @throws IOException if an error occurs while copying the files */ - public static Path saveTest(MultipartFile file, long projectId) throws IOException { - // Check if the file is empty - if (file.isEmpty()) { - throw new IOException("File is empty"); + public static void copyFilesAsZip(List files, Path path) throws IOException { + // Write directly to a zip file in the path variable + File zipFile = new File(path.toString()); + System.out.println(zipFile.getAbsolutePath()); + Logger.getGlobal().info("Filexists: " + zipFile.exists()); + if (zipFile.exists() && !zipFile.canWrite()) { + Logger.getGlobal().info("Setting writable"); + boolean res = zipFile.setWritable(true); + if (!res) { + throw new IOException("Cannot write to zip file"); + } + } + + try (ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFile))) { + for (File file : files) { + // add file to zip + zipOutputStream.putNextEntry(new ZipEntry(file.getName())); + FileInputStream fileInputStream = new FileInputStream(file); + byte[] buffer = new byte[1024]; + int len; + while ((len = fileInputStream.read(buffer)) > 0) { + zipOutputStream.write(buffer, 0, len); + } + fileInputStream.close(); + zipOutputStream.closeEntry(); + } } + } - // Create directory if it doesn't exist - Path projectDirectory = getTestPath(projectId); - if (!Files.exists(projectDirectory)) { - Files.createDirectories(projectDirectory); + public static ResponseEntity getZipFileAsResponse(Path path, String filename) { + // Get the file from the server + Resource zipFile = Filehandler.getFileAsResource(path); + if (zipFile == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File not found."); } - // Save the file to the server - Path filePath = projectDirectory.resolve(Objects.requireNonNull(file.getOriginalFilename())); - Files.write(filePath, file.getBytes()); + // Set headers for the response + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename); + headers.add(HttpHeaders.CONTENT_TYPE, "application/zip"); - return filePath; + return ResponseEntity.ok() + .headers(headers) + .body(zipFile); } - /** - * Get the structure test file contents as string - * @param path path of the structure test file - * @return the structure test file contents as string - * @throws IOException if an error occurs while reading the file - */ - public static String getStructureTestString(Path path) throws IOException { - try { - return Files.readString(path); - } catch (IOException e) { - throw new IOException("Error while reading testfile: " + e.getMessage()); - } + + public static void addExistingZip(ZipOutputStream groupZipOut, String zipFileName, File zipFile) throws IOException { + ZipEntry zipEntry = new ZipEntry(zipFileName); + groupZipOut.putNextEntry(zipEntry); + + // Read the content of the zip file and write it to the group zip output stream + Files.copy(zipFile.toPath(), groupZipOut); + + groupZipOut.closeEntry(); } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java index 346bc21c..1c8fbeaf 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/GroupFeedbackUtil.java @@ -1,7 +1,11 @@ package com.ugent.pidgeon.util; -import com.ugent.pidgeon.model.json.UpdateGroupScoreRequest; -import com.ugent.pidgeon.postgre.models.*; +import com.ugent.pidgeon.json.UpdateGroupScoreRequest; +import com.ugent.pidgeon.postgre.models.GroupEntity; +import com.ugent.pidgeon.postgre.models.GroupFeedbackEntity; +import com.ugent.pidgeon.postgre.models.GroupFeedbackId; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.repository.GroupFeedbackRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; @@ -100,15 +104,15 @@ public CheckResult checkGroupFeedbackUpdateJson(UpdateGroupScoreRequest re return new CheckResult<>(projectCheck.getStatus(), projectCheck.getMessage(), null); } Integer maxScore = projectCheck.getData().getMaxScore(); - if (request.getScore() == null || request.getFeedback() == null) { - return new CheckResult<>(HttpStatus.BAD_REQUEST, "Score and feedback need to be provided", null); + if (request.getFeedback() == null) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "Feedbacks need to be provided", null); } - if (maxScore != null && request.getScore() < 0) { + if (request.getScore() != null && request.getScore() < 0) { return new CheckResult<>(HttpStatus.BAD_REQUEST, "Score can't be lower than 0", null); } - if (maxScore != null && request.getScore() > maxScore) { + if (maxScore != null && request.getScore() != null && request.getScore() > maxScore) { return new CheckResult<>(HttpStatus.BAD_REQUEST, "Score can't be higher than the defined max score (" + maxScore + ")", null); } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/GroupUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/GroupUtil.java index cfe89425..8ea2dde9 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/GroupUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/GroupUtil.java @@ -7,6 +7,7 @@ import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; import com.ugent.pidgeon.postgre.repository.GroupRepository; +import java.time.OffsetDateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; @@ -23,6 +24,8 @@ public class GroupUtil { private ClusterUtil clusterUtil; @Autowired private ProjectUtil projectUtil; + @Autowired + private UserUtil userUtil; /** @@ -58,7 +61,7 @@ public CheckResult canGetGroup(long groupId, UserEntity user) { * @return CheckResult with the status of the check */ public CheckResult isAdminOfGroup(long groupId, UserEntity user) { - if (!groupRepository.isAdminOfGroup(groupId, user.getId()) && !user.getRole().equals(UserRole.admin)) { + if (!groupRepository.isAdminOfGroup(user.getId(), groupId) && !user.getRole().equals(UserRole.admin)) { return new CheckResult<>(HttpStatus.FORBIDDEN, "User is not an admin of this group", null); } return new CheckResult<>(HttpStatus.OK, "", null); @@ -99,15 +102,27 @@ public CheckResult canAddUserToGroup(long groupId, long userId, UserEntity if (group == null) { return new CheckResult<>(HttpStatus.NOT_FOUND, "Group not found", null); } + + boolean isAdmin = false; + if (user.getId() != userId) { CheckResult admin = isAdminOfGroup(groupId, user); if (admin.getStatus() != HttpStatus.OK) { return admin; } + isAdmin = true; } else { if (!groupRepository.userAccessToGroup(userId, groupId)) { return new CheckResult<>(HttpStatus.FORBIDDEN, "User is not part of the course", null); } + if (groupClusterRepository.inArchivedCourse(group.getClusterId())) { + return new CheckResult<>(HttpStatus.FORBIDDEN, "Cannot join a group in an archived course", null); + } + } + + UserEntity userToAdd = userUtil.getUserIfExists(userId); + if (userToAdd == null) { + return new CheckResult<>(HttpStatus.NOT_FOUND, "User not found", null); } if (groupClusterRepository.userInGroupForCluster(group.getClusterId(), userId)) { @@ -121,13 +136,22 @@ public CheckResult canAddUserToGroup(long groupId, long userId, UserEntity return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while checking cluster", null); } - if (cluster.getData().getMaxSize() <= groupRepository.countUsersInGroup(groupId)) { + if (cluster.getData().getMaxSize() <= groupRepository.countUsersInGroup(groupId) && !isAdmin) { return new CheckResult<>(HttpStatus.FORBIDDEN, "Group is full", null); } if (clusterUtil.isIndividualCluster(group.getClusterId())) { return new CheckResult<>(HttpStatus.FORBIDDEN, "Cannot add user to individual group", null); } + OffsetDateTime lockGroupTime = cluster.getData().getLockGroupsAfter(); + if (lockGroupTime != null && lockGroupTime.isBefore(OffsetDateTime.now()) && !isAdmin) { + return new CheckResult<>(HttpStatus.FORBIDDEN, "Groups are locked", null); + } + + if (isAdminOfGroup(groupId, userToAdd).getStatus().equals(HttpStatus.OK)) { + return new CheckResult<>(HttpStatus.FORBIDDEN, "Cannot add a course admin to a group", null); + } + return new CheckResult<>(HttpStatus.OK, "", null); } @@ -148,6 +172,19 @@ public CheckResult canRemoveUserFromGroup(long groupId, long userId, UserE if (admin.getStatus() != HttpStatus.OK) { return admin; } + + } else { + if (groupClusterRepository.inArchivedCourse(group.getClusterId())) { + return new CheckResult<>(HttpStatus.FORBIDDEN, "Cannot leave a group in an archived course", null); + } + CheckResult cluster = clusterUtil.getClusterIfExists(group.getClusterId()); + if (cluster.getStatus() != HttpStatus.OK) { + return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while checking cluster", null); + } + OffsetDateTime lockGroupTime = cluster.getData().getLockGroupsAfter(); + if (lockGroupTime != null && lockGroupTime.isBefore(OffsetDateTime.now())) { + return new CheckResult<>(HttpStatus.FORBIDDEN, "Groups are locked", null); + } } if (!groupRepository.userInGroup(groupId, userId)) { return new CheckResult<>(HttpStatus.NOT_FOUND, "User is not in the group", null); @@ -167,16 +204,16 @@ public CheckResult canRemoveUserFromGroup(long groupId, long userId, UserE * @param user user that wants to get the submissions * @return CheckResult with the status of the check */ - public CheckResult canGetProjectGroupData(long groupId, long projectId, UserEntity user) { + public CheckResult canGetProjectGroupData(Long groupId, long projectId, UserEntity user) { CheckResult projectCheck = projectUtil.getProjectIfExists(projectId); if (projectCheck.getStatus() != HttpStatus.OK) { return new CheckResult<>(projectCheck.getStatus(), projectCheck.getMessage(), null); } ProjectEntity project = projectCheck.getData(); - if (groupRepository.findByIdAndClusterId(groupId, project.getGroupClusterId()).isEmpty()) { + if (groupId != null && groupRepository.findByIdAndClusterId(groupId, project.getGroupClusterId()).isEmpty()) { return new CheckResult<>(HttpStatus.NOT_FOUND, "Group not part of the project", null); } - boolean inGroup = groupRepository.userInGroup(groupId, user.getId()); + boolean inGroup = groupId != null && groupRepository.userInGroup(groupId, user.getId()); boolean isAdmin = user.getRole().equals(UserRole.admin) || projectUtil.isProjectAdmin(projectId, user).getStatus().equals(HttpStatus.OK); if (inGroup || isAdmin) { return new CheckResult<>(HttpStatus.OK, "", null); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/ProjectUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/ProjectUtil.java index e4f2a4c1..88832852 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/ProjectUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/ProjectUtil.java @@ -1,23 +1,15 @@ package com.ugent.pidgeon.util; -import com.ugent.pidgeon.controllers.ApiRoutes; -import com.ugent.pidgeon.model.ProjectResponseJson; -import com.ugent.pidgeon.model.json.CourseReferenceJson; -import com.ugent.pidgeon.model.json.ProjectJson; -import com.ugent.pidgeon.model.json.ProjectProgressJson; -import com.ugent.pidgeon.postgre.models.*; -import com.ugent.pidgeon.postgre.models.types.CourseRelation; +import com.ugent.pidgeon.json.ProjectJson; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.UserRole; -import com.ugent.pidgeon.postgre.repository.CourseUserRepository; -import com.ugent.pidgeon.postgre.repository.GroupRepository; import com.ugent.pidgeon.postgre.repository.ProjectRepository; -import com.ugent.pidgeon.postgre.repository.SubmissionRepository; +import java.time.OffsetDateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; -import java.time.OffsetDateTime; - @Component public class ProjectUtil { @@ -96,11 +88,10 @@ public CheckResult getProjectIfAdmin(long projectId, UserEntity u public CheckResult checkProjectJson(ProjectJson projectJson, long courseId) { if (projectJson.getName() == null || projectJson.getDescription() == null || - projectJson.getMaxScore() == null || projectJson.getGroupClusterId() == null || projectJson.getDeadline() == null) { return new CheckResult<>(HttpStatus.BAD_REQUEST, - "name, description, maxScore and deadline are required fields", null); + "name, description and deadline are required fields", null); } if (projectJson.getName().isBlank()) { @@ -117,8 +108,8 @@ public CheckResult checkProjectJson(ProjectJson projectJson, long courseId return new CheckResult<>(HttpStatus.BAD_REQUEST, "Deadline is in the past", null); } - if (projectJson.getMaxScore() < 0) { - return new CheckResult<>(HttpStatus.BAD_REQUEST, "Max score cannot be negative", null); + if (projectJson.getMaxScore() != null && projectJson.getMaxScore() <= 0) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "Max score cannot be negative or zero", null); } return new CheckResult<>(HttpStatus.OK, "", null); @@ -140,8 +131,7 @@ public CheckResult canGetProject(long projectId, UserEntity user) boolean studentof = projectRepository.userPartOfProject(projectId, user.getId()); boolean isAdmin = - (user.getRole() == UserRole.admin) || (projectRepository.adminOfProject(projectId, - user.getId())); + (user.getRole() == UserRole.admin); if (studentof || isAdmin) { return new CheckResult<>(HttpStatus.OK, "", project); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/StringMatcher.java b/backend/app/src/main/java/com/ugent/pidgeon/util/StringMatcher.java index ff37bb79..0b1a6dd1 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/StringMatcher.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/StringMatcher.java @@ -8,6 +8,7 @@ public class StringMatcher { "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@" + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"; + private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX); /** diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/SubmissionUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/SubmissionUtil.java index 02343fa9..c46f0c28 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/SubmissionUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/SubmissionUtil.java @@ -1,17 +1,18 @@ package com.ugent.pidgeon.util; +import com.ugent.pidgeon.postgre.models.GroupEntity; import com.ugent.pidgeon.postgre.models.ProjectEntity; import com.ugent.pidgeon.postgre.models.SubmissionEntity; import com.ugent.pidgeon.postgre.models.UserEntity; +import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; import com.ugent.pidgeon.postgre.repository.GroupRepository; import com.ugent.pidgeon.postgre.repository.SubmissionRepository; +import java.time.OffsetDateTime; +import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; -import java.time.OffsetDateTime; -import java.util.logging.Logger; - @Component public class SubmissionUtil { @@ -24,6 +25,8 @@ public class SubmissionUtil { private SubmissionRepository submissionRepository; @Autowired private GroupUtil groupUtil; + @Autowired + private GroupClusterRepository groupClusterRepository; /** @@ -37,10 +40,11 @@ public CheckResult canGetSubmission(long submissionId, UserEnt if (submission == null) { return new CheckResult<>(HttpStatus.NOT_FOUND, "Submission not found", null); } - if (groupUtil.canGetProjectGroupData(submission.getGroupId(), submission.getProjectId(), user).getStatus().equals(HttpStatus.OK)) { + CheckResult groupCheck = groupUtil.canGetProjectGroupData(submission.getGroupId(), submission.getProjectId(), user); + if (groupCheck.getStatus().equals(HttpStatus.OK)) { return new CheckResult<>(HttpStatus.OK, "", submission); } else { - return new CheckResult<>(HttpStatus.FORBIDDEN, "User does not have access to this submission", null); + return new CheckResult<>(groupCheck.getStatus(), groupCheck.getMessage(), null); } } @@ -55,10 +59,11 @@ public CheckResult canDeleteSubmission(long submissionId, User if (submission == null) { return new CheckResult<>(HttpStatus.NOT_FOUND, "Submission not found", null); } - if (projectUtil.isProjectAdmin(submission.getProjectId(), user).getStatus().equals(HttpStatus.OK)) { + CheckResult projectCheck = projectUtil.isProjectAdmin(submission.getProjectId(), user); + if (projectCheck.getStatus().equals(HttpStatus.OK)) { return new CheckResult<>(HttpStatus.OK, "", submission); } else { - return new CheckResult<>(HttpStatus.FORBIDDEN, "User does not have access to delete this submission", null); + return new CheckResult<>(projectCheck.getStatus(), projectCheck.getMessage(), null); } } @@ -69,20 +74,37 @@ public CheckResult canDeleteSubmission(long submissionId, User * @return CheckResult with the status of the check and the group id */ public CheckResult checkOnSubmit(long projectId, UserEntity user) { - Long groupId = groupRepository.groupIdByProjectAndUser(projectId, user.getId()); + CheckResult projectCheck = projectUtil.getProjectIfExists(projectId); + if (projectCheck.getStatus() != HttpStatus.OK) { + return new CheckResult<> (projectCheck.getStatus(), projectCheck.getMessage(), null); + } + + ProjectEntity project = projectCheck.getData(); if (!projectUtil.userPartOfProject(projectId, user.getId())) { return new CheckResult<>(HttpStatus.FORBIDDEN, "You aren't part of this project", null); } - CheckResult projectCheck = projectUtil.getProjectIfExists(projectId); - if (projectCheck.getStatus() != HttpStatus.OK) { - return new CheckResult<> (projectCheck.getStatus(), projectCheck.getMessage(), null); + Long groupId = groupRepository.groupIdByProjectAndUser(projectId, user.getId()); + if (groupId == null) { + CheckResult projectAdminCheck = projectUtil.isProjectAdmin(projectId, user); + if (projectAdminCheck.getStatus() != HttpStatus.OK) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "User is not part of a group for this project", null); + } + } else { + CheckResult groupCheck = groupUtil.getGroupIfExists(groupId); + if (groupCheck.getStatus() != HttpStatus.OK) { + return new CheckResult<>(groupCheck.getStatus(), groupCheck.getMessage(), null); + } + + if (groupClusterRepository.inArchivedCourse(project.getGroupClusterId())) { + return new CheckResult<>(HttpStatus.FORBIDDEN, "Cannot submit for a project in an archived course", null); + } } - ProjectEntity project = projectCheck.getData(); + OffsetDateTime time = OffsetDateTime.now(); Logger.getGlobal().info("Time: " + time + " Deadline: " + project.getDeadline()); - if (time.isAfter(project.getDeadline())) { + if (time.isAfter(project.getDeadline()) && groupId != null) { return new CheckResult<>(HttpStatus.FORBIDDEN, "Project deadline has passed", null); } return new CheckResult<>(HttpStatus.OK, "", groupId); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/TestRunner.java b/backend/app/src/main/java/com/ugent/pidgeon/util/TestRunner.java new file mode 100644 index 00000000..74578600 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/TestRunner.java @@ -0,0 +1,77 @@ +package com.ugent.pidgeon.util; + +import com.ugent.pidgeon.model.submissionTesting.DockerOutput; +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; +import com.ugent.pidgeon.model.submissionTesting.SubmissionTemplateModel; +import com.ugent.pidgeon.postgre.models.TestEntity; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.zip.ZipFile; +import org.springframework.stereotype.Component; + +@Component +public class TestRunner { + + public SubmissionTemplateModel.SubmissionResult runStructureTest( + ZipFile file, TestEntity testEntity, SubmissionTemplateModel model) throws IOException { + // There is no structure test for this project + if(testEntity == null || testEntity.getStructureTemplate() == null){ + return null; + } + String structureTemplateString = testEntity.getStructureTemplate(); + + // Parse the file + model.parseSubmissionTemplate(structureTemplateString); + return model.checkSubmission(file); + } + + public DockerOutput runDockerTest(ZipFile file, TestEntity testEntity, Path outputPath, DockerSubmissionTestModel model, long projectId) throws IOException { + // Get the test file from the server + String testScript = testEntity.getDockerTestScript(); + String testTemplate = testEntity.getDockerTestTemplate(); + + // The first script must always be null, otherwise there is nothing to run on the container + if (testScript == null) { + return null; + } + + // Init container and add input files + try { + + + model.addZipInputFiles(file); + Path path = Filehandler.getTestExtraFilesPath(projectId).resolve(Filehandler.EXTRA_TESTFILES_FILENAME); + if (Files.exists(path)) { + model.addUtilFiles(path); + } + DockerOutput output; + + if (testTemplate == null) { + // This docker test is configured in the simple mode (store test console logs) + output = model.runSubmission(testScript); + } else { + // This docker test is configured in the template mode (store json with feedback) + output = model.runSubmissionWithTemplate(testScript, testTemplate); + } + // Get list of artifact files generated on submission + List artifacts = model.getArtifacts(); + + // Copy all files as zip into the output directory + if (artifacts != null && !artifacts.isEmpty()) { + Filehandler.copyFilesAsZip(artifacts, outputPath); + } + + // Cleanup garbage files and container + model.cleanUp(); + + return output; + } catch (Exception e) { + model.cleanUp(); + throw new IOException("Error while running docker tests: " + e.getMessage()); + } + } + +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java index bf359735..5746b4ef 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java @@ -1,16 +1,16 @@ package com.ugent.pidgeon.util; -import com.ugent.pidgeon.controllers.ApiRoutes; -import com.ugent.pidgeon.model.json.TestJson; +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; +import com.ugent.pidgeon.model.submissionTesting.SubmissionTemplateModel; import com.ugent.pidgeon.postgre.models.ProjectEntity; import com.ugent.pidgeon.postgre.models.TestEntity; import com.ugent.pidgeon.postgre.models.UserEntity; +import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.TestRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; @Component public class TestUtil { @@ -31,12 +31,12 @@ public TestEntity getTestIfExists(long projectId) { } /** - * Check if a user can get update a test + * Check if a user can update a test * @param projectId id of the project * @param user user that wants to update the test * @param dockerImage docker image for the test - * @param dockerTest docker test file - * @param structureTest structure test file + * @param dockerScript docker script for the test + * @param dockerTemplate docker template for the test * @param httpMethod http method used to update the test * @return CheckResult with the status of the check and the test and project */ @@ -44,10 +44,12 @@ public CheckResult> checkForTestUpdate( long projectId, UserEntity user, String dockerImage, - MultipartFile dockerTest, - MultipartFile structureTest, + String dockerScript, + String dockerTemplate, + String structureTemplate, HttpMethod httpMethod ) { + CheckResult projectCheck = projectUtil.getProjectIfAdmin(projectId, user); if (!projectCheck.getStatus().equals(HttpStatus.OK)) { return new CheckResult<>(projectCheck.getStatus(), projectCheck.getMessage(), null); @@ -67,12 +69,40 @@ public CheckResult> checkForTestUpdate( return new CheckResult<>(HttpStatus.CONFLICT, "Tests already exist for this project", null); } - if (!httpMethod.equals(HttpMethod.PATCH)) { - if (dockerImage == null || dockerTest == null || structureTest == null) { - return new CheckResult<>(HttpStatus.BAD_REQUEST, "Missing parameters: dockerimage (string), dockertest (file), structuretest (file) are required", null); - } + if(!httpMethod.equals(HttpMethod.PATCH) && dockerImage != null && dockerScript == null) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "A test script is required if u add a dockerimage.", null); + } + if (!httpMethod.equals(HttpMethod.PATCH) && dockerScript != null && dockerImage == null) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "A docker image is required if u add a script", null); + } + + if(dockerImage != null && !DockerSubmissionTestModel.imageExists(dockerImage)) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "A valid docker image is required in a docker test.", null); + } + + if (!httpMethod.equals(HttpMethod.PATCH) && dockerTemplate != null && dockerImage == null) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "A test script and image are required in a docker template test.", null); + } + + if(httpMethod.equals(HttpMethod.PATCH) && dockerScript != null && testEntity.getDockerImage() == null && dockerImage == null) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "No docker image is configured for this test", null); + } + + if(httpMethod.equals(HttpMethod.PATCH) && dockerImage != null && testEntity.getDockerTestScript() == null && dockerScript == null) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "No docker test script is configured for this test", null); } + try { + // throws error if there are issues in the template + if(dockerTemplate != null) DockerSubmissionTestModel.tryTemplate(dockerTemplate); + if(structureTemplate != null) SubmissionTemplateModel.tryTemplate(structureTemplate); + + } catch(IllegalArgumentException e){ + return new CheckResult<>(HttpStatus.BAD_REQUEST, e.getMessage(), null); + } + + + return new CheckResult<>(HttpStatus.OK, "", new Pair<>(testEntity, projectEntity)); } @@ -96,5 +126,28 @@ public CheckResult getTestIfAdmin(long projectId, UserEntity user) { return new CheckResult<>(HttpStatus.OK, "", testEntity); } + public CheckResult> getTestWithAdminStatus(long projectId, UserEntity user) { + TestEntity testEntity = getTestIfExists(projectId); + if (testEntity == null) { + return new CheckResult<>(HttpStatus.NOT_FOUND, "No tests found for project with id: " + projectId, null); + } + + boolean userPartOfProject = projectUtil.userPartOfProject(projectId, user.getId()); + if (!userPartOfProject) { + return new CheckResult<>(HttpStatus.FORBIDDEN, "You are not part of this project", null); + } + boolean admin = false; + + CheckResult isProjectAdmin = projectUtil.isProjectAdmin(projectId, user); + if (isProjectAdmin.getStatus().equals(HttpStatus.OK)) { + admin = true; + } else if (!isProjectAdmin.getStatus().equals(HttpStatus.FORBIDDEN)){ + return new CheckResult<>(isProjectAdmin.getStatus(), isProjectAdmin.getMessage(), null); + } else if (user.getRole().equals(UserRole.admin)) { + admin = true; + } + + return new CheckResult<>(HttpStatus.OK, "", new Pair<>(testEntity, admin)); + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/UserUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/UserUtil.java index b8f414ad..05553cae 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/UserUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/UserUtil.java @@ -1,7 +1,6 @@ package com.ugent.pidgeon.util; -import com.ugent.pidgeon.model.json.UserReferenceJson; -import com.ugent.pidgeon.model.json.UserUpdateJson; +import com.ugent.pidgeon.json.UserUpdateJson; import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; diff --git a/backend/app/src/main/resources/application.properties b/backend/app/src/main/resources/application.properties index ff099426..7b44679e 100644 --- a/backend/app/src/main/resources/application.properties +++ b/backend/app/src/main/resources/application.properties @@ -25,6 +25,6 @@ server.port=8080 # TODO: this is just temporary, we will need to think of an actual limit at some point -spring.servlet.multipart.max-file-size=10MB -spring.servlet.multipart.max-request-size=10MB +spring.servlet.multipart.max-file-size=50MB +spring.servlet.multipart.max-request-size=50MB diff --git a/backend/app/src/test/java/com/ugent/pidgeon/CustomObjectMapper.java b/backend/app/src/test/java/com/ugent/pidgeon/CustomObjectMapper.java index ee3c199f..8bf52de8 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/CustomObjectMapper.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/CustomObjectMapper.java @@ -1,15 +1,18 @@ package com.ugent.pidgeon; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import java.time.OffsetDateTime; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; public class CustomObjectMapper { public static ObjectMapper createObjectMapper() { - ObjectMapper objectMapper = new ObjectMapper(); - SimpleModule module = new SimpleModule(); - module.addDeserializer(OffsetDateTime.class, new OffsetDateTimeDeserializer()); - objectMapper.registerModule(module); - return objectMapper; + return JsonMapper.builder() // or different mapper for other format + .addModule(new ParameterNamesModule()) + .addModule(new Jdk8Module()) + .addModule(new JavaTimeModule()) + // and possibly other configuration, modules, then: + .build(); } } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/PidgeonApplicationTests.java b/backend/app/src/test/java/com/ugent/pidgeon/PidgeonApplicationTests.java index 85846c69..76d2b4f7 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/PidgeonApplicationTests.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/PidgeonApplicationTests.java @@ -1,6 +1,5 @@ package com.ugent.pidgeon; -import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ClusterControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ClusterControllerTest.java index 90182e61..9978b95d 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ClusterControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ClusterControllerTest.java @@ -1,13 +1,40 @@ package com.ugent.pidgeon.controllers; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ugent.pidgeon.CustomObjectMapper; +import com.ugent.pidgeon.json.GroupClusterJson; +import com.ugent.pidgeon.json.GroupJson; import com.ugent.pidgeon.postgre.models.CourseEntity; import com.ugent.pidgeon.postgre.models.GroupClusterEntity; import com.ugent.pidgeon.postgre.models.GroupEntity; import com.ugent.pidgeon.postgre.models.types.CourseRelation; import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; +import com.ugent.pidgeon.postgre.repository.GroupMemberRepository; import com.ugent.pidgeon.postgre.repository.GroupRepository; -import com.ugent.pidgeon.postgre.repository.GroupUserRepository; -import com.ugent.pidgeon.util.*; +import com.ugent.pidgeon.util.CheckResult; +import com.ugent.pidgeon.util.ClusterUtil; +import com.ugent.pidgeon.util.CommonDatabaseActions; +import com.ugent.pidgeon.util.CourseUtil; +import com.ugent.pidgeon.util.EntityToJsonConverter; +import com.ugent.pidgeon.util.Pair; +import java.time.OffsetDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Objects; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -16,16 +43,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; - -import java.util.List; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(MockitoExtension.class) public class ClusterControllerTest extends ControllerTest{ @@ -35,72 +53,130 @@ public class ClusterControllerTest extends ControllerTest{ @Mock GroupRepository groupRepository; @Mock - GroupUserRepository groupUserRepository; + GroupMemberRepository groupMemberRepository; @Mock private ClusterUtil clusterUtil; @Mock private EntityToJsonConverter entityToJsonConverter; + @Mock private CourseUtil courseUtil; @Mock private CommonDatabaseActions commonDatabaseActions; + @InjectMocks private ClusterController clusterController; private CourseEntity courseEntity; private GroupClusterEntity groupClusterEntity; + private GroupClusterJson groupClusterJson; private GroupEntity groupEntity; + private GroupJson groupJson; + private final Long courseId = 1L; + + private final ObjectMapper objectMapper = CustomObjectMapper.createObjectMapper(); @BeforeEach public void setup() { - mockMvc = MockMvcBuilders.standaloneSetup(clusterController) - .defaultRequest(MockMvcRequestBuilders.get("/**") - .with(request -> { - request.setUserPrincipal(SecurityContextHolder.getContext().getAuthentication()); - return request; - })) - .build(); - - courseEntity = new CourseEntity("name", "description"); - groupClusterEntity = new GroupClusterEntity(1L, 20, "clustername", 5); + setUpController(clusterController); + + courseEntity = new CourseEntity("name", "description",2024); + courseEntity.setId(32L); + groupClusterEntity = new GroupClusterEntity(courseEntity.getId(), 20, "clustername", 5); + groupClusterEntity.setId(29L); + groupClusterJson = new GroupClusterJson( + groupClusterEntity.getId(), + groupClusterEntity.getName(), + groupClusterEntity.getMaxSize(), + groupClusterEntity.getGroupAmount(), + OffsetDateTime.now(), + Collections.emptyList(), + null, + ""); groupEntity = new GroupEntity("groupName", 1L); + groupEntity.setId(78L); + groupJson = new GroupJson(groupClusterEntity.getMaxSize(), groupEntity.getId(), groupEntity.getName(), ""); } @Test public void testGetClustersForCourse() throws Exception { - when(courseUtil.getCourseIfUserInCourse(anyLong(), any())) + String url = ApiRoutes.COURSE_BASE_PATH + "/" + courseId + "/clusters"; + + /* If the user is enrolled in the course, the clusters are returned */ + when(courseUtil.getCourseIfUserInCourse(courseId, getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(courseEntity, CourseRelation.enrolled))); - when(groupClusterRepository.findClustersWithoutInvidualByCourseId(anyLong())).thenReturn(List.of(groupClusterEntity)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1/clusters")) - .andExpect(status().isOk()); + when(groupClusterRepository.findClustersWithoutInvidualByCourseId(courseId)).thenReturn(List.of(groupClusterEntity)); + when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, true)).thenReturn(groupClusterJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(groupClusterJson)))); + + verify(entityToJsonConverter, times(1)).clusterEntityToClusterJson(groupClusterEntity, true); + + + /* If user is course_admin, studentnumber isn't hidden */ + when(courseUtil.getCourseIfUserInCourse(courseId, getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(courseEntity, CourseRelation.course_admin))); + when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, false)).thenReturn(groupClusterJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(groupClusterJson)))); + verify(entityToJsonConverter, times(1)).clusterEntityToClusterJson(groupClusterEntity, false); + + /* If a certain check fails, the corresponding status code is returned */ when(courseUtil.getCourseIfUserInCourse(anyLong(), any())) .thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1/clusters")) + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isBadRequest()); } @Test public void testCreateClusterForCourse() throws Exception { - String request = "{\"name\": \"test\", \"capacity\": 20, \"groupCount\": 5}"; - when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", courseEntity)); - when(clusterUtil.checkGroupClusterCreateJson(any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + String url = ApiRoutes.COURSE_BASE_PATH + "/" + courseId +"/clusters"; + + /* If the user is an admin of the course and the json is valid, the cluster is created */ + String request = "{\"name\": \"test\", \"capacity\": 20, \"groupCount\": 5, \"lockGroupsAfter\": null}"; + when(courseUtil.getCourseIfAdmin(courseId, getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", courseEntity)); + when(clusterUtil.checkGroupClusterCreateJson(argThat( + json -> json.name().equals("test") && json.capacity().equals(20) && json.groupCount().equals(5) && json.lockGroupsAfter() == null + ))).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); when(groupClusterRepository.save(any())).thenReturn(groupClusterEntity); - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH + "/1/clusters") + when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, false)).thenReturn(groupClusterJson); + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupClusterJson))); + + /* lockGroupsAfter not null */ + request = "{\"name\": \"test\", \"capacity\": 20, \"groupCount\": 5, \"lockGroupsAfter\": \"2024-01-01T00:00:00Z\"}"; + reset(clusterUtil); + when(clusterUtil.checkGroupClusterCreateJson(argThat( + json -> json.name().equals("test") && json.capacity().equals(20) && json.groupCount().equals(5) && json.lockGroupsAfter().equals(OffsetDateTime.parse("2024-01-01T00:00:00Z")) + ))).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isCreated()); + + /* If the json is invalid, the corresponding status code is returned */ + reset(clusterUtil); when(clusterUtil.checkGroupClusterCreateJson(any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH + "/1/clusters") + mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isIAmATeapot()); + /* If the user is not an admin of the course, the corresponding status code is returned */ when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH + "/1/clusters") + mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isBadRequest()); @@ -108,53 +184,229 @@ public void testCreateClusterForCourse() throws Exception { @Test public void testGetCluster() throws Exception { - when(clusterUtil.getGroupClusterEntityIfNotIndividual(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterEntity)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.CLUSTER_BASE_PATH + "/1")) - .andExpect(status().isOk()); + String url = ApiRoutes.CLUSTER_BASE_PATH + "/" + groupClusterEntity.getId(); + + /* If the user has acces to the cluster and it isn't an individual cluster, the cluster is returned */ + /* User is not an admin, studentNumber should be hidden */ + when(courseUtil.getCourseIfAdmin(courseEntity.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", courseEntity)); + when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, true)).thenReturn(groupClusterJson); + when(clusterUtil.getGroupClusterEntityIfNotIndividual(groupClusterEntity.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterEntity)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupClusterJson))); + + verify(entityToJsonConverter, times(1)).clusterEntityToClusterJson(groupClusterEntity, true); + /* User is an admin, studentNumber should be visible */ + when(courseUtil.getCourseIfAdmin(courseEntity.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", courseEntity)); + when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, false)).thenReturn(groupClusterJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupClusterJson))); + + verify(entityToJsonConverter, times(1)).clusterEntityToClusterJson(groupClusterEntity, false); + + /* If any check fails, the corresponding status code is returned */ when(clusterUtil.getGroupClusterEntityIfNotIndividual(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.CLUSTER_BASE_PATH + "/1")) + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isIAmATeapot()); } //This function also tests doGroupClusterUpdate @Test public void testUpdateCluster() throws Exception { - String request = "{\"name\": \"clustername\", \"capacity\": 20}"; - when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(anyLong(), any())) + String url = ApiRoutes.CLUSTER_BASE_PATH + "/" + groupClusterEntity.getId(); + String request = "{\"name\": \"newclustername\", \"capacity\": 22, \"lockGroupsAfter\": \"2024-01-01T00:00:00Z\"}"; + String originalname = groupClusterEntity.getName(); + Integer originalcapacity = groupClusterEntity.getMaxSize(); + /* If the user is an admin of the cluster, the cluster isn't individual and the json is valid, the cluster is updated */ + GroupClusterEntity copy = new GroupClusterEntity(1L, 20, "newclustername", 5); + when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(groupClusterEntity.getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterEntity)); - when(clusterUtil.checkGroupClusterUpdateJson(any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.CLUSTER_BASE_PATH + "/1") + when(clusterUtil.checkGroupClusterUpdateJson( + argThat(json -> json.getName().equals("newclustername") && json.getCapacity().equals(22) && json.getLockGroupsAfter().equals(OffsetDateTime.parse("2024-01-01T00:00:00Z"))) + )).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + copy.setName("newclustername"); + GroupClusterJson updated = new GroupClusterJson(1L, "newclustername", 20, 5, OffsetDateTime.now(), Collections.emptyList(), null, ""); + when(groupClusterRepository.save(groupClusterEntity)).thenReturn(copy); + when(entityToJsonConverter.clusterEntityToClusterJson(copy, false)).thenReturn(updated); + mockMvc.perform(MockMvcRequestBuilders.put(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(updated))); + assertNotEquals(originalname, groupClusterEntity.getName()); + assertNotEquals(originalcapacity, groupClusterEntity.getMaxSize()); + /* If the json is invalid, the corresponding status code is returned */ + reset(clusterUtil); + when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(groupClusterEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterEntity)); when(clusterUtil.checkGroupClusterUpdateJson(any())).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.CLUSTER_BASE_PATH + "/1") + mockMvc.perform(MockMvcRequestBuilders.put(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isForbidden()); + /* If the user is not an admin of the cluster or the cluster is individual, the corresponding status code is returned */ when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.CLUSTER_BASE_PATH + "/1") + mockMvc.perform(MockMvcRequestBuilders.put(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isBadRequest()); } + @Test + public void testFillCluster() throws Exception { + String url = ApiRoutes.CLUSTER_BASE_PATH + "/" + groupClusterEntity.getId() + "/fill"; + String request = """ + { + "group1": [3, 2], + "group2": [4, 5] + } + """; + + long newGroupEntityId = 89L; + GroupEntity newGroupEntity = new GroupEntity("group1", groupClusterEntity.getId()); + newGroupEntity.setId(newGroupEntityId); + long newGroupEntityId2 = 221L; + GroupEntity newGroupEntity2 = new GroupEntity("group2", groupClusterEntity.getId()); + newGroupEntity2.setId(newGroupEntityId2); + /* All checks succeed */ + when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(groupClusterEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterEntity)); + + when(groupRepository.findAllByClusterId(groupClusterEntity.getId())).thenReturn(List.of(groupEntity)); + when(clusterUtil.checkFillClusterJson(argThat( + json -> { + boolean check = json.getClusterGroupMembers().size() == 2; + check = check && json.getClusterGroupMembers().get("group1").length == 2; + check = check && json.getClusterGroupMembers().get("group1")[0] == 3; + check = check && json.getClusterGroupMembers().get("group1")[1] == 2; + check = check && json.getClusterGroupMembers().get("group2").length == 2; + check = check && json.getClusterGroupMembers().get("group2")[0] == 4; + check = check && json.getClusterGroupMembers().get("group2")[1] == 5; + return check; + } + ), eq(groupClusterEntity))) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + + when(groupRepository.save(argThat( + g1 -> g1 != null && g1.getName().equals("group1") && g1.getClusterId() == groupClusterEntity.getId() + ))).thenReturn(newGroupEntity); + + when(groupRepository.save(argThat( + g2 -> g2 != null && g2.getName().equals("group2") && g2.getClusterId() == groupClusterEntity.getId() + ))).thenReturn(newGroupEntity2); + + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()); + + verify(commonDatabaseActions, times(1)).removeGroup(groupEntity.getId()); + verify(groupMemberRepository, times(1)).addMemberToGroup(newGroupEntityId, 2); + verify(groupMemberRepository, times(1)).addMemberToGroup(newGroupEntityId, 3); + verify(groupMemberRepository, times(1)).addMemberToGroup(newGroupEntityId2, 4); + verify(groupMemberRepository, times(1)).addMemberToGroup(newGroupEntityId2, 5); + assertEquals(2, groupClusterEntity.getGroupAmount()); + verify(groupClusterRepository, times(1)).save(groupClusterEntity); + + /* Error when checking json */ + reset(clusterUtil); + when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(groupClusterEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterEntity)); + when(clusterUtil.checkFillClusterJson(any(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isIAmATeapot()); + + /* Error when getting group cluster entity */ + reset(clusterUtil); + when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(groupClusterEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isIAmATeapot()); + + /* Unexepcted error */ + reset(clusterUtil); + when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(groupClusterEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterEntity)); + when(groupRepository.findAllByClusterId(groupClusterEntity.getId())).thenThrow(new RuntimeException()); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isInternalServerError()); + + } + @Test public void testPatchCluster() throws Exception { + String url = ApiRoutes.CLUSTER_BASE_PATH + "/" + groupClusterEntity.getId(); + + /* If the user is an admin of the cluster and the json is valid, the cluster is updated */ + String originalname = groupClusterEntity.getName(); + Integer originalcapacity = groupClusterEntity.getMaxSize(); + /* If fields are null they are not updated */ String request = "{\"name\": null, \"capacity\": null}"; - when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(anyLong(), any())) + when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(groupClusterEntity.getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterEntity)); - when(clusterUtil.checkGroupClusterUpdateJson(any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.CLUSTER_BASE_PATH + "/1") + when(clusterUtil.checkGroupClusterUpdateJson( + argThat(json -> Objects.equals(json.getName(), groupClusterEntity.getName()) + && json.getCapacity() == groupClusterEntity.getMaxSize()) + )).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(groupClusterRepository.save(groupClusterEntity)).thenReturn(groupClusterEntity); + when(entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, false)).thenReturn(groupClusterJson); + mockMvc.perform(MockMvcRequestBuilders.patch(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupClusterJson))); + assertEquals(originalname, groupClusterEntity.getName()); + assertEquals(originalcapacity, groupClusterEntity.getMaxSize()); + /* If fields are not null they are updated */ + request = "{\"name\": \"newclustername\", \"capacity\": 22, \"lockGroupsAfter\": \"2024-01-01T00:00:00Z\"}"; + reset(clusterUtil); + when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(groupClusterEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterEntity)); + GroupClusterEntity copy = new GroupClusterEntity(1L, 20, "newclustername", 5); + when(clusterUtil.checkGroupClusterUpdateJson( + argThat(json -> json.getName().equals("newclustername") && json.getCapacity().equals(22)) + )).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + GroupClusterJson updated = new GroupClusterJson(1L, "newclustername", 22, 5, OffsetDateTime.now(), Collections.emptyList(), null, ""); + when(groupClusterRepository.save(groupClusterEntity)).thenReturn(copy); + when(entityToJsonConverter.clusterEntityToClusterJson(copy, false)).thenReturn(updated); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(updated))); + assertNotEquals(originalname, groupClusterEntity.getName()); + assertNotEquals(originalcapacity, groupClusterEntity.getMaxSize()); + assertNotNull(groupClusterEntity.getLockGroupsAfter()); + + /* If the json is invalid, the corresponding status code is returned */ + reset(clusterUtil); + when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(groupClusterEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterEntity)); + when(clusterUtil.checkGroupClusterUpdateJson(any())).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isForbidden()); + + /* If the user is not an admin of the cluster or the cluster is individual, the corresponding status code is returned */ when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.CLUSTER_BASE_PATH + "/1") + mockMvc.perform(MockMvcRequestBuilders.patch(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isIAmATeapot()); @@ -162,43 +414,57 @@ public void testPatchCluster() throws Exception { @Test public void testDeleteCluster() throws Exception { - when(clusterUtil.canDeleteCluster(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - when(commonDatabaseActions.deleteClusterById(anyLong())).thenReturn(new CheckResult<>(HttpStatus.OK,"", null)); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.CLUSTER_BASE_PATH + "/1")) + String url = ApiRoutes.CLUSTER_BASE_PATH + "/" + groupClusterEntity.getId(); + + /* If the user can delete the cluster, the cluster is deleted */ + when(clusterUtil.canDeleteCluster(groupClusterEntity.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(commonDatabaseActions.deleteClusterById(groupClusterEntity.getId())).thenReturn(new CheckResult<>(HttpStatus.OK,"", null)); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isNoContent()); + /* If the delete fails, the corresponding status code is returned */ when(commonDatabaseActions.deleteClusterById(anyLong())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT,"", null)); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.CLUSTER_BASE_PATH + "/1")) + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isIAmATeapot()); + /* If the user can't delete the cluster, the corresponding status code is returned */ when(clusterUtil.canDeleteCluster(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN,"", null)); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.CLUSTER_BASE_PATH + "/1")) + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isForbidden()); } @Test public void testCreateGroupForCluster() throws Exception { + String url = ApiRoutes.CLUSTER_BASE_PATH + "/" + groupClusterEntity.getId() + "/groups"; String request = "{\"name\": \"test\"}"; - when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterEntity)); - when(groupRepository.save(any())).thenReturn(groupEntity); - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.CLUSTER_BASE_PATH + "/1/groups") + /* If the user is an admin of the cluster and the json is valid, the group is created */ + when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(groupClusterEntity.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterEntity)); + when(groupRepository.save(argThat( + group -> group.getName().equals("test") && group.getClusterId() == groupClusterEntity.getId() + ))).thenReturn(groupEntity); + when(entityToJsonConverter.groupEntityToJson(groupEntity, false)).thenReturn(groupJson); + mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) - .andExpect(status().isCreated()); + .andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupJson))); + /* if the user is not an admin or the cluster is individual, the corresponding status code is returned */ when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.CLUSTER_BASE_PATH + "/1/groups") + mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isForbidden()); + /* If the json is invalid, the corresponding status code is returned */ request = "{\"name\": \"\"}"; - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.CLUSTER_BASE_PATH + "/1/groups") + mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isBadRequest()); request = "{\"name\": null}"; - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.CLUSTER_BASE_PATH + "/1/groups") + mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isBadRequest()); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ControllerTest.java index 79af404e..78ffe882 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ControllerTest.java @@ -1,55 +1,83 @@ package com.ugent.pidgeon.controllers; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.lenient; + +import com.ugent.pidgeon.GlobalErrorHandler; +import com.ugent.pidgeon.auth.RolesInterceptor; import com.ugent.pidgeon.model.Auth; import com.ugent.pidgeon.model.User; import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.UserRepository; +import java.util.ArrayList; +import java.util.Optional; +import java.util.logging.Logger; import org.junit.jupiter.api.BeforeEach; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.web.servlet.MockMvc; - -import java.util.ArrayList; -import java.util.Optional; - -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.lenient; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; public class ControllerTest { protected MockMvc mockMvc; + private UserEntity mockUser; @Mock protected UserRepository userRepository; + RolesInterceptor rolesInterceptor; + @BeforeEach public void testSetUp() { MockitoAnnotations.openMocks(this); - User user = new User("displayName", "firstName", "lastName", "email", "test"); + User user = new User("displayName", "firstName", "lastName", "email", "test", "studentnummer"); Auth authUser = new Auth(user, new ArrayList<>()); SecurityContextHolder.getContext().setAuthentication(authUser); - // Only stubbing necessary methods for the test - UserEntity userEntity = mockUser(); - authUser.setUserEntity(userEntity); - lenient().when(userRepository.findById(anyLong())).thenReturn(Optional.of(userEntity)); + mockUser = new UserEntity( + user.firstName, + user.lastName, + user.email, + UserRole.teacher, + user.oid, + "studentnummer" + ); + mockUser.setId(1L); + authUser.setUserEntity(mockUser); + lenient().when(userRepository.findById(anyLong())).thenReturn(Optional.of(mockUser)); lenient().when(userRepository.findCourseIdsByUserId(anyLong())).thenReturn(new ArrayList<>()); - // when(userRepository.findUserByAzureId(anyString())).thenReturn(userEntity); + lenient().when(userRepository.findUserByAzureId("test")).thenReturn(Optional.of(mockUser)); + Logger.getGlobal().info("User: " + mockUser); + rolesInterceptor = new RolesInterceptor(userRepository); + // when(userRepository.findUserByAzureId(anyString())).thenReturn(userEntity); + } + protected void setUpController(Object controller) { + mockMvc = MockMvcBuilders.standaloneSetup(controller) + .addInterceptors(rolesInterceptor) + .setControllerAdvice(new GlobalErrorHandler()) + .defaultRequest(MockMvcRequestBuilders.get("/**") + .with(request -> { + request.setUserPrincipal(SecurityContextHolder.getContext().getAuthentication()); + return request; + })) + .build(); } - protected UserEntity mockUser() { - UserEntity userEntity = new UserEntity(); - userEntity.setId(1L); - userEntity.setRole(UserRole.student); - return userEntity; + protected UserEntity getMockUser() { + return mockUser; } + protected void setMockUserRoles(UserRole role) { + mockUser.setRole(role); + } } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/CourseControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/CourseControllerTest.java index 8648b00d..555bc3ba 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/CourseControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/CourseControllerTest.java @@ -1,38 +1,69 @@ package com.ugent.pidgeon.controllers; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyIterable; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ugent.pidgeon.CustomObjectMapper; +import com.ugent.pidgeon.json.CourseJoinInformationJson; +import com.ugent.pidgeon.json.CourseReferenceJson; +import com.ugent.pidgeon.json.CourseWithInfoJson; +import com.ugent.pidgeon.json.CourseWithRelationJson; +import com.ugent.pidgeon.json.ProjectProgressJson; +import com.ugent.pidgeon.json.UserReferenceJson; +import com.ugent.pidgeon.json.UserReferenceWithRelation; import com.ugent.pidgeon.model.ProjectResponseJson; -import com.ugent.pidgeon.model.json.CourseReferenceJson; -import com.ugent.pidgeon.model.json.CourseWithInfoJson; -import com.ugent.pidgeon.model.json.ProjectProgressJson; -import com.ugent.pidgeon.model.json.UserReferenceJson; -import com.ugent.pidgeon.postgre.models.*; +import com.ugent.pidgeon.postgre.models.CourseEntity; +import com.ugent.pidgeon.postgre.models.CourseUserEntity; +import com.ugent.pidgeon.postgre.models.GroupClusterEntity; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.CourseRelation; import com.ugent.pidgeon.postgre.models.types.UserRole; -import com.ugent.pidgeon.postgre.repository.*; -import com.ugent.pidgeon.util.*; +import com.ugent.pidgeon.postgre.repository.CourseRepository; +import com.ugent.pidgeon.postgre.repository.CourseUserRepository; +import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; +import com.ugent.pidgeon.postgre.repository.ProjectRepository; +import com.ugent.pidgeon.postgre.repository.UserRepository; +import com.ugent.pidgeon.util.CheckResult; +import com.ugent.pidgeon.util.CommonDatabaseActions; +import com.ugent.pidgeon.util.CourseUtil; +import com.ugent.pidgeon.util.EntityToJsonConverter; +import com.ugent.pidgeon.util.Pair; +import com.ugent.pidgeon.util.UserUtil; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; - -import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(MockitoExtension.class) public class CourseControllerTest extends ControllerTest { @@ -65,25 +96,47 @@ public class CourseControllerTest extends ControllerTest { @InjectMocks private CourseController courseController; + private CourseEntity archivedCourse; + private CourseEntity activeCourse; + private CourseWithInfoJson activeCourseJson; + + private final ObjectMapper objectMapper = CustomObjectMapper.createObjectMapper(); + + @BeforeEach public void setup() { - mockMvc = MockMvcBuilders.standaloneSetup(courseController) - .defaultRequest(MockMvcRequestBuilders.get("/**") - .with(request -> { - request.setUserPrincipal(SecurityContextHolder.getContext().getAuthentication()); - return request; - })) - .build(); + setUpController(courseController); + + archivedCourse = new CourseEntity("archivedname", "description",2024); + archivedCourse.setArchivedAt(OffsetDateTime.now()); + archivedCourse.setId(1); + activeCourse = new CourseEntity("name", "description",2024); + archivedCourse.setId(2); + + activeCourseJson = new CourseWithInfoJson( + activeCourse.getId(), + activeCourse.getName(), + activeCourse.getDescription(), + new UserReferenceJson("", "", 0L, ""), + new ArrayList<>(), + "", + "", + "", + OffsetDateTime.now(), + OffsetDateTime.now(), + activeCourse.getCourseYear()); + } @Test public void testGetUserCourses() throws Exception { - CourseEntity course = mock(CourseEntity.class); + + /* Mock active course return */ when(userRepository.findCourseIdsByUserId(anyLong())). thenReturn(List.of(new UserRepository.CourseIdWithRelation[]{new UserRepository.CourseIdWithRelation() { @Override public Long getCourseId() { - return 1L; + return activeCourse.getId(); } @Override @@ -93,16 +146,89 @@ public CourseRelation getRelation() { @Override public String getName() { - return ""; + return activeCourse.getName(); } }})); - when(courseRepository.findById(anyLong())).thenReturn(Optional.empty()); + CourseWithRelationJson courseJson = new CourseWithRelationJson( + "", + CourseRelation.course_admin, + activeCourse.getName(), + activeCourse.getId(), + activeCourse.getArchivedAt(), + 2, + activeCourse.getCreatedAt(), + activeCourse.getCourseYear() + ); + when(entityToJsonConverter.courseEntityToCourseWithRelation(activeCourse, CourseRelation.course_admin)). + thenReturn(courseJson); + when(courseRepository.findById(activeCourse.getId())).thenReturn(Optional.of(activeCourse)); + /* Mock archived course return */ + when(userRepository.findArchivedCoursesByUserId(anyLong())). + thenReturn(List.of(new UserRepository.CourseIdWithRelation[]{new UserRepository.CourseIdWithRelation() { + @Override + public Long getCourseId() { + return archivedCourse.getId(); + } + + @Override + public CourseRelation getRelation() { + return CourseRelation.course_admin; + } + + @Override + public String getName() { + return archivedCourse.getName(); + } + }})); + CourseWithRelationJson archivedCourseJson = new CourseWithRelationJson( + "", + CourseRelation.course_admin, + archivedCourse.getName(), + archivedCourse.getId(), + archivedCourse.getArchivedAt(), + 2, + archivedCourse.getCreatedAt(), + archivedCourse.getCourseYear() + ); + when(entityToJsonConverter.courseEntityToCourseWithRelation(archivedCourse, CourseRelation.course_admin)). + thenReturn(archivedCourseJson); + when(courseRepository.findById(archivedCourse.getId())).thenReturn(Optional.of(archivedCourse)); + + /* If no archived param, return archived and active courses */ mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH)) - .andExpect(status().isOk()); - when(courseRepository.findById(anyLong())).thenReturn(Optional.of(course)); + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(courseJson, archivedCourseJson)))); + + /* If archived param is false, return only active courses */ + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "?archived=false")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(courseJson)))); + + /* If archived param is true, return only archived courses */ + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "?archived=true")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(archivedCourseJson)))); + + /* If course doesn't get found, it just gets filtered */ + when(courseRepository.findById(activeCourse.getId())).thenReturn(Optional.empty()); mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(archivedCourseJson)))); + + /* If no courses are found, return empty list */ + when(userRepository.findCourseIdsByUserId(anyLong())).thenReturn(Collections.emptyList()); + when(userRepository.findArchivedCoursesByUserId(anyLong())).thenReturn(Collections.emptyList()); + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[]")); + + /* If error occurs, return 500 */ when(userRepository.findCourseIdsByUserId(anyLong())).thenThrow(new RuntimeException()); mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH)) .andExpect(status().isInternalServerError()); @@ -111,28 +237,65 @@ public String getName() { @Test public void testCreateCourse() throws Exception { - String courseJson = "{\"name\": \"test\", \"description\": \"description\"}"; - when(courseUtil.checkCourseJson(any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - when(courseRepository.save(any())).thenReturn(null); - when(courseUserRepository.save(any())).thenReturn(null); - when(groupClusterRepository.save(any())).thenReturn(null); - when(courseUtil.getJoinLink(any(), any())).thenReturn(""); - when(entityToJsonConverter.courseEntityToCourseWithInfo(any(), any())). - thenReturn(new CourseWithInfoJson(0L, "", "", new UserReferenceJson("", "", 0L), - new ArrayList<>(), "", "")); + String courseJson = "{\"name\": \"test\", \"description\": \"description\",\"courseYear\" : 2024}"; + /* If everything is correct, return 200 */ + when(courseUtil.checkCourseJson(argThat( + json -> json.getName().equals("test") && + json.getDescription().equals("description") && + json.getYear() == 2024 + ), eq(getMockUser()), eq(null))).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(courseRepository.save(any())).thenReturn(activeCourse); + when(courseUserRepository.save(argThat( + courseUser -> courseUser.getCourseId() == activeCourse.getId() && + courseUser.getUserId() == getMockUser().getId() && + courseUser.getRelation().equals(CourseRelation.creator) + ))).thenReturn(null); + when(groupClusterRepository.save(argThat( + groupCluster -> groupCluster.getCourseId() == activeCourse.getId() && + groupCluster.getMaxSize() == 1 && + groupCluster.getGroupAmount() == 0 + ))).thenReturn(null); + when(courseUtil.getJoinLink(activeCourse.getJoinKey(), ""+activeCourse.getId())).thenReturn(""); + when(entityToJsonConverter.courseEntityToCourseWithInfo(activeCourse, "", false)). + thenReturn(activeCourseJson); mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH) .contentType(MediaType.APPLICATION_JSON) .content(courseJson)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(activeCourseJson))); + + verify(courseUserRepository, times(1)).save(argThat(courseUser -> + courseUser.getCourseId() == activeCourse.getId() && + courseUser.getUserId() == getMockUser().getId() && + courseUser.getRelation().equals(CourseRelation.creator) + )); + verify(groupClusterRepository, times(1)).save(argThat(groupCluster -> + groupCluster.getCourseId() == activeCourse.getId() && + groupCluster.getMaxSize() == 1 && + groupCluster.getGroupAmount() == 0 + )); - when(courseUtil.checkCourseJson(any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + /* If user is not a teacher, return 403 */ + setMockUserRoles(UserRole.student); + mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH) + .contentType(MediaType.APPLICATION_JSON) + .content(courseJson)) + .andExpect(status().isForbidden()); + setMockUserRoles(UserRole.teacher); + + /* If course json is invalid, return 400 */ + reset(courseUtil); + when(courseUtil.checkCourseJson(any(), any(), any())).thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); + when(courseUtil.checkCourseJson(any(), any(),any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH) .contentType(MediaType.APPLICATION_JSON) .content(courseJson)) .andExpect(status().isIAmATeapot()); - when(courseUtil.checkCourseJson(any())).thenThrow(new RuntimeException()); + /* If error occurs, return 500 */ + when(courseUtil.checkCourseJson(any(), any(), any())).thenThrow(new RuntimeException()); mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH) .contentType(MediaType.APPLICATION_JSON) .content(courseJson)) @@ -143,366 +306,866 @@ public void testCreateCourse() throws Exception { // This function also tests all lines of doCourseUpdate @Test public void testUpdateCourse() throws Exception { - String courseJson = "{\"name\": \"test\", \"description\": \"description\"}"; - when(courseUtil.getCourseIfAdmin(anyLong(), any())). - thenReturn(new CheckResult<>(HttpStatus.OK, "", new CourseEntity())); - when(courseUtil.checkCourseJson(any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - when(courseRepository.save(any())).thenReturn(null); - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.COURSE_BASE_PATH + "/1") - .contentType(MediaType.APPLICATION_JSON) - .content(courseJson)) - .andExpect(status().isOk()); - - when(courseUtil.checkCourseJson(any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.COURSE_BASE_PATH + "/1") - .contentType(MediaType.APPLICATION_JSON) - .content(courseJson)) - .andExpect(status().isIAmATeapot()); - - when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.COURSE_BASE_PATH + "/1") - .contentType(MediaType.APPLICATION_JSON) - .content(courseJson)) - .andExpect(status().isForbidden()); - - when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenThrow(new RuntimeException()); - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.COURSE_BASE_PATH + "/1") - .contentType(MediaType.APPLICATION_JSON) - .content(courseJson)) - .andExpect(status().isInternalServerError()); + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId(); + String courseJson = "{\"name\": \"test\", \"description\": \"description\",\"courseYear\" : 2024}"; + CourseEntity updatedEntity = new CourseEntity("test", "description",2024); + CourseWithInfoJson updatedJson = new CourseWithInfoJson( + activeCourse.getId(), + "test", + "description", + new UserReferenceJson("", "", 0L, ""), + new ArrayList<>(), + "", + "", + "", + OffsetDateTime.now(), + OffsetDateTime.now(), + 2023 + ); + /* If admin and valid json, update course and return 200 */ + when(courseUtil.getCourseIfAdmin(activeCourse.getId(), getMockUser())). + thenReturn(new CheckResult<>(HttpStatus.OK, "", activeCourse)); + when(courseUtil.checkCourseJson( + argThat( + json -> json.getName().equals("test") && + json.getDescription().equals("description") && + json.getYear() == 2024 + ), + eq(getMockUser()), + eq(activeCourse.getId()))).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(courseUtil.getJoinLink(activeCourse.getJoinKey(), ""+activeCourse.getId())).thenReturn(""); + when(courseRepository.save(activeCourse)).thenReturn(updatedEntity); + when(entityToJsonConverter.courseEntityToCourseWithInfo(updatedEntity, "", false)).thenReturn(updatedJson); + activeCourse.setArchivedAt(OffsetDateTime.now()); + OffsetDateTime originalArchivedAt = activeCourse.getArchivedAt(); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(courseJson)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(updatedJson))); + assertEquals(originalArchivedAt, activeCourse.getArchivedAt()); + activeCourse.setArchivedAt(null); + + + /* If courseJson has archived field, update archived accordingly */ + String courseJsonWithArchivedTrue = "{\"name\": \"test\", \"description\": \"description\",\"courseYear\" : 2024, \"archived\": \"true\"}"; + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(courseJsonWithArchivedTrue)) + .andExpect(status().isOk()); + assertNotNull(activeCourse.getArchivedAt()); + + + String courseJsonWithArchivedFalse = "{\"name\": \"test\", \"description\": \"description\",\"courseYear\" : 2024, \"archived\": \"false\"}"; + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(courseJsonWithArchivedFalse)) + .andExpect(status().isOk()); + assertNull(activeCourse.getArchivedAt()); + + + /* If invalid json, return corresponding statuscode */ + reset(courseUtil); + when(courseUtil.getCourseIfAdmin(activeCourse.getId(), getMockUser())). + thenReturn(new CheckResult<>(HttpStatus.OK, "", activeCourse)); + when(courseUtil.checkCourseJson( + argThat( + json -> json.getName().equals("test") && + json.getDescription().equals("description") && + json.getYear() == 2024 + ), + eq(getMockUser()), + eq(activeCourse.getId()))).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(courseJson)) + .andExpect(status().isIAmATeapot()); + + /* If not admin, return 403 */ + when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(courseJson)) + .andExpect(status().isForbidden()); + + /* If error occurs, return 500 */ + when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenThrow(new RuntimeException()); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(courseJson)) + .andExpect(status().isInternalServerError()); } @Test public void testPatchCourse() throws Exception { - String courseJson = "{\"name\": null, \"description\": \"description\"}"; - when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", new CourseEntity())); - when(courseUtil.checkCourseJson(any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - when(courseRepository.save(any())).thenReturn(null); + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId(); + String originalName = activeCourse.getName(); + String originalDescription = activeCourse.getDescription(); + Integer originalYear = activeCourse.getCourseYear(); + CourseEntity updatedEntity = new CourseEntity("test", "description2",2024); + CourseWithInfoJson updatedJson = new CourseWithInfoJson( + activeCourse.getId(), + "test", + "description2", + new UserReferenceJson("", "", 0L, ""), + new ArrayList<>(), + "", + "", + "", + OffsetDateTime.now(), + OffsetDateTime.now(), + 2023 + ); + /* If admin and valid json, update course and return 200 */ + when(courseUtil.getCourseIfAdmin(activeCourse.getId(), getMockUser())). + thenReturn(new CheckResult<>(HttpStatus.OK, "", activeCourse)); + when(courseUtil.checkCourseJson(any(), any(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(courseUtil.getJoinLink(activeCourse.getJoinKey(), ""+activeCourse.getId())).thenReturn(""); + when(courseRepository.save(activeCourse)).thenReturn(updatedEntity); + when(entityToJsonConverter.courseEntityToCourseWithInfo(updatedEntity, "", false)).thenReturn(updatedJson); + /* If field is not present, do not update it */ + String patchCourseJson = "{\"name\": \"test\"}"; + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(patchCourseJson)) + .andExpect(status().isOk()); + String finalOriginalDescription = originalDescription; + verify(courseUtil, times(1)).checkCourseJson(argThat( + json -> json.getName().equals("test") && + json.getDescription().equals(finalOriginalDescription) && + Objects.equals(json.getYear(), originalYear) + ), eq(getMockUser()), eq(activeCourse.getId())); + assertNotEquals(originalName, activeCourse.getName()); + assertEquals(originalDescription, activeCourse.getDescription()); + assertEquals(originalYear, activeCourse.getCourseYear()); + assertNull(activeCourse.getArchivedAt()); + originalName = activeCourse.getName(); + + String patchCourseJsonNoName = "{\"description\": \"description88\"}"; + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(patchCourseJsonNoName)) + .andExpect(status().isOk()); + String finalOriginalName = originalName; + verify(courseUtil, times(1)).checkCourseJson(argThat( + json -> json.getName().equals(finalOriginalName) && + json.getDescription().equals("description88") && + Objects.equals(json.getYear(), originalYear) + ), eq(getMockUser()), eq(activeCourse.getId())); + assertEquals(originalName, activeCourse.getName()); + assertNotEquals(originalDescription, activeCourse.getDescription()); + assertEquals(originalYear, activeCourse.getCourseYear()); + assertNull(activeCourse.getArchivedAt()); + + /* If fields are present, update them */ + String requestJson = "{\"name\": \"test2\", \"description\": \"description2\",\"courseYear\" : 2034}"; + originalDescription = activeCourse.getDescription(); + activeCourse.setArchivedAt(OffsetDateTime.now()); + OffsetDateTime originalArchivedAt = activeCourse.getArchivedAt(); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(updatedJson))); + verify(courseUtil, times(1)).checkCourseJson(argThat( + json -> json.getName().equals("test2") && + json.getDescription().equals("description2") && + json.getYear() == 2034 + ), eq(getMockUser()), eq(activeCourse.getId())); + assertNotEquals(originalName, activeCourse.getName()); + assertNotEquals(originalDescription, activeCourse.getDescription()); + assertNotEquals(originalYear, activeCourse.getCourseYear()); + assertEquals(originalArchivedAt, activeCourse.getArchivedAt()); + activeCourse.setArchivedAt(null); + + /* If courseJson has archived field, update archived accordingly */ + String courseJsonWithArchivedTrue = "{\"name\": \"test\", \"description\": \"description\",\"courseYear\" : 2024, \"archived\": \"true\"}"; + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(courseJsonWithArchivedTrue)) + .andExpect(status().isOk()); + assertNotNull(activeCourse.getArchivedAt()); - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.COURSE_BASE_PATH + "/1") - .contentType(MediaType.APPLICATION_JSON) - .content(courseJson)) - .andExpect(status().isOk()); - courseJson = "{\"name\": \"name\", \"description\": null}"; - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.COURSE_BASE_PATH + "/1") - .contentType(MediaType.APPLICATION_JSON) - .content(courseJson)) - .andExpect(status().isOk()); + String courseJsonWithArchivedFalse = "{\"name\": \"test\", \"description\": \"description\",\"courseYear\" : 2024, \"archived\": \"false\"}"; + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(courseJsonWithArchivedFalse)) + .andExpect(status().isOk()); + assertNull(activeCourse.getArchivedAt()); + + /* If no fields are present, change nothing */ + String emptyJson = "{\"ietswatnietboeit\": \"test\"}"; + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(emptyJson)) + .andExpect(status().isOk()); + assertEquals("test", activeCourse.getName()); + assertEquals("description", activeCourse.getDescription()); + assertEquals(2024, activeCourse.getCourseYear()); + - courseJson = "{\"name\": null, \"description\": null}"; - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.COURSE_BASE_PATH + "/1") - .contentType(MediaType.APPLICATION_JSON) - .content(courseJson)) - .andExpect(status().isBadRequest()); + /* If invalid json, return corresponding statuscode */ + when(courseUtil.checkCourseJson(any(), any(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andExpect(status().isIAmATeapot()); + + /* If not admin, return 403 */ when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.COURSE_BASE_PATH + "/1") - .contentType(MediaType.APPLICATION_JSON) - .content(courseJson)) - .andExpect(status().isForbidden()); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andExpect(status().isForbidden()); + /* If error occurs, return 500 */ when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenThrow(new RuntimeException()); - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.COURSE_BASE_PATH + "/1") - .contentType(MediaType.APPLICATION_JSON) - .content(courseJson)) - .andExpect(status().isInternalServerError()); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andExpect(status().isInternalServerError()); } @Test public void testGetCourseByCourseId() throws Exception { - when(courseUtil.getJoinLink(any(), any())).thenReturn(""); - when(entityToJsonConverter.courseEntityToCourseWithInfo(any(), any())). - thenReturn(new CourseWithInfoJson(0L, "", "", new UserReferenceJson("", "", 0L), - new ArrayList<>(), "", "")); - when(courseUtil.getCourseIfUserInCourse(anyLong(), any(UserEntity.class))). - thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(new CourseEntity(), null))); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1")) - .andExpect(status().isOk()); - + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId(); + /* If user is admin, return course with joinKey information */ + when(courseUtil.getJoinLink(activeCourse.getJoinKey(), ""+activeCourse.getId())).thenReturn(""); + when(entityToJsonConverter.courseEntityToCourseWithInfo(any(), any(), anyBoolean())). + thenReturn(activeCourseJson); + when(courseUtil.getCourseIfUserInCourse(activeCourse.getId(), getMockUser())). + thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(activeCourse, CourseRelation.course_admin))); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(activeCourseJson))); + verify(entityToJsonConverter, times(1)).courseEntityToCourseWithInfo(activeCourse, "", false); + + /* If user is not admin, return course without joinKey information */ + when(courseUtil.getCourseIfUserInCourse(activeCourse.getId(), getMockUser())). + thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(activeCourse, CourseRelation.enrolled))); + mockMvc.perform(MockMvcRequestBuilders.get(url)).andExpect(status().isOk()); + verify(entityToJsonConverter, times(1)).courseEntityToCourseWithInfo(activeCourse, "", true); + + /* If course is not found, or user no acces return corresponding status */ when(courseUtil.getCourseIfUserInCourse(anyLong(), any(UserEntity.class))). thenReturn(new CheckResult<>(HttpStatus.NOT_FOUND, "", new Pair<>(null, null))); - - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1")) + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isNotFound()); + when(courseUtil.getCourseIfUserInCourse(anyLong(), any(UserEntity.class))). + thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", new Pair<>(null, null))); + mockMvc.perform(MockMvcRequestBuilders.get(url)).andExpect(status().isForbidden()); } @Test public void testDeleteCourse() throws Exception { + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId(); + + /* If user is the creator of the course, delete succeeds and also deletes linked projects & coursses */ ProjectEntity project = new ProjectEntity(1, "name", "description", 1L, 1L, true, 20, OffsetDateTime.now()); GroupClusterEntity groupCluster = new GroupClusterEntity(1L, 20, "cluster", 5); - when(courseUtil.getCourseIfUserInCourse(anyLong(), any())). - thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(new CourseEntity(), CourseRelation.creator))); - when(courseRepository.findAllProjectsByCourseId(anyLong())).thenReturn(List.of(project)); - when(commonDatabaseActions.deleteProject(anyLong())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - when(groupClusterRepository.findByCourseId(anyLong())).thenReturn(List.of(groupCluster)); - when(commonDatabaseActions.deleteClusterById(anyLong())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1")) + when(courseUtil.getCourseIfUserInCourse(activeCourse.getId(), getMockUser())). + thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(activeCourse, CourseRelation.creator))); + when(courseRepository.findAllProjectsByCourseId(activeCourse.getId())).thenReturn(List.of(project)); + when(commonDatabaseActions.deleteProject(project.getId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(groupClusterRepository.findByCourseId(activeCourse.getId())).thenReturn(List.of(groupCluster)); + when(commonDatabaseActions.deleteClusterById(groupCluster.getId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + CourseUserEntity courseUser = new CourseUserEntity(1L, 1L, CourseRelation.creator); + List courseUsers = List.of(courseUser); + when(courseUserRepository.findAllUsersByCourseId(activeCourse.getId())).thenReturn(courseUsers); + doNothing().when(courseUserRepository).deleteAll(anyIterable()); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isOk()); + verify(courseUserRepository, times(1)).deleteAll(courseUsers); + /* If something goes wrong while deleting a cluster or project, return corresponding status */ when(commonDatabaseActions.deleteClusterById(anyLong())).thenReturn(new CheckResult<>(HttpStatus.NO_CONTENT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1")) + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isNoContent()); when(commonDatabaseActions.deleteProject(anyLong())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1")) + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isIAmATeapot()); + /* If user isn't in course or course doesn't exist return corresponding status */ when(courseUtil.getCourseIfUserInCourse(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", new Pair<>(null, null))); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1")) + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isBadRequest()); + /* If user isn't the creator of the course, return 403 */ when(courseUtil.getCourseIfUserInCourse(anyLong(), any())). thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(new CourseEntity(), CourseRelation.enrolled))); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1")) + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isForbidden()); + /* If a unexpected error occurs, return 500 */ when(courseUtil.getCourseIfUserInCourse(anyLong(), any())).thenThrow(new RuntimeException()); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1")) + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isInternalServerError()); } @Test public void testGetProjectsByCourseId() throws Exception { - CourseEntity course = new CourseEntity("name", "descripton"); - course.setId(1); - List projects = Arrays.asList(new ProjectEntity(), new ProjectEntity()); - when(courseUtil.getCourseIfUserInCourse(anyLong(), any())) - .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(course, CourseRelation.creator))); - when(projectRepository.findByCourseId(anyLong())).thenReturn(projects); - when(entityToJsonConverter.projectEntityToProjectResponseJson(any(ProjectEntity.class), any(CourseEntity.class), any(UserEntity.class))).thenReturn(new ProjectResponseJson( - new CourseReferenceJson("", "Test Course", 1L), - OffsetDateTime.MIN, - "", - 1L, - "Test Description", - "", - "", - 1, - true, - new ProjectProgressJson(1, 1), - 1L - )); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1/projects")) - .andExpect(status().isOk()); + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/projects"; + Pair creatorPair = new Pair<>(activeCourse, CourseRelation.creator); + Pair enrolledPair = new Pair<>(activeCourse, CourseRelation.enrolled); + ProjectEntity project = new ProjectEntity(1, "name", "description", 1L, 1L, true, 20, OffsetDateTime.now()); + ProjectResponseJson projectJson = new ProjectResponseJson( + new CourseReferenceJson("", "Test Course", 1L, OffsetDateTime.now()), + OffsetDateTime.MIN, + "", + 1L, + "Test Description", + "", + "", + 1, + true, + new ProjectProgressJson(1, 1), + 1L, + 1L, + OffsetDateTime.now() + ); + /* If user is in course, return projects */ + when(courseUtil.getCourseIfUserInCourse(activeCourse.getId(),getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", creatorPair)); + + List projects = List.of(project); + when(projectRepository.findByCourseId(activeCourse.getId())).thenReturn(projects); + when(entityToJsonConverter.projectEntityToProjectResponseJson(project, activeCourse, getMockUser())).thenReturn(projectJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(projectJson)))); + + /* If a project isn't visible, and user role is student, it should not be returned */ + project.setVisible(false); + when(courseUtil.getCourseIfUserInCourse(activeCourse.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", enrolledPair)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[]")); + when(courseUtil.getCourseIfUserInCourse(activeCourse.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", creatorPair)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(projectJson)))); + + /* If user not in course, or course doesn't exit or any other check fails, return corresponding status */ when(courseUtil.getCourseIfUserInCourse(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1/projects")) + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isIAmATeapot()); } @Test public void testJoinCourse() throws Exception { - CourseEntity course = new CourseEntity("name", "descripton"); - course.setId(1); - when(courseUtil.checkJoinLink(anyLong(), any(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", course)); - when(commonDatabaseActions.createNewIndividualClusterGroup(anyLong(), anyLong())).thenReturn(true); - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH + "/1/join/1908")) - .andExpect(status().isOk()); + String urlWithKey = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/join/1908"; + String urlWithoutKey = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/join"; + CourseEntity course = activeCourse; + /* If join key is correct, course is not archived and no error occurs, return 200 */ + when(courseUtil.checkJoinLink(activeCourse.getId(), "1908", getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", course)); + when(courseUtil.checkJoinLink(activeCourse.getId(), null, getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", course)); + when(commonDatabaseActions.createNewIndividualClusterGroup(activeCourse.getId(), getMockUser())).thenReturn(true); + when(courseUtil.getJoinLink(course.getJoinKey(), ""+course.getId())).thenReturn(""); + when(entityToJsonConverter.courseEntityToCourseWithInfo(activeCourse, "", false)).thenReturn(activeCourseJson); + mockMvc.perform(MockMvcRequestBuilders.post(urlWithKey)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(activeCourseJson))); + mockMvc.perform(MockMvcRequestBuilders.post(urlWithoutKey)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(activeCourseJson))); + verify(courseUserRepository, times(2)).save(argThat(courseUser -> + courseUser.getCourseId() == activeCourse.getId() && + courseUser.getUserId() == getMockUser().getId() && + courseUser.getRelation().equals(CourseRelation.enrolled) + )); + + /* If course is archived, return 403 */ + activeCourse.setArchivedAt(OffsetDateTime.now()); + mockMvc.perform(MockMvcRequestBuilders.post(urlWithKey)) + .andExpect(status().isForbidden()); + mockMvc.perform(MockMvcRequestBuilders.post(urlWithoutKey)) + .andExpect(status().isForbidden()); + activeCourse.setArchivedAt(null); - when(commonDatabaseActions.createNewIndividualClusterGroup(anyLong(), anyLong())).thenReturn(false); - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH + "/1/join/1908")) + /* If an error occures when creating individual cluster group, return 500 */ + when(commonDatabaseActions.createNewIndividualClusterGroup(anyLong(), any())).thenReturn(false); + mockMvc.perform(MockMvcRequestBuilders.post(urlWithKey)) + .andExpect(status().isInternalServerError()); + mockMvc.perform(MockMvcRequestBuilders.post(urlWithoutKey)) .andExpect(status().isInternalServerError()); + /* If join key check fails return corresponding status */ when(courseUtil.checkJoinLink(anyLong(), any(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH + "/1/join/1908")) - .andExpect(status().isIAmATeapot()); + mockMvc.perform(MockMvcRequestBuilders.post(urlWithKey)) + .andExpect(status().isIAmATeapot()); + mockMvc.perform(MockMvcRequestBuilders.post(urlWithoutKey)) + .andExpect(status().isIAmATeapot()); } @Test - public void testGetJoinKey() throws Exception { - CourseEntity course = new CourseEntity("name", "descripton"); - course.setId(1); - when(courseUtil.checkJoinLink(anyLong(), any(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", course)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1/join/1908")) - .andExpect(status().isOk()); - - when(courseUtil.checkJoinLink(anyLong(), any(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", course)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1/join/1908")) - .andExpect(status().isIAmATeapot()); - } - - @Test - public void testJoinCourseNoKey() throws Exception { - CourseEntity course = new CourseEntity("name", "descripton"); - course.setId(1); - when(courseUtil.checkJoinLink(anyLong(), any(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", course)); - when(commonDatabaseActions.createNewIndividualClusterGroup(anyLong(), anyLong())).thenReturn(true); - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH + "/1/join")) - .andExpect(status().isOk()); - - when(commonDatabaseActions.createNewIndividualClusterGroup(anyLong(), anyLong())).thenReturn(false); - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH + "/1/join")) - .andExpect(status().isInternalServerError()); + public void testGetJoinInformation() throws Exception { + String urlWithKey = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/join/1908"; + String urlWithoutKey = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/join"; + + CourseEntity course = activeCourse; + CourseJoinInformationJson courseJoinInformationJson = new CourseJoinInformationJson( + activeCourse.getName(), + activeCourse.getDescription() + ); + /* If join key is correct, course is not archived and no error occurs, return 200 */ + when(courseUtil.checkJoinLink(activeCourse.getId(), "1908", getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", course)); + when(courseUtil.checkJoinLink(activeCourse.getId(), null, getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", course)); + mockMvc.perform(MockMvcRequestBuilders.get(urlWithKey)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(courseJoinInformationJson))); + mockMvc.perform(MockMvcRequestBuilders.get(urlWithoutKey)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(courseJoinInformationJson))); + + /* If course is archived, reutrn 403 */ + activeCourse.setArchivedAt(OffsetDateTime.now()); + mockMvc.perform(MockMvcRequestBuilders.get(urlWithKey)) + .andExpect(status().isForbidden()); + mockMvc.perform(MockMvcRequestBuilders.get(urlWithoutKey)) + .andExpect(status().isForbidden()); + activeCourse.setArchivedAt(null); + + /* If join key check fails return corresponding status */ + when(courseUtil.checkJoinLink(anyLong(), any(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.get(urlWithKey)) + .andExpect(status().isIAmATeapot()); + mockMvc.perform(MockMvcRequestBuilders.get(urlWithoutKey)) + .andExpect(status().isIAmATeapot()); } - @Test - public void testGetCourseJoinKeyNoKey() throws Exception { - CourseEntity course = new CourseEntity("name", "descripton"); - course.setId(1); - when(courseUtil.checkJoinLink(anyLong(), any(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", course)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1/join")) - .andExpect(status().isOk()); - - } @Test public void testLeaveCourse() throws Exception { - when(courseUtil.canLeaveCourse(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", CourseRelation.enrolled)); - when(commonDatabaseActions.removeIndividualClusterGroup(anyLong(), anyLong())).thenReturn(true); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1/leave")) + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/leave"; + /* If user can leave course, return 200 */ + /* If role is enrolled, an individualclustergroup should be deleted */ + when(courseUtil.canLeaveCourse(activeCourse.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", CourseRelation.enrolled)); + when(commonDatabaseActions.removeIndividualClusterGroup(activeCourse.getId(), getMockUser().getId())).thenReturn(true); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isOk()); + verify(commonDatabaseActions, times(1)).removeIndividualClusterGroup(activeCourse.getId(), getMockUser().getId()); + + /* If the role isn't enrolled, no individualclustergroup should be deleted */ + when(courseUtil.canLeaveCourse(activeCourse.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", CourseRelation.course_admin)); + reset(commonDatabaseActions); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isOk()); + verify(commonDatabaseActions, times(0)).removeIndividualClusterGroup(activeCourse.getId(), getMockUser().getId()); + /* If something goes wrong while deleting individual cluster group, return 500 */ + when(courseUtil.canLeaveCourse(activeCourse.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", CourseRelation.enrolled)); when(commonDatabaseActions.removeIndividualClusterGroup(anyLong(), anyLong())).thenReturn(false); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1/leave")) + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isInternalServerError()); + verify(commonDatabaseActions, times(1)).removeIndividualClusterGroup(activeCourse.getId(), getMockUser().getId()); + /* If user can't leave course for some reason, return corresponding error code */ when(courseUtil.canLeaveCourse(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1/leave")) - .andExpect(status().isBadRequest()); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isBadRequest()); + /* If an unexpected error occurs, return 500 */ when(courseUtil.canLeaveCourse(anyLong(), any())).thenThrow(new RuntimeException()); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1/leave")) + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isInternalServerError()); } @Test public void testRemoveCourseMember() throws Exception { - String userIdJson = "{\"userId\": 1}"; - when(courseUtil.canDeleteUser(anyLong(), anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", CourseRelation.enrolled)); - when(commonDatabaseActions.removeIndividualClusterGroup(anyLong(), anyLong())).thenReturn(true); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1/members/2") - .contentType(MediaType.APPLICATION_JSON) - .content(userIdJson)) + long userId = 2L; + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/members/2"; + + /* If user can remove other people, and course exists, return 200 */ + /* If user is admin, removeIndividualClusterGroup gets called */ + when(courseUtil.canDeleteUser(activeCourse.getId(), userId, getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", CourseRelation.course_admin)); + when(commonDatabaseActions.removeIndividualClusterGroup(activeCourse.getId(), userId)).thenReturn(true); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isOk()); + verify(courseUserRepository, times(1)).deleteById(argThat( + id -> id.getCourseId() == activeCourse.getId() && id.getUserId() == userId + )); + verify(commonDatabaseActions, times(0)).removeIndividualClusterGroup(activeCourse.getId(), userId); + + /* If user enrolled, removeIndividualClusterGroup gets called */ + when(courseUtil.canDeleteUser(activeCourse.getId(), userId, getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", CourseRelation.enrolled)); + when(commonDatabaseActions.removeIndividualClusterGroup(activeCourse.getId(), userId)).thenReturn(true); + reset(courseUserRepository); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isOk()); + verify(courseUserRepository, times(1)).deleteById(argThat( + id -> id.getCourseId() == activeCourse.getId() && id.getUserId() == userId + )); + verify(commonDatabaseActions, times(1)).removeIndividualClusterGroup(activeCourse.getId(), userId); - when(commonDatabaseActions.removeIndividualClusterGroup(anyLong(), anyLong())).thenReturn(false); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1/members/2") - .contentType(MediaType.APPLICATION_JSON) - .content(userIdJson)) + /* If something goes wrong when removing individual group, return 500 */ + when(commonDatabaseActions.removeIndividualClusterGroup(activeCourse.getId(), userId)).thenReturn(false); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isInternalServerError()); + /* If user can't delete the other use, return corresponding status*/ when(courseUtil.canDeleteUser(anyLong(), anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1/members/2") - .contentType(MediaType.APPLICATION_JSON) - .content(userIdJson)) + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isIAmATeapot()); } @Test public void testAddCourseMember() throws Exception { - String request = "{\"userId\": 1, \"relation\": \"enrolled\"}"; - when(courseUtil.canUpdateUserInCourse(anyLong(), any(), any(), any())). - thenReturn(new CheckResult<>(HttpStatus.OK, "", new CourseUserEntity(1, 1, CourseRelation.enrolled))); - when(commonDatabaseActions.createNewIndividualClusterGroup(anyLong(), anyLong())) - .thenReturn(true); - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH + "/1/members") + String requestString = "{\"userId\": 1, \"relation\": \"enrolled\"}"; + String requestStringAdmin = "{\"userId\": 1, \"relation\": \"course_admin\"}"; + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/members"; + CourseUserEntity courseUser = new CourseUserEntity(activeCourse.getId(), 1, CourseRelation.enrolled); + UserEntity user = new UserEntity("name", "surname", "email", UserRole.teacher, "id", ""); + /* If all checks succeed, return 201 */ + + when(courseUtil.canUpdateUserInCourse( + eq(activeCourse.getId()), + argThat( + request -> request.getUserId() == 1 && request.getRelationAsEnum().equals(CourseRelation.course_admin) + ), + eq(getMockUser()), + eq(HttpMethod.POST))).thenReturn(new CheckResult<>(HttpStatus.OK, "", courseUser)); + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(requestStringAdmin)) + .andExpect(status().isCreated()); + /* If user is not enrolled, there is no attempt to create individual cluster group */ + verify(userUtil, times(0)).getUserIfExists(anyLong()); + verify(courseUserRepository, times(1)).save(argThat( + courseUserEntity -> courseUserEntity.getCourseId() == activeCourse.getId() && + courseUserEntity.getUserId() == 1 && + courseUserEntity.getRelation().equals(CourseRelation.course_admin) + )); + verify(commonDatabaseActions, times(0)).createNewIndividualClusterGroup(anyLong(), any()); + + reset(courseUtil); + when(courseUtil.canUpdateUserInCourse( + eq(activeCourse.getId()), + argThat( + request -> request.getUserId() == 1 && request.getRelationAsEnum().equals(CourseRelation.enrolled) + ), + eq(getMockUser()), + eq(HttpMethod.POST))) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", courseUser)); + when(userUtil.getUserIfExists(anyLong())).thenReturn(user); + when(commonDatabaseActions.createNewIndividualClusterGroup(activeCourse.getId(), user)).thenReturn(true); + mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) - .content(request)) + .content(requestString)) .andExpect(status().isCreated()); + verify(courseUserRepository, times(1)).save(argThat( + courseUserEntity -> courseUserEntity.getCourseId() == activeCourse.getId() && + courseUserEntity.getUserId() == 1 && + courseUserEntity.getRelation().equals(CourseRelation.enrolled) + )); - when(commonDatabaseActions.createNewIndividualClusterGroup(anyLong(), anyLong())) + /* If something goes wrong when creating individual cluster, return 500 */ + when(commonDatabaseActions.createNewIndividualClusterGroup(anyLong(), any())) .thenReturn(false); - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH + "/1/members") + mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) - .content(request)) + .content(requestString)) .andExpect(status().isInternalServerError()); - when(courseUtil.canUpdateUserInCourse(anyLong(), any(), any(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.post(ApiRoutes.COURSE_BASE_PATH + "/1/members") + /* If user isn't found, return 404 */ + when(userUtil.getUserIfExists(anyLong())).thenReturn(null); + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(requestString)) + .andExpect(status().isNotFound()); + + /* If user can't be added to the course, return corresponding status */ + reset(courseUtil); + when(courseUtil.canUpdateUserInCourse( + anyLong(), any(), any(), any())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) - .content(request)) + .content(requestString)) .andExpect(status().isIAmATeapot()); } @Test public void testUpdateCourseMember() throws Exception { - String request = "{\"userId\": 1, \"relation\": \"enrolled\"}"; - when(courseUtil.canUpdateUserInCourse(anyLong(), any(), any(), any())). - thenReturn(new CheckResult<>(HttpStatus.OK, "", new CourseUserEntity(1, 1, CourseRelation.enrolled))); - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.COURSE_BASE_PATH + "/1/members/2") - .contentType(MediaType.APPLICATION_JSON) - .content(request)) - .andExpect(status().isOk()); + long userId = 2L; + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/members/" + userId; + String request = "{\"relation\": \"enrolled\"}"; + String adminRequest = "{\"relation\": \"course_admin\"}"; + UserEntity user = new UserEntity("name", "surname", "email", UserRole.teacher, "id", ""); + CourseUserEntity enrolledUser = new CourseUserEntity(activeCourse.getId(), userId, CourseRelation.enrolled); + CourseUserEntity adminUser = new CourseUserEntity(activeCourse.getId(), userId, CourseRelation.course_admin); + /* If all checks succeed, 200 gets returned */ + /* If the new role is the same as the old, no changes to individualgroupClusters are done */ + when(courseUtil.canUpdateUserInCourse( + eq(activeCourse.getId()), + argThat( + requestJson -> requestJson.getRelationAsEnum().equals(CourseRelation.enrolled) + ), + eq(getMockUser()), + eq(HttpMethod.PATCH))).thenReturn(new CheckResult<>(HttpStatus.OK, "", enrolledUser)); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()); + verify(commonDatabaseActions, times(0)).removeIndividualClusterGroup(anyLong(), anyLong()); + verify(commonDatabaseActions, times(0)).createNewIndividualClusterGroup(anyLong(), any()); + verify(courseUserRepository, times(0)).save(any()); + + /* If the new role is enrolled, individual clustergroup should be created */ + reset(courseUtil); + when(courseUtil.canUpdateUserInCourse( + eq(activeCourse.getId()), + argThat( + requestJson -> requestJson.getRelationAsEnum().equals(CourseRelation.enrolled) + ), + eq(getMockUser()), + eq(HttpMethod.PATCH))).thenReturn(new CheckResult<>(HttpStatus.OK, "", adminUser)); + when(userUtil.getUserIfExists(userId)).thenReturn(user); + when(commonDatabaseActions.createNewIndividualClusterGroup(activeCourse.getId(), user)).thenReturn(true); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()); + verify(commonDatabaseActions, times(1)).createNewIndividualClusterGroup(activeCourse.getId(), user); + assertEquals(CourseRelation.enrolled, adminUser.getRelation()); + verify(courseUserRepository, times(1)).save(adminUser); + adminUser.setRelation(CourseRelation.course_admin); + /* If something goes wrong when creating individual cluster, return 500 */ + reset(commonDatabaseActions); + when(commonDatabaseActions.createNewIndividualClusterGroup(anyLong(), any())) + .thenReturn(false); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isInternalServerError()); - when(courseUtil.canUpdateUserInCourse(anyLong(), any(), any(), any())). - thenReturn(new CheckResult<>(HttpStatus.OK, "", new CourseUserEntity(1, 1, CourseRelation.course_admin))); - when(commonDatabaseActions.removeIndividualClusterGroup(anyLong(), anyLong())).thenReturn(false); - request = "{\"userId\": 1, \"relation\": \"course_admin\"}"; - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.COURSE_BASE_PATH + "/1/members/2") - .contentType(MediaType.APPLICATION_JSON) - .content(request)) + /* If the user doesn't get found when trying to create individualadmin group should return 404 */ + when(userUtil.getUserIfExists(anyLong())).thenReturn(null); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isNotFound()); + + /* If the new role is course_admin, individual clustergroup should be deleted */ + reset(commonDatabaseActions); + reset(courseUtil); + when(courseUtil.canUpdateUserInCourse( + eq(activeCourse.getId()), + argThat( + requestJson -> requestJson.getRelationAsEnum().equals(CourseRelation.course_admin) + ), + eq(getMockUser()), + eq(HttpMethod.PATCH))).thenReturn(new CheckResult<>(HttpStatus.OK, "", enrolledUser)); + when(commonDatabaseActions.removeIndividualClusterGroup(activeCourse.getId(), userId)).thenReturn(true); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(adminRequest)) + .andExpect(status().isOk()); + verify(commonDatabaseActions, times(1)).removeIndividualClusterGroup(activeCourse.getId(), userId); + assertEquals(CourseRelation.course_admin, enrolledUser.getRelation()); + verify(courseUserRepository, times(1)).save(enrolledUser); + enrolledUser.setRelation(CourseRelation.enrolled); + + /* If something goes wrong when deleting individual cluster, return 500 */ + reset(commonDatabaseActions); + when(commonDatabaseActions.removeIndividualClusterGroup(anyLong(), anyLong())) + .thenReturn(false); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(adminRequest)) .andExpect(status().isInternalServerError()); - when(courseUtil.canUpdateUserInCourse(anyLong(), any(), any(), any())).thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.COURSE_BASE_PATH + "/1/members/2") - .contentType(MediaType.APPLICATION_JSON) - .content(request)) - .andExpect(status().isBadRequest()); + /* If user can't be updated, return corresponding status */ + reset(courseUtil); + when(courseUtil.canUpdateUserInCourse(anyLong(), any(), any(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isIAmATeapot()); + } @Test public void testGetCourseMembers() throws Exception { - CourseEntity course = new CourseEntity("name", "descripton"); - course.setId(1); - when(courseUtil.getCourseIfUserInCourse(anyLong(), any())). - thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(course, CourseRelation.course_admin))); - when(courseUserRepository.findAllMembers(anyLong())).thenReturn( - List.of(new CourseUserEntity(1, 2, CourseRelation.creator)) + CourseUserEntity courseUserEntity = new CourseUserEntity(1L, 1L, CourseRelation.enrolled); + UserEntity user = new UserEntity("name", "surname", "email", UserRole.teacher, "id", ""); + UserReferenceWithRelation userJson = new UserReferenceWithRelation( + new UserReferenceJson("name", "surname", 1L, ""), + ""+CourseRelation.enrolled ); - when(userUtil.getUserIfExists(anyLong())).thenReturn(new UserEntity("name", "surname", "email", UserRole.teacher, "id")); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1/members")) - .andExpect(status().isOk()); + List userList = List.of(courseUserEntity); + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/members"; + /* If user is in course, return members */ + when(courseUtil.getCourseIfUserInCourse(activeCourseJson.courseId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(activeCourse, CourseRelation.enrolled))); + when(courseUserRepository.findAllMembers(activeCourseJson.courseId())).thenReturn(userList); + when(userUtil.getUserIfExists(courseUserEntity.getUserId())).thenReturn(user); + /* User is enrolled so studentNumber should be hidden */ + when(entityToJsonConverter.userEntityToUserReferenceWithRelation(user, CourseRelation.enrolled, true)).thenReturn(userJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + verify(entityToJsonConverter, times(1)).userEntityToUserReferenceWithRelation(user, CourseRelation.enrolled, true); + + /* If user is admin studentNumber should be visible */ + when(courseUtil.getCourseIfUserInCourse(activeCourseJson.courseId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(activeCourse, CourseRelation.course_admin))); + when(entityToJsonConverter.userEntityToUserReferenceWithRelation(user, CourseRelation.enrolled, false)).thenReturn(userJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + verify(entityToJsonConverter, times(1)).userEntityToUserReferenceWithRelation(user, CourseRelation.enrolled, false); + + /* If user doesn't get found it gets filtered out */ + when(userUtil.getUserIfExists(anyLong())).thenReturn(null); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().json("[]")); + + + /* If user is not in course, or course not found or ... return corresponding status */ + when(courseUtil.getCourseIfUserInCourse(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); - when(courseUtil.getCourseIfUserInCourse(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1/members")) - .andExpect(status().isForbidden()); } @Test public void testGetCourseKey() throws Exception { - CourseEntity course = new CourseEntity("name", "descripton"); - course.setId(1); - when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", course)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1/joinLink")) - .andExpect(status().isOk()); - + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/joinKey"; + /* If user is admin and course exists, returns joinKey */ + activeCourse.setJoinKey("1908"); + when(courseUtil.getCourseIfAdmin(activeCourse.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", activeCourse)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().string("1908")); + activeCourse.setJoinKey(null); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().string("")); + + /* If any check fails, return corresponding status */ when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1/joinLink")) + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.COURSE_BASE_PATH + "/1/joinKey")) .andExpect(status().isIAmATeapot()); } @Test public void testGetAndCreateCourseKey() throws Exception { - CourseEntity course = new CourseEntity("name", "descripton"); - course.setId(1); - when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", course)); - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.COURSE_BASE_PATH + "/1/joinLink")) - .andExpect(status().isOk()); + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/joinKey"; + /* If user is admin and course exists, update and returns joinKey */ + activeCourse.setJoinKey("1908"); + when(courseUtil.getCourseIfAdmin(activeCourse.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", activeCourse)); + mockMvc.perform(MockMvcRequestBuilders.put(url)) + .andExpect(status().isOk()) + .andExpect(content().string(not(equalTo("")))) + .andExpect(content().string(not(equalTo("1908")))); + assertNotEquals("1908", activeCourse.getJoinKey()); + verify(courseRepository, times(1)).save(activeCourse); + + /* If any check fails, return corresponding status */ + when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn( + new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.put(url)) + .andExpect(status().isIAmATeapot()); - when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.COURSE_BASE_PATH + "/1/joinLink")) - .andExpect(status().isIAmATeapot()); } @Test public void testDeleteCourseKey() throws Exception { - CourseEntity course = new CourseEntity("name", "descripton"); - course.setId(1); - when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", course)); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1/joinLink")) - .andExpect(status().isOk()); + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/joinKey"; + /* If user is admin and course exists, update and returns joinKey */ + activeCourse.setJoinKey("1908"); + when(courseUtil.getCourseIfAdmin(activeCourse.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", activeCourse)); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isOk()) + .andExpect(content().string("")); + assertNull(activeCourse.getJoinKey()); + verify(courseRepository, times(1)).save(activeCourse); + + /* If any check fails, return corresponding status */ + when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn( + new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isIAmATeapot()); + } + + @Test + public void testCopyCourse() throws Exception { + String url = ApiRoutes.COURSE_BASE_PATH + "/" + activeCourse.getId() + "/copy"; + CourseEntity copiedCourse = new CourseEntity("name", "description", 2024); + CourseWithInfoJson copiedCourseJson = new CourseWithInfoJson( + 2L, + "name", + "description", + new UserReferenceJson("", "", 0L, ""), + new ArrayList<>(), + "", + "", + "", + OffsetDateTime.now(), + OffsetDateTime.now(), + 2024 + ); + /* If user is creator, can copy course */ + when(courseUtil.getCourseIfUserInCourse(activeCourse.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(activeCourse, CourseRelation.creator))); + when(commonDatabaseActions.copyCourse(activeCourse, getMockUser().getId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", copiedCourse)); + when(courseUtil.getJoinLink(copiedCourse.getJoinKey(), ""+copiedCourse.getId())).thenReturn(""); + when(entityToJsonConverter.courseEntityToCourseWithInfo(copiedCourse, "", false)).thenReturn(copiedCourseJson); + mockMvc.perform(MockMvcRequestBuilders.post(url)) + .andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(copiedCourseJson))); + + /* If something goes wrong when copying course, return corresponding status */ + when(commonDatabaseActions.copyCourse(activeCourse, getMockUser().getId())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.post(url)) + .andExpect(status().isIAmATeapot()); + + /* If user isn't the creator, return 403 */ + when(courseUtil.getCourseIfUserInCourse(activeCourse.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(activeCourse, CourseRelation.course_admin))); + mockMvc.perform(MockMvcRequestBuilders.post(url)) + .andExpect(status().isForbidden()); + + /* If user isn't in course, or course not found return corresponding status code */ + when(courseUtil.getCourseIfUserInCourse(activeCourse.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.NOT_FOUND, "", new Pair<>(null, null))); + mockMvc.perform(MockMvcRequestBuilders.post(url)) + .andExpect(status().isNotFound()); + + /* If an unexpected error occurs, return 500 */ + when(courseUtil.getCourseIfUserInCourse(activeCourse.getId(), getMockUser())).thenThrow(new RuntimeException()); + mockMvc.perform(MockMvcRequestBuilders.post(url)) + .andExpect(status().isInternalServerError()); - when(courseUtil.getCourseIfAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.COURSE_BASE_PATH + "/1/joinLink")) - .andExpect(status().isIAmATeapot()); } + } \ No newline at end of file diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupControllerTest.java index 9412904f..cbc4dee9 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupControllerTest.java @@ -1,33 +1,32 @@ package com.ugent.pidgeon.controllers; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ugent.pidgeon.CustomObjectMapper; +import com.ugent.pidgeon.json.GroupJson; import com.ugent.pidgeon.postgre.models.GroupEntity; -import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; import com.ugent.pidgeon.postgre.repository.GroupRepository; import com.ugent.pidgeon.util.CheckResult; import com.ugent.pidgeon.util.CommonDatabaseActions; import com.ugent.pidgeon.util.EntityToJsonConverter; import com.ugent.pidgeon.util.GroupUtil; -import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; - -import java.util.Optional; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(MockitoExtension.class) public class GroupControllerTest extends ControllerTest { @Mock @@ -39,67 +38,102 @@ public class GroupControllerTest extends ControllerTest { @Mock private CommonDatabaseActions commonDatabaseActions; + private final ObjectMapper objectMapper = CustomObjectMapper.createObjectMapper(); + @InjectMocks private GroupController groupController; private GroupEntity groupEntity; + private GroupJson groupJson; + private final Integer capacity = 40; @BeforeEach public void setup() { - mockMvc = MockMvcBuilders.standaloneSetup(groupController) - .defaultRequest(MockMvcRequestBuilders.get("/**") - .with(request -> { request.setUserPrincipal(SecurityContextHolder.getContext().getAuthentication()); return request; })) - .build(); + setUpController(groupController); groupEntity = new GroupEntity("Group test", 1L); + groupEntity.setId(5L); + groupJson = new GroupJson( + capacity, + groupEntity.getId(), + groupEntity.getName(), + "" + ); } @Test public void testGetGroupById() throws Exception { - when(groupUtil.getGroupIfExists(anyLong())) + String url = ApiRoutes.GROUP_BASE_PATH + "/" + groupEntity.getId(); + /* If group exists and users has acces, return groupJson */ + when(groupUtil.getGroupIfExists(groupEntity.getId())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupEntity)); - when(groupUtil.canGetGroup(anyLong(), any())) + when(groupUtil.canGetGroup(groupEntity.getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.GROUP_BASE_PATH + "/1")) - .andExpect(status().isOk()); + /* User is admin, student number should not be hidden */ + when(groupUtil.isAdminOfGroup(groupEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(entityToJsonConverter.groupEntityToJson(groupEntity, false)).thenReturn(groupJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string(objectMapper.writeValueAsString(groupJson))); + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, false); + + /* User is not admin, student number should be hidden */ + when(groupUtil.isAdminOfGroup(groupEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + when(entityToJsonConverter.groupEntityToJson(groupEntity, true)).thenReturn(groupJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string(objectMapper.writeValueAsString(groupJson))); + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, true); + + /* If the user doesn't have acces to group, return forbidden */ when(groupUtil.canGetGroup(anyLong(), any())) .thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.GROUP_BASE_PATH + "/1")) + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isBadRequest()); + /* If group doesn't exist, return not found */ when(groupUtil.getGroupIfExists(anyLong())) .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.GROUP_BASE_PATH + "/1")) + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isIAmATeapot()); } //this function also fully tests doGroupNameUpdate @Test public void testUpdateGroupName() throws Exception { + String url = ApiRoutes.GROUP_BASE_PATH + "/" + groupEntity.getId(); + /* If all checks pass, update and return groupJson */ String request = "{\"name\":\"Test Group\"}\n"; - when(groupUtil.canUpdateGroup(anyLong(), any())) + when(groupUtil.canUpdateGroup(groupEntity.getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupEntity)); - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.GROUP_BASE_PATH + "/1") + mockMvc.perform(MockMvcRequestBuilders.put(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isOk()); + assertEquals(groupEntity.getName(), "Test Group"); + /* If user can't update group, return corresponding status */ when(groupUtil.canUpdateGroup(anyLong(), any())) .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.GROUP_BASE_PATH + "/1") + mockMvc.perform(MockMvcRequestBuilders.put(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isIAmATeapot()); + /* If name isn't provided, return bad request */ request = "{\"name\":\"\"}\n"; - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.GROUP_BASE_PATH + "/1") + mockMvc.perform(MockMvcRequestBuilders.put(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isBadRequest()); request = "{\"name\":null}\n"; - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.GROUP_BASE_PATH + "/1") + mockMvc.perform(MockMvcRequestBuilders.put(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isBadRequest()); @@ -107,26 +141,59 @@ public void testUpdateGroupName() throws Exception { @Test public void testPatchGroupName() throws Exception { + String url = ApiRoutes.GROUP_BASE_PATH + "/" + groupEntity.getId(); + /* If all checks pass, update and return groupJson */ String request = "{\"name\":\"Test Group\"}\n"; - when(groupUtil.canUpdateGroup(anyLong(), any())) + when(groupUtil.canUpdateGroup(groupEntity.getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupEntity)); - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.GROUP_BASE_PATH + "/1") + mockMvc.perform(MockMvcRequestBuilders.patch(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isOk()); + assertEquals(groupEntity.getName(), "Test Group"); + + /* If user can't update group, return corresponding status */ + when(groupUtil.canUpdateGroup(anyLong(), any())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isIAmATeapot()); + + /* If name isn't provided, return bad request */ + request = "{\"name\":\"\"}\n"; + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + + request = "{\"name\":null}\n"; + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); } @Test public void testDeleteGroup() throws Exception { - when(groupUtil.canUpdateGroup(anyLong(), any())) + String url = ApiRoutes.GROUP_BASE_PATH + "/" + groupEntity.getId(); + /* If all checks pass, delete and return groupJson */ + when(groupUtil.canUpdateGroup(groupEntity.getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupEntity)); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.GROUP_BASE_PATH + "/1")) + when(commonDatabaseActions.removeGroup(groupEntity.getId())).thenReturn(true); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isNoContent()); + /* If something goes wrong while deleting, return internal server error */ + when(commonDatabaseActions.removeGroup(groupEntity.getId())).thenReturn(false); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isInternalServerError()); + + /* If user can't update group, return corresponding status */ when(groupUtil.canUpdateGroup(anyLong(), any())) - .thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.GROUP_BASE_PATH + "/1")) - .andExpect(status().isBadRequest()); + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isIAmATeapot()); } } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java index a4295874..59412d2d 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java @@ -1,28 +1,50 @@ package com.ugent.pidgeon.controllers; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ugent.pidgeon.CustomObjectMapper; +import com.ugent.pidgeon.json.GroupFeedbackJson; +import com.ugent.pidgeon.json.GroupFeedbackJsonWithProject; +import com.ugent.pidgeon.postgre.models.CourseEntity; import com.ugent.pidgeon.postgre.models.GroupFeedbackEntity; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.types.CourseRelation; import com.ugent.pidgeon.postgre.repository.GroupFeedbackRepository; +import com.ugent.pidgeon.postgre.repository.GroupRepository; +import com.ugent.pidgeon.postgre.repository.ProjectRepository; import com.ugent.pidgeon.util.CheckResult; +import com.ugent.pidgeon.util.CourseUtil; import com.ugent.pidgeon.util.EntityToJsonConverter; import com.ugent.pidgeon.util.GroupFeedbackUtil; import com.ugent.pidgeon.util.GroupUtil; +import com.ugent.pidgeon.util.Pair; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Objects; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; + @ExtendWith(MockitoExtension.class) public class GroupFeedbackControllerTest extends ControllerTest { @@ -30,148 +52,273 @@ public class GroupFeedbackControllerTest extends ControllerTest { @Mock private GroupFeedbackRepository groupFeedbackRepository; @Mock + private ProjectRepository projectRepository; + @Mock private GroupFeedbackUtil groupFeedbackUtil; @Mock + private GroupRepository groupRepository; + @Mock private GroupUtil groupUtil; @Mock + private CourseUtil courseUtil; + @Mock private EntityToJsonConverter entityToJsonConverter; @InjectMocks private GroupFeedbackController groupFeedbackController; private GroupFeedbackEntity groupFeedbackEntity; + private GroupFeedbackJson groupFeedbackJson; + + private final ObjectMapper objectMapper = CustomObjectMapper.createObjectMapper(); @BeforeEach public void setup() { - mockMvc = MockMvcBuilders.standaloneSetup(groupFeedbackController) - .defaultRequest(MockMvcRequestBuilders.get("/**") - .with(request -> { - request.setUserPrincipal(SecurityContextHolder.getContext().getAuthentication()); - return request; - })) - .build(); - groupFeedbackEntity = new GroupFeedbackEntity(1L, 1L, 0F, "good job.... NOT!"); + setUpController(groupFeedbackController); + groupFeedbackEntity = new GroupFeedbackEntity(4L, 6L, 1F, "good job.... NOT!"); + groupFeedbackJson = new GroupFeedbackJson(groupFeedbackEntity.getScore(), groupFeedbackEntity.getFeedback(), + groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId()); + } @Test public void testUpdateGroupScore() throws Exception { - String request = "{\"score\": null,\"feedback\": null}"; - when(groupFeedbackUtil.checkGroupFeedbackUpdate(anyLong(), anyLong(), any(), any())).thenReturn( - new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); - when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(any(), anyLong())).thenReturn( - new CheckResult<>(HttpStatus.OK, "", null)); - mockMvc.perform(MockMvcRequestBuilders.patch( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1")) + String url = ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", ""+groupFeedbackEntity.getGroupId()) + .replace("{projectid}", ""+groupFeedbackEntity.getProjectId()); + String requestAllNull = "{\"score\": null,\"feedback\": null}"; + String requestScoreNull = "{\"score\": null,\"feedback\": \"Heel goed gedaan\"}"; + String requestFeedbackNull = "{\"score\": 4.4,\"feedback\": null}"; + String request = "{\"score\": 4.4,\"feedback\": \"Heel goed gedaan\"}"; + String originalFeedback = groupFeedbackEntity.getFeedback(); + Float orginalScore = groupFeedbackEntity.getScore(); + /* If all checks succeed, group feedback is updated succesfully */ + /* If fields are null, nothing is changed */ + when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), + HttpMethod.PATCH)).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); + when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(argThat( + json -> json.getScore() == groupFeedbackEntity.getScore() && json.getFeedback() + .equals(groupFeedbackEntity.getFeedback())), eq(groupFeedbackEntity.getProjectId()))) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity)).thenReturn(groupFeedbackJson); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(requestAllNull)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupFeedbackJson))); + assertEquals(originalFeedback, groupFeedbackEntity.getFeedback()); + assertEquals(orginalScore, groupFeedbackEntity.getScore()); + verify(groupFeedbackRepository, times(1)).save(groupFeedbackEntity); + /* If score is null, only feedback is updated */ + reset(groupFeedbackUtil); + when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), + HttpMethod.PATCH)).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); + when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(argThat( + json -> json.getScore() == groupFeedbackEntity.getScore() && json.getFeedback().equals("Heel goed gedaan")), eq(groupFeedbackEntity.getProjectId()))) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + groupFeedbackJson.setFeedback("Heel goed gedaan"); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(requestScoreNull)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupFeedbackJson))); + assertEquals("Heel goed gedaan", groupFeedbackEntity.getFeedback()); + assertEquals(orginalScore, groupFeedbackEntity.getScore()); + verify(groupFeedbackRepository, times(2)).save(groupFeedbackEntity); + groupFeedbackEntity.setFeedback(originalFeedback); + groupFeedbackJson.setFeedback(originalFeedback); + /* If feedback is null, only score is updated */ + reset(groupFeedbackUtil); + when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), + HttpMethod.PATCH)).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); + when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(argThat( + json -> json.getScore() == 4.4F && json.getFeedback().equals(groupFeedbackEntity.getFeedback())), eq(groupFeedbackEntity.getProjectId()))) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + groupFeedbackJson.setScore(4.4F); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(requestFeedbackNull)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupFeedbackJson))); + assertEquals(originalFeedback, groupFeedbackEntity.getFeedback()); + assertEquals(4.4F, groupFeedbackEntity.getScore()); + verify(groupFeedbackRepository, times(3)).save(groupFeedbackEntity); + groupFeedbackEntity.setScore(orginalScore); + groupFeedbackJson.setScore(orginalScore); + /* If all fields are filled, both are updated */ + reset(groupFeedbackUtil); + when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), + HttpMethod.PATCH)).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); + when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(argThat( + json -> json.getScore() == 4.4F && json.getFeedback().equals("Heel goed gedaan")), eq(groupFeedbackEntity.getProjectId()))) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + groupFeedbackJson.setFeedback("Heel goed gedaan"); + groupFeedbackJson.setScore(4.4F); + mockMvc.perform(MockMvcRequestBuilders.patch(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupFeedbackJson))); + assertEquals("Heel goed gedaan", groupFeedbackEntity.getFeedback()); + assertEquals(4.4F, groupFeedbackEntity.getScore()); + verify(groupFeedbackRepository, times(4)).save(groupFeedbackEntity); - when(groupFeedbackRepository.save(any())).thenThrow(new RuntimeException()); - mockMvc.perform(MockMvcRequestBuilders.patch( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1")) + /* If an exception is thrown, return internal server error */ + doThrow(new RuntimeException()).when(groupFeedbackRepository).save(any()); + mockMvc.perform(MockMvcRequestBuilders.patch(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isInternalServerError()); + /* If json check fails, return corresponding status code */ + reset(groupFeedbackUtil); + when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), + HttpMethod.PATCH)).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(any(), anyLong())).thenReturn( - new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.patch( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1")) + new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); + mockMvc.perform(MockMvcRequestBuilders.patch(url) .contentType(MediaType.APPLICATION_JSON) - .content(request)) - .andExpect(status().isIAmATeapot()); + .content(requestAllNull)) + .andExpect(status().isBadRequest()); - when(groupFeedbackUtil.checkGroupFeedbackUpdate(anyLong(), anyLong(), any(), any())).thenReturn( - new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); - mockMvc.perform(MockMvcRequestBuilders.patch( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1")) + /* If group feedback check fails, return corresponding status code */ + when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), + HttpMethod.PATCH)).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.patch(url) .contentType(MediaType.APPLICATION_JSON) - .content(request)) - .andExpect(status().isForbidden()); - } - - @Test - public void testDeleteGroupScore() throws Exception { - when(groupFeedbackUtil.checkGroupFeedbackUpdate(anyLong(), anyLong(), any(), any())) - .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); - mockMvc.perform(MockMvcRequestBuilders.delete( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1"))) - .andExpect(status().isOk()); - - doThrow(new RuntimeException()).when(groupFeedbackRepository).delete(any()); - mockMvc.perform(MockMvcRequestBuilders.delete( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1"))) - .andExpect(status().isInternalServerError()); - - when(groupFeedbackUtil.checkGroupFeedbackUpdate(anyLong(), anyLong(), any(), any())) - .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.delete( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1"))) + .content(requestAllNull)) .andExpect(status().isIAmATeapot()); + } @Test public void testUpdateGroupScorePut() throws Exception { + String url = ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", ""+groupFeedbackEntity.getGroupId()) + .replace("{projectid}", ""+groupFeedbackEntity.getProjectId()); String request = "{\"score\": 4.4,\"feedback\": \"Heel goed gedaan\"}"; - when(groupFeedbackUtil.checkGroupFeedbackUpdate(anyLong(), anyLong(), any(), any())) - .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); - when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(any(), anyLong())).thenReturn( - new CheckResult<>(HttpStatus.OK, "", null)); - mockMvc.perform(MockMvcRequestBuilders.put( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1")) + /* If all checks succeed, group feedback is updated succesfully */ + /* If all fields are filled, both are updated */ + reset(groupFeedbackUtil); + when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), + HttpMethod.PUT)).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); + when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(argThat( + json -> json.getScore() == 4.4F && json.getFeedback().equals("Heel goed gedaan")), eq(groupFeedbackEntity.getProjectId()))) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity)).thenReturn(groupFeedbackJson); + groupFeedbackJson.setFeedback("Heel goed gedaan"); + groupFeedbackJson.setScore(4.4F); + mockMvc.perform(MockMvcRequestBuilders.put(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupFeedbackJson))); + assertEquals("Heel goed gedaan", groupFeedbackEntity.getFeedback()); + assertEquals(4.4F, groupFeedbackEntity.getScore()); + verify(groupFeedbackRepository, times(1)).save(groupFeedbackEntity); + + /* If an exception is thrown, return internal server error */ + doThrow(new RuntimeException()).when(groupFeedbackRepository).save(any()); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isInternalServerError()); + /* If json check fails, return corresponding status code */ + reset(groupFeedbackUtil); + when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), + HttpMethod.PUT)).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(any(), anyLong())).thenReturn( new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.put( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1")) + mockMvc.perform(MockMvcRequestBuilders.put(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isBadRequest()); - when(groupFeedbackUtil.checkGroupFeedbackUpdate(anyLong(), anyLong(), any(), any())) - .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.put( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1")) + /* If group feedback check fails, return corresponding status code */ + when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), + HttpMethod.PUT)).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.put(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isIAmATeapot()); } @Test - public void testAddGroupScore() throws Exception { - String request = "{\"score\": 4.4,\"feedback\": \"Heel goed gedaan\"}"; - when(groupFeedbackUtil.checkGroupFeedbackUpdate(anyLong(), anyLong(), any(), any())) + public void testDeleteGroupScore() throws Exception { + String url = ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", ""+groupFeedbackEntity.getGroupId()) + .replace("{projectid}", ""+groupFeedbackEntity.getProjectId()); + /* If user can delete group feedback, delete it */ + when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), HttpMethod.DELETE)) .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); - when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(any(), anyLong())).thenReturn( - new CheckResult<>(HttpStatus.OK, "", null)); - mockMvc.perform(MockMvcRequestBuilders.post( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1")) - .contentType(MediaType.APPLICATION_JSON) - .content(request)) - .andExpect(status().isCreated()); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isOk()); + verify(groupFeedbackRepository, times(1)).delete(groupFeedbackEntity); + + /* If an exception is thrown, return internal server error */ + doThrow(new RuntimeException()).when(groupFeedbackRepository).delete(any()); + mockMvc.perform(MockMvcRequestBuilders.delete(url)).andExpect(status().isInternalServerError()); + + /* If the groupfeedback can't be deleted by the user, return corresponding status code */ + when(groupFeedbackUtil.checkGroupFeedbackUpdate(anyLong(), anyLong(), any(), any())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isIAmATeapot()); + } + + + + @Test + public void testAddGroupScore() throws Exception { + String url = ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", ""+groupFeedbackEntity.getGroupId()) + .replace("{projectid}", ""+groupFeedbackEntity.getProjectId()); + String request = "{\"score\": " + groupFeedbackEntity.getScore() + ",\"feedback\": \"" + groupFeedbackEntity.getFeedback() + "\"}"; + /* If all checks succeed, group feedback is added succesfully */ + when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), HttpMethod.POST)) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(argThat( + json -> Objects.equals(json.getScore(), groupFeedbackEntity.getScore()) && json.getFeedback().equals(groupFeedbackEntity.getFeedback())), eq(groupFeedbackEntity.getProjectId()))) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(groupFeedbackRepository.save(any())).thenReturn(groupFeedbackEntity); + when(entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity)).thenReturn(groupFeedbackJson); + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupFeedbackJson))); + verify(groupFeedbackRepository, times(1)).save(argThat( + groupFeedback -> Objects.equals(groupFeedback.getScore(), groupFeedbackEntity.getScore()) && + groupFeedback.getFeedback().equals(groupFeedbackEntity.getFeedback()) && + groupFeedback.getGroupId() == groupFeedbackEntity.getGroupId() && + groupFeedback.getProjectId() == groupFeedbackEntity.getProjectId())); + /* If an exception is thrown, return internal server error */ + reset(groupFeedbackRepository); when(groupFeedbackRepository.save(any())).thenThrow(new RuntimeException()); - mockMvc.perform(MockMvcRequestBuilders.post( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1")) + mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isInternalServerError()); + /* If json check fails, return corresponding status code */ + reset(groupFeedbackUtil); + when(groupFeedbackUtil.checkGroupFeedbackUpdate(anyLong(), anyLong(), any(), any())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", null)); when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(any(), anyLong())).thenReturn( new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.post( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1")) + mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isBadRequest()); + /* If user can't add group feedback, return corresponding status code */ when(groupFeedbackUtil.checkGroupFeedbackUpdate(anyLong(), anyLong(), any(), any())).thenReturn( new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); - mockMvc.perform(MockMvcRequestBuilders.post( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1")) + mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isForbidden()); @@ -179,32 +326,112 @@ public void testAddGroupScore() throws Exception { @Test public void testGetGroupScore() throws Exception { - when(groupFeedbackUtil.checkGroupFeedback(anyLong(), anyLong())).thenReturn( - new CheckResult<>(HttpStatus.OK, "", null)); - when(groupUtil.canGetProjectGroupData(anyLong(), anyLong(), any())).thenReturn( - new CheckResult<>(HttpStatus.OK, "", null)); - when(groupFeedbackUtil.getGroupFeedbackIfExists(anyLong(), anyLong())).thenReturn( - new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); - mockMvc.perform(MockMvcRequestBuilders.get( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1"))) - .andExpect(status().isOk()); + String url = ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", ""+groupFeedbackEntity.getGroupId()) + .replace("{projectid}", ""+groupFeedbackEntity.getProjectId()); + /* If all checks succeed, group feedback is returned */ + when(groupFeedbackUtil.checkGroupFeedback(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity)).thenReturn(groupFeedbackJson); + when(groupUtil.canGetProjectGroupData(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(groupFeedbackUtil.getGroupFeedbackIfExists(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); + when(entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity)).thenReturn(groupFeedbackJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupFeedbackJson))); - when(groupFeedbackUtil.getGroupFeedbackIfExists(anyLong(), anyLong())).thenReturn( - new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1"))) + /* If feedback doesn't exist, return not found */ + reset(groupFeedbackUtil); + when(groupFeedbackUtil.checkGroupFeedback(anyLong(), anyLong())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(groupFeedbackUtil.getGroupFeedbackIfExists(anyLong(), anyLong())) + .thenReturn(new CheckResult<>(HttpStatus.NOT_FOUND, "", null)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isNotFound()); + + /* User can't get project group data, return forbidden */ + reset(groupUtil); + when(groupUtil.canGetProjectGroupData(anyLong(), anyLong(), any())) + .thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isForbidden()); + + /* If check fails, return corresponding status code */ + when(groupFeedbackUtil.checkGroupFeedback(anyLong(), anyLong())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); + } + + @Test + public void testGetCourseGrades() throws Exception { + CourseEntity courseEntity = new CourseEntity("Test course", "TestCourseDescription", 2013); + courseEntity.setId(99L); + ProjectEntity project1 = new ProjectEntity(courseEntity.getId(),"Test project", "TestProjectDescription", 1L, 11L, true, 44, OffsetDateTime.now()); + ProjectEntity project2 = new ProjectEntity(courseEntity.getId(),"Test project", "TestProjectDescription", 2L, 11L, true, 44, OffsetDateTime.now()); + project2.setId(1L); + project1.setId(2L); + long project1GroupId = 4L; + long project2GroupId = 5L; + GroupFeedbackJsonWithProject groupFeedbackJsonWithProject1 = new GroupFeedbackJsonWithProject(project1.getName(), "UrlOfProject1", project1.getId(), groupFeedbackJson, project1.getMaxScore()); + GroupFeedbackJsonWithProject groupFeedbackJsonWithProject2 = new GroupFeedbackJsonWithProject(project2.getName(), "UrlOfProject2", project2.getId(), null, project2.getMaxScore()); + List groupFeedbackJsonWithProjects = List.of(groupFeedbackJsonWithProject1, groupFeedbackJsonWithProject2); + String url = ApiRoutes.COURSE_BASE_PATH + "/" + courseEntity.getId() + "/grades"; + /* If all checks succeed, course grades are returned */ + when(courseUtil.getCourseIfUserInCourse(courseEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(courseEntity, CourseRelation.enrolled))); + when(projectRepository.findByCourseId(courseEntity.getId())) + .thenReturn(List.of(project1, project2)); + when(groupRepository.groupIdByProjectAndUser(project1.getId(), getMockUser().getId())) + .thenReturn(project1GroupId); + when(groupRepository.groupIdByProjectAndUser(project2.getId(), getMockUser().getId())) + .thenReturn(project2GroupId); + when(groupFeedbackUtil.getGroupFeedbackIfExists(project1GroupId, project1.getId())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupFeedbackEntity)); + when(groupFeedbackUtil.getGroupFeedbackIfExists(project2GroupId, project2.getId())) + .thenReturn(new CheckResult<>(HttpStatus.NOT_FOUND, "", null)); + when(entityToJsonConverter.groupFeedbackEntityToJsonWithProject(groupFeedbackEntity, project1)) + .thenReturn(groupFeedbackJsonWithProject1); + when(entityToJsonConverter.groupFeedbackEntityToJsonWithProject(null, project2)) + .thenReturn(groupFeedbackJsonWithProject2); + + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupFeedbackJsonWithProjects))); + + /* If project is not visible, filter it out */ + project2.setVisible(false); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(groupFeedbackJsonWithProject1)))); + project2.setVisible(true); + + /* If user is not yet in group also have null as group feedback */ + when(groupRepository.groupIdByProjectAndUser(project1.getId(), getMockUser().getId())) + .thenReturn(null); + GroupFeedbackJsonWithProject project1NoGroup = new GroupFeedbackJsonWithProject(project1.getName(), "UrlOfProject1", project1.getId(), null, project1.getMaxScore()); + groupFeedbackJsonWithProjects = List.of(project1NoGroup, groupFeedbackJsonWithProject2); + when(entityToJsonConverter.groupFeedbackEntityToJsonWithProject(null, project1)) + .thenReturn(project1NoGroup); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(groupFeedbackJsonWithProjects))); + + /* If user isn't enrolled in the course, return BAD REQUEST */ + when(courseUtil.getCourseIfUserInCourse(courseEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(courseEntity, CourseRelation.course_admin))); + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isBadRequest()); - when(groupUtil.canGetProjectGroupData(anyLong(), anyLong(), any())).thenReturn( - new CheckResult<>(HttpStatus.CONFLICT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1"))) - .andExpect(status().isConflict()); - - when(groupFeedbackUtil.checkGroupFeedback(anyLong(), anyLong())).thenReturn( - new CheckResult<>(HttpStatus.BANDWIDTH_LIMIT_EXCEEDED, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get( - ApiRoutes.GROUP_FEEDBACK_PATH.replace("{groupid}", "1").replace("{projectid}", "1"))) - .andExpect(status().isBandwidthLimitExceeded()); + /* If course check fails, return corresponding status code */ + when(courseUtil.getCourseIfUserInCourse(courseEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); } } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupMembersControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupMembersControllerTest.java index 9e90bb1c..f8297828 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupMembersControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupMembersControllerTest.java @@ -1,10 +1,14 @@ package com.ugent.pidgeon.controllers; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ugent.pidgeon.CustomObjectMapper; +import com.ugent.pidgeon.json.UserReferenceJson; import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.GroupMemberRepository; @@ -19,9 +23,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; @ExtendWith(MockitoExtension.class) public class GroupMembersControllerTest extends ControllerTest { @@ -36,123 +39,169 @@ public class GroupMembersControllerTest extends ControllerTest { @InjectMocks private GroupMemberController groupMemberController; + private final ObjectMapper objectMapper = CustomObjectMapper.createObjectMapper(); + private UserEntity userEntity; + private UserEntity userEntity2; + private UserReferenceJson userReferenceJson; + private UserReferenceJson userReferenceJson2; + private final long groupId = 10L; @BeforeEach public void setup() { - mockMvc = MockMvcBuilders.standaloneSetup(groupMemberController) - .defaultRequest(MockMvcRequestBuilders.get("/**") - .with(request -> { - request.setUserPrincipal(SecurityContextHolder.getContext().getAuthentication()); - return request; - })) - .build(); - userEntity = new UserEntity("name", "surname", "email", UserRole.student, "azureid"); - userEntity.setId(1L); + setUpController(groupMemberController); + userEntity = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); + userEntity.setId(5L); + userEntity2 = new UserEntity("name2", "surname2", "email2", UserRole.student, "azureid2", ""); + userEntity2.setId(6L); + userReferenceJson = new UserReferenceJson(userEntity.getName(), userEntity.getEmail(), userEntity.getId(), ""); + userReferenceJson2 = new UserReferenceJson(userEntity2.getName(), userEntity2.getEmail(), userEntity2.getId(), ""); } @Test public void testRemoveMemberFromGroup() throws Exception { - when(groupUtil.canRemoveUserFromGroup(anyLong(), anyLong(), any())) + String url = ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", ""+groupId) + "/" + userEntity.getId(); + /* If all checks pass, the user is removed from the group */ + when(groupUtil.canRemoveUserFromGroup(groupId, userEntity.getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - when(groupMemberRepository.removeMemberFromGroup(anyLong(), anyLong())).thenReturn(1); - mockMvc.perform(MockMvcRequestBuilders.delete( - ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1") + "/1")) + when(groupMemberRepository.removeMemberFromGroup(groupId, userEntity.getId())).thenReturn(1); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isNoContent()); + verify(groupMemberRepository, times(1)).removeMemberFromGroup(groupId, userEntity.getId()); - when(groupMemberRepository.removeMemberFromGroup(anyLong(), anyLong())).thenReturn(0); - mockMvc.perform(MockMvcRequestBuilders.delete( - ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1") + "/1")) + /* If something goes wrong return internal server error */ + when(groupMemberRepository.removeMemberFromGroup(groupId, userEntity.getId())).thenReturn(0); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isInternalServerError()); - when(groupUtil.canRemoveUserFromGroup(anyLong(), anyLong(), any())) + /* If use can't be removed from group return corresponding status */ + when(groupUtil.canRemoveUserFromGroup(groupId, userEntity.getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.delete( - ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1") + "/1")) + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isBadRequest()); } @Test public void testRemoveMemberFromGroupInferred() throws Exception { - when(groupUtil.canRemoveUserFromGroup(anyLong(), anyLong(), any())) + String url = ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", ""+groupId); + /* If all checks pass, the user is removed from the group */ + when(groupUtil.canRemoveUserFromGroup(groupId, getMockUser().getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - when(groupMemberRepository.removeMemberFromGroup(anyLong(), anyLong())).thenReturn(1); - mockMvc.perform( - MockMvcRequestBuilders.delete(ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1"))) + when(groupMemberRepository.removeMemberFromGroup(groupId, getMockUser().getId())).thenReturn(1); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isNoContent()); + verify(groupMemberRepository, times(1)).removeMemberFromGroup(groupId, getMockUser().getId()); - when(groupMemberRepository.removeMemberFromGroup(anyLong(), anyLong())).thenReturn(0); - mockMvc.perform( - MockMvcRequestBuilders.delete(ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1"))) + /* If something goes wrong return internal server error */ + when(groupMemberRepository.removeMemberFromGroup(groupId, getMockUser().getId())).thenReturn(0); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isInternalServerError()); - when(groupUtil.canRemoveUserFromGroup(anyLong(), anyLong(), any())) + /* If use can't be removed from group return corresponding status */ + when(groupUtil.canRemoveUserFromGroup(groupId, getMockUser().getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform( - MockMvcRequestBuilders.delete(ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1"))) + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isBadRequest()); } @Test public void testAddMemberToGroup() throws Exception { - when(groupUtil.canAddUserToGroup(anyLong(), anyLong(), any())) + String url = ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", ""+groupId) + "/" + userEntity.getId(); + + /* If all checks succeed, the user is added to the group */ + when(groupUtil.canAddUserToGroup(groupId, userEntity.getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - when(groupMemberRepository.findAllMembersByGroupId(anyLong())) - .thenReturn(List.of(userEntity)); - mockMvc.perform(MockMvcRequestBuilders.post( - ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1") + "/1")) - .andExpect(status().isOk()); - - when(groupMemberRepository.findAllMembersByGroupId(anyLong())).thenThrow( - new RuntimeException()); - mockMvc.perform(MockMvcRequestBuilders.post( - ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1") + "/1")) + when(groupMemberRepository.findAllMembersByGroupId(groupId)) + .thenReturn(List.of(userEntity, userEntity2)); + when(entityToJsonConverter.userEntityToUserReference(userEntity, false)).thenReturn(userReferenceJson); + when(entityToJsonConverter.userEntityToUserReference(userEntity2, false)).thenReturn(userReferenceJson2); + mockMvc.perform(MockMvcRequestBuilders.post(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userReferenceJson, userReferenceJson2)))); + verify(groupMemberRepository, times(1)).addMemberToGroup(groupId, userEntity.getId()); + + /* If something goes wrong return internal server error */ + when(groupMemberRepository.addMemberToGroup(groupId, userEntity.getId())).thenThrow(new RuntimeException()); + mockMvc.perform(MockMvcRequestBuilders.post(url)) .andExpect(status().isInternalServerError()); - when(groupUtil.canAddUserToGroup(anyLong(), anyLong(), any())) - .thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.post( - ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1") + "/1")) - .andExpect(status().isBadRequest()); + /* If user can't be added to group return corresponding status */ + when(groupUtil.canAddUserToGroup(groupId, userEntity.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.post(url)) + .andExpect(status().isIAmATeapot()); + } @Test public void testAddMemberToGroupInferred() throws Exception { - when(groupUtil.canAddUserToGroup(anyLong(), anyLong(), any())) + String url = ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", ""+groupId); + UserReferenceJson mockUserJson = new UserReferenceJson(getMockUser().getName(), getMockUser().getEmail(), getMockUser().getId(), getMockUser().getStudentNumber()); + + /* If all checks succeed, the user is added to the group */ + when(groupUtil.canAddUserToGroup(groupId, getMockUser().getId(), getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - when(groupMemberRepository.findAllMembersByGroupId(anyLong())) - .thenReturn(List.of(userEntity)); - mockMvc.perform( - MockMvcRequestBuilders.post(ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1"))) - .andExpect(status().isOk()); - - when(groupMemberRepository.findAllMembersByGroupId(anyLong())).thenThrow( - new RuntimeException()); - mockMvc.perform( - MockMvcRequestBuilders.post(ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1"))) + when(groupMemberRepository.findAllMembersByGroupId(groupId)) + .thenReturn(List.of(getMockUser(), userEntity2)); + when(entityToJsonConverter.userEntityToUserReference(getMockUser(), true)).thenReturn(mockUserJson); + when(entityToJsonConverter.userEntityToUserReference(userEntity2, true)).thenReturn(userReferenceJson2); + mockMvc.perform(MockMvcRequestBuilders.post(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(mockUserJson, userReferenceJson2)))); + verify(groupMemberRepository, times(1)).addMemberToGroup(groupId, getMockUser().getId()); + + /* If something goes wrong return internal server error */ + when(groupMemberRepository.addMemberToGroup(groupId, getMockUser().getId())).thenThrow(new RuntimeException()); + mockMvc.perform(MockMvcRequestBuilders.post(url)) .andExpect(status().isInternalServerError()); - when(groupUtil.canAddUserToGroup(anyLong(), anyLong(), any())) - .thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform( - MockMvcRequestBuilders.post(ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1"))) - .andExpect(status().isBadRequest()); + /* If user can't be added to group return corresponding status */ + when(groupUtil.canAddUserToGroup(groupId, getMockUser().getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.post(url)) + .andExpect(status().isIAmATeapot()); } @Test public void testFindAllMembersByGroupId() throws Exception { - when(groupUtil.canGetGroup(anyLong(), any())) + String url = ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", ""+groupId); + List members = List.of(userEntity, userEntity2); + List userReferenceJsons = List.of(userReferenceJson, userReferenceJson2); + when(groupMemberRepository.findAllMembersByGroupId(groupId)).thenReturn(members); + /* User is admin of group so don't hide studentNumbers */ + when(groupUtil.isAdminOfGroup(groupId, getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(entityToJsonConverter.userEntityToUserReference(userEntity, false)).thenReturn(userReferenceJson); + when(entityToJsonConverter.userEntityToUserReference(userEntity2, false)).thenReturn(userReferenceJson2); + + /* If user can get group return list of members */ + when(groupUtil.canGetGroup(groupId, getMockUser())) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - when(groupMemberRepository.findAllMembersByGroupId(anyLong())) - .thenReturn(List.of(userEntity)); - mockMvc.perform( - MockMvcRequestBuilders.get(ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1"))) - .andExpect(status().isOk()); - - when(groupUtil.canGetGroup(anyLong(), any())) - .thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform( - MockMvcRequestBuilders.get(ApiRoutes.GROUP_MEMBER_BASE_PATH.replace("{groupid}", "1"))) - .andExpect(status().isBadRequest()); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(userReferenceJsons))); + + verify(entityToJsonConverter, times(1)).userEntityToUserReference(userEntity, false); + verify(entityToJsonConverter, times(1)).userEntityToUserReference(userEntity2, false); + + /* If user isn't admin, studentNumbers should be hidden */ + when(groupUtil.isAdminOfGroup(groupId, getMockUser())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + when(entityToJsonConverter.userEntityToUserReference(userEntity, true)).thenReturn(userReferenceJson); + when(entityToJsonConverter.userEntityToUserReference(userEntity2, true)).thenReturn(userReferenceJson2); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(userReferenceJsons))); + + verify(entityToJsonConverter, times(1)).userEntityToUserReference(userEntity, true); + verify(entityToJsonConverter, times(1)).userEntityToUserReference(userEntity2, true); + + /* If use can't get group return corresponding status */ + when(groupUtil.canGetGroup(groupId, getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); } } \ No newline at end of file diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ProjectControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ProjectControllerTest.java index 5142d2b2..a816a0e7 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/ProjectControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/ProjectControllerTest.java @@ -1,15 +1,39 @@ package com.ugent.pidgeon.controllers; -import com.ugent.pidgeon.model.Auth; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ugent.pidgeon.CustomObjectMapper; +import com.ugent.pidgeon.json.CourseReferenceJson; +import com.ugent.pidgeon.json.GroupJson; +import com.ugent.pidgeon.json.ProjectProgressJson; +import com.ugent.pidgeon.json.ProjectResponseJsonWithStatus; +import com.ugent.pidgeon.json.UserProjectsJson; import com.ugent.pidgeon.model.ProjectResponseJson; -import com.ugent.pidgeon.model.json.CourseReferenceJson; -import com.ugent.pidgeon.model.json.ProjectJson; -import com.ugent.pidgeon.model.json.ProjectProgressJson; -import com.ugent.pidgeon.model.json.userProjectsJson; -import com.ugent.pidgeon.postgre.models.*; +import com.ugent.pidgeon.postgre.models.CourseEntity; +import com.ugent.pidgeon.postgre.models.GroupClusterEntity; +import com.ugent.pidgeon.postgre.models.GroupEntity; +import com.ugent.pidgeon.postgre.models.ProjectEntity; import com.ugent.pidgeon.postgre.models.types.CourseRelation; -import com.ugent.pidgeon.postgre.models.types.UserRole; -import com.ugent.pidgeon.postgre.repository.*; +import com.ugent.pidgeon.postgre.repository.CourseRepository; +import com.ugent.pidgeon.postgre.repository.CourseUserRepository; +import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; +import com.ugent.pidgeon.postgre.repository.GroupRepository; +import com.ugent.pidgeon.postgre.repository.ProjectRepository; +import com.ugent.pidgeon.postgre.repository.TestRepository; import com.ugent.pidgeon.util.CheckResult; import com.ugent.pidgeon.util.ClusterUtil; import com.ugent.pidgeon.util.CommonDatabaseActions; @@ -17,30 +41,19 @@ import com.ugent.pidgeon.util.EntityToJsonConverter; import com.ugent.pidgeon.util.Pair; import com.ugent.pidgeon.util.ProjectUtil; -import java.util.Objects; +import java.time.OffsetDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.ArgumentMatchers; -import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.web.servlet.MockMvc; - - -import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -public class ProjectControllerTest { - - protected MockMvc mockMvc; +public class ProjectControllerTest extends ControllerTest { @InjectMocks private ProjectController projectController; @@ -78,646 +91,742 @@ public class ProjectControllerTest { @Mock private GroupRepository grouprRepository; + private final ObjectMapper objectMapper = CustomObjectMapper.createObjectMapper(); + private ProjectEntity projectEntity; + private ProjectEntity projectEntity2; + private ProjectResponseJson projectResponseJson; + private ProjectResponseJson projectResponseJson2; + private CourseEntity courseEntity; + private CourseEntity courseEntity2; + private final long groupClusterId = 7L; + @BeforeEach void setUp() { - MockitoAnnotations.openMocks(this); - } - - - @Test - void testGetProjectShouldReturnOneProject() { - // Mock data - Auth auth = mock(Auth.class); - ProjectEntity project = new ProjectEntity(); - project.setName("Project 1"); - project.setId(1L); - project.setVisible(true); - List projects = new ArrayList<>(); - projects.add(project); - UserEntity user = new UserEntity("Test", "De Tester", "test.tester@test.com", UserRole.student, - "azure"); - user.setId(1L); - - // Mock repository behavior - when(projectRepository.findProjectsByUserId(anyLong())).thenReturn(projects); - when(auth.getUserEntity()).thenReturn(user); - when(courseUtil.getCourseIfUserInCourse(anyLong(), any())) - .thenReturn(new CheckResult<>(HttpStatus.OK, "", - new Pair<>(new CourseEntity(), CourseRelation.enrolled))); - - // Call controller method - ResponseEntity response = projectController.getProjects(auth); - - // Verify response - assertInstanceOf(userProjectsJson.class, response.getBody()); - userProjectsJson responseBody = (userProjectsJson) response.getBody(); - assertEquals(1, responseBody.enrolledProjects().size()); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - } - - @Test - void testGetProjectShouldReturnMultipleProject() { - // Mock data - Auth auth = mock(Auth.class); - ProjectEntity project1 = new ProjectEntity(); - project1.setName("Project 1"); - project1.setId(1L); - project1.setVisible(true); - ProjectEntity project2 = new ProjectEntity(); - project2.setName("Project 2"); - project2.setId(2L); - project2.setVisible(true); - List projects = new ArrayList<>(); - projects.add(project1); - projects.add(project2); - UserEntity user = new UserEntity("Test", "De Tester", "test.tester@test.com", UserRole.student, - "azure"); - user.setId(1L); - - // Mock repository behavior - when(projectRepository.findProjectsByUserId(anyLong())).thenReturn(projects); - when(auth.getUserEntity()).thenReturn(user); - when(courseUtil.getCourseIfUserInCourse(anyLong(), any())) - .thenReturn(new CheckResult<>(HttpStatus.OK, "", - new Pair<>(new CourseEntity(), CourseRelation.enrolled))); - - // Call controller method - ResponseEntity response = projectController.getProjects(auth); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertInstanceOf(userProjectsJson.class, response.getBody()); - userProjectsJson responseBody = (userProjectsJson) response.getBody(); - assertEquals(2, responseBody.enrolledProjects().size()); - } - - - @Test - void testGetProjectByIdShouldReturnProject() { - // Mock data - // auth object - Auth auth = mock(Auth.class); - // projects - ProjectEntity project1 = new ProjectEntity(); - project1.setName("Project 1"); - project1.setId(1L); - project1.setVisible(true); - ProjectEntity project2 = new ProjectEntity(); - project2.setName("Project 2"); - project2.setId(2L); - project2.setVisible(true); - project2.setCourseId(1L); - ProjectEntity project3 = new ProjectEntity(); - project3.setName("Project 3"); - project3.setId(3L); - project3.setVisible(true); - List projects = new ArrayList<>(); - projects.add(project1); - projects.add(project2); - projects.add(project3); - // users - UserEntity user = new UserEntity("Test", "De Tester", "test.tester@test.com", UserRole.student, - "azure"); - user.setId(1L); - //check results - CourseEntity courseEntity = new CourseEntity(); - CheckResult checkResult = new CheckResult<>(HttpStatus.OK, "TestProject", - project2); - CheckResult> courseCheck = new CheckResult<>(HttpStatus.OK, "TestCourse", - new Pair<>(courseEntity, CourseRelation.enrolled)); - - // Mock repository behavior - when(projectUtil.canGetProject(2L, user)).thenReturn(checkResult); - when(courseUtil.getCourseIfUserInCourse(1L, user)).thenReturn(courseCheck); - when(auth.getUserEntity()).thenReturn(user); - when(entityToJsonConverter.projectEntityToProjectResponseJson(project2, courseCheck.getData().getFirst(), - user)).thenReturn(new ProjectResponseJson( - new CourseReferenceJson("TestCourse", ApiRoutes.COURSE_BASE_PATH + "/" + 1L, 1L), - OffsetDateTime.MAX, - "Test", 2L, "TestProject", "testUrl", "testUrl", 0, true, new ProjectProgressJson(0, 0), - 1L)); - - // Call controller method - ResponseEntity response = projectController.getProjectById(2L, auth); - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - ProjectResponseJson responseBody = (ProjectResponseJson) response.getBody(); - assert responseBody != null; - assertEquals(2L, responseBody.projectId()); + setUpController(projectController); + + courseEntity = new CourseEntity("courseName", "courseUrl", 2020); + courseEntity.setId(24L); + courseEntity2 = new CourseEntity("courseName2", "courseUrl2", 2021); + courseEntity2.setId(25L); + + projectEntity = new ProjectEntity( + courseEntity.getId(), + "projectName", + "projectDescription", + groupClusterId, + 38L, + true, + 34, + OffsetDateTime.now() + ); + projectEntity.setId(64); + projectResponseJson = new ProjectResponseJson( + new CourseReferenceJson(courseEntity.getName(), "course1URL", courseEntity.getId(), null), + OffsetDateTime.now(), + projectEntity.getName(), + projectEntity.getId(), + projectEntity.getDescription(), + "submissionUrl", + "testUrl", + projectEntity.getMaxScore(), + projectEntity.isVisible(), + new ProjectProgressJson(0, 0), + 1L, + groupClusterId, + OffsetDateTime.now() + ); + + projectEntity2 = new ProjectEntity( + courseEntity2.getId(), + "projectName2", + "projectDescription2", + groupClusterId, + 39L, + true, + 32, + OffsetDateTime.now() + ); + projectEntity2.setId(65); + projectResponseJson2 = new ProjectResponseJson( + new CourseReferenceJson(courseEntity2.getName(), "course2URL", courseEntity2.getId(), null), + OffsetDateTime.now(), + projectEntity2.getName(), + projectEntity2.getId(), + projectEntity2.getDescription(), + "submissionUrl", + "testUrl", + projectEntity2.getMaxScore(), + projectEntity2.isVisible(), + new ProjectProgressJson(0, 0), + 1L, + groupClusterId, + OffsetDateTime.now() + ); } @Test - void testGetProjectByIdShouldFailReasonCanNotGetProject() { - // Mock data - // auth object - Auth auth = mock(Auth.class); - // projects - ProjectEntity project1 = new ProjectEntity(); - project1.setName("Project 1"); - project1.setId(1L); - ProjectEntity project2 = new ProjectEntity(); - project2.setName("Project 2"); - project2.setId(2L); - project2.setCourseId(1L); - ProjectEntity project3 = new ProjectEntity(); - project3.setName("Project 3"); - project3.setId(3L); - List projects = new ArrayList<>(); - projects.add(project1); - projects.add(project2); - projects.add(project3); - // users - UserEntity user = new UserEntity("Test", "De Tester", "test.tester@test.com", UserRole.student, - "azure"); - user.setId(1L); - //check results - CourseEntity courseEntity = new CourseEntity(); - CheckResult checkResult = new CheckResult<>(HttpStatus.FORBIDDEN, - "testProjectForbidden", - project2); - CheckResult courseCheck = new CheckResult<>(HttpStatus.OK, "TestCourse", - courseEntity); - - // Mock repository behavior - when(projectUtil.canGetProject(2L, user)).thenReturn(checkResult); - when(courseUtil.getCourseIfExists(1L)).thenReturn(courseCheck); - when(auth.getUserEntity()).thenReturn(user); - when(entityToJsonConverter.projectEntityToProjectResponseJson(project2, courseCheck.getData(), - user)).thenReturn(new ProjectResponseJson( - new CourseReferenceJson("TestCourse", ApiRoutes.COURSE_BASE_PATH + "/" + 1L, 1L), - OffsetDateTime.MAX, - "Test", 2L, "TestProject", "testUrl", "testUrl", 0, true, new ProjectProgressJson(0, 0), - 1L)); - - // Call controller method - ResponseEntity response = projectController.getProjectById(2L, auth); - // Verify response - assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); - assertEquals("testProjectForbidden", Objects.requireNonNull(response.getBody()).toString()); - + void testGetProjects() throws Exception { + String url = ApiRoutes.PROJECT_BASE_PATH; + List projectEntities = List.of(projectEntity, projectEntity2); + ProjectResponseJsonWithStatus projectJsonWithStatus = new ProjectResponseJsonWithStatus( + projectResponseJson2, + "completed" + ); + when(courseUtil.getCourseIfUserInCourse(courseEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", new Pair<>(courseEntity, CourseRelation.creator)) + ); + when(courseUtil.getCourseIfUserInCourse(courseEntity2.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", new Pair<>(courseEntity2, CourseRelation.enrolled)) + ); + when(projectRepository.findProjectsByUserId(getMockUser().getId())).thenReturn(projectEntities); + when(entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, getMockUser())) + .thenReturn(projectResponseJson); + when(entityToJsonConverter.projectEntityToProjectResponseJsonWithStatus(projectEntity2, courseEntity2, getMockUser())) + .thenReturn(projectJsonWithStatus); + + /* Returns the user's projects */ + UserProjectsJson userProjectsJson = new UserProjectsJson( + List.of(projectJsonWithStatus), + List.of(projectResponseJson) + ); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(userProjectsJson))); + + /* If project isn't visible and role enrolled, don't return it */ + projectEntity2.setVisible(false); + userProjectsJson = new UserProjectsJson( + Collections.emptyList(), + List.of(projectResponseJson) + ); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(userProjectsJson))); + + /* If project isn't visible but visibleAfter is passed, update visibility */ + projectEntity2.setVisibleAfter(OffsetDateTime.now().minusDays(1)); + userProjectsJson = new UserProjectsJson( + List.of(projectJsonWithStatus), + List.of(projectResponseJson) + ); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(userProjectsJson))); + + verify(projectRepository, times(1)).save(projectEntity2); + assertTrue(projectEntity2.isVisible()); + + /* If project isn't visible and visibleAfter is in the future, don't return it */ + projectEntity2.setVisible(false); + projectEntity2.setVisibleAfter(OffsetDateTime.now().plusDays(1)); + userProjectsJson = new UserProjectsJson( + Collections.emptyList(), + List.of(projectResponseJson) + ); + + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(userProjectsJson))); + + assertFalse(projectEntity2.isVisible()); + + /* If a coursecheck fails, return corresponding status */ + when(courseUtil.getCourseIfUserInCourse(courseEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null) + ); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); } - @Test - void testGetProjectByIdShouldFailReasonCanNotGetCourse() { - // Mock data - // auth object - Auth auth = mock(Auth.class); - // projects - ProjectEntity project1 = new ProjectEntity(); - project1.setName("Project 1"); - project1.setId(1L); - project1.setVisible(true); - ProjectEntity project2 = new ProjectEntity(); - project2.setName("Project 2"); - project2.setId(2L); - project2.setVisible(true); - project2.setCourseId(1L); - ProjectEntity project3 = new ProjectEntity(); - project3.setName("Project 3"); - project3.setId(3L); - project3.setVisible(true); - List projects = new ArrayList<>(); - projects.add(project1); - projects.add(project2); - projects.add(project3); - // users - UserEntity user = new UserEntity("Test", "De Tester", "test.tester@test.com", UserRole.student, - "azure"); - user.setId(1L); - //check results - CourseEntity courseEntity = new CourseEntity(); - CheckResult checkResult = new CheckResult<>(HttpStatus.OK, "TestProject", - project2); - CheckResult> courseCheck = new CheckResult<>(HttpStatus.FORBIDDEN, "testCourseForbidden", - new Pair<>(courseEntity, CourseRelation.enrolled)); - - // Mock repository behavior - when(projectUtil.canGetProject(2L, user)).thenReturn(checkResult); - when(courseUtil.getCourseIfUserInCourse(1L, user)).thenReturn(courseCheck); - when(auth.getUserEntity()).thenReturn(user); - when(entityToJsonConverter.projectEntityToProjectResponseJson(project2, courseCheck.getData().getFirst(), - user)).thenReturn(new ProjectResponseJson( - new CourseReferenceJson("TestCourse", ApiRoutes.COURSE_BASE_PATH + "/" + 1L, 1L), - OffsetDateTime.MAX, - "Test", 2L, "TestProject", "testUrl", "testUrl", 0, true, new ProjectProgressJson(0, 0), - 1L)); - - // Call controller method - ResponseEntity response = projectController.getProjectById(2L, auth); - // Verify response - assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); - assertEquals("testCourseForbidden", Objects.requireNonNull(response.getBody()).toString()); - + void testGetProject() throws Exception { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + projectEntity.getId(); + + /* If user can get project, return project */ + when(projectUtil.canGetProject(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", projectEntity) + ); + when(courseUtil.getCourseIfUserInCourse(projectEntity.getCourseId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", new Pair<>(courseEntity, CourseRelation.enrolled)) + ); + when(entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, getMockUser())) + .thenReturn(projectResponseJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(projectResponseJson))); + + /* If user is enrolled and project not visible, return forbidden */ + projectEntity.setVisible(false); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isNotFound()); + + /* if visibleAfter is passed, update visibility */ + projectEntity.setVisibleAfter(OffsetDateTime.now().minusDays(1)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()); + + verify(projectRepository, times(1)).save(projectEntity); + assertTrue(projectEntity.isVisible()); + + /* If visibleAfter is in the future, return 404 */ + projectEntity.setVisible(false); + projectEntity.setVisibleAfter(OffsetDateTime.now().plusDays(1)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isNotFound()); + + assertFalse(projectEntity.isVisible()); + + /* If user is not enrolled and project not visible, return project */ + when(courseUtil.getCourseIfUserInCourse(projectEntity.getCourseId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", new Pair<>(courseEntity, CourseRelation.course_admin)) + ); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(projectResponseJson))); + + /* If get course with relation check fails, return correpsonding status */ + when(courseUtil.getCourseIfUserInCourse(projectEntity.getCourseId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null) + ); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); + + /* If user can't get project, return corresponding status */ + when(projectUtil.canGetProject(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null) + ); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); } @Test - public void testCreateProjectShouldMakeProject() { - // Mock data - long courseId = 1L; - ProjectJson projectJson = - new ProjectJson("Test Project", "Test Description", 1L, 1L, true, 100, OffsetDateTime.MAX); - ProjectEntity projectEntity = - new ProjectEntity(1, "Test Project", "Test Description", 1L, 1L, true, 100, - OffsetDateTime.MAX); - Auth auth = mock(Auth.class); - UserEntity user = new UserEntity(); - user.setId(1L); - when(auth.getUserEntity()).thenReturn(user); - - CourseEntity courseEntity = new CourseEntity(); - courseEntity.setId(courseId); - - CheckResult checkAcces = new CheckResult<>(HttpStatus.OK, "TestIsAdmin", - courseEntity); - - CheckResult checkResult = new CheckResult<>(HttpStatus.OK, "TestProjectJson", null); - - // Mock repository behavior - when(projectRepository.save(projectEntity)).thenReturn(projectEntity); - - when(courseUtil.getCourseIfAdmin(courseId, user)).thenReturn(checkAcces); - when(projectUtil.checkProjectJson(projectJson, courseId)).thenReturn(checkResult); - when(courseRepository.findById(courseId)).thenReturn(Optional.of(courseEntity)); - when(courseUserRepository.findById(ArgumentMatchers.any(CourseUserId.class))).thenReturn( - Optional.of(new CourseUserEntity(1, 1, CourseRelation.course_admin))); - when(groupClusterRepository.findById(projectJson.getGroupClusterId())).thenReturn( - Optional.of(new GroupClusterEntity(1L, 20, "Testcluster", 10))); - when(projectRepository.save(ArgumentMatchers.any(ProjectEntity.class))).thenReturn( - new ProjectEntity(1, "Test Project", "Test Description", 1L, 1L, true, 100, - OffsetDateTime.MAX)); - // Call controller method - ResponseEntity responseEntity = projectController.createProject(courseId, projectJson, - auth); - - // Verify response - assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + public void testCreateProject() throws Exception { + String url = ApiRoutes.COURSE_BASE_PATH + "/" + courseEntity.getId() + "/projects"; + projectEntity.setVisibleAfter(OffsetDateTime.now().plusDays(1)); + String request = "{\n" + + " \"name\": \"" + projectEntity.getName() + "\",\n" + + " \"description\": \"" + projectEntity.getDescription() + "\",\n" + + " \"groupClusterId\": " + projectEntity.getGroupClusterId() + ",\n" + + " \"visible\": " + projectEntity.isVisible() + ",\n" + + " \"maxScore\": " + projectEntity.getMaxScore() + ",\n" + + " \"deadline\": \"" + projectEntity.getDeadline() + "\",\n" + + " \"visibleAfter\": \"" + projectEntity.getVisibleAfter() + "\"\n" + + "}"; + + /* If all checks succeed, create course */ + when(courseUtil.getCourseIfAdmin(courseEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", courseEntity) + ); + when(projectUtil.checkProjectJson(argThat( + json -> json.getName().equals(projectEntity.getName() ) + && json.getDescription().equals(projectEntity.getDescription()) + && json.getGroupClusterId().equals(projectEntity.getGroupClusterId()) + && json.isVisible().equals(projectEntity.isVisible()) + && json.getMaxScore().equals(projectEntity.getMaxScore()) + && json.getDeadline().toInstant().equals(projectEntity.getDeadline().toInstant()) + ), eq(courseEntity.getId()))).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(projectRepository.save(any())).thenReturn(projectEntity); + when(entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, + getMockUser())) + .thenReturn(projectResponseJson); + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(projectResponseJson))); + verify(projectRepository).save(argThat( + project -> project.getName().equals(projectEntity.getName()) + && project.getDescription().equals(projectEntity.getDescription()) + && project.getGroupClusterId() == projectEntity.getGroupClusterId() + && project.isVisible().equals(projectEntity.isVisible()) + && project.getMaxScore().equals(projectEntity.getMaxScore()) + && project.getDeadline().toInstant().equals(projectEntity.getDeadline().toInstant()) + && project.getVisibleAfter().toInstant().equals(projectEntity.getVisibleAfter().toInstant()) + )); + + /* If groupClusterId is not provided, use invalid groupClusterId */ + reset(projectUtil); + request = "{\n" + + " \"name\": \"" + projectEntity.getName() + "\",\n" + + " \"description\": \"" + projectEntity.getDescription() + "\",\n" + + " \"visible\": " + projectEntity.isVisible() + ",\n" + + " \"maxScore\": " + projectEntity.getMaxScore() + ",\n" + + " \"deadline\": \"" + projectEntity.getDeadline() + "\"\n" + + "}"; + GroupClusterEntity individualClusterEntity = new GroupClusterEntity(courseEntity.getId(), 2, "Individual", 1); + + when(groupClusterRepository.findIndividualClusterByCourseId(courseEntity.getId())).thenReturn( + Optional.of(individualClusterEntity)); + when(projectUtil.checkProjectJson(argThat( + json -> json.getName().equals(projectEntity.getName() ) + && json.getDescription().equals(projectEntity.getDescription()) + && json.getGroupClusterId().equals(individualClusterEntity.getId()) + && json.isVisible().equals(projectEntity.isVisible()) + && json.getMaxScore().equals(projectEntity.getMaxScore()) + && json.getDeadline().toInstant().equals(projectEntity.getDeadline().toInstant()) + ), eq(courseEntity.getId()))).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()); + verify(projectRepository).save(argThat( + project -> project.getName().equals(projectEntity.getName()) + && project.getDescription().equals(projectEntity.getDescription()) + && project.getGroupClusterId() == individualClusterEntity.getId() + && project.isVisible().equals(projectEntity.isVisible()) + && project.getMaxScore().equals(projectEntity.getMaxScore()) + && project.getDeadline().toInstant().equals(projectEntity.getDeadline().toInstant()) + )); + + /* If unexpected error occurs, return internal server error */ + doThrow(new RuntimeException()).when(projectRepository).save(any()); + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isInternalServerError()); + + /* If project json is invalid, return corresponding status */ + reset(projectUtil); + when(projectUtil.checkProjectJson(any(), anyLong())).thenReturn( + new CheckResult<>(HttpStatus.BAD_REQUEST, "", null) + ); + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + + /* If no individual cluster is found, return internal server error */ + when(groupClusterRepository.findIndividualClusterByCourseId(courseEntity.getId())).thenReturn(Optional.empty()); + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isInternalServerError()); + + /* If user no access to course, return corresponding status code */ + when(courseUtil.getCourseIfAdmin(courseEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null) + ); + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isIAmATeapot()); } - @Test - public void testCreateProjectShouldFailReasonCanNotGetCourse() { - // Mock data - long courseId = 1L; - ProjectJson projectJson = - new ProjectJson("Test Project", "Test Description", 1L, 1L, true, 100, OffsetDateTime.MAX); - ProjectEntity projectEntity = - new ProjectEntity(1, "Test Project", "Test Description", 1L, 1L, true, 100, - OffsetDateTime.MAX); - Auth auth = mock(Auth.class); - UserEntity user = new UserEntity(); - user.setId(1L); - when(auth.getUserEntity()).thenReturn(user); - - CourseEntity courseEntity = new CourseEntity(); - courseEntity.setId(courseId); - - CheckResult checkAcces = new CheckResult<>(HttpStatus.FORBIDDEN, "TestIsAdmin", - courseEntity); - - CheckResult checkResult = new CheckResult<>(HttpStatus.OK, "TestProjectJson", null); - - // Mock repository behavior - when(projectRepository.save(projectEntity)).thenReturn(projectEntity); - - when(courseUtil.getCourseIfAdmin(courseId, user)).thenReturn(checkAcces); - when(projectUtil.checkProjectJson(projectJson, courseId)).thenReturn(checkResult); - when(courseRepository.findById(courseId)).thenReturn(Optional.of(courseEntity)); - when(courseUserRepository.findById(ArgumentMatchers.any(CourseUserId.class))).thenReturn( - Optional.of(new CourseUserEntity(1, 1, CourseRelation.course_admin))); - when(groupClusterRepository.findById(projectJson.getGroupClusterId())).thenReturn( - Optional.of(new GroupClusterEntity(1L, 20, "Testcluster", 10))); - when(projectRepository.save(ArgumentMatchers.any(ProjectEntity.class))).thenReturn( - new ProjectEntity(1, "Test Project", "Test Description", 1L, 1L, true, 100, - OffsetDateTime.MAX)); - // Call controller method - ResponseEntity responseEntity = projectController.createProject(courseId, projectJson, - auth); - - // Verify response - assertEquals(HttpStatus.FORBIDDEN, responseEntity.getStatusCode()); + void testPutProjectById() throws Exception { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + projectEntity.getId(); + OffsetDateTime newDeadline = OffsetDateTime.now().plusDays(1); + String request = "{\n" + + " \"name\": \"" + "UpdatedName" + "\",\n" + + " \"description\": \"" + "UpdatedDescription" + "\",\n" + + " \"groupClusterId\": " + groupClusterId * 4 + ",\n" + + " \"visible\": " + false + ",\n" + + " \"maxScore\": " + (projectEntity.getMaxScore() + 33) + ",\n" + + " \"deadline\": \"" + newDeadline + "\"\n" + + "}"; + String orginalName = projectEntity.getName(); + String orginalDescription = projectEntity.getDescription(); + long orginalGroupClusterId = projectEntity.getGroupClusterId(); + boolean orginalVisible = projectEntity.isVisible(); + int orginalMaxScore = projectEntity.getMaxScore(); + OffsetDateTime orginalDeadline = projectEntity.getDeadline(); + ProjectResponseJson updatedJson = new ProjectResponseJson( + new CourseReferenceJson(courseEntity.getName(), "course1URL", courseEntity.getId(), null), + newDeadline, + "UpdatedName", + projectEntity.getId(), + "UpdatedDescription", + "submissionUrl", + "testUrl", + projectEntity.getMaxScore() + 33, + false, + new ProjectProgressJson(0, 0), + 1L, + groupClusterId * 4, + OffsetDateTime.now() + ); + /* If all checks pass, update and return the project */ + when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", projectEntity) + ); + when(projectUtil.checkProjectJson(argThat( + json -> json.getName().equals("UpdatedName") + && json.getDescription().equals("UpdatedDescription") + && json.getGroupClusterId().equals(groupClusterId * 4) + && json.isVisible().equals(false) + && json.getMaxScore().equals(projectEntity.getMaxScore() + 33) + && json.getDeadline().toInstant().equals(newDeadline.toInstant()) + ), eq(courseEntity.getId()))).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(projectRepository.save(any())).thenReturn(projectEntity); + when(courseRepository.findById(courseEntity.getId())).thenReturn(Optional.of(courseEntity)); + when(entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, + getMockUser())) + .thenReturn(updatedJson); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(updatedJson))); + assertEquals(projectEntity.getName(), "UpdatedName"); + assertEquals(projectEntity.getDescription(), "UpdatedDescription"); + assertEquals(projectEntity.getGroupClusterId(), groupClusterId * 4); + assertEquals(projectEntity.isVisible(), false); + assertEquals(projectEntity.getMaxScore(), orginalMaxScore + 33); + assertEquals(projectEntity.getDeadline().toInstant(), newDeadline.toInstant()); + verify(projectRepository, times(1)).save(projectEntity); + projectEntity.setName(orginalName); + projectEntity.setDescription(orginalDescription); + projectEntity.setGroupClusterId(orginalGroupClusterId); + projectEntity.setVisible(orginalVisible); + projectEntity.setMaxScore(orginalMaxScore); + projectEntity.setDeadline(orginalDeadline); + + /* If visible after is passed, update visibility */ + projectEntity.setVisibleAfter(OffsetDateTime.now().minusDays(1)); + request = "{\n" + + " \"name\": \"" + "UpdatedName" + "\",\n" + + " \"description\": \"" + "UpdatedDescription" + "\",\n" + + " \"groupClusterId\": " + groupClusterId * 4 + ",\n" + + " \"visible\": " + false + ",\n" + + " \"maxScore\": " + (projectEntity.getMaxScore() + 33) + ",\n" + + " \"deadline\": \"" + newDeadline + "\",\n" + + " \"visibleAfter\": \"" + OffsetDateTime.now().minusDays(1) + "\"\n" + + "}"; + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)); + + verify(projectRepository, times(2)).save(projectEntity); + assertTrue(projectEntity.isVisible()); + + /* If visible after isn't passed, don't update visibility */ + projectEntity.setVisible(false); + request = "{\n" + + " \"name\": \"" + "UpdatedName" + "\",\n" + + " \"description\": \"" + "UpdatedDescription" + "\",\n" + + " \"groupClusterId\": " + groupClusterId * 4 + ",\n" + + " \"visible\": " + false + ",\n" + + " \"maxScore\": " + (projectEntity.getMaxScore() + 33) + ",\n" + + " \"deadline\": \"" + newDeadline + "\",\n" + + " \"visibleAfter\": \"" + OffsetDateTime.now().plusDays(1) + "\"\n" + + "}"; + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)); + + assertFalse(projectEntity.isVisible()); + + projectEntity.setName(orginalName); + projectEntity.setDescription(orginalDescription); + projectEntity.setGroupClusterId(orginalGroupClusterId); + projectEntity.setVisible(orginalVisible); + projectEntity.setMaxScore(orginalMaxScore); + projectEntity.setDeadline(orginalDeadline); + + /* If groupClusterId is not provided, use invalid groupClusterId */ + reset(projectUtil); + request = "{\n" + + " \"name\": \"" + "UpdatedName" + "\",\n" + + " \"description\": \"" + "UpdatedDescription" + "\",\n" + + " \"visible\": " + false + ",\n" + + " \"maxScore\": " + (projectEntity.getMaxScore() + 33) + ",\n" + + " \"deadline\": \"" + newDeadline + "\"\n" + + "}"; + GroupClusterEntity individualClusterEntity = new GroupClusterEntity(courseEntity.getId(), 2, "Individual", 1); + + when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", projectEntity) + ); + when(groupClusterRepository.findIndividualClusterByCourseId(courseEntity.getId())).thenReturn( + Optional.of(individualClusterEntity)); + when(projectUtil.checkProjectJson(argThat( + json -> json.getName().equals("UpdatedName") + && json.getDescription().equals("UpdatedDescription") + && json.getGroupClusterId().equals(individualClusterEntity.getId()) + && json.isVisible().equals(false) + && json.getMaxScore().equals(projectEntity.getMaxScore() + 33) + && json.getDeadline().toInstant().equals(newDeadline.toInstant()) + ), eq(courseEntity.getId()))).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()); + assertEquals(projectEntity.getName(), "UpdatedName"); + assertEquals(projectEntity.getDescription(), "UpdatedDescription"); + assertEquals(projectEntity.getGroupClusterId(), individualClusterEntity.getId()); + assertEquals(projectEntity.isVisible(), false); + assertEquals(projectEntity.getMaxScore(), orginalMaxScore + 33); + assertEquals(projectEntity.getDeadline().toInstant(), newDeadline.toInstant()); + verify(projectRepository, times(4)).save(projectEntity); + projectEntity.setGroupClusterId(orginalGroupClusterId); + + + + /* If project json is invalid, return corresponding status */ + reset(projectUtil); + when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", projectEntity) + ); + when(projectUtil.checkProjectJson(any(), anyLong())).thenReturn( + new CheckResult<>(HttpStatus.BAD_REQUEST, "", null) + ); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + + /* If individual cluster is not found, return internal server error */ + when(groupClusterRepository.findIndividualClusterByCourseId(courseEntity.getId())).thenReturn(Optional.empty()); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isInternalServerError()); + + /* If user has no acces to project, return corresponding status */ + when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null) + ); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isIAmATeapot()); } - @Test - public void testCreateProjectShouldFailReasonCanNotGetProjectJson() { - // Mock data - long courseId = 1L; - ProjectJson projectJson = - new ProjectJson("Test Project", "Test Description", 1L, 1L, true, 100, OffsetDateTime.MAX); - ProjectEntity projectEntity = - new ProjectEntity(1, "Test Project", "Test Description", 1L, 1L, true, 100, - OffsetDateTime.MAX); - Auth auth = mock(Auth.class); - UserEntity user = new UserEntity(); - user.setId(1L); - when(auth.getUserEntity()).thenReturn(user); - - CourseEntity courseEntity = new CourseEntity(); - courseEntity.setId(courseId); - - CheckResult checkAcces = new CheckResult<>(HttpStatus.OK, "TestIsAdmin", - courseEntity); - - CheckResult checkResult = new CheckResult<>(HttpStatus.FORBIDDEN, "TestProjectJson", - null); - - // Mock repository behavior - when(projectRepository.save(projectEntity)).thenReturn(projectEntity); - - when(courseUtil.getCourseIfAdmin(courseId, user)).thenReturn(checkAcces); - when(projectUtil.checkProjectJson(projectJson, courseId)).thenReturn(checkResult); - when(courseRepository.findById(courseId)).thenReturn(Optional.of(courseEntity)); - when(courseUserRepository.findById(ArgumentMatchers.any(CourseUserId.class))).thenReturn( - Optional.of(new CourseUserEntity(1, 1, CourseRelation.course_admin))); - when(groupClusterRepository.findById(projectJson.getGroupClusterId())).thenReturn( - Optional.of(new GroupClusterEntity(1L, 20, "Testcluster", 10))); - when(projectRepository.save(ArgumentMatchers.any(ProjectEntity.class))).thenReturn( - new ProjectEntity(1, "Test Project", "Test Description", 1L, 1L, true, 100, - OffsetDateTime.MAX)); - // Call controller method - ResponseEntity responseEntity = projectController.createProject(courseId, projectJson, - auth); - - // Verify response - assertEquals(HttpStatus.FORBIDDEN, responseEntity.getStatusCode()); + @Test // Same as above but patch instead of put + void testPatchProjectById() throws Exception { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + projectEntity.getId(); + OffsetDateTime newDeadline = OffsetDateTime.now().plusDays(1); + String request = "{\n" + + " \"name\": \"" + "UpdatedName" + "\",\n" + + " \"description\": \"" + "UpdatedDescription" + "\",\n" + + " \"groupClusterId\": " + groupClusterId * 4 + ",\n" + + " \"visible\": " + false + ",\n" + + " \"maxScore\": " + (projectEntity.getMaxScore() + 33) + ",\n" + + " \"deadline\": \"" + newDeadline + "\",\n" + + " \"visibleAfter\": \"" + OffsetDateTime.now().plusDays(1) + "\"\n" + + "}"; + String orginalName = projectEntity.getName(); + String orginalDescription = projectEntity.getDescription(); + long orginalGroupClusterId = projectEntity.getGroupClusterId(); + boolean orginalVisible = projectEntity.isVisible(); + int orginalMaxScore = projectEntity.getMaxScore(); + OffsetDateTime orginalDeadline = projectEntity.getDeadline(); + ProjectResponseJson updatedJson = new ProjectResponseJson( + new CourseReferenceJson(courseEntity.getName(), "course1URL", courseEntity.getId(), null), + newDeadline, + "UpdatedName", + projectEntity.getId(), + "UpdatedDescription", + "submissionUrl", + "testUrl", + projectEntity.getMaxScore() + 33, + false, + new ProjectProgressJson(0, 0), + 1L, + groupClusterId * 4, + OffsetDateTime.now() + ); + /* If all checks pass, update and return the project */ + when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", projectEntity) + ); + when(projectUtil.checkProjectJson(argThat( + json -> json.getName().equals("UpdatedName") + && json.getDescription().equals("UpdatedDescription") + && json.getGroupClusterId().equals(groupClusterId * 4) + && json.isVisible().equals(false) + && json.getMaxScore().equals(projectEntity.getMaxScore() + 33) + && json.getDeadline().toInstant().equals(newDeadline.toInstant()) + ), eq(courseEntity.getId()))).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(projectRepository.save(any())).thenReturn(projectEntity); + when(courseRepository.findById(courseEntity.getId())).thenReturn(Optional.of(courseEntity)); + when(entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, + getMockUser())) + .thenReturn(updatedJson); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(updatedJson))); + assertEquals(projectEntity.getName(), "UpdatedName"); + assertEquals(projectEntity.getDescription(), "UpdatedDescription"); + assertEquals(projectEntity.getGroupClusterId(), groupClusterId * 4); + assertEquals(projectEntity.isVisible(), false); + assertEquals(projectEntity.getMaxScore(), orginalMaxScore + 33); + assertEquals(projectEntity.getDeadline().toInstant(), newDeadline.toInstant()); + verify(projectRepository, times(1)).save(projectEntity); + projectEntity.setName(orginalName); + projectEntity.setDescription(orginalDescription); + projectEntity.setGroupClusterId(orginalGroupClusterId); + projectEntity.setVisible(orginalVisible); + projectEntity.setMaxScore(orginalMaxScore); + projectEntity.setDeadline(orginalDeadline); + + /* If project json is invalid, return corresponding status */ + reset(projectUtil); + when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", projectEntity) + ); + when(projectUtil.checkProjectJson(any(), anyLong())).thenReturn( + new CheckResult<>(HttpStatus.BAD_REQUEST, "", null) + ); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + + /* Only update the fields that are provided */ + reset(projectUtil); + request = "{\n" + + " \"name\": \"" + "UpdatedName" + "\",\n" + + " \"description\": \"" + "UpdatedDescription" + "\"\n" + + "}"; + when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", projectEntity) + ); + when(projectUtil.checkProjectJson(argThat( + json -> json.getName().equals("UpdatedName") + && json.getDescription().equals("UpdatedDescription") + && json.getGroupClusterId().equals(orginalGroupClusterId) + && json.isVisible().equals(orginalVisible) + && json.getMaxScore().equals(orginalMaxScore) + && json.getDeadline().toInstant().equals(orginalDeadline.toInstant()) + ), eq(courseEntity.getId()))).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()); + assertEquals(projectEntity.getName(), "UpdatedName"); + assertEquals(projectEntity.getDescription(), "UpdatedDescription"); + assertEquals(projectEntity.getGroupClusterId(), orginalGroupClusterId); + assertEquals(projectEntity.isVisible(), orginalVisible); + assertEquals(projectEntity.getMaxScore(), orginalMaxScore); + assertEquals(projectEntity.getDeadline().toInstant(), orginalDeadline.toInstant()); + verify(projectRepository, times(2)).save(projectEntity); + projectEntity.setName(orginalName); + projectEntity.setDescription(orginalDescription); + + /* Different fields not present */ + reset(projectUtil); + request = "{\n" + + " \"deadline\": \"" + newDeadline + "\"\n" + + "}"; + when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", projectEntity) + ); + when(projectUtil.checkProjectJson(argThat( + json -> json.getName().equals(orginalName) + && json.getDescription().equals(orginalDescription) + && json.getGroupClusterId().equals(orginalGroupClusterId) + && json.isVisible().equals(orginalVisible) + && json.getMaxScore().equals(orginalMaxScore) + && json.getDeadline().toInstant().equals(newDeadline.toInstant()) + ), eq(courseEntity.getId()))).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()); + assertEquals(projectEntity.getName(), orginalName); + assertEquals(projectEntity.getDescription(), orginalDescription); + assertEquals(projectEntity.getGroupClusterId(), orginalGroupClusterId); + assertEquals(projectEntity.isVisible(), orginalVisible); + assertEquals(projectEntity.getMaxScore(), orginalMaxScore); + assertEquals(projectEntity.getDeadline().toInstant(), newDeadline.toInstant()); + verify(projectRepository, times(3)).save(projectEntity); + projectEntity.setDeadline(orginalDeadline); + + /* If user has no acces to project, return corresponding status */ + when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null) + ); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isIAmATeapot()); } @Test - public void testCreateProjectShouldFailReasonInternalServer1() { - // Mock data - long courseId = 1L; - ProjectJson projectJson = - new ProjectJson("Test Project", "Test Description", 1L, 1L, true, 100, OffsetDateTime.MAX); - ProjectEntity projectEntity = - new ProjectEntity(1, "Test Project", "Test Description", 1L, 1L, true, 100, - OffsetDateTime.MAX); - Auth auth = mock(Auth.class); - UserEntity user = new UserEntity(); - user.setId(1L); - when(auth.getUserEntity()).thenReturn(user); - - CheckResult checkResult = new CheckResult<>(HttpStatus.FORBIDDEN, "TestProjectJson", - null); - - // Mock repository behavior - when(projectRepository.save(projectEntity)).thenReturn(projectEntity); - - when(projectUtil.checkProjectJson(projectJson, courseId)).thenReturn(checkResult); - when(courseUserRepository.findById(ArgumentMatchers.any(CourseUserId.class))).thenReturn( - Optional.of(new CourseUserEntity(1, 1, CourseRelation.course_admin))); - when(groupClusterRepository.findById(projectJson.getGroupClusterId())).thenReturn( - Optional.of(new GroupClusterEntity(1L, 20, "Testcluster", 10))); - when(projectRepository.save(ArgumentMatchers.any(ProjectEntity.class))).thenReturn( - new ProjectEntity(1, "Test Project", "Test Description", 1L, 1L, true, 100, - OffsetDateTime.MAX)); - // Call controller method - ResponseEntity responseEntity = projectController.createProject(courseId, projectJson, - auth); - - // Verify response - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, responseEntity.getStatusCode()); - } - - - @Test - public void testCreateProjectShouldFailReasonInternalServer2() { - // Mock data - long courseId = 1L; - ProjectJson projectJson = - new ProjectJson("Test Project", "Test Description", null, 1L, true, 100, - OffsetDateTime.MAX); - ProjectEntity projectEntity = - new ProjectEntity(1, "Test Project", "Test Description", 1L, 1L, true, 100, - OffsetDateTime.MAX); - Auth auth = mock(Auth.class); - UserEntity user = new UserEntity(); - user.setId(1L); - when(auth.getUserEntity()).thenReturn(user); - - CourseEntity courseEntity = new CourseEntity(); - courseEntity.setId(courseId); - - CheckResult checkAcces = new CheckResult<>(HttpStatus.OK, "TestIsAdmin", - courseEntity); - - CheckResult checkResult = new CheckResult<>(HttpStatus.OK, "TestProjectJson", - null); - - // Mock repository behavior - when(projectRepository.save(projectEntity)).thenReturn(projectEntity); - when(courseUtil.getCourseIfAdmin(courseId, user)).thenReturn(checkAcces); - when(projectUtil.checkProjectJson(projectJson, courseId)).thenReturn(checkResult); - when(courseRepository.findById(courseId)).thenReturn(Optional.of(courseEntity)); - when(courseUserRepository.findById(ArgumentMatchers.any(CourseUserId.class))).thenReturn( - Optional.of(new CourseUserEntity(1, 1, CourseRelation.course_admin))); - when(groupClusterRepository.findById(projectJson.getGroupClusterId())).thenReturn( - Optional.of(new GroupClusterEntity(1L, 20, "Testcluster", 10))); - when(projectRepository.save(ArgumentMatchers.any(ProjectEntity.class))).thenReturn( - new ProjectEntity(1, "Test Project", "Test Description", 1L, 1L, true, 100, - OffsetDateTime.MAX)); - - // Call controller method - ResponseEntity responseEntity = projectController.createProject(courseId, projectJson, - auth); - - // Verify response - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, responseEntity.getStatusCode()); - assertEquals("Internal error while creating project without group, contact an administrator", - responseEntity.getBody()); - } - - - @Test - void testPutProjectByIdShouldUpdateProject() { - // Mock data - long projectId = 1L; - long userId = 1L; - long courseId = 1L; - Auth auth = mock(Auth.class); - UserEntity user = new UserEntity(); - user.setId(userId); - ProjectEntity projectEntity = new ProjectEntity(1, "Test Project", "old description", 1L, 1L, - false, 100, OffsetDateTime.MAX); - CourseEntity courseEntity = new CourseEntity(); - courseEntity.setId(courseId); - ProjectJson projectJson = new ProjectJson("Test Project", "new description", 1L, 1L, true, 100, - OffsetDateTime.MAX); - - ProjectEntity newProjectEntity = new ProjectEntity(1, "Test Project", "new description", 1L, 1L, - true, 100, OffsetDateTime.MAX); - - CheckResult checkResult = new CheckResult<>(HttpStatus.OK, "TestProject", - projectEntity); - CheckResult checkProject = new CheckResult<>(HttpStatus.OK, "TestProjectJson", null); - - // Mock behavior - when(auth.getUserEntity()).thenReturn(user); - when(projectUtil.getProjectIfAdmin(projectId, user)).thenReturn(checkResult); - when(projectUtil.checkProjectJson(projectJson, projectEntity.getCourseId())).thenReturn( - checkProject); - when(projectRepository.save(projectEntity)).thenReturn(projectEntity); - when(courseRepository.findById(projectId)).thenReturn(Optional.of(courseEntity)); - when(entityToJsonConverter.projectEntityToProjectResponseJson(any(), any(), any())).thenReturn( - new ProjectResponseJson( - new CourseReferenceJson("TestCourse", ApiRoutes.COURSE_BASE_PATH + "/" + 1L, 1L), - OffsetDateTime.MAX, - "Test", 2L, "TestProject", "testUrl", "testUrl", 0, true, new ProjectProgressJson(0, 0), - 1L)); - // Call controller method - ResponseEntity responseEntity = projectController.putProjectById(projectId, projectJson, - auth); - - // Verify response - assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); - - - } - - @Test - void testPatchProjectByIdShouldUpdateProject() { - // Mock data - long projectId = 1L; - long userId = 1L; - long courseId = 1L; - Auth auth = mock(Auth.class); - UserEntity user = new UserEntity(); - user.setId(userId); - ProjectEntity projectEntity = new ProjectEntity(1, "Test Project", "old description", 1L, 1L, - false, 100, OffsetDateTime.MAX); - CourseEntity courseEntity = new CourseEntity(); - courseEntity.setId(courseId); - ProjectJson projectJson = new ProjectJson("Test Project", "new description", null, 1L, true, - 100, - OffsetDateTime.MAX); - - ProjectEntity newProjectEntity = new ProjectEntity(1, "Test Project", "new description", 1L, 1L, - true, 100, OffsetDateTime.MAX); - - CheckResult checkResult = new CheckResult<>(HttpStatus.OK, "TestProject", - projectEntity); - CheckResult checkProject = new CheckResult<>(HttpStatus.OK, "TestProjectJson", null); - - // Mock behavior - when(auth.getUserEntity()).thenReturn(user); - when(projectUtil.getProjectIfAdmin(projectId, user)).thenReturn(checkResult); - when(projectUtil.checkProjectJson(projectJson, projectEntity.getCourseId())).thenReturn( - checkProject); - when(projectRepository.save(projectEntity)).thenReturn(projectEntity); - when(courseRepository.findById(projectId)).thenReturn(Optional.of(courseEntity)); - when(entityToJsonConverter.projectEntityToProjectResponseJson(any(), any(), any())).thenReturn( - new ProjectResponseJson( - new CourseReferenceJson("TestCourse", ApiRoutes.COURSE_BASE_PATH + "/" + 1L, 1L), - OffsetDateTime.MAX, - "Test", 2L, "TestProject", "testUrl", "testUrl", 0, true, new ProjectProgressJson(0, 0), - 1L)); - // Call controller method - ResponseEntity responseEntity = projectController.patchProjectById(projectId, projectJson, - auth); - - // Verify response - assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); - } - - - @Test - void testDeleteProjectByIdShouldDeleteProject() { - // Mock data - long projectId = 1L; - long userId = 1L; - long courseId = 1L; - Auth auth = mock(Auth.class); - UserEntity user = new UserEntity(); - user.setId(userId); - ProjectEntity projectEntity = new ProjectEntity(1, "Test Project", "old description", 1L, 1L, - false, 100, OffsetDateTime.MAX); - CourseEntity courseEntity = new CourseEntity(); - courseEntity.setId(courseId); - - CheckResult projectCheck = new CheckResult<>(HttpStatus.OK, "TestProject", - projectEntity); - CheckResult deleteResult = new CheckResult<>(HttpStatus.OK, "TestDelete", null); - // Mock behavior - when(auth.getUserEntity()).thenReturn(user); - when(projectUtil.getProjectIfAdmin(projectId, user)).thenReturn(projectCheck); - when(commonDatabaseActions.deleteProject(projectId)).thenReturn(deleteResult); - - // Call controller method - ResponseEntity responseEntity = projectController.deleteProjectById(projectId, auth); - - // Verify response - assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); - } - - @Test - void testDeleteProjectByIdShouldFailReasonCanNotGetProject() { - // Mock data - long projectId = 1L; - long userId = 1L; - long courseId = 1L; - Auth auth = mock(Auth.class); - UserEntity user = new UserEntity(); - user.setId(userId); - ProjectEntity projectEntity = new ProjectEntity(1, "Test Project", "old description", 1L, 1L, - false, 100, OffsetDateTime.MAX); - CourseEntity courseEntity = new CourseEntity(); - courseEntity.setId(courseId); - - CheckResult projectCheck = new CheckResult<>(HttpStatus.FORBIDDEN, "TestProject", - projectEntity); - CheckResult deleteResult = new CheckResult<>(HttpStatus.OK, "TestDelete", null); - // Mock behavior - when(auth.getUserEntity()).thenReturn(user); - when(projectUtil.getProjectIfAdmin(projectId, user)).thenReturn(projectCheck); - when(commonDatabaseActions.deleteProject(projectId)).thenReturn(deleteResult); - - // Call controller method - ResponseEntity responseEntity = projectController.deleteProjectById(projectId, auth); - - // Verify response - assertEquals(HttpStatus.FORBIDDEN, responseEntity.getStatusCode()); - } - - @Test - void testGetGroupsOfProjectShouldReturnGroups() { - // Mock data - long projectId = 1L; - long userId = 1L; - long courseId = 1L; - long groupId = 1L; - Auth auth = mock(Auth.class); - UserEntity user = new UserEntity(); - user.setId(userId); - ProjectEntity projectEntity = new ProjectEntity(1, "Test Project", "old description", 1L, 1L, - false, 100, OffsetDateTime.MAX); - CourseEntity courseEntity = new CourseEntity(); - courseEntity.setId(courseId); - List groupIds = new ArrayList<>(); - groupIds.add(groupId); - List groups = new ArrayList<>(); - GroupEntity groupEntity = new GroupEntity(); + void testGetGroupsOfProject() throws Exception { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + projectEntity.getId() + "/groups"; + GroupEntity groupEntity = new GroupEntity("groupName", 1L); + long groupId = 83L; groupEntity.setId(groupId); - groups.add(groupEntity); - - CheckResult projectCheck = new CheckResult<>(HttpStatus.OK, "TestProject", - projectEntity); - CheckResult> groupCheck = new CheckResult<>(HttpStatus.OK, "TestGroups", - groups); - // Mock behavior - when(auth.getUserEntity()).thenReturn(user); - when(projectUtil.canGetProject(projectId, user)).thenReturn(projectCheck); + GroupJson groupJson = new GroupJson(44, groupEntity.getId(), groupEntity.getName(), "groupClusterUrl"); + + /* If all checks pass, return groups */ + when(projectUtil.canGetProject(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", projectEntity) + ); when(clusterUtil.isIndividualCluster(projectEntity.getGroupClusterId())).thenReturn(false); - when(projectRepository.findGroupIdsByProjectId(projectId)).thenReturn(groupIds); + when(projectRepository.findGroupIdsByProjectId(projectEntity.getId())).thenReturn(List.of(groupId)); when(grouprRepository.findById(groupId)).thenReturn(Optional.of(groupEntity)); - // Call controller method - ResponseEntity responseEntity = projectController.getGroupsOfProject(projectId, auth); - - // Verify response - assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + /* User is admin so studentNumber shouldn't be hidden */ + when(projectUtil.isProjectAdmin(projectEntity.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(entityToJsonConverter.groupEntityToJson(groupEntity, false)).thenReturn(groupJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(groupJson)))); + + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, false); + + /* If user is not admin, studentNumber should be hidden */ + when(projectUtil.isProjectAdmin(projectEntity.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + when(entityToJsonConverter.groupEntityToJson(groupEntity, true)).thenReturn(groupJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(groupJson)))); + + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, true); + + /* If inidividual cluster return no content */ + when(clusterUtil.isIndividualCluster(projectEntity.getGroupClusterId())).thenReturn(true); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isNoContent()); + + /* If user has no acces to project, return corresponding status */ + when(projectUtil.canGetProject(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null) + ); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); } - + @Test + void testDeleteProjectById() throws Exception { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + projectEntity.getId(); + + /* If all checks pass, delete project */ + when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.OK, "", projectEntity) + ); + when(commonDatabaseActions.deleteProject(projectEntity.getId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isOk()); + + /* If deleting project fails, return corresponding status */ + when(commonDatabaseActions.deleteProject(projectEntity.getId())).thenReturn(new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "", null)); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isInternalServerError()); + + /* If user has no acces to project, return corresponding status */ + when(projectUtil.getProjectIfAdmin(projectEntity.getId(), getMockUser())).thenReturn( + new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null) + ); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isIAmATeapot()); + } } \ No newline at end of file diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java index e3db0139..40002aad 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java @@ -1,39 +1,87 @@ package com.ugent.pidgeon.controllers; -import com.ugent.pidgeon.model.json.GroupFeedbackJson; -import com.ugent.pidgeon.model.json.GroupJson; -import com.ugent.pidgeon.model.json.LastGroupSubmissionJson; -import com.ugent.pidgeon.model.json.SubmissionJson; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ugent.pidgeon.CustomObjectMapper; +import com.ugent.pidgeon.json.DockerTestFeedbackJson; +import com.ugent.pidgeon.json.GroupFeedbackJson; +import com.ugent.pidgeon.json.GroupJson; +import com.ugent.pidgeon.json.LastGroupSubmissionJson; +import com.ugent.pidgeon.json.SubmissionJson; +import com.ugent.pidgeon.model.submissionTesting.DockerOutput; +import com.ugent.pidgeon.model.submissionTesting.DockerTestOutput; +import com.ugent.pidgeon.model.submissionTesting.SubmissionTemplateModel.SubmissionResult; import com.ugent.pidgeon.postgre.models.FileEntity; import com.ugent.pidgeon.postgre.models.GroupEntity; import com.ugent.pidgeon.postgre.models.GroupFeedbackEntity; import com.ugent.pidgeon.postgre.models.SubmissionEntity; -import com.ugent.pidgeon.postgre.repository.*; -import com.ugent.pidgeon.util.*; +import com.ugent.pidgeon.postgre.models.TestEntity; +import com.ugent.pidgeon.postgre.models.types.DockerTestState; +import com.ugent.pidgeon.postgre.models.types.DockerTestType; +import com.ugent.pidgeon.postgre.repository.FileRepository; +import com.ugent.pidgeon.postgre.repository.GroupFeedbackRepository; +import com.ugent.pidgeon.postgre.repository.GroupRepository; +import com.ugent.pidgeon.postgre.repository.ProjectRepository; +import com.ugent.pidgeon.postgre.repository.SubmissionRepository; +import com.ugent.pidgeon.postgre.repository.TestRepository; +import com.ugent.pidgeon.util.CheckResult; +import com.ugent.pidgeon.util.CommonDatabaseActions; +import com.ugent.pidgeon.util.EntityToJsonConverter; +import com.ugent.pidgeon.util.Filehandler; +import com.ugent.pidgeon.util.GroupUtil; +import com.ugent.pidgeon.util.ProjectUtil; +import com.ugent.pidgeon.util.SubmissionUtil; +import com.ugent.pidgeon.util.TestRunner; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; -import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; - -import java.time.OffsetDateTime; -import java.util.List; -import java.util.Optional; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(MockitoExtension.class) public class SubmissionControllerTest extends ControllerTest { + @Mock private GroupRepository groupRepository; @Mock @@ -46,6 +94,8 @@ public class SubmissionControllerTest extends ControllerTest { private TestRepository testRepository; @Mock private GroupFeedbackRepository groupFeedbackRepository; + @Mock + private TestRunner testRunner; @Mock private SubmissionUtil submissionUtil; @@ -60,6 +110,7 @@ public class SubmissionControllerTest extends ControllerTest { @InjectMocks private SubmissionController submissionController; + private final ObjectMapper objectMapper = CustomObjectMapper.createObjectMapper(); private SubmissionEntity submission; private List groupIds; @@ -70,140 +121,814 @@ public class SubmissionControllerTest extends ControllerTest { private GroupFeedbackEntity groupFeedbackEntity; private MockMultipartFile mockMultipartFile; private FileEntity fileEntity; + private LastGroupSubmissionJson lastGroupSubmissionJson; + private TestEntity testEntity; + + + public static File createTestFile() throws IOException { + // Create a temporary directory + File tempDir = Files.createTempDirectory("SELAB6CANDELETEtest-dir").toFile(); + // Create a temporary file within the directory + File tempFile = File.createTempFile("SELAB6CANDELETEtest-file", ".zip", tempDir); + + // Create some content to write into the zip file + String content = "Hello, this is a test file!"; + byte[] bytes = content.getBytes(); + + // Write the content into a file inside the zip file + try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempFile))) { + ZipEntry entry = new ZipEntry("test.txt"); + zipOut.putNextEntry(entry); + zipOut.write(bytes); + zipOut.closeEntry(); + } + + // Return the File object representing the zip file + return tempFile; + } @BeforeEach public void setup() { - mockMvc = MockMvcBuilders.standaloneSetup(submissionController) - .defaultRequest(MockMvcRequestBuilders.get("/**") - .with(request -> { - request.setUserPrincipal(SecurityContextHolder.getContext().getAuthentication()); - return request; - })) - .build(); - submission = new SubmissionEntity(1L, 1L, 1L, OffsetDateTime.MIN, true, true); - groupIds = List.of(1L); - submissionJson = new SubmissionJson(1L, "projecturl", "groupurl", 1L, - 1L, "fileurl", true, OffsetDateTime.MIN, true, - "structurefeedbackurl", "dockerfeedbackurl"); - groupJson = new GroupJson(1, 1L, "groupname", "groupclusterurl"); - groupFeedbackJson = new GroupFeedbackJson(0F, "feedback", 1L, 1L); - groupEntity = new GroupEntity("groupname", 1L); - groupFeedbackEntity = new GroupFeedbackEntity(1L, 1L, 0F, "feedback"); + setUpController(submissionController); + + submission = new SubmissionEntity(22L, 45L, 99L, OffsetDateTime.MIN, true, true); + submission.setId(56L); + groupIds = List.of(45L); + submissionJson = new SubmissionJson( + submission.getId(), + "projecturl", + "groupurl", + submission.getProjectId(), + submission.getGroupId(), + "fileurl", + true, + OffsetDateTime.MIN, + "structureFeedback", + new DockerTestFeedbackJson(DockerTestType.NONE, "", true), + null, + "artifacturl" + ); + groupEntity = new GroupEntity("groupname", 99L); + groupEntity.setId(submission.getGroupId()); + groupJson = new GroupJson(3, groupEntity.getId(), "groupname", "groupclusterurl"); + + groupFeedbackEntity = new GroupFeedbackEntity(groupEntity.getId(), + submission.getProjectId(), 3F, "feedback"); + groupFeedbackJson = new GroupFeedbackJson(groupFeedbackEntity.getScore(), + groupFeedbackEntity.getFeedback(), groupFeedbackEntity.getGroupId(), + groupFeedbackEntity.getProjectId()); + byte[] fileContent = "Your file content".getBytes(); - mockMultipartFile = new MockMultipartFile("file", "filename.txt", MediaType.TEXT_PLAIN_VALUE, fileContent); + mockMultipartFile = new MockMultipartFile("file", "filename.txt", + MediaType.TEXT_PLAIN_VALUE, fileContent); fileEntity = new FileEntity("name", "dir/name", 1L); + fileEntity.setId(submission.getFileId()); + + lastGroupSubmissionJson = new LastGroupSubmissionJson( + submissionJson, + groupJson, + groupFeedbackJson + ); + + testEntity = new TestEntity( + "dockerImage", + "dockerTestScript", + "dockerTestTemplate", + "structureTemplate" + ); + } @Test public void testGetSubmission() throws Exception { - when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1")) - .andExpect(status().isOk()); + String url = ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId(); + /* all checks succeed */ + when(submissionUtil.canGetSubmission(submission.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); + when(entityToJsonConverter.getSubmissionJson(submission)).thenReturn(submissionJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(submissionJson))); - when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1")) + /* User can't get submission */ + when(submissionUtil.canGetSubmission(submission.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isIAmATeapot()); } @Test public void testGetSubmissions() throws Exception { - List lastGroupSubmissionJsons = List.of(new LastGroupSubmissionJson(submissionJson, groupJson, groupFeedbackJson)); - when(projectUtil.isProjectAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - when(projectRepository.findGroupIdsByProjectId(anyLong())).thenReturn(groupIds); - when(groupRepository.findById(anyLong())).thenReturn(Optional.of(groupEntity)); - when(entityToJsonConverter.groupEntityToJson(any())).thenReturn(groupJson); - when(groupFeedbackRepository.getGroupFeedback(anyLong(), anyLong())).thenReturn(groupFeedbackEntity); - when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(anyLong(), anyLong())).thenReturn(Optional.of(submission)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.PROJECT_BASE_PATH + "/1/submissions")) - .andExpect(status().isOk()); + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + submission.getProjectId() + "/submissions"; + /* all checks succeed */ + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(projectRepository.findGroupIdsByProjectId(submission.getProjectId())).thenReturn(groupIds); + when(groupRepository.findById(groupIds.get(0))).thenReturn(Optional.of(groupEntity)); + when(entityToJsonConverter.groupEntityToJson(groupEntity, false)).thenReturn(groupJson); + when(groupFeedbackRepository.getGroupFeedback(groupEntity.getId(), submission.getProjectId())).thenReturn(groupFeedbackEntity); + when(entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity)).thenReturn(groupFeedbackJson); + when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(submission.getProjectId(), groupEntity.getId())).thenReturn(Optional.of(submission)); + when(entityToJsonConverter.getSubmissionJson(submission)).thenReturn(submissionJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(lastGroupSubmissionJson)))); - when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(anyLong(), anyLong())).thenReturn(Optional.empty()); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.PROJECT_BASE_PATH + "/1/submissions")) - .andExpect(status().isOk()); + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, false); - when(groupFeedbackRepository.getGroupFeedback(anyLong(), anyLong())).thenReturn(null); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.PROJECT_BASE_PATH + "/1/submissions")) - .andExpect(status().isOk()); + /* no submission */ + when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(submission.getProjectId(), groupEntity.getId())).thenReturn(Optional.empty()); + lastGroupSubmissionJson.setSubmission(null); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(lastGroupSubmissionJson)))); + + /* no feedback */ + when(groupFeedbackRepository.getGroupFeedback(groupEntity.getId(), submission.getProjectId())).thenReturn(null); + lastGroupSubmissionJson.setFeedback(null); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(lastGroupSubmissionJson)))); - when(groupRepository.findById(anyLong())).thenReturn(Optional.empty()); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.PROJECT_BASE_PATH + "/1/submissions")) + /* Unexpected error */ + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())).thenThrow(new RuntimeException()); + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isInternalServerError()); - when(projectUtil.isProjectAdmin(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.PROJECT_BASE_PATH + "/1/submissions")) - .andExpect(status().isBadRequest()); + /* group not found */ + reset(projectUtil); + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(groupRepository.findById(groupIds.get(0))).thenReturn(Optional.empty()); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isInternalServerError()); + + /* User can't get project */ + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); - when(projectUtil.isProjectAdmin(anyLong(), any())).thenThrow(new RuntimeException()); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.PROJECT_BASE_PATH + "/1/submissions")) - .andExpect(status().isInternalServerError()); } @Test public void testSubmitFile() throws Exception { - //TODO: dit ook een correcte test laten uitvoeren met dummyfiles - when(submissionUtil.checkOnSubmit(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", 1L)); - when(fileRepository.save(any())).thenReturn(fileEntity); - when(submissionRepository.save(any())).thenReturn(submission); - mockMvc.perform(MockMvcRequestBuilders.multipart(ApiRoutes.PROJECT_BASE_PATH + "/1/submit") - .file(mockMultipartFile)) + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + submission.getProjectId() + "/submit"; + /* all checks succeed */ + when(submissionUtil.checkOnSubmit(submission.getProjectId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupEntity.getId())); + when(fileRepository.save(argThat( + file -> file.getUploadedBy() == getMockUser().getId() + ))).thenReturn(fileEntity); + when(submissionRepository.save(argThat( + sub -> { + Duration duration = Duration.between(sub.getSubmissionTime(), OffsetDateTime.now()); + return sub.getProjectId() == submission.getProjectId() && + sub.getGroupId() == groupEntity.getId() && + sub.getFileId() == fileEntity.getId() && + duration.getSeconds() < 2; + } + ))).thenReturn(submission); + Path path = Path.of(fileEntity.getPath()); + Path artifactPath = Path.of("artifactPath"); + File file = createTestFile(); + try (MockedStatic mockedFileHandler = mockStatic(Filehandler.class)) { + mockedFileHandler.when(() -> Filehandler.getSubmissionPath(submission.getProjectId(), groupEntity.getId(), submission.getId())).thenReturn(path); + mockedFileHandler.when(() -> Filehandler.saveFile(path, mockMultipartFile, Filehandler.SUBMISSION_FILENAME)).thenReturn(file); + mockedFileHandler.when(() -> Filehandler.getSubmissionArtifactPath(anyLong(), anyLong(), anyLong())).thenReturn(artifactPath); + + when(testRunner.runStructureTest(any(), eq(testEntity), any())).thenReturn(null); + when(testRunner.runDockerTest(any(), eq(testEntity), eq(artifactPath), any(), eq(submission.getProjectId()))).thenReturn(null); + + when(entityToJsonConverter.getSubmissionJson(submission)).thenReturn(submissionJson); + + when(testRepository.findByProjectId(submission.getProjectId())).thenReturn(Optional.of(testEntity)); + when(entityToJsonConverter.getSubmissionJson(submission)).thenReturn(submissionJson); + + + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(submissionJson))); + + /* assertEquals(DockerTestState.running, submission.getDockerTestState()); */ // This executes too quickly so we can't test this + + Thread.sleep(2000); + + // File repository needs to save again after setting path + verify(fileRepository, times(1)).save(argThat( + f -> f.getId() == fileEntity.getId() && f.getPath().equals(fileEntity.getPath()) + )); + + // Submissions should be update 3 times, once for the initial save, once for structuretest, once for docker test. + // The first one is being checked by the when(...) + verify(submissionRepository, times(2)).save(argThat( + s -> s.getId() == submission.getId() + )); + + assertEquals(DockerTestState.aborted, submission.getDockerTestState()); + + /* structuretestResult isn't null */ + submission.setStructureAccepted(false); + submission.setStructureFeedback(""); + SubmissionResult submissionResult = new SubmissionResult(true, "structureFeedback-test"); + when(testRunner.runStructureTest(any(), eq(testEntity), any())).thenReturn(submissionResult); + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(submissionJson))); + + assertTrue(submission.getStructureAccepted()); + assertEquals("structureFeedback-test", submission.getStructureFeedback()); + + /* Correctly updates the dockertype */ + testEntity.setDockerTestTemplate("dockerTestTemplate"); + testEntity.setDockerTestScript("dockerTestScript"); + submission.setDockerType(DockerTestType.NONE); + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(submissionJson))); + assertEquals(DockerTestType.TEMPLATE, submission.getDockerTestType()); + + testEntity.setDockerTestTemplate(null); + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(submissionJson))); + assertEquals(DockerTestType.SIMPLE, submission.getDockerTestType()); + + testEntity.setDockerTestScript(null); + testEntity.setDockerTestTemplate(null); + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(submissionJson))); + assertEquals(DockerTestType.NONE, submission.getDockerTestType()); + + /* A valid docker result is returned */ + testEntity.setDockerImage("dockerImage"); + testEntity.setDockerTestScript("dockerTestScript"); + DockerOutput dockerOutput = new DockerTestOutput( List.of("dockerFeedback-test"), true); + when(testRunner.runDockerTest(any(), eq(testEntity), eq(artifactPath), any(), eq(submission.getProjectId()))).thenReturn(dockerOutput); + submission.setDockerAccepted(false); + submission.setDockerFeedback("dockerFeedback-test"); + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(submissionJson))); + + Thread.sleep(2000); + + assertTrue(submission.getDockerAccepted()); + assertEquals("dockerFeedback-test", submission.getDockerFeedback()); + assertEquals(DockerTestState.finished, submission.getDockerTestState()); + + /* No testEntity */ + when(testRepository.findByProjectId(submission.getProjectId())).thenReturn(Optional.empty()); + reset(testRunner); + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(submissionJson))); + verify(testRunner, times(0)).runStructureTest(any(), eq(testEntity), any()); + verify(testRunner, times(0)).runDockerTest(any(), eq(testEntity), eq(artifactPath), any(), eq(submission.getProjectId())); + + /* Unexpected error */ + reset(fileRepository); + when(fileRepository.save(any())).thenThrow(new RuntimeException()); + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile)) .andExpect(status().isInternalServerError()); + + /* CheckOnSUbmit fails */ + when(submissionUtil.checkOnSubmit(submission.getProjectId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile)) + .andExpect(status().isIAmATeapot()); + + + + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } @Test public void testGetSubmissionFile() throws Exception { - //TODO: dit ook een correcte test laten uitvoeren met dummyfiles - when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); - when(fileRepository.findById(anyLong())).thenReturn(Optional.of(fileEntity)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/file")) - .andExpect(status().isInternalServerError()); + try (MockedStatic mockedFileHandler = mockStatic(Filehandler.class)) { + String url = ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId() + "/file"; + Path path = Path.of(fileEntity.getPath()); + File file = createTestFile(); + Resource mockedResource = new FileSystemResource(file); + + /* all checks succeed */ + when(submissionUtil.canGetSubmission(submission.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); + when(fileRepository.findById(submission.getFileId())).thenReturn(Optional.of(fileEntity)); + mockedFileHandler.when(() -> Filehandler.getFileAsResource(path)).thenReturn(mockedResource); + mockedFileHandler.when(() -> Filehandler.getZipFileAsResponse(path, fileEntity.getName())).thenCallRealMethod(); + + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string( + HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileEntity.getName())) + .andExpect(content().bytes(mockedResource.getInputStream().readAllBytes())); + - when(fileRepository.findById(anyLong())).thenReturn(Optional.empty()); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/file")) + /* file not found */ + when(fileRepository.findById(submission.getFileId())).thenReturn(Optional.empty()); + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isNotFound()); - when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/file")) - .andExpect(status().isForbidden()); + /* User can't get submission */ + when(submissionUtil.canGetSubmission(submission.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); + } } @Test - public void testGetStructureFeedback() throws Exception { - when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/structurefeedback")) - .andExpect(status().isOk()); + public void testGetSubmissionArtifacts() throws Exception { + try (MockedStatic mockedFileHandler = mockStatic(Filehandler.class)) { + String url = ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId() + "/artifacts"; + Path path = Path.of("artifactPath"); + File file = createTestFile(); + Resource mockedResource = new FileSystemResource(file); + + /* all checks succeed */ + when(submissionUtil.canGetSubmission(submission.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); + mockedFileHandler.when(() -> Filehandler.getSubmissionArtifactPath(submission.getProjectId(), submission.getGroupId(), submission.getId())).thenReturn(path); + mockedFileHandler.when(() -> Filehandler.getFileAsResource(path)).thenReturn(mockedResource); + + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string( + HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=artifacts.zip")) + .andExpect(content().bytes(mockedResource.getInputStream().readAllBytes())); + + + /* Resource not found */ + mockedFileHandler.when(() -> Filehandler.getFileAsResource(path)).thenReturn(null); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isNotFound()); + + /* Unexpected error */ + mockedFileHandler.reset(); + mockedFileHandler.when(() -> Filehandler.getSubmissionArtifactPath(submission.getProjectId(), submission.getGroupId(), submission.getId())).thenThrow(new RuntimeException()); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isInternalServerError()); - when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/structurefeedback")) + /* User can't get submission */ + when(submissionUtil.canGetSubmission(submission.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isIAmATeapot()); + } } @Test - public void testGetDockerFeedback() throws Exception { - when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/dockerfeedback")) + public void testDeleteSubmissionById() throws Exception { + String url = ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId(); + /* all checks succeed */ + when(submissionUtil.canDeleteSubmission(submission.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) .andExpect(status().isOk()); + + verify(commonDatabaseActions, times(1)).deleteSubmissionById(submission.getId()); + + /* User can't delete submission */ + when(submissionUtil.canDeleteSubmission(submission.getId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isIAmATeapot()); } @Test - public void testDeleteSubmissionById() throws Exception { - when(submissionUtil.canDeleteSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.SUBMISSION_BASE_PATH + "/1")) - .andExpect(status().isOk()); + public void testGetSubmissionsForGroup() throws Exception { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + submission.getProjectId() + "/submissions/" + groupEntity.getId(); + /* all checks succeed */ + when(groupUtil.canGetProjectGroupData(groupEntity.getId(), submission.getProjectId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(submissionRepository.findByProjectIdAndGroupId(submission.getProjectId(), groupEntity.getId())).thenReturn(List.of(submission)); + when(entityToJsonConverter.getSubmissionJson(submission)).thenReturn(submissionJson); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(submissionJson)))); + + /* No submissions */ + when(submissionRepository.findByProjectIdAndGroupId(submission.getProjectId(), groupEntity.getId())).thenReturn(List.of()); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[]")); - when(submissionUtil.canDeleteSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.delete(ApiRoutes.SUBMISSION_BASE_PATH + "/1")) + /* User can't get group */ + when(groupUtil.canGetProjectGroupData(groupEntity.getId(), submission.getProjectId(), getMockUser())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isIAmATeapot()); } @Test - public void testGetSubmissionsForGroup() throws Exception { - when(groupUtil.canGetProjectGroupData(anyLong(), anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.PROJECT_BASE_PATH + "/1/submissions/1")) - .andExpect(status().isOk()); + public void testGetAdminSubmissions() { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + submission.getProjectId() + "/adminsubmissions"; - when(groupUtil.canGetProjectGroupData(anyLong(), anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.PROJECT_BASE_PATH + "/1/submissions/1")) + /* all checks succeed */ + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(submissionRepository.findAdminSubmissionsByProjectId(submission.getProjectId())) + .thenReturn(List.of(submission)); + when(entityToJsonConverter.getSubmissionJson(submission)).thenReturn(submissionJson); + + try { + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(submissionJson)))); + } catch (Exception e) { + e.printStackTrace(); + } + + /* No submissions */ + when(submissionRepository.findAdminSubmissionsByProjectId(submission.getProjectId())) + .thenReturn(List.of()); + + try { + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[]")); + } catch (Exception e) { + e.printStackTrace(); + } + + /* User can't get project */ + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + + try { + mockMvc.perform(MockMvcRequestBuilders.get(url)) .andExpect(status().isIAmATeapot()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + public void testGetSubmissionsFiles() { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + submission.getProjectId() + "/submissions/files"; + + /* Create temp zip file for submission */ + File file = null; + try { + file = createTestFile(); + } catch (IOException e) { + e.printStackTrace(); + } + assertNotNull(file); + fileEntity.setPath(file.getAbsolutePath()); + + + /* All checks succeed */ + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + + when(projectRepository.findGroupIdsByProjectId(submission.getProjectId())).thenReturn(groupIds); + when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(submission.getProjectId(), groupEntity.getId())).thenReturn(Optional.of(submission)); + when(fileRepository.findById(submission.getFileId())).thenReturn(Optional.of(fileEntity)); + + try { + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=allsubmissions.zip")) + .andReturn(); + + byte[] content = mvcResult.getResponse().getContentAsByteArray(); + + boolean groupzipfound = false; + boolean fileszipfound = false; + /* Check contents of file */ + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(content))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals("group-" + submission.getGroupId() + ".zip")) { + groupzipfound = true; + /* Check if there is a zipfile inside the zipfile with name 'files.zip' */ + + // Create a new ByteArrayOutputStream to store the content of the group zip file + ByteArrayOutputStream groupZipContent = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = zis.read(buffer)) != -1) { + groupZipContent.write(buffer, 0, bytesRead); + } + + byte[] groupZipContentBytes = groupZipContent.toByteArray(); + + ByteArrayInputStream groupZipByteStream = new ByteArrayInputStream(groupZipContentBytes); + + // Create a new ZipInputStream using the ByteArrayInputStream + try (ZipInputStream groupZipInputStream = new ZipInputStream(groupZipByteStream)) { + ZipEntry groupEntry; + while ((groupEntry = groupZipInputStream.getNextEntry()) != null) { + if (groupEntry.getName().equals("files.zip")) { + fileszipfound = true; + } + } + } + } + } + } + assertTrue(groupzipfound); + assertTrue(fileszipfound); + + } catch (Exception e) { + e.printStackTrace(); + } + + /* With arifact */ + url += "?artifacts=true"; + // Create artifact tempfile + File artifactFile = null; + try { + artifactFile = createTestFile(); + } catch (IOException e) { + e.printStackTrace(); + } + assertNotNull(artifactFile); + + try (MockedStatic mockedFileHandler = mockStatic(Filehandler.class)) { + mockedFileHandler.when(() -> Filehandler. + getSubmissionArtifactPath(submission.getProjectId(), groupEntity.getId(), submission.getId())) + .thenReturn(Path.of(artifactFile.getAbsolutePath())); + mockedFileHandler.when(() -> Filehandler.addExistingZip(any(), any(), any())) + .thenCallRealMethod(); + mockedFileHandler.when(() -> Filehandler.getZipFileAsResponse(any(), any())) + .thenCallRealMethod(); + mockedFileHandler.when(() -> Filehandler.getFileAsResource(any())) + .thenCallRealMethod(); + + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=allsubmissions.zip")) + .andReturn(); + + byte[] content = mvcResult.getResponse().getContentAsByteArray(); + + boolean groupzipfound = false; + boolean fileszipfound = false; + boolean artifactzipfound = false; + + /* Check contents of file */ + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(content))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals("group-" + submission.getGroupId() + ".zip")) { + groupzipfound = true; + /* Check if there is a zipfile inside the zipfile with name 'files.zip' */ + + // Create a new ByteArrayOutputStream to store the content of the group zip file + ByteArrayOutputStream groupZipContent = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = zis.read(buffer)) != -1) { + groupZipContent.write(buffer, 0, bytesRead); + } + + byte[] groupZipContentBytes = groupZipContent.toByteArray(); + + ByteArrayInputStream groupZipByteStream = new ByteArrayInputStream(groupZipContentBytes); + + // Create a new ZipInputStream using the ByteArrayInputStream + try (ZipInputStream groupZipInputStream = new ZipInputStream(groupZipByteStream)) { + ZipEntry groupEntry; + while ((groupEntry = groupZipInputStream.getNextEntry()) != null) { + if (groupEntry.getName().equals("files.zip")) { + fileszipfound = true; + } else if (groupEntry.getName().equals("artifacts.zip")) { + artifactzipfound = true; + } + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + assertTrue(groupzipfound); + assertTrue(fileszipfound); + assertTrue(artifactzipfound); + } catch (Exception e) { + e.printStackTrace(); + } + + /* With artifact but no artifact file, should just return the zip without an artifacts.zip */ + try (MockedStatic mockedFileHandler = mockStatic(Filehandler.class)) { + mockedFileHandler.when(() -> Filehandler. + getSubmissionArtifactPath(submission.getProjectId(), groupEntity.getId(), submission.getId())) + .thenReturn(Path.of("nonexistent")); + mockedFileHandler.when(() -> Filehandler.addExistingZip(any(), any(), any())) + .thenCallRealMethod(); + mockedFileHandler.when(() -> Filehandler.getZipFileAsResponse(any(), any())) + .thenCallRealMethod(); + mockedFileHandler.when(() -> Filehandler.getFileAsResource(any())) + .thenCallRealMethod(); + + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=allsubmissions.zip")) + .andReturn(); + + byte[] content = mvcResult.getResponse().getContentAsByteArray(); + + boolean groupzipfound = false; + boolean fileszipfound = false; + boolean artifactzipfound = false; + + /* Check contents of file */ + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(content))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals("group-" + submission.getGroupId() + ".zip")) { + groupzipfound = true; + /* Check if there is a zipfile inside the zipfile with name 'files.zip' */ + + // Create a new ByteArrayOutputStream to store the content of the group zip file + ByteArrayOutputStream groupZipContent = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = zis.read(buffer)) != -1) { + groupZipContent.write(buffer, 0, bytesRead); + } + + byte[] groupZipContentBytes = groupZipContent.toByteArray(); + + ByteArrayInputStream groupZipByteStream = new ByteArrayInputStream(groupZipContentBytes); + + // Create a new ZipInputStream using the ByteArrayInputStream + try (ZipInputStream groupZipInputStream = new ZipInputStream(groupZipByteStream)) { + ZipEntry groupEntry; + while ((groupEntry = groupZipInputStream.getNextEntry()) != null) { + if (groupEntry.getName().equals("files.zip")) { + fileszipfound = true; + } else if (groupEntry.getName().equals("artifacts.zip")) { + artifactzipfound = true; + } + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + assertTrue(groupzipfound); + assertTrue(fileszipfound); + assertFalse(artifactzipfound); + + } catch (Exception e) { + e.printStackTrace(); + } + + /* With artifact parameter false */ + url = url.replace("?artifacts=true", "?artifacts=false"); + + try { + + + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=allsubmissions.zip")) + .andReturn(); + + byte[] content = mvcResult.getResponse().getContentAsByteArray(); + + boolean groupzipfound = false; + boolean fileszipfound = false; + boolean artifactzipfound = false; + /* Check contents of file */ + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(content))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals("group-" + submission.getGroupId() + ".zip")) { + groupzipfound = true; + /* Check if there is a zipfile inside the zipfile with name 'files.zip' */ + + // Create a new ByteArrayOutputStream to store the content of the group zip file + ByteArrayOutputStream groupZipContent = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = zis.read(buffer)) != -1) { + groupZipContent.write(buffer, 0, bytesRead); + } + + byte[] groupZipContentBytes = groupZipContent.toByteArray(); + + ByteArrayInputStream groupZipByteStream = new ByteArrayInputStream(groupZipContentBytes); + + // Create a new ZipInputStream using the ByteArrayInputStream + try (ZipInputStream groupZipInputStream = new ZipInputStream(groupZipByteStream)) { + ZipEntry groupEntry; + while ((groupEntry = groupZipInputStream.getNextEntry()) != null) { + if (groupEntry.getName().equals("files.zip")) { + fileszipfound = true; + } else if (groupEntry.getName().equals("artifacts.zip")) { + artifactzipfound = true; + } + } + } + } + } + } + assertTrue(groupzipfound); + assertTrue(fileszipfound); + + } catch (Exception e) { + e.printStackTrace(); + } + + /* File not found, should return empty zip */ + when(fileRepository.findById(submission.getFileId())).thenReturn(Optional.empty()); + + try { + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=allsubmissions.zip")) + .andReturn(); + + byte[] content = mvcResult.getResponse().getContentAsByteArray(); + + boolean zipfound = false; + /* Check contents of file */ + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(content))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + zipfound = true; + } + } + assertFalse(zipfound); + + } catch (Exception e) { + e.printStackTrace(); + } + + /* Submission not found, should return empty zip */ + when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(submission.getProjectId(), groupEntity.getId())).thenReturn(Optional.empty()); + + try { + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/zip")) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=allsubmissions.zip")) + .andReturn(); + + byte[] content = mvcResult.getResponse().getContentAsByteArray(); + + boolean zipfound = false; + /* Check contents of file */ + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(content))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + zipfound = true; + } + } + assertFalse(zipfound); + + } catch (Exception e) { + e.printStackTrace(); + } + + /* Not admin */ + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + + try { + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); + } catch (Exception e) { + e.printStackTrace(); + } + + /* Unexecpted error */ + when(projectUtil.isProjectAdmin(submission.getProjectId(), getMockUser())) + .thenThrow(new RuntimeException()); + + try { + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isInternalServerError()); + } catch (Exception e) { + e.printStackTrace(); + } } } \ No newline at end of file diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java index ce87d826..71f73418 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/TestControllerTest.java @@ -1,52 +1,903 @@ package com.ugent.pidgeon.controllers; -import com.ugent.pidgeon.postgre.models.GroupEntity; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ugent.pidgeon.CustomObjectMapper; +import com.ugent.pidgeon.json.TestJson; +import com.ugent.pidgeon.json.TestUpdateJson; +import com.ugent.pidgeon.postgre.models.FileEntity; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.TestEntity; import com.ugent.pidgeon.postgre.repository.FileRepository; import com.ugent.pidgeon.postgre.repository.ProjectRepository; import com.ugent.pidgeon.postgre.repository.TestRepository; +import com.ugent.pidgeon.util.CheckResult; +import com.ugent.pidgeon.util.CommonDatabaseActions; +import com.ugent.pidgeon.util.EntityToJsonConverter; +import com.ugent.pidgeon.util.FileUtil; import com.ugent.pidgeon.util.Filehandler; +import com.ugent.pidgeon.util.Pair; +import com.ugent.pidgeon.util.TestUtil; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.OffsetDateTime; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Optional; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(MockitoExtension.class) public class TestControllerTest extends ControllerTest{ @Mock - private ProjectRepository projectRepository; + private TestUtil testUtil; + @Mock + private TestRepository testRepository; + @Mock + private ProjectRepository projectRepository; @Mock private FileRepository fileRepository; @Mock - private TestRepository testRepository; + private EntityToJsonConverter entityToJsonConverter; + @Mock + private CommonDatabaseActions commonDatabaseActions; + + @Mock + private FileUtil fileUtil; + + @InjectMocks - private ControllerTest testController; + private TestController testController; + + + private final ObjectMapper objectMapper = CustomObjectMapper.createObjectMapper(); + + private MockMultipartFile mockMultipartFile; + private FileEntity fileEntity; + private ProjectEntity project; + private TestEntity test; + private TestJson testJson; @BeforeEach public void setup() { - mockMvc = MockMvcBuilders.standaloneSetup(testController) - .defaultRequest(MockMvcRequestBuilders.get("/**") - .with(request -> { request.setUserPrincipal(SecurityContextHolder.getContext().getAuthentication()); return request; })) - .build(); + setUpController(testController); + + project = new ProjectEntity( + 67, + "projectName", + "projectDescription", + 5, + 38L, + true, + 34, + OffsetDateTime.now() + ); + project.setId(64); + + test = new TestEntity( + "dockerImageBasic", + "dockerTestScriptBasic", + "dockerTestTemplateBasic", + "structureTemplateBasic" + ); + test.setId(990); + testJson = new TestJson( + "projectUrl", + test.getDockerImage(), + test.getDockerTestScript(), + test.getDockerTestTemplate(), + test.getStructureTemplate(), + "extraFilesUrl", + "extraFilesName" + + ); + + byte[] fileContent = "Your file content".getBytes(); + mockMultipartFile = new MockMultipartFile("file", "filename.txt", + MediaType.TEXT_PLAIN_VALUE, fileContent); + + fileEntity = new FileEntity("name", "dir/name", 1L); + fileEntity.setId(77L); + } + + @Test + public void testUpdateTest() throws Exception { + when(testRepository.imageIsUsed(any())).thenReturn(true); + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + project.getId() + "/tests"; + String dockerImage = "dockerImage"; + String dockerTestScript = "dockerTestScript"; + String dockerTestTemplate = "dockerTestTemplate"; + String structureTemplate = "structureTemplate"; + + TestUpdateJson testUpdateJson = new TestUpdateJson( + dockerImage, + dockerTestScript, + dockerTestTemplate, + structureTemplate + ); + + TestJson testJson = new TestJson( + "projectUrl", + dockerImage, + dockerTestScript, + dockerTestTemplate, + structureTemplate, + "extraFilesUrl", + "extraFilesName" + ); + /* All checks succeed */ + when(testUtil.checkForTestUpdate( + eq(project.getId()), + eq(getMockUser()), + eq(dockerImage), + eq(dockerTestScript), + eq(dockerTestTemplate), + eq(structureTemplate), + eq(HttpMethod.POST) + )).thenReturn(new CheckResult<>(HttpStatus.OK, "",new Pair<>(null, project))); + + when(testRepository.save(argThat( + testEntity -> testEntity.getDockerImage().equals(dockerImage) && + testEntity.getDockerTestScript().equals(dockerTestScript) && + testEntity.getDockerTestTemplate().equals(dockerTestTemplate) && + testEntity.getStructureTemplate().equals(structureTemplate) + ))).thenReturn(test); + + when(entityToJsonConverter.testEntityToTestJson(test, project.getId())).thenReturn(testJson); + + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testUpdateJson)) + ).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + verify(projectRepository, times(1)).save(project); + assertEquals(test.getId(), project.getTestId()); + + /* fields are blank */ + String dockerImageBlank = ""; + String dockerTestScriptBlank = ""; + String dockerTemplateBlank = ""; + String structureTemplateBlank = ""; + + testJson = new TestJson( + "projectUrl", + null, + null, + null, + null, + "extraFilesUrl", + "extraFilesName" + ); + testUpdateJson = new TestUpdateJson( + dockerImageBlank, + dockerTestScriptBlank, + dockerTemplateBlank, + structureTemplateBlank + ); + reset(testUtil); + when(testUtil.checkForTestUpdate( + eq(project.getId()), + eq(getMockUser()), + eq(null), + eq(null), + eq(null), + eq(null), + eq(HttpMethod.POST) + )).thenReturn(new CheckResult<>(HttpStatus.OK, "",new Pair<>(null, project))); + + reset(testRepository); + when(testRepository.save(argThat( + testEntity -> testEntity.getDockerImage() == null && + testEntity.getDockerTestScript() == null && + testEntity.getDockerTestTemplate() == null && + testEntity.getStructureTemplate() == null + ))).thenReturn(test); + + when(entityToJsonConverter.testEntityToTestJson(test, project.getId())).thenReturn(testJson); + + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testUpdateJson)) + ).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + /* Fields are null */ + String dockerImageNull = null; + String dockerTestScriptNull = null; + String dockerTemplateNull = null; + String structureTemplateNull = null; + + testUpdateJson = new TestUpdateJson( + dockerImageNull, + dockerTestScriptNull, + dockerTemplateNull, + structureTemplateNull + ); + + when(testRepository.imageIsUsed(any())).thenReturn(true); + + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testUpdateJson)) + ).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + /* Check fails */ + when(testUtil.checkForTestUpdate( + eq(project.getId()), + eq(getMockUser()), + eq(dockerImage), + eq(dockerTestScript), + eq(dockerTestTemplate), + eq(structureTemplate), + eq(HttpMethod.POST) + )).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)); + + testUpdateJson = new TestUpdateJson( + dockerImage, + dockerTestScript, + dockerTestTemplate, + structureTemplate + ); + + mockMvc.perform(MockMvcRequestBuilders.post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testUpdateJson)) + ).andExpect(status().isIAmATeapot()); + } + + @Test + public void testPutTest() throws Exception { + when(testRepository.imageIsUsed(any())).thenReturn(true); + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + project.getId() + "/tests"; + + String originalDockerImage = test.getDockerImage(); + String originalDockerTestScript = test.getDockerTestScript(); + String originalDockerTestTemplate = test.getDockerTestTemplate(); + String originalStructureTemplate = test.getStructureTemplate(); + + String dockerImage = "dockerImage"; + String dockerTestScript = "dockerTestScript"; + String dockerTestTemplate = "dockerTestTemplate"; + String structureTemplate = "structureTemplate"; + + TestUpdateJson testUpdateJson = new TestUpdateJson( + dockerImage, + dockerTestScript, + dockerTestTemplate, + structureTemplate + ); + + test.setDockerImage(null); + test.setDockerTestScript(null); + test.setDockerTestTemplate(null); + + TestJson testJson = new TestJson( + "projectUrl", + dockerImage, + dockerTestScript, + dockerTestTemplate, + structureTemplate, + "extraFilesUrl", + "extraFilesName" + ); + /* All checks succeed */ + when(testUtil.checkForTestUpdate( + eq(project.getId()), + eq(getMockUser()), + eq(dockerImage), + eq(dockerTestScript), + eq(dockerTestTemplate), + eq(structureTemplate), + eq(HttpMethod.PUT) + )).thenReturn(new CheckResult<>(HttpStatus.OK, "",new Pair<>(test, project))); + + when(testRepository.save(test)).thenReturn(test); + + when(entityToJsonConverter.testEntityToTestJson(test, project.getId())).thenReturn(testJson); + + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testUpdateJson)) + ).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + verify(projectRepository, times(1)).save(project); + assertEquals(test.getId(), project.getTestId()); + assertEquals(dockerImage, test.getDockerImage()); + assertEquals(dockerTestScript, test.getDockerTestScript()); + assertEquals(dockerTestTemplate, test.getDockerTestTemplate()); + assertEquals(structureTemplate, test.getStructureTemplate()); + + test.setDockerImage(originalDockerImage); + test.setDockerTestScript(originalDockerTestScript); + test.setDockerTestTemplate(originalDockerTestTemplate); + + /* fields are blank */ + String dockerImageBlank = ""; + String dockerTestScriptBlank = ""; + String dockerTemplateBlank = ""; + String structureTemplateBlank = ""; + + testUpdateJson = new TestUpdateJson( + dockerImageBlank, + dockerTestScriptBlank, + dockerTemplateBlank, + structureTemplateBlank + ); + + testJson = new TestJson( + "projectUrl", + null, + null, + null, + null, + "extraFilesUrl", + "extraFilesName" + ); + reset(testUtil); + when(testUtil.checkForTestUpdate( + eq(project.getId()), + eq(getMockUser()), + eq(null), + eq(null), + eq(null), + eq(null), + eq(HttpMethod.PUT) + )).thenReturn(new CheckResult<>(HttpStatus.OK, "",new Pair<>(test, project))); + + reset(testRepository); + when(testRepository.save(argThat( + testEntity -> testEntity.getDockerImage() == null && + testEntity.getDockerTestScript() == null && + testEntity.getDockerTestTemplate() == null && + testEntity.getStructureTemplate() == null + ))).thenReturn(test); + + when(entityToJsonConverter.testEntityToTestJson(test, project.getId())).thenReturn(testJson); + + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testUpdateJson)) + ).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + assertNull(test.getDockerImage()); + assertNull(test.getDockerTestScript()); + assertNull(test.getDockerTestTemplate()); + assertNull(test.getStructureTemplate()); + + test.setDockerImage(originalDockerImage); + test.setDockerTestScript(originalDockerTestScript); + test.setDockerTestTemplate(originalDockerTestTemplate); + test.setStructureTemplate(originalStructureTemplate); + + /* Fields are null */ + String dockerImageNull = null; + String dockerTestScriptNull = null; + String dockerTemplateNull = null; + String structureTemplateNull = null; + + when(testRepository.imageIsUsed(any())).thenReturn(true); + + testUpdateJson = new TestUpdateJson( + dockerImageNull, + dockerTestScriptNull, + dockerTemplateNull, + structureTemplateNull + ); + + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testUpdateJson)) + ).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + assertNull(test.getDockerImage()); + assertNull(test.getDockerTestScript()); + assertNull(test.getDockerTestTemplate()); + assertNull(test.getStructureTemplate()); + + test.setDockerImage(originalDockerImage); + test.setDockerTestScript(originalDockerTestScript); + test.setDockerTestTemplate(originalDockerTestTemplate); + test.setStructureTemplate(originalStructureTemplate); + + /* Check fails */ + when(testUtil.checkForTestUpdate( + eq(project.getId()), + eq(getMockUser()), + eq(dockerImage), + eq(dockerTestScript), + eq(dockerTestTemplate), + eq(structureTemplate), + eq(HttpMethod.PUT) + )).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)); + + testUpdateJson = new TestUpdateJson( + dockerImage, + dockerTestScript, + dockerTestTemplate, + structureTemplate + ); + + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testUpdateJson)) + ).andExpect(status().isIAmATeapot()); + + } + + @Test + public void testGetPatch() throws Exception { + when(testRepository.imageIsUsed(any())).thenReturn(true); + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + project.getId() + "/tests"; + + String dockerImage = "dockerImage"; + String dockerTestScript = "dockerTestScript"; + String dockerTestTemplate = "dockerTestTemplate"; + String structureTemplate = "structureTemplate"; + + when(testUtil.checkForTestUpdate( + eq(project.getId()), + eq(getMockUser()), + eq(dockerImage), + eq(null), + eq(null), + eq(null), + eq(HttpMethod.PATCH) + )).thenReturn(new CheckResult<>(HttpStatus.OK, "",new Pair<>(test, project))); + + when(testRepository.save(test)).thenReturn(test); + when(entityToJsonConverter.testEntityToTestJson(test, project.getId())).thenReturn(testJson); + + /* Start with test all null, fill them in one by one */ + test.setDockerImage(null); + test.setDockerTestScript(null); + test.setDockerTestTemplate(null); + test.setStructureTemplate(null); + + TestUpdateJson testUpdateJson = new TestUpdateJson( + dockerImage, + null, + null, + null + ); + + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testUpdateJson)) + ).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + assertEquals(dockerImage, test.getDockerImage()); + assertNull(test.getDockerTestScript()); + assertNull(test.getDockerTestTemplate()); + assertNull(test.getStructureTemplate()); + + verify(projectRepository, times(1)).save(project); + assertEquals(test.getId(), project.getTestId()); + + reset(testUtil); + when(testUtil.checkForTestUpdate( + eq(project.getId()), + eq(getMockUser()), + eq(null), + eq(dockerTestScript), + eq(null), + eq(null), + eq(HttpMethod.PATCH) + )).thenReturn(new CheckResult<>(HttpStatus.OK, "",new Pair<>(test, project))); + + testUpdateJson = new TestUpdateJson( + null, + dockerTestScript, + null, + null + ); + + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testUpdateJson)) + ).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + assertEquals(dockerImage, test.getDockerImage()); + assertEquals(dockerTestScript, test.getDockerTestScript()); + assertNull(test.getDockerTestTemplate()); + assertNull(test.getStructureTemplate()); + + verify(projectRepository, times(2)).save(project); + assertEquals(test.getId(), project.getTestId()); + + reset(testUtil); + when(testUtil.checkForTestUpdate( + eq(project.getId()), + eq(getMockUser()), + eq(null), + eq(null), + eq(dockerTestTemplate), + eq(null), + eq(HttpMethod.PATCH) + )).thenReturn(new CheckResult<>(HttpStatus.OK, "",new Pair<>(test, project))); + + testUpdateJson = new TestUpdateJson( + null, + null, + dockerTestTemplate, + null + ); + + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testUpdateJson)) + ).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + assertEquals(dockerImage, test.getDockerImage()); + assertEquals(dockerTestScript, test.getDockerTestScript()); + assertEquals(dockerTestTemplate, test.getDockerTestTemplate()); + assertNull(test.getStructureTemplate()); + + verify(projectRepository, times(3)).save(project); + assertEquals(test.getId(), project.getTestId()); + + reset(testUtil); + when(testUtil.checkForTestUpdate( + eq(project.getId()), + eq(getMockUser()), + eq(null), + eq(null), + eq(null), + eq(structureTemplate), + eq(HttpMethod.PATCH) + )).thenReturn(new CheckResult<>(HttpStatus.OK, "",new Pair<>(test, project))); + + testUpdateJson = new TestUpdateJson( + null, + null, + null, + structureTemplate + ); + + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testUpdateJson)) + ).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + assertEquals(dockerImage, test.getDockerImage()); + assertEquals(dockerTestScript, test.getDockerTestScript()); + assertEquals(dockerTestTemplate, test.getDockerTestTemplate()); + assertEquals(structureTemplate, test.getStructureTemplate()); + + verify(projectRepository, times(4)).save(project); + assertEquals(test.getId(), project.getTestId()); + + /* Check fails */ + when(testUtil.checkForTestUpdate( + eq(project.getId()), + eq(getMockUser()), + eq(dockerImage), + eq(null), + eq(null), + eq(null), + eq(HttpMethod.PATCH) + )).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)); + + testUpdateJson = new TestUpdateJson( + dockerImage, + null, + null, + null + ); + + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testUpdateJson)) + ).andExpect(status().isIAmATeapot()); + } + + @Test + public void testGetTest() throws Exception { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + project.getId() + "/tests"; + + /* All checks succeed */ + when(testUtil.getTestWithAdminStatus(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(test, true))); + + when(entityToJsonConverter.testEntityToTestJson(test, project.getId())).thenReturn(testJson); + + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + /* Check succeed but user isn't admin */ + when(testUtil.getTestWithAdminStatus(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(test, false))); + + testJson.setDockerImage(null); + testJson.setDockerScript(null); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + /* Check fails */ + when(testUtil.getTestWithAdminStatus(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)); + + mockMvc.perform(MockMvcRequestBuilders.get(url)); + } + + @Test + public void testDeleteTest() throws Exception { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + project.getId() + "/tests"; + + /* All checks succeed */ + when(testUtil.checkForTestUpdate( + eq(project.getId()), + eq(getMockUser()), + eq(null), + eq(null), + eq(null), + eq(null), + eq(HttpMethod.DELETE) + )).thenReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(test, project))); + + when(commonDatabaseActions.deleteTestById(project, test)).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isOk()); + + /* Deleting fails */ + when(commonDatabaseActions.deleteTestById(project, test)).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)); + + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isIAmATeapot()); + + /* Check fails */ + when(testUtil.checkForTestUpdate( + eq(project.getId()), + eq(getMockUser()), + eq(null), + eq(null), + eq(null), + eq(null), + eq(HttpMethod.DELETE) + )).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)); + + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isIAmATeapot()); + } + + public static File createTestFile() throws IOException { + // Create a temporary directory + File tempDir = Files.createTempDirectory("SELAB6CANDELETEtest-dir").toFile(); + + // Create a temporary file within the directory + File tempFile = File.createTempFile("SELAB6CANDELETEtest-file", ".zip", tempDir); + + // Create some content to write into the zip file + String content = "Hello, this is a test file!"; + byte[] bytes = content.getBytes(); + + // Write the content into a file inside the zip file + try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempFile))) { + ZipEntry entry = new ZipEntry("test.txt"); + zipOut.putNextEntry(entry); + zipOut.write(bytes); + zipOut.closeEntry(); + } + + // Return the File object representing the zip file + return tempFile; + } + + @Test + public void testUploadExtraTestFiles() throws IOException { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + project.getId() + "/tests/extrafiles"; + /* All checks succeed */ + when(testUtil.getTestIfAdmin(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", test)); + + Path savePath = Path.of("savePath"); + File file = createTestFile(); + + try (MockedStatic mockedFilehandler = mockStatic(Filehandler.class)) { + mockedFilehandler.when(() -> Filehandler.getTestExtraFilesPath(project.getId())).thenReturn(savePath); + mockedFilehandler.when(() -> Filehandler.saveFile(savePath, mockMultipartFile, Filehandler.EXTRA_TESTFILES_FILENAME)) + .thenReturn(file); + + when(fileRepository.save(argThat( + fileEntity -> fileEntity.getName().equals(mockMultipartFile.getOriginalFilename()) && + fileEntity.getPath() + .equals(savePath.resolve(Filehandler.EXTRA_TESTFILES_FILENAME).toString()) && + fileEntity.getUploadedBy() == getMockUser().getId() + ))).thenReturn(fileEntity); + + when(testRepository.save(test)).thenReturn(test); + when(entityToJsonConverter.testEntityToTestJson(test, project.getId())).thenReturn(testJson); + + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile) + .with(request -> { + request.setMethod("PUT"); + return request; + }) + ).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + verify(testRepository, times(1)).save(test); + assertEquals(fileEntity.getId(), test.getExtraFilesId()); + + /* Unexpected error */ + mockedFilehandler.when(() -> Filehandler.saveFile(savePath, mockMultipartFile, Filehandler.EXTRA_TESTFILES_FILENAME)) + .thenThrow(new IOException("Unexpected error")); + + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile) + .with(request -> { + request.setMethod("PUT"); + return request; + }) + ).andExpect(status().isInternalServerError()); + + /* Check fails */ + when(testUtil.getTestIfAdmin(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)); + + mockMvc.perform(MockMvcRequestBuilders.multipart(url) + .file(mockMultipartFile) + .with(request -> { + request.setMethod("PUT"); + return request; + }) + ).andExpect(status().isIAmATeapot()); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testDeleteExtraFiles() throws Exception { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + project.getId() + "/tests/extrafiles"; + test.setExtraFilesId(fileEntity.getId()); + + /* All checks succeed */ + when(testUtil.getTestIfAdmin(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", test)); + + when(fileRepository.findById(test.getExtraFilesId())).thenReturn(Optional.of(fileEntity)); + when(testRepository.save(test)).thenReturn(test); + when(entityToJsonConverter.testEntityToTestJson(test, project.getId())).thenReturn(testJson); + + when(fileUtil.deleteFileById(test.getExtraFilesId())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(testJson))); + + verify(testRepository, times(1)).save(test); + verify(fileUtil, times(1)).deleteFileById(fileEntity.getId()); + assertNull(test.getExtraFilesId()); + + /* Unexpected error when deleting file */ + test.setExtraFilesId(fileEntity.getId()); + when(fileUtil.deleteFileById(test.getExtraFilesId())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "Unexpected error", null)); + + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isIAmATeapot()); + + /* Error thrown */ + test.setExtraFilesId(fileEntity.getId()); + when(fileUtil.deleteFileById(test.getExtraFilesId())) + .thenThrow(new RuntimeException("Error thrown")); + + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isInternalServerError()); + + /* No extra files */ + test.setExtraFilesId(null); + + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isNotFound()); + + /* Check fails */ + when(testUtil.getTestIfAdmin(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)); + + mockMvc.perform(MockMvcRequestBuilders.delete(url)) + .andExpect(status().isIAmATeapot()); + } + + @Test + public void getExtraTestFiles() { + String url = ApiRoutes.PROJECT_BASE_PATH + "/" + project.getId() + "/tests/extrafiles"; + + ResponseEntity mockResponseEntity = ResponseEntity.ok().build(); + test.setExtraFilesId(fileEntity.getId()); + + /* All checks succeed */ + when(testUtil.getTestIfAdmin(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", test)); + + when(fileRepository.findById(test.getExtraFilesId())).thenReturn(Optional.of(fileEntity)); + + try (MockedStatic mockedFilehandler = mockStatic(Filehandler.class)) { + mockedFilehandler.when(() -> Filehandler.getZipFileAsResponse(argThat( + path -> path.toString().equals(fileEntity.getPath()) + ), eq(fileEntity.getName()))) + .thenReturn(mockResponseEntity); + + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()); + + /* Files not found */ + when(fileRepository.findById(test.getExtraFilesId())).thenReturn(Optional.empty()); + + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isNotFound()); + + /* No extra files */ + test.setExtraFilesId(null); + + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isNotFound()); + + /* check fails */ + when(testUtil.getTestIfAdmin(project.getId(), getMockUser())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)); + + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isIAmATeapot()); + } catch (Exception e) { + throw new RuntimeException(e); + } } - //TODO: tests schrijven eens de backend stabiel is. } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/UserControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/UserControllerTest.java index c2a1cdc4..e10fb21b 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/UserControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/UserControllerTest.java @@ -1,14 +1,25 @@ package com.ugent.pidgeon.controllers; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ugent.pidgeon.CustomObjectMapper; +import com.ugent.pidgeon.json.UserJson; import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.util.CheckResult; import com.ugent.pidgeon.util.UserUtil; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -17,9 +28,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; @ExtendWith(MockitoExtension.class) public class UserControllerTest extends ControllerTest { @@ -31,81 +40,384 @@ public class UserControllerTest extends ControllerTest { private UserController userController; private UserEntity userEntity; + private UserJson userJson; + private UserJson mockUserJson; + private final ObjectMapper objectMapper = CustomObjectMapper.createObjectMapper(); @BeforeEach public void setup() { - mockMvc = MockMvcBuilders.standaloneSetup(userController) - .defaultRequest(MockMvcRequestBuilders.get("/**") - .with(request -> { - request.setUserPrincipal(SecurityContextHolder.getContext().getAuthentication()); - return request; - })) - .build(); - userEntity = new UserEntity("name", "surname", "email", UserRole.student, "azureId"); + setUpController(userController); + userEntity = new UserEntity("Bob", "Testman", "email", UserRole.student, "azureId", ""); + userEntity.setId(74L); + mockUserJson = new UserJson(getMockUser()); + userJson = new UserJson(userEntity); } @Test public void testGetUserById() throws Exception { + String url = ApiRoutes.USERS_BASE_PATH + "/" + getMockUser().getId(); + String urlSomeoneElse = ApiRoutes.USERS_BASE_PATH + "/" + userEntity.getId(); + /* Can get ur own user information */ + when(userUtil.getUserIfExists(getMockUser().getId())).thenReturn(getMockUser()); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(mockUserJson))); + + /* Can't get someone else's user information */ when(userUtil.getUserIfExists(anyLong())).thenReturn(userEntity); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.USER_BASE_PATH + "/1")) - .andExpect(status().isOk()); + mockMvc.perform(MockMvcRequestBuilders.get(urlSomeoneElse)) + .andExpect(status().isForbidden()); + + /* Admin can get someone else's user information */ + getMockUser().setRole(UserRole.admin); + mockMvc.perform(MockMvcRequestBuilders.get(urlSomeoneElse)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(userJson))); + /* If user not found return 404 */ when(userUtil.getUserIfExists(anyLong())).thenReturn(null); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.USER_BASE_PATH + "/1")) + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.USERS_BASE_PATH + "/999")) .andExpect(status().isNotFound()); + } + + private String createGetUsersUrl(String name, String surname, String email) { + String start = ApiRoutes.USERS_BASE_PATH; + boolean first = true; + if (name != null) { + start += "?name=" + name; + first = false; + } + if (surname != null) { + if (first) { + start += "?surname=" + surname; + first = false; + } else { + start += "&surname=" + surname; + } + } + if (email != null) { + if (first) { + start += "?email=" + email; + first = false; + } else { + start += "&email=" + email; + } + } + return start; + } - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.USER_BASE_PATH + "/2")) + @Test + public void testGetUsersByNameOrSurname() throws Exception { + setMockUserRoles(UserRole.admin); + /* If email is present in the url, user gets returned based on email */ + String url = createGetUsersUrl(null, null, "email"); + when(userRepository.findByEmail("email")).thenReturn(userEntity); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + + /* If email and name are present they need to match case insensitive */ + url = createGetUsersUrl("name", null, "email"); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[]")); + + url = createGetUsersUrl(userEntity.getName(), null, "email"); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + + url = createGetUsersUrl(userEntity.getName().toUpperCase(), null, "email"); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + + /* If email and surname are present they need to match case insensitive */ + url = createGetUsersUrl(null, "surname", "email"); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[]")); + + url = createGetUsersUrl(null, userEntity.getSurname(), "email"); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + + /* If all three are present they need to match case insensitive */ + url = createGetUsersUrl("name", "surname", "email"); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[]")); + + url = createGetUsersUrl(userEntity.getName(), userEntity.getSurname(), "email"); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + + url = createGetUsersUrl(userEntity.getName().toUpperCase(), userEntity.getSurname().toUpperCase(), "email"); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + + url = createGetUsersUrl(null, userEntity.getSurname().toUpperCase(), "email"); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + + /* If no user with email return empty list */ + when(userRepository.findByEmail("email")).thenReturn(null); + url = createGetUsersUrl(null, null, "email"); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[]")); + + /* If email isn't present in the url, users get returned based on name and surname */ + url = createGetUsersUrl("name", "surname", null); + when(userRepository.findByName("name", "surname")).thenReturn(List.of(userEntity)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + + /* If both name and surname are less than 3 characters, return empty list */ + url = createGetUsersUrl("na", "su", null); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[]")); + + /* If one of the two is long enough, return the user */ + url = createGetUsersUrl("name", "su", null); + when(userRepository.findByName("name", "su")).thenReturn(List.of(userEntity)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + + /* If only name, return based on name, needs to be longer then 3 characters */ + url = createGetUsersUrl("name", null, null); + when(userRepository.findByName("name", "")).thenReturn(List.of(userEntity)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + + url = createGetUsersUrl("na", null, null); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[]")); + + /* If only surname, return based on surname, needs to be longer then 3 characters */ + url = createGetUsersUrl(null, "surname", null); + when(userRepository.findByName("", "surname")).thenReturn(List.of(userEntity)); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(List.of(userJson)))); + + url = createGetUsersUrl(null, "su", null); + mockMvc.perform(MockMvcRequestBuilders.get(url)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[]")); + + /* Only admin can use this route */ + setMockUserRoles(UserRole.student); + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.USERS_BASE_PATH)) + .andExpect(status().isForbidden()); + + setMockUserRoles(UserRole.teacher); + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.USERS_BASE_PATH)) .andExpect(status().isForbidden()); } @Test - public void testGetUserByAzureId() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.USER_AUTH_PATH)) - .andExpect(status().isOk()); + public void testGetLoggedInUser() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.LOGGEDIN_USER_PATH)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(mockUserJson))); } @Test public void testUpdateUserById() throws Exception { - String request = "{\"name\":\"John\",\"surname\":\"Doe\",\"email\":\"john@example.com\",\"role\":\"admin\"}"; - when(userUtil.checkForUserUpdateJson(anyLong(), any())). - thenReturn(new CheckResult<>(HttpStatus.OK, "", userEntity)); - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.USER_BASE_PATH + "/1") + setMockUserRoles(UserRole.admin); + String url = ApiRoutes.USERS_BASE_PATH + "/" + userEntity.getId(); + String request = "{\"name\":\"John\",\"surname\":\"Doe\",\"email\":\"john@example.com\",\"role\":\"teacher\"}"; + UserEntity updateUserEntity = new UserEntity("John", "Doe", "john@example.com", UserRole.teacher, "azureId", ""); + updateUserEntity.setId(userEntity.getId()); + UserJson updatedUserJson = new UserJson(updateUserEntity); + + when(userUtil.checkForUserUpdateJson(eq(userEntity.getId()), argThat( + json -> json.getName().equals("John") && + json.getSurname().equals("Doe") && + json.getEmail().equals("john@example.com") && + json.getRoleAsEnum().equals(UserRole.teacher))) + ) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", userEntity)); + mockMvc.perform(MockMvcRequestBuilders.put(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(updatedUserJson))); + verify(userRepository, times(1)).save(userEntity); + assertEquals("John", userEntity.getName()); + assertEquals("Doe", userEntity.getSurname()); + assertEquals("john@example.com", userEntity.getEmail()); + assertEquals(UserRole.teacher, userEntity.getRole()); + + /* If updatecheck fails return corresponding status */ + reset(userUtil); + when(userUtil.checkForUserUpdateJson(anyLong(), any())) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isIAmATeapot()); + + /* Only admin can update user */ + setMockUserRoles(UserRole.student); + mockMvc.perform(MockMvcRequestBuilders.put(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isForbidden()); - when(userUtil.checkForUserUpdateJson(anyLong(), any())). - thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.USER_BASE_PATH + "/1") + setMockUserRoles(UserRole.teacher); + mockMvc.perform(MockMvcRequestBuilders.put(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) - .andExpect(status().isBadRequest()); + .andExpect(status().isForbidden()); } @Test public void testPatchUserById() throws Exception { - String request = "{\"name\": null,\"surname\": null,\"email\": null,\"role\": null}"; - when(userUtil.getUserIfExists(anyLong())).thenReturn(userEntity); - when(userUtil.checkForUserUpdateJson(anyLong(), any())) + setMockUserRoles(UserRole.admin); + String url = ApiRoutes.USERS_BASE_PATH + "/" + userEntity.getId(); + String request = "{\"name\":\"John\",\"surname\":\"Doe\",\"email\":\"john@example.com\",\"role\":\"teacher\"}"; + UserEntity updateUserEntity = new UserEntity("John", "Doe", "john@example.com", UserRole.teacher, "azureId", ""); + updateUserEntity.setId(userEntity.getId()); + UserJson updatedUserJson = new UserJson(updateUserEntity); + String originalName = userEntity.getName(); + String originalSurname = userEntity.getSurname(); + String originalEmail = userEntity.getEmail(); + UserRole originalRole = userEntity.getRole(); + + /* If all fields are present, update them all */ + when(userUtil.getUserIfExists(userEntity.getId())).thenReturn(userEntity); + when(userUtil.checkForUserUpdateJson(eq(userEntity.getId()), argThat( + json -> json.getName().equals("John") && + json.getSurname().equals("Doe") && + json.getEmail().equals("john@example.com") && + json.getRoleAsEnum().equals(UserRole.teacher))) + ) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", userEntity)); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(updatedUserJson))); + verify(userRepository, times(1)).save(userEntity); + assertEquals("John", userEntity.getName()); + assertEquals("Doe", userEntity.getSurname()); + assertEquals("john@example.com", userEntity.getEmail()); + assertEquals(UserRole.teacher, userEntity.getRole()); + userEntity.setName(originalName); + userEntity.setSurname(originalSurname); + userEntity.setEmail(originalEmail); + userEntity.setRole(originalRole); + + + /* If not all fields are present, update only the ones that are */ + request = "{\"name\":\"Tom\"}"; + reset(userUtil); + when(userUtil.getUserIfExists(userEntity.getId())).thenReturn(userEntity); + when(userUtil.checkForUserUpdateJson(eq(userEntity.getId()), argThat( + json -> json.getName().equals("Tom") && + json.getSurname().equals(userEntity.getSurname()) && + json.getEmail().equals(userEntity.getEmail()) && + json.getRoleAsEnum().equals(userEntity.getRole()))) + ) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", userEntity)); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()); + + verify(userRepository, times(2)).save(userEntity); + assertEquals("Tom", userEntity.getName()); + assertEquals(originalSurname, userEntity.getSurname()); + assertEquals(originalEmail, userEntity.getEmail()); + assertEquals(originalRole, userEntity.getRole()); + userEntity.setName(originalName); + + request = "{\"surname\":\"Riddle\"}"; + reset(userUtil); + when(userUtil.getUserIfExists(userEntity.getId())).thenReturn(userEntity); + when(userUtil.checkForUserUpdateJson(eq(userEntity.getId()), argThat( + json -> json.getName().equals(userEntity.getName()) && + json.getSurname().equals("Riddle") && + json.getEmail().equals(userEntity.getEmail()) && + json.getRoleAsEnum().equals(userEntity.getRole()))) + ) .thenReturn(new CheckResult<>(HttpStatus.OK, "", userEntity)); - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.USER_BASE_PATH + "/1") + mockMvc.perform(MockMvcRequestBuilders.patch(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isOk()); + verify(userRepository, times(3)).save(userEntity); + assertEquals(originalName, userEntity.getName()); + assertEquals("Riddle", userEntity.getSurname()); + assertEquals(originalEmail, userEntity.getEmail()); + assertEquals(originalRole, userEntity.getRole()); + + /* If updatecheck fails return corresponding status */ + reset(userUtil); + when(userUtil.getUserIfExists(userEntity.getId())).thenReturn(userEntity); when(userUtil.checkForUserUpdateJson(anyLong(), any())) - .thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "", null)); - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.USER_BASE_PATH + "/1") + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + mockMvc.perform(MockMvcRequestBuilders.patch(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) - .andExpect(status().isBadRequest()); + .andExpect(status().isIAmATeapot()); - when(userUtil.getUserIfExists(anyLong())).thenReturn(null); - mockMvc.perform(MockMvcRequestBuilders.patch(ApiRoutes.USER_BASE_PATH + "/1") + /* If user doesn't exist return 404 */ + when(userUtil.getUserIfExists(userEntity.getId())).thenReturn(null); + mockMvc.perform(MockMvcRequestBuilders.patch(url) .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isNotFound()); + + /* Only admin can update user */ + setMockUserRoles(UserRole.student); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isForbidden()); + + setMockUserRoles(UserRole.teacher); + mockMvc.perform(MockMvcRequestBuilders.patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isForbidden()); } + } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java b/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java new file mode 100644 index 00000000..fec7b489 --- /dev/null +++ b/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java @@ -0,0 +1,242 @@ +package com.ugent.pidgeon.docker; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; +import com.ugent.pidgeon.model.submissionTesting.DockerSubtestResult; +import com.ugent.pidgeon.model.submissionTesting.DockerTemplateTestOutput; +import com.ugent.pidgeon.model.submissionTesting.DockerTestOutput; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +public class DockerSubmissionTestTest { + + @AfterEach + void cleanUp() { + File file = new File(System.getProperty("user.dir") + "/tmp/test"); + try { + FileUtils.deleteDirectory(file); + } catch (IOException e) { + e.printStackTrace(); + } + } + + File initTestFile(String text, String fileName) { + String localFileLocation = System.getProperty("user.dir") + "/tmp/test/" + fileName; + File file = new File(localFileLocation); + try { + file.getParentFile().mkdirs(); + file.createNewFile(); + FileUtils.writeStringToFile(file, text, "UTF-8"); + } catch (Exception e) { + e.printStackTrace(); + } + return file; + } + + // Check if we can catch the console output of a script. + @Test + void scriptSucceeds() throws InterruptedException { + DockerSubmissionTestModel.installImage("fedora:latest"); + // Load docker container + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("fedora"); + // Run script + DockerTestOutput to = stm.runSubmission("echo 'PUSH ALLOWED' > /shared//output/testOutput"); + assertTrue(to.allowed); + stm.cleanUp(); + } + + @Test + void scriptFails() throws InterruptedException { + //make sure docker image is installed + DockerSubmissionTestModel.installImage("fedora:latest"); + // Load docker container + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("fedora"); + // Run script + // Example for running a bash script correctly + DockerTestOutput to = stm.runSubmission("echo 'PUSH DENIED' > /shared/output/testOutput"); + assertFalse(to.allowed); + stm.cleanUp(); + } + + @Test + void catchesConsoleLogs() throws InterruptedException { + DockerSubmissionTestModel.installImage("alpine:latest"); + // Load docker container + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine"); + // Run script + // Example for running a bash script correctly + DockerTestOutput to = stm.runSubmission( + "echo 'Woopdie Woop Scoop! ~ KW'; echo 'PUSH ALLOWED' > /shared/output/testOutput"); + + assertTrue(to.allowed); + assertEquals(to.logs.get(0), "Woopdie Woop Scoop! ~ KW\n"); + stm.cleanUp(); + } + + @Test + void correctlyReceivesInputFiles() throws InterruptedException { + DockerSubmissionTestModel.installImage("alpine:latest"); + // Load docker container + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine"); + + // Create an input file in tmp/test/input + File file = initTestFile("This is a test input file\n", "testInput"); + stm.addInputFiles(new File[]{file}); + // Run script + // Example for running a bash script correctly + DockerTestOutput to = stm.runSubmission( + "cat /shared/input/testInput; echo PUSH ALLOWED > /shared/output/testOutput"); + assertEquals(to.logs.get(0), "This is a test input file\n"); + stm.cleanUp(); + } + + @Test + void templateTest() throws InterruptedException { + String testOne = "@HelloWorld\n" + + ">Description=\"Test for hello world!\"\n" + + ">Required\n" + + "HelloWorld!\n"; + String testTwo = "@HelloWorld2\n" + + ">Optional\n" + + "HelloWorld2!\n"; + String template = testOne + "\n" + testTwo + "\n"; + + File[] files = new File[]{initTestFile("#!/bin/sh\necho 'HelloWorld!'", "HelloWorld.sh"), + initTestFile("#!/bin/sh\necho 'HelloWorld2!'", "HelloWorld2.sh")}; + + String script = + "chmod +x /shared/input/HelloWorld.sh;" + + "chmod +x /shared/input/HelloWorld2.sh;" + + "/shared/input/HelloWorld.sh > /shared/output/HelloWorld;" + + "/shared/input/HelloWorld2.sh > /shared/output/HelloWorld2"; + + DockerSubmissionTestModel.installImage("alpine:latest"); + // Load docker container + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine:latest"); + stm.addInputFiles(files); + DockerTemplateTestOutput result = stm.runSubmissionWithTemplate(script, template); + + // Extract subtests + List results = result.getSubtestResults(); + + stm.cleanUp(); + + // Testing for the template parser capabilities + assertEquals(results.size(), 2); + + assertTrue(results.get(0).isRequired()); + assertFalse(results.get(1).isRequired()); + + assertEquals(results.get(0).getCorrect(), "HelloWorld!\n"); + assertEquals(results.get(1).getCorrect(), "HelloWorld2!\n"); + + assertEquals(results.get(0).getTestDescription(), "Test for hello world!"); + assertEquals(results.get(1).getTestDescription(), ""); + + // Test the docker output + assertEquals(results.get(0).getOutput(), "HelloWorld!\n"); + assertEquals(results.get(1).getOutput(), "HelloWorld2!\n"); + + assertTrue(result.isAllowed()); + } + + @Test + void artifactTest() throws IOException { + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine:latest"); + String script = + "echo 'HelloWorld!' > /shared/artifacts/HelloWorld"; + + DockerTestOutput to = stm.runSubmission(script); + assertFalse(to.allowed); + // check file properties + List files = stm.getArtifacts(); + assertEquals(files.size(), 1); + assertEquals(files.get(0).getName(), "HelloWorld"); + // check file contents + assertEquals("HelloWorld!\n", FileUtils.readFileToString(files.get(0), "UTF-8")); + stm.cleanUp(); + } + + @Test + void zipFileInputTest() throws IOException { + // construct zip with hello world contents + String sb = "Hello Happy World!"; + + File f = new File("src/test/test-cases/DockerSubmissionTestTest/d__test.zip"); + ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f)); + ZipEntry e = new ZipEntry("helloworld.txt"); + out.putNextEntry(e); + + byte[] data = sb.getBytes(); + out.write(data, 0, data.length); + out.closeEntry(); + out.close(); + + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine:latest"); + // get zipfile + stm.addZipInputFiles(new ZipFile(f)); + DockerTestOutput output = stm.runSubmission("cat /shared/input/helloworld.txt"); + // run and check if zipfile was properly received + assertEquals( "Hello Happy World!", output.logs.get(0)); + stm.cleanUp(); + + } + @Test + void dockerImageDoesNotExist(){ + assertFalse(DockerSubmissionTestModel.imageExists("BADUBADUBADUBADUBADUBADUB - miauw :3")); + assertFalse(DockerSubmissionTestModel.imageExists("alpine:v69696969")); + assertTrue(DockerSubmissionTestModel.imageExists("alpine:latest")); + } + + @Test + void tryTemplate(){ + assertThrows(IllegalArgumentException.class,() -> DockerSubmissionTestModel.tryTemplate("This is not a valid template")); + + + assertDoesNotThrow(() -> DockerSubmissionTestModel.tryTemplate("@HelloWorld\n" + + ">Description=\"Test for hello world!\"\n" + + ">Required\n" + + "HelloWorld!")); + assertDoesNotThrow(() -> DockerSubmissionTestModel.tryTemplate("@helloworld\n" + + ">required\n" + + ">description=\"Helloworldtest\"\n" + + "Hello World\n" + + "\n" + + "@helloworld2\n" + + "bruh\n")); + } + + @Test + void testDockerReceivesUtilFiles(){ + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine:latest"); + Path zipLocation = Path.of("src/test/test-cases/DockerSubmissionTestTest/d__test.zip"); // simple zip with one file + Path zipLocation2 = Path.of("src/test/test-cases/DockerSubmissionTestTest/helloworld.zip"); // complicated zip with multiple files and folder structure + stm.addUtilFiles(zipLocation); + stm.addUtilFiles(zipLocation2); + DockerTestOutput to = stm.runSubmission("find /shared/extra/"); + List logs = to.logs.stream().map(log -> log.replaceAll("\n", "")).sorted().toList(); + assertEquals("/shared/extra/", logs.get(0)); + assertEquals("/shared/extra/helloworld", logs.get(1)); + assertEquals("/shared/extra/helloworld.txt", logs.get(2)); + assertEquals("/shared/extra/helloworld/emptyfolder", logs.get(3)); + assertEquals("/shared/extra/helloworld/helloworld1.txt", logs.get(4)); + assertEquals("/shared/extra/helloworld/helloworld2.txt", logs.get(5)); // I don't understand the order of find :sob: but it is important all files are found. + assertEquals("/shared/extra/helloworld/helloworld3.txt", logs.get(6)); + stm.cleanUp(); + } + +} diff --git a/backend/app/src/test/java/com/ugent/pidgeon/global/GlobalErrorHandlerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/global/GlobalErrorHandlerTest.java new file mode 100644 index 00000000..945bab26 --- /dev/null +++ b/backend/app/src/test/java/com/ugent/pidgeon/global/GlobalErrorHandlerTest.java @@ -0,0 +1,71 @@ +package com.ugent.pidgeon.global; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.ugent.pidgeon.controllers.ApiRoutes; +import com.ugent.pidgeon.controllers.ControllerTest; +import com.ugent.pidgeon.controllers.UserController; +import com.ugent.pidgeon.postgre.models.types.UserRole; +import com.ugent.pidgeon.util.UserUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + + + +@ExtendWith(MockitoExtension.class) +public class GlobalErrorHandlerTest extends ControllerTest { + + @Mock + private UserUtil userUtil; + + @InjectMocks + private UserController userController; + + + @BeforeEach + public void setUp() { + setUpController(userController); + } + + @Test + public void testHandleHttpMessageNotReadableException() throws Exception { + setMockUserRoles(UserRole.admin); + mockMvc.perform(MockMvcRequestBuilders.put(ApiRoutes.USERS_BASE_PATH + "/1") + .contentType("application/json") + .content("") + ).andExpect(status().isBadRequest()); + } + + @Test + public void testHandleNoHandlerFound() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/api/doesntexist", 1L) + ).andExpect(status().isNotFound()); + } + + @Test + public void testHandleMethodNotSupportedException() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.multipart(ApiRoutes.USERS_BASE_PATH + "/1") + ).andExpect(status().isMethodNotAllowed()); + } + + @Test + public void testHandleMethodArgumentTypeMismatchException() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.USERS_BASE_PATH + "/string") + ).andExpect(status().isBadRequest()); + } + + @Test + public void testUnexpectedException() throws Exception { + when(userUtil.getUserIfExists(anyLong())).thenThrow(new RuntimeException("Unexpected exception")); + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.USERS_BASE_PATH + "/1") + ).andExpect(status().isInternalServerError()); + } + +} diff --git a/backend/app/src/test/java/com/ugent/pidgeon/global/RolesInterceptorTest.java b/backend/app/src/test/java/com/ugent/pidgeon/global/RolesInterceptorTest.java new file mode 100644 index 00000000..286e8b84 --- /dev/null +++ b/backend/app/src/test/java/com/ugent/pidgeon/global/RolesInterceptorTest.java @@ -0,0 +1,85 @@ +package com.ugent.pidgeon.global; + + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.ugent.pidgeon.controllers.ApiRoutes; +import com.ugent.pidgeon.controllers.ControllerTest; +import com.ugent.pidgeon.controllers.UserController; +import com.ugent.pidgeon.postgre.models.types.UserRole; +import com.ugent.pidgeon.util.UserUtil; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@ExtendWith(MockitoExtension.class) +public class RolesInterceptorTest extends ControllerTest { + + @Mock + private UserUtil userUtil; + + @InjectMocks + private UserController userController; + + + @BeforeEach + public void setUp() { + setUpController(userController); + } + + @Test + void testEverthingWorks() throws Exception { + when(userUtil.getUserIfExists(getMockUser().getId())).thenReturn(getMockUser()); + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.USERS_BASE_PATH + "/1") + ).andExpect(status().isOk()); + } + + @Test + void testNotRequiredRole() throws Exception { + setMockUserRoles(UserRole.student); + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.USERS_BASE_PATH) + ).andExpect(status().isForbidden()); + } + + @Test + void adminSucceedsAllRoleCheck() throws Exception { + setMockUserRoles(UserRole.admin); + when(userUtil.getUserIfExists(getMockUser().getId())).thenReturn(getMockUser()); + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.USERS_BASE_PATH + "/1") + ).andExpect(status().isOk()); + } + + @Test + void testUserDoesntExistYet() throws Exception { + reset(userRepository); + when(userUtil.getUserIfExists(getMockUser().getId())).thenReturn(getMockUser()); + when(userRepository.findUserByAzureId(getMockUser().getAzureId())).thenReturn(Optional.empty()); + when(userRepository.save(argThat( + user -> { + Duration duration = Duration.between(user.getCreatedAt(), OffsetDateTime.now()); + return user.getRole() == UserRole.student && + user.getAzureId().equals(getMockUser().getAzureId()) && + user.getName().equals(getMockUser().getName()) && + user.getSurname().equals(getMockUser().getSurname()) && + user.getEmail().equals(getMockUser().getEmail()) && + user.getStudentNumber().equals(getMockUser().getStudentNumber()) && + duration.getSeconds() < 5; + } + ))).thenReturn(getMockUser()); + mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.USERS_BASE_PATH + "/" + getMockUser().getId()) + ).andExpect(status().isOk()); + + } + + +} diff --git a/backend/app/src/test/java/com/ugent/pidgeon/model/AuthTest.java b/backend/app/src/test/java/com/ugent/pidgeon/model/AuthTest.java index 6f415d42..08c4a0eb 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/model/AuthTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/model/AuthTest.java @@ -1,16 +1,14 @@ package com.ugent.pidgeon.model; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import java.util.List; - public class AuthTest { - private final User testUser = new User("John Doe", "John", "Doe", "john.doe@gmail.com", "123456"); + private final User testUser = new User("John Doe", "John", "Doe", "john.doe@gmail.com", "123456", ""); private final List authLijst = List.of(new SimpleGrantedAuthority("READ_AUTHORITY")); private final Auth auth = new Auth(testUser, authLijst); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/model/DockerSubmissionTestTest.java b/backend/app/src/test/java/com/ugent/pidgeon/model/DockerSubmissionTestTest.java deleted file mode 100644 index 97c89e12..00000000 --- a/backend/app/src/test/java/com/ugent/pidgeon/model/DockerSubmissionTestTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.ugent.pidgeon.model; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; -import com.ugent.pidgeon.model.submissionTesting.DockerSubtestResult; -import com.ugent.pidgeon.model.submissionTesting.DockerTemplateTestResult; -import com.ugent.pidgeon.model.submissionTesting.DockerTestOutput; -import java.io.File; -import java.util.List; -import org.apache.commons.io.FileUtils; -import org.junit.jupiter.api.Test; - -public class DockerSubmissionTestTest { - - File initTestFile(String text, String fileName) { - String localFileLocation = System.getProperty("user.dir") + "/tmp/test/" + fileName; - File file = new File(localFileLocation); - try { - file.getParentFile().mkdirs(); - file.createNewFile(); - FileUtils.writeStringToFile(file, text, "UTF-8"); - } catch (Exception e) { - e.printStackTrace(); - } - return file; - } - - // Check if we can catch the console output of a script. - @Test - void scriptSucceeds() throws InterruptedException { - DockerSubmissionTestModel.addDocker("fedora:latest"); - // Load docker container - DockerSubmissionTestModel stm = new DockerSubmissionTestModel("fedora"); - // Run script - DockerTestOutput to = stm.runSubmission("echo 'PUSH ALLOWED' > /shared//output/testOutput"); - assertTrue(to.allowed); - } - - @Test - void scriptFails() throws InterruptedException { - //make sure docker image is installed - DockerSubmissionTestModel.addDocker("fedora:latest"); - // Load docker container - DockerSubmissionTestModel stm = new DockerSubmissionTestModel("fedora"); - // Run script - // Example for running a bash script correctly - DockerTestOutput to = stm.runSubmission("echo 'PUSH DENIED' > /shared/output/testOutput"); - assertFalse(to.allowed); - } - - @Test - void catchesConsoleLogs() throws InterruptedException { - DockerSubmissionTestModel.addDocker("alpine:latest"); - // Load docker container - DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine"); - // Run script - // Example for running a bash script correctly - DockerTestOutput to = stm.runSubmission("echo 'Woopdie Woop Scoop! ~ KW'; echo 'PUSH ALLOWED' > /shared/output/testOutput"); - - assertTrue(to.allowed); - assertEquals(to.logs.get(0), "Woopdie Woop Scoop! ~ KW\n"); - } - - @Test - void correctlyReceivesInputFiles() throws InterruptedException { - DockerSubmissionTestModel.addDocker("alpine:latest"); - // Load docker container - DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine"); - - // Create an input file in tmp/test/input - File file = initTestFile("This is a test input file\n", "testInput"); - - // Run script - // Example for running a bash script correctly - DockerTestOutput to = stm.runSubmission("cat /shared/input/testInput; echo PUSH ALLOWED > /shared/output/testOutput", new File[]{file}); - assertEquals(to.logs.get(0), "This is a test input file\n"); - } - - @Test - void templateTest() throws InterruptedException { - String testOne = "@HelloWorld\n" + - ">Description=\"Test for hello world!\"\n" + - ">Required\n" + - "HelloWorld!"; - String testTwo = "@HelloWorld2\n" + - ">Optional\n" + - "HelloWorld2!\n"; - String template = testOne + "\n" + testTwo; - - File[] files = new File[]{initTestFile("#!/bin/sh\necho 'HelloWorld!'", "HelloWorld.sh"), - initTestFile("#!/bin/sh\necho 'HelloWorld2!'", "HelloWorld2.sh")}; - - String script = - "chmod +x /shared/input/HelloWorld.sh;" + - "chmod +x /shared/input/HelloWorld2.sh;" + - "/shared/input/HelloWorld.sh > /shared/output/HelloWorld;" + - "/shared/input/HelloWorld2.sh > /shared/output/HelloWorld2"; - - DockerSubmissionTestModel.addDocker("alpine:latest"); - // Load docker container - DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine"); - DockerTemplateTestResult result = stm.runSubmissionWithTemplate(script, template, files); - - // Extract subtests - List results = result.getSubtestResults(); - - // Testing for the template parser capabilities - assertEquals(results.size(), 2); - - assertTrue(results.get(0).isRequired()); - assertFalse(results.get(1).isRequired()); - - assertEquals(results.get(0).getCorrect(), "HelloWorld!\n"); - assertEquals(results.get(1).getCorrect(), "HelloWorld2!\n"); - - assertEquals(results.get(0).getTestDescription(), "Test for hello world!"); - assertEquals(results.get(1).getTestDescription(), ""); - - // Test the docker output - assertEquals(results.get(0).getOutput(), "HelloWorld!\n"); - assertEquals(results.get(1).getOutput(), "HelloWorld2!\n"); - - assertTrue(result.isAllowed()); - - } - - -} diff --git a/backend/app/src/test/java/com/ugent/pidgeon/model/FileStructureTest.java b/backend/app/src/test/java/com/ugent/pidgeon/model/FileStructureTest.java index 3d7a74c3..6c95ea93 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/model/FileStructureTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/model/FileStructureTest.java @@ -1,14 +1,15 @@ package com.ugent.pidgeon.model; -import com.ugent.pidgeon.model.submissionTesting.SubmissionTemplateModel; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import com.ugent.pidgeon.model.submissionTesting.SubmissionTemplateModel; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; public class FileStructureTest { final String testDirectory = "src/test/test-cases/FileStructureTestCases/"; @@ -43,6 +44,17 @@ void isEmpty(){ void denyFileExtension(){ assertFalse(runTest("noClassExtensions")); } + + @Test + void tryTemplateTest(){ + assertDoesNotThrow(() -> SubmissionTemplateModel.tryTemplate("template")); + assertDoesNotThrow(() -> SubmissionTemplateModel.tryTemplate("src/\n index.js\n seconfilehehe.file")); + assertThrows(IllegalArgumentException.class, () -> SubmissionTemplateModel.tryTemplate("src/\n index.js\n seconfilehehe.file\n thirdfile")); + //check trailing newline + assertDoesNotThrow(() -> SubmissionTemplateModel.tryTemplate("src/\n\tindex.js\n")); + + } + private boolean runTest(String testpath){ SubmissionTemplateModel model = new SubmissionTemplateModel(); if(testpath.lastIndexOf('/') != testpath.length() - 1){ diff --git a/backend/app/src/test/java/com/ugent/pidgeon/model/UserTest.java b/backend/app/src/test/java/com/ugent/pidgeon/model/UserTest.java index 38e15043..e410fca3 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/model/UserTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/model/UserTest.java @@ -5,7 +5,7 @@ public class UserTest { - private final User testUser = new User("John Doe", "John", "Doe", "john.doe@gmail.com", "123456"); + private final User testUser = new User("John Doe", "John", "Doe", "john.doe@gmail.com", "123456", ""); @Test public void isNotNull() { diff --git a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/CourseEntityTest.java b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/CourseEntityTest.java index 741bc9c5..53ad4b7c 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/CourseEntityTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/CourseEntityTest.java @@ -1,11 +1,10 @@ package com.ugent.pidgeon.postgre.models; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.OffsetDateTime; - -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class CourseEntityTest { @@ -55,7 +54,7 @@ public void testJoinKey() { public void testConstructor() { String name = "Test Course"; String description = "Test Description"; - CourseEntity course = new CourseEntity(name, description); + CourseEntity course = new CourseEntity(name, description,2024); assertEquals(name, course.getName()); assertEquals(description, course.getDescription()); } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupClusterEntityTest.java b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupClusterEntityTest.java index ce035f82..03db59b7 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupClusterEntityTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupClusterEntityTest.java @@ -1,11 +1,10 @@ package com.ugent.pidgeon.postgre.models; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.OffsetDateTime; - -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class GroupClusterEntityTest { diff --git a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupEntityTest.java b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupEntityTest.java index 6b19d116..ca544fd0 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupEntityTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupEntityTest.java @@ -1,10 +1,10 @@ package com.ugent.pidgeon.postgre.models; +import static org.junit.jupiter.api.Assertions.assertEquals; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - public class GroupEntityTest { private GroupEntity groupEntity; diff --git a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupFeedbackEntityTest.java b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupFeedbackEntityTest.java index 7d3afa37..3ae52cb2 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupFeedbackEntityTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupFeedbackEntityTest.java @@ -1,10 +1,10 @@ package com.ugent.pidgeon.postgre.models; +import static org.junit.jupiter.api.Assertions.assertEquals; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - public class GroupFeedbackEntityTest { private GroupFeedbackEntity groupFeedbackEntity; diff --git a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupFeedbackIdTest.java b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupFeedbackIdTest.java index c4077399..0748558d 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupFeedbackIdTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupFeedbackIdTest.java @@ -1,10 +1,10 @@ package com.ugent.pidgeon.postgre.models; +import static org.junit.jupiter.api.Assertions.assertEquals; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - public class GroupFeedbackIdTest { private GroupFeedbackId groupFeedbackId; diff --git a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupUserEntityTest.java b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupUserEntityTest.java index b4043b3e..f9663472 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupUserEntityTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupUserEntityTest.java @@ -1,10 +1,10 @@ package com.ugent.pidgeon.postgre.models; +import static org.junit.jupiter.api.Assertions.assertEquals; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - public class GroupUserEntityTest { private GroupUserEntity groupUserEntity; diff --git a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupUserIdTest.java b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupUserIdTest.java index a3657e66..0d1a9cf3 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupUserIdTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/GroupUserIdTest.java @@ -1,10 +1,10 @@ package com.ugent.pidgeon.postgre.models; +import static org.junit.jupiter.api.Assertions.assertEquals; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - public class GroupUserIdTest { private GroupUserId groupUserId; diff --git a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/OffsetDateTimeSerializerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/OffsetDateTimeSerializerTest.java index fe6a934c..0d1ec1e0 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/OffsetDateTimeSerializerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/OffsetDateTimeSerializerTest.java @@ -2,13 +2,12 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.time.OffsetDateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import java.io.IOException; -import java.time.OffsetDateTime; - public class OffsetDateTimeSerializerTest { private OffsetDateTimeSerializer offsetDateTimeSerializer; diff --git a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/ProjectEntityTest.java b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/ProjectEntityTest.java index dfc9e6ae..2f86a523 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/ProjectEntityTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/ProjectEntityTest.java @@ -1,11 +1,10 @@ package com.ugent.pidgeon.postgre.models; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.OffsetDateTime; - -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class ProjectEntityTest { diff --git a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/SubmissionEntityTest.java b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/SubmissionEntityTest.java index d1c2ec37..bf715eb8 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/SubmissionEntityTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/SubmissionEntityTest.java @@ -1,11 +1,10 @@ package com.ugent.pidgeon.postgre.models; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.OffsetDateTime; - -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class SubmissionEntityTest { diff --git a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/TestEntityTest.java b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/TestEntityTest.java index ac9158c6..6e39fc9a 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/TestEntityTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/TestEntityTest.java @@ -1,10 +1,10 @@ package com.ugent.pidgeon.postgre.models; +import static org.junit.jupiter.api.Assertions.assertEquals; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - public class TestEntityTest { private TestEntity testEntity; @@ -29,27 +29,32 @@ public void testDockerImage() { } @Test - public void testDockerTestId() { - long dockerTestId = 1L; - testEntity.setDockerTestId(dockerTestId); - assertEquals(dockerTestId, testEntity.getDockerTestId()); + public void testDockerTestScript() { + String dockerTestScript = "Docker Test Script"; + testEntity.setDockerTestScript(dockerTestScript); + assertEquals(dockerTestScript, testEntity.getDockerTestScript()); } @Test public void testStructureTestId() { - long structureTestId = 1L; - testEntity.setStructureTestId(structureTestId); - assertEquals(structureTestId, testEntity.getStructureTestId()); + String template = "@Testone\nHello World!"; + testEntity.setStructureTemplate(template); + assertEquals(template, testEntity.getStructureTemplate()); } @Test public void testConstructor() { - String dockerImage = "Docker Image"; - long dockerTestId = 1L; - long structureTestId = 1L; - TestEntity test = new TestEntity(dockerImage, dockerTestId, structureTestId); + String dockerImage = "Docker image"; + String dockerTestScript = "echo 'hello'"; + String dockerTestTemplate = "@testone\nHello World!"; + String structureTestId = "src/"; + + TestEntity test = new TestEntity(dockerImage, dockerTestScript, dockerTestTemplate, structureTestId); + assertEquals(dockerImage, test.getDockerImage()); - assertEquals(dockerTestId, test.getDockerTestId()); - assertEquals(structureTestId, test.getStructureTestId()); + assertEquals(dockerTestScript, test.getDockerTestScript()); + assertEquals(dockerTestTemplate, test.getDockerTestTemplate()); + assertEquals(structureTestId, test.getStructureTemplate()); + } } \ No newline at end of file diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/ClusterUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/ClusterUtilTest.java new file mode 100644 index 00000000..ad7f94e4 --- /dev/null +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/ClusterUtilTest.java @@ -0,0 +1,315 @@ +package com.ugent.pidgeon.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.ugent.pidgeon.json.ClusterFillJson; +import com.ugent.pidgeon.json.GroupClusterCreateJson; +import com.ugent.pidgeon.json.GroupClusterUpdateJson; +import com.ugent.pidgeon.postgre.models.CourseUserEntity; +import com.ugent.pidgeon.postgre.models.GroupClusterEntity; +import com.ugent.pidgeon.postgre.models.UserEntity; +import com.ugent.pidgeon.postgre.models.types.CourseRelation; +import com.ugent.pidgeon.postgre.models.types.UserRole; +import com.ugent.pidgeon.postgre.repository.CourseUserRepository; +import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; + +@ExtendWith(MockitoExtension.class) +public class ClusterUtilTest { + + @Mock + private GroupClusterRepository groupClusterRepository; + @Mock + private CourseUserRepository courseUserRepository; + @Mock + private CourseUtil courseUtil; + + @Spy + @InjectMocks + private ClusterUtil clusterUtil; + + private GroupClusterEntity clusterEntity; + + private UserEntity mockUser; + + @BeforeEach + public void setUp() { + clusterEntity = new GroupClusterEntity(1L, 20, "clustername", 5); + clusterEntity.setId(4L); + mockUser = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); + } + + @Test + void testIsIndividualCluster() { + when(groupClusterRepository.findById(clusterEntity.getId())).thenReturn(Optional.of(clusterEntity)); + // Test if the cluster is an individual cluster + clusterEntity.setMaxSize(1); + assertTrue(clusterUtil.isIndividualCluster(clusterEntity)); + assertTrue(clusterUtil.isIndividualCluster(clusterEntity.getId())); + + + // Test if the cluster is not an individual cluster + clusterEntity.setMaxSize(2); + assertFalse(clusterUtil.isIndividualCluster(clusterEntity)); + assertFalse(clusterUtil.isIndividualCluster(clusterEntity.getId())); + + // Test if the cluster is null + when(groupClusterRepository.findById(clusterEntity.getId())).thenReturn(Optional.empty()); + assertFalse(clusterUtil.isIndividualCluster(null)); + assertFalse(clusterUtil.isIndividualCluster(clusterEntity.getId())); + } + + @Test + void testCanDeleteCluster() { + /* All checks succeed */ + doReturn(new CheckResult<>(HttpStatus.OK, "", clusterEntity)) + .when(clusterUtil) + .getGroupClusterEntityIfAdminAndNotIndividual(clusterEntity.getId(), mockUser); + + when(groupClusterRepository.usedInProject(clusterEntity.getId())).thenReturn(false); + + CheckResult result = clusterUtil.canDeleteCluster(clusterEntity.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* usedInProject returns true */ + when(groupClusterRepository.usedInProject(clusterEntity.getId())).thenReturn(true); + result = clusterUtil.canDeleteCluster(clusterEntity.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* getGroupClusterEntity fails */ + when(clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(clusterEntity.getId(), mockUser)) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "Group cluster does not exist", null)); + + result = clusterUtil.canDeleteCluster(clusterEntity.getId(), mockUser); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + } + + @Test + void testGetGroupClusterEntityIfNotIndividual() { + /* All checks succeed */ + when(groupClusterRepository.findById(clusterEntity.getId())).thenReturn(Optional.of(clusterEntity)); + when(courseUtil.getCourseIfUserInCourse(clusterEntity.getCourseId(), mockUser)) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + + doReturn(false).when(clusterUtil).isIndividualCluster(clusterEntity); + CheckResult result = + clusterUtil.getGroupClusterEntityIfNotIndividual(clusterEntity.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + assertEquals(clusterEntity, result.getData()); + + /* Group cluster is individual cluster */ + doReturn(true).when(clusterUtil).isIndividualCluster(clusterEntity); + result = clusterUtil.getGroupClusterEntityIfNotIndividual(clusterEntity.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* Course check fails, return corresponding status */ + when(courseUtil.getCourseIfUserInCourse(clusterEntity.getCourseId(), mockUser)) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "Course does not exist", null)); + result = clusterUtil.getGroupClusterEntityIfNotIndividual(clusterEntity.getId(), mockUser); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + /* Group cluster does not exist */ + when(groupClusterRepository.findById(clusterEntity.getId())).thenReturn(Optional.empty()); + result = clusterUtil.getGroupClusterEntityIfNotIndividual(clusterEntity.getId(), mockUser); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); + } + + @Test + void testGetGroupClusterEntityIfAdminAndNotIndividual() { + /* All checks succeed */ + when(clusterUtil.getGroupClusterEntityIfNotIndividual(clusterEntity.getId(), mockUser)) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", clusterEntity)); + + when(courseUtil.getCourseIfAdmin(clusterEntity.getCourseId(), mockUser)) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + + CheckResult result = + clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(clusterEntity.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + assertEquals(clusterEntity, result.getData()); + + /* Course check fails, return corresponding status */ + when(courseUtil.getCourseIfAdmin(clusterEntity.getCourseId(), mockUser)) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + result = clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(clusterEntity.getId(), mockUser); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + /* getGroupClusterEntityIfNotIndividual fails */ + when(clusterUtil.getGroupClusterEntityIfNotIndividual(clusterEntity.getId(), mockUser)) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + result = clusterUtil.getGroupClusterEntityIfAdminAndNotIndividual(clusterEntity.getId(), mockUser); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + } + + @Test + void testPartOfCourse() { + /* All checks succeed */ + when(groupClusterRepository.findById(clusterEntity.getId())).thenReturn(Optional.of(clusterEntity)); + + CheckResult result = clusterUtil.partOfCourse(clusterEntity.getId(), clusterEntity.getCourseId()); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Group cluster not linked to course */ + result = clusterUtil.partOfCourse(clusterEntity.getId(), clusterEntity.getCourseId() + 1); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* Group cluster does not exist */ + when(groupClusterRepository.findById(clusterEntity.getId())).thenReturn(Optional.empty()); + result = clusterUtil.partOfCourse(clusterEntity.getId(), clusterEntity.getCourseId()); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); + } + + @Test + void testGetClusterIfExists() { + /* All checks succeed */ + when(groupClusterRepository.findById(clusterEntity.getId())).thenReturn(Optional.of(clusterEntity)); + + CheckResult result = clusterUtil.getClusterIfExists(clusterEntity.getId()); + assertEquals(HttpStatus.OK, result.getStatus()); + assertEquals(clusterEntity, result.getData()); + + /* Group cluster does not exist */ + when(groupClusterRepository.findById(clusterEntity.getId())).thenReturn(Optional.empty()); + result = clusterUtil.getClusterIfExists(clusterEntity.getId()); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); + } + + @Test + void testCheckGroupClusterUpdateJson() { + GroupClusterUpdateJson json = new GroupClusterUpdateJson(); + /* All checks succeed */ + json.setCapacity(5); + json.setName("clustername"); + CheckResult result = clusterUtil.checkGroupClusterUpdateJson(json); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Capacity is smaller than 1 */ + json.setCapacity(0); + result = clusterUtil.checkGroupClusterUpdateJson(json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Name is empty */ + json.setName(""); + result = clusterUtil.checkGroupClusterUpdateJson(json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Capacity is null */ + json.setCapacity(null); + result = clusterUtil.checkGroupClusterUpdateJson(json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Name is null */ + json.setCapacity(5); + json.setName(null); + result = clusterUtil.checkGroupClusterUpdateJson(json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + } + + @Test + void testCheckGroupClusterCreateJson() { + GroupClusterCreateJson json = new GroupClusterCreateJson("clustername", 5, 5, null); + /* All checks succeed */ + CheckResult result = clusterUtil.checkGroupClusterCreateJson(json); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* GroupCount is negative */ + json = new GroupClusterCreateJson("clustername", 5, -5, null); + result = clusterUtil.checkGroupClusterCreateJson(json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Capacity is smaller than 1 */ + json = new GroupClusterCreateJson("clustername", 0, 5, null); + result = clusterUtil.checkGroupClusterCreateJson(json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Name is empty */ + json = new GroupClusterCreateJson("", 5, 5, null); + result = clusterUtil.checkGroupClusterCreateJson(json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Capacity is null */ + json = new GroupClusterCreateJson("clustername", null, 5, null); + result = clusterUtil.checkGroupClusterCreateJson(json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Name is null */ + json = new GroupClusterCreateJson(null, 5, 5, null); + result = clusterUtil.checkGroupClusterCreateJson(json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* GroupCount is null */ + json = new GroupClusterCreateJson("clustername", 5, null, null); + result = clusterUtil.checkGroupClusterCreateJson(json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + } + + @Test + public void testCheckFillClusterJson() { + ClusterFillJson fillJson = new ClusterFillJson(); + CourseUserEntity enrolledCU = new CourseUserEntity(22L, 5L, CourseRelation.enrolled); + fillJson.addClusterGroupMembers("Group1", new Long[]{5L, 2L}); + fillJson.addClusterGroupMembers("Group2", new Long[]{3L, 10L}); + + when(courseUserRepository.findById(any())).thenReturn(Optional.of(enrolledCU)); + + CheckResult result = clusterUtil.checkFillClusterJson(fillJson, clusterEntity); + assertEquals(HttpStatus.OK, result.getStatus()); + + verify(courseUserRepository, times(1)).findById(argThat( + arg -> arg.getCourseId() == clusterEntity.getCourseId() && arg.getUserId() == 5L)); + verify(courseUserRepository, times(1)).findById(argThat( + arg -> arg.getCourseId() == clusterEntity.getCourseId() && arg.getUserId() == 2L)); + verify(courseUserRepository, times(1)).findById(argThat( + arg -> arg.getCourseId() == clusterEntity.getCourseId() && arg.getUserId() == 3L)); + verify(courseUserRepository, times(1)).findById(argThat( + arg -> arg.getCourseId() == clusterEntity.getCourseId() && arg.getUserId() == 10L)); + + /* User admin in course */ + CourseUserEntity courseAdminCU = new CourseUserEntity(22L, 5L, CourseRelation.course_admin); + when(courseUserRepository.findById(argThat( + arg -> arg.getCourseId() == clusterEntity.getCourseId() && arg.getUserId() == 5L))) + .thenReturn(Optional.of(courseAdminCU)); + + result = clusterUtil.checkFillClusterJson(fillJson, clusterEntity); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* User not found in course */ + reset(courseUserRepository); + when(courseUserRepository.findById(any())).thenReturn(Optional.of(enrolledCU)); + when(courseUserRepository.findById(argThat( + arg -> arg.getCourseId() == clusterEntity.getCourseId() && arg.getUserId() == 3L))) + .thenReturn(Optional.empty()); + + result = clusterUtil.checkFillClusterJson(fillJson, clusterEntity); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* trying to add user twice */ + reset(courseUserRepository); + when(courseUserRepository.findById(any())).thenReturn(Optional.of(enrolledCU)); + fillJson.addClusterGroupMembers("Group3", new Long[]{5L, 4L}); + + result = clusterUtil.checkFillClusterJson(fillJson, clusterEntity); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + } + + +} diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/CommonDataBaseActionsTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/CommonDataBaseActionsTest.java new file mode 100644 index 00000000..4991ec55 --- /dev/null +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/CommonDataBaseActionsTest.java @@ -0,0 +1,608 @@ +package com.ugent.pidgeon.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.ugent.pidgeon.postgre.models.CourseEntity; +import com.ugent.pidgeon.postgre.models.GroupClusterEntity; +import com.ugent.pidgeon.postgre.models.GroupEntity; +import com.ugent.pidgeon.postgre.models.GroupFeedbackEntity; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.SubmissionEntity; +import com.ugent.pidgeon.postgre.models.TestEntity; +import com.ugent.pidgeon.postgre.models.UserEntity; +import com.ugent.pidgeon.postgre.models.types.CourseRelation; +import com.ugent.pidgeon.postgre.repository.CourseRepository; +import com.ugent.pidgeon.postgre.repository.CourseUserRepository; +import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; +import com.ugent.pidgeon.postgre.repository.GroupFeedbackRepository; +import com.ugent.pidgeon.postgre.repository.GroupRepository; +import com.ugent.pidgeon.postgre.repository.GroupUserRepository; +import com.ugent.pidgeon.postgre.repository.ProjectRepository; +import com.ugent.pidgeon.postgre.repository.SubmissionRepository; +import com.ugent.pidgeon.postgre.repository.TestRepository; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; + +@ExtendWith(MockitoExtension.class) +public class CommonDataBaseActionsTest { + + @Mock + private GroupRepository groupRepository; + @Mock + private GroupClusterRepository groupClusterRepository; + @Mock + private GroupUserRepository groupUserRepository; + @Mock + private ProjectRepository projectRepository; + @Mock + private GroupFeedbackRepository groupFeedbackRepository; + @Mock + private SubmissionRepository submissionRepository; + @Mock + private TestRepository testRepository; + @Mock + private CourseUserRepository courseUserRepository; + @Mock + private CourseRepository courseRepository; + + @Mock + private FileUtil fileUtil; + + @Spy + @InjectMocks + private CommonDatabaseActions commonDatabaseActions; + + private GroupClusterEntity groupClusterEntity; + private GroupEntity groupEntity; + private UserEntity userEntity; + private CourseEntity courseEntity; + private ProjectEntity projectEntity; + private GroupFeedbackEntity groupFeedbackEntity; + private SubmissionEntity submissionEntity; + private TestEntity testEntity; + + + @BeforeEach + public void setUp() { + courseEntity = new CourseEntity("name", "description",2024); + courseEntity.setId(9L); + + groupClusterEntity = new GroupClusterEntity( + courseEntity.getId(), + 20, + "clusterName", + 5 + ); + groupClusterEntity.setGroupAmount(5); + groupClusterEntity.setId(9L); + + groupEntity = new GroupEntity( + "groupName", + groupClusterEntity.getId() + ); + groupEntity.setId(4L); + groupEntity.setClusterId(groupClusterEntity.getId()); + + userEntity = new UserEntity(); + userEntity.setId(44L); + + + + testEntity = new TestEntity( + "dockerImageBasic", + "dockerTestScriptBasic", + "dockerTestTemplateBasic", + "structureTemplateBasic" + ); + testEntity.setId(38L); + + projectEntity = new ProjectEntity( + courseEntity.getId(), + "projectName", + "projectDescription", + groupClusterEntity.getId(), + testEntity.getId(), + true, + 34, + OffsetDateTime.now() + ); + projectEntity.setId(64); + + groupFeedbackEntity = new GroupFeedbackEntity( + groupEntity.getId(), + projectEntity.getId(), + 5.0f, + "feedback" + ); + + submissionEntity = new SubmissionEntity( + 22, + 45L, + 99L, + OffsetDateTime.MIN, + true, + true + ); + + + + } + + @Test + public void removeGroup() { + long groupId = groupEntity.getId(); + int originalGroupCount = groupClusterEntity.getGroupAmount(); + + when(groupRepository.findById(groupId)).thenReturn(Optional.of(groupEntity)); + when(groupClusterRepository.findById(groupEntity.getClusterId())).thenReturn(Optional.of(groupClusterEntity)); + + assertTrue(commonDatabaseActions.removeGroup(groupId)); + verify(groupRepository, times(1)).deleteGroupUsersByGroupId(groupId); + verify(groupRepository, times(1)).deleteSubmissionsByGroupId(groupId); + verify(groupRepository, times(1)).deleteGroupFeedbacksByGroupId(groupId); + verify(groupRepository, times(1)).deleteById(groupId); + verify(groupClusterRepository, times(1)).save(groupClusterEntity); + + assertEquals(originalGroupCount - 1, groupClusterEntity.getGroupAmount()); + + /* Group not found */ + when(groupRepository.findById(groupId)).thenReturn(Optional.empty()); + assertTrue(commonDatabaseActions.removeGroup(groupId)); + verify(groupRepository, times(1)).deleteGroupUsersByGroupId(groupId); + verify(groupRepository, times(1)).deleteSubmissionsByGroupId(groupId); + verify(groupRepository, times(1)).deleteGroupFeedbacksByGroupId(groupId); + verify(groupRepository, times(1)).deleteById(groupId); + verify(groupClusterRepository, times(1)).save(groupClusterEntity); + + assertEquals(originalGroupCount - 1, groupClusterEntity.getGroupAmount()); + + /* Unexpected error */ + when(groupRepository.findById(groupId)).thenThrow(new RuntimeException()); + assertFalse(commonDatabaseActions.removeGroup(groupId)); + } + + @Test + public void testCreateNewIndividualClusterGroup () { + int originalGroupCount = groupClusterEntity.getGroupAmount(); + + when(groupClusterRepository.findIndividualClusterByCourseId(courseEntity.getId())).thenReturn( + Optional.of(groupClusterEntity)); + when(groupRepository.save(argThat( + group -> + group.getClusterId() == groupClusterEntity.getId() && + group.getName().equals(userEntity.getName() + " " + userEntity.getSurname()) + ))).thenReturn(groupEntity); + assertTrue( + commonDatabaseActions.createNewIndividualClusterGroup(courseEntity.getId(), userEntity)); + + verify(groupClusterRepository, times(1)).save(groupClusterEntity); + verify(groupUserRepository, times(1)).save(argThat( + groupUser -> + groupUser.getGroupId() == groupEntity.getId() && + groupUser.getUserId() == userEntity.getId() + )); + assertEquals(originalGroupCount + 1, groupClusterEntity.getGroupAmount()); + + /* Group cluster not found */ + when(groupClusterRepository.findIndividualClusterByCourseId(courseEntity.getId())).thenReturn( + Optional.empty()); + assertFalse(commonDatabaseActions.createNewIndividualClusterGroup(courseEntity.getId(), userEntity)); + } + + @Test + public void testRemoveIndividualClusterGroup() { + long groupId = groupEntity.getId(); + int originalGroupCount = groupClusterEntity.getGroupAmount(); + + when(groupClusterRepository.findIndividualClusterByCourseId(courseEntity.getId())).thenReturn( + Optional.of(groupClusterEntity)); + when(groupRepository.groupByClusterAndUser(groupClusterEntity.getId(), userEntity.getId())) + .thenReturn(Optional.of(groupEntity)); + + assertTrue(commonDatabaseActions.removeIndividualClusterGroup(courseEntity.getId(), + userEntity.getId())); + + verify(commonDatabaseActions, times(1)).removeGroup(groupId); + verify(groupClusterRepository, times(1)).save(groupClusterEntity); + assertEquals(originalGroupCount - 1, groupClusterEntity.getGroupAmount()); + + /* Group not found */ + when(groupRepository.groupByClusterAndUser(groupClusterEntity.getId(), userEntity.getId())) + .thenReturn(Optional.empty()); + assertFalse(commonDatabaseActions.removeIndividualClusterGroup(courseEntity.getId(), + userEntity.getId())); + + /* Group cluster not found */ + when(groupClusterRepository.findIndividualClusterByCourseId(courseEntity.getId())).thenReturn( + Optional.empty()); + assertFalse(commonDatabaseActions.removeIndividualClusterGroup(courseEntity.getId(), + userEntity.getId())); + } + + @Test + public void testDeleteProject() { + List groupFeedbackEntities = List.of(groupFeedbackEntity); + List submissionEntities = List.of(submissionEntity); + when(projectRepository.findById(projectEntity.getId())).thenReturn(Optional.of(projectEntity)); + when(groupFeedbackRepository.findByProjectId(projectEntity.getId())).thenReturn(groupFeedbackEntities); + when(submissionRepository.findByProjectId(projectEntity.getId())).thenReturn(submissionEntities); + doReturn(new CheckResult<>(HttpStatus.OK, "", null)).when(commonDatabaseActions).deleteSubmissionById(submissionEntity.getId()); + when(testRepository.findById(projectEntity.getTestId())).thenReturn(Optional.of(testEntity)); + doReturn(new CheckResult<>(HttpStatus.OK, "", null)).when(commonDatabaseActions).deleteTestById(projectEntity, testEntity); + + CheckResult result = commonDatabaseActions.deleteProject(projectEntity.getId()); + assertEquals(HttpStatus.OK, result.getStatus()); + + verify(projectRepository, times(1)).delete(projectEntity); + verify(groupFeedbackRepository, times(1)).deleteAll(groupFeedbackEntities); + + /* No test */ + reset(testRepository); + projectEntity.setTestId(null); + result = commonDatabaseActions.deleteProject(projectEntity.getId()); + assertEquals(HttpStatus.OK, result.getStatus()); + + verify(testRepository, times(0)).delete(testEntity); + + + /* Test not found */ + projectEntity.setTestId(testEntity.getId()); + when(testRepository.findById(projectEntity.getTestId())).thenReturn(Optional.empty()); + result = commonDatabaseActions.deleteProject(projectEntity.getId()); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); + + /* Test deletion failed */ + when(testRepository.findById(projectEntity.getTestId())).thenReturn(Optional.of(testEntity)); + doReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)).when(commonDatabaseActions).deleteTestById(projectEntity, testEntity); + result = commonDatabaseActions.deleteProject(projectEntity.getId()); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + /* Submission deletion failed */ + reset(commonDatabaseActions); + doReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)).when(commonDatabaseActions).deleteSubmissionById(submissionEntity.getId()); + result = commonDatabaseActions.deleteProject(projectEntity.getId()); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + /* Project not found */ + when(projectRepository.findById(projectEntity.getId())).thenReturn(Optional.empty()); + result = commonDatabaseActions.deleteProject(projectEntity.getId()); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); + + /* Unexpected error */ + when(projectRepository.findById(projectEntity.getId())).thenThrow(new RuntimeException()); + result = commonDatabaseActions.deleteProject(projectEntity.getId()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatus()); + } + + @Test + public void testDeleteSubmissionById() { + when(submissionRepository.findById(submissionEntity.getId())).thenReturn(Optional.of(submissionEntity)); + when(fileUtil.deleteFileById(submissionEntity.getFileId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + CheckResult result = commonDatabaseActions.deleteSubmissionById(submissionEntity.getId()); + assertEquals(HttpStatus.OK, result.getStatus()); + + verify(submissionRepository, times(1)).delete(submissionEntity); + + /* File deletion failed */ + when(fileUtil.deleteFileById(submissionEntity.getFileId())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)); + result = commonDatabaseActions.deleteSubmissionById(submissionEntity.getId()); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + /* Submission not found */ + when(submissionRepository.findById(submissionEntity.getId())).thenReturn(Optional.empty()); + result = commonDatabaseActions.deleteSubmissionById(submissionEntity.getId()); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); + + /* Unexpected error */ + when(submissionRepository.findById(submissionEntity.getId())).thenThrow(new RuntimeException()); + result = commonDatabaseActions.deleteSubmissionById(submissionEntity.getId()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatus()); + } + + @Test + public void testDeleteTestById() { + when(testRepository.imageIsUsed(testEntity.getDockerImage())).thenReturn(false); + + CheckResult result = commonDatabaseActions.deleteTestById(projectEntity, testEntity); + assertEquals(HttpStatus.OK, result.getStatus()); + + verify(testRepository, times(1)).deleteById(testEntity.getId()); + verify(projectRepository, times(1)).save(projectEntity); + assertNull(projectEntity.getTestId()); + + /* Image is used */ + when(testRepository.imageIsUsed(testEntity.getDockerImage())).thenReturn(true); + result = commonDatabaseActions.deleteTestById(projectEntity, testEntity); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Unexpected error */ + when(testRepository.imageIsUsed(testEntity.getDockerImage())).thenThrow(new RuntimeException()); + result = commonDatabaseActions.deleteTestById(projectEntity, testEntity); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatus()); + } + + @Test + public void testDeleteClusterById() { + List groupEntities = List.of(groupEntity); + when(groupRepository.findAllByClusterId(groupClusterEntity.getId())).thenReturn(groupEntities); + doReturn(true).when(commonDatabaseActions).removeGroup(groupEntity.getId()); + + CheckResult result = commonDatabaseActions.deleteClusterById(groupClusterEntity.getId()); + assertEquals(HttpStatus.OK, result.getStatus()); + + verify(groupClusterRepository, times(1)).deleteById(groupClusterEntity.getId()); + + /* Group deletion failed */ + reset(commonDatabaseActions); + doReturn(false).when(commonDatabaseActions).removeGroup(groupEntity.getId()); + result = commonDatabaseActions.deleteClusterById(groupClusterEntity.getId()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatus()); + + /* Unexpected error */ + when(groupRepository.findAllByClusterId(groupClusterEntity.getId())).thenThrow(new RuntimeException()); + result = commonDatabaseActions.deleteClusterById(groupClusterEntity.getId()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatus()); + } + + @Test + public void testCopyCourse() { + String originalCourseKey = "courseKey"; + courseEntity.setJoinKey(originalCourseKey); + Long newCourseId = 39L; + CourseEntity newCourse = new CourseEntity(courseEntity.getName(), courseEntity.getDescription(), courseEntity.getCourseYear()); + newCourse.setJoinKey("randomnewkey"); + newCourse.setId(newCourseId); + + GroupClusterEntity individualCluster = new GroupClusterEntity( + courseEntity.getId(), 20, "clustername", 5 + ); + when(courseRepository.save(argThat( + course -> + course.getName().equals(courseEntity.getName()) && + course.getDescription().equals(courseEntity.getDescription()) && + course.getCourseYear() == courseEntity.getCourseYear() && + !course.getJoinKey().equals(originalCourseKey) + ))).thenReturn(newCourse); + + when(groupClusterRepository.findIndividualClusterByCourseId(courseEntity.getId())).thenReturn( + Optional.of(individualCluster)); + + doReturn(new CheckResult<>(HttpStatus.OK, "", individualCluster)) + .when(commonDatabaseActions).copyGroupCluster(individualCluster, newCourseId, false); + + List groupClusterEntities = List.of(groupClusterEntity); + long newGroupClusterId = 42L; + GroupClusterEntity groupClusterCopy = new GroupClusterEntity( + 67L, groupClusterEntity.getGroupAmount(), groupClusterEntity.getName(), groupClusterEntity.getMaxSize() + ); + groupClusterCopy.setId(newGroupClusterId); + when(groupClusterRepository.findClustersWithoutInvidualByCourseId(courseEntity.getId())) + .thenReturn(groupClusterEntities); + doReturn(new CheckResult<>(HttpStatus.OK, "", groupClusterCopy)) + .when(commonDatabaseActions).copyGroupCluster(groupClusterEntity, newCourseId, true); + + List projectEntities = List.of(projectEntity); + projectEntity.setGroupClusterId(groupClusterEntity.getId()); + when(projectRepository.findByCourseId(courseEntity.getId())).thenReturn(projectEntities); + doReturn(new CheckResult<>(HttpStatus.OK, "", null)) + .when(commonDatabaseActions).copyProject(projectEntity, newCourseId, newGroupClusterId); + + CheckResult result = commonDatabaseActions.copyCourse(courseEntity, userEntity.getId()); + assertEquals(HttpStatus.OK, result.getStatus()); + + verify(courseUserRepository, times(1)).save(argThat( + courseUser -> + courseUser.getCourseId() == result.getData().getId() && + courseUser.getUserId() == userEntity.getId() && + courseUser.getRelation().equals(CourseRelation.creator) + )); + + assertNotEquals(originalCourseKey, result.getData().getJoinKey()); + assertEquals(newCourseId, result.getData().getId()); + + CheckResult failedResult; + /* Copyproject fails */ + doReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)) + .when(commonDatabaseActions).copyProject(projectEntity, newCourseId, newGroupClusterId); + failedResult = commonDatabaseActions.copyCourse(courseEntity, userEntity.getId()); + assertEquals(HttpStatus.I_AM_A_TEAPOT, failedResult.getStatus()); + + /* CopyGroupCluster fails */ + doReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)) + .when(commonDatabaseActions).copyGroupCluster(groupClusterEntity, newCourseId, true); + failedResult = commonDatabaseActions.copyCourse(courseEntity, userEntity.getId()); + assertEquals(HttpStatus.I_AM_A_TEAPOT, failedResult.getStatus()); + + /* CopyIndividualCluster fails */ + doReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)) + .when(commonDatabaseActions).copyGroupCluster(individualCluster, newCourseId, false); + failedResult = commonDatabaseActions.copyCourse(courseEntity, userEntity.getId()); + assertEquals(HttpStatus.I_AM_A_TEAPOT, failedResult.getStatus()); + + /* Individual cluster isn't found */ + when(groupClusterRepository.findIndividualClusterByCourseId(courseEntity.getId())).thenReturn( + Optional.empty()); + failedResult = commonDatabaseActions.copyCourse(courseEntity, userEntity.getId()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, failedResult.getStatus()); + + } + + @Test + public void testCopyGroupCluster() { + long newCourseId = 39L; + GroupClusterEntity newGroupCluster = new GroupClusterEntity( + newCourseId, groupClusterEntity.getGroupAmount(), groupClusterEntity.getName(), groupClusterEntity.getMaxSize() + ); + newGroupCluster.setId(42L); + + when(groupClusterRepository.save(argThat( + groupCluster -> + groupCluster.getCourseId() == newCourseId && + groupCluster.getGroupAmount() == groupClusterEntity.getGroupAmount() && + groupCluster.getName().equals(groupClusterEntity.getName()) && + groupCluster.getMaxSize() == groupClusterEntity.getMaxSize() + ))).thenReturn(newGroupCluster); + + List groupEntities = List.of(groupEntity); + when(groupRepository.findAllByClusterId(groupClusterEntity.getId())).thenReturn(groupEntities); + when(groupRepository.save(argThat( + group -> + group.getClusterId() == newGroupCluster.getId() && + group.getName().equals(groupEntity.getName()) + ))).thenReturn(groupEntity); + + CheckResult result = commonDatabaseActions.copyGroupCluster(groupClusterEntity, newCourseId, true); + assertEquals(HttpStatus.OK, result.getStatus()); + assertEquals(newGroupCluster, result.getData()); + + /* Don't copy groups */ + reset(groupClusterRepository); + when(groupClusterRepository.save(argThat( + groupCluster -> + groupCluster.getCourseId() == newCourseId && + groupCluster.getGroupAmount() == 0 && + groupCluster.getName().equals(groupClusterEntity.getName()) && + groupCluster.getMaxSize() == groupClusterEntity.getMaxSize() + ))).thenReturn(newGroupCluster); + result = commonDatabaseActions.copyGroupCluster(groupClusterEntity, newCourseId, false); + assertEquals(HttpStatus.OK, result.getStatus()); + assertEquals(newGroupCluster, result.getData()); + + verify(groupRepository, times(1)).save(any()); + } + + @Test + public void testCopyProject() { + long newCourseId = 39L; + long newGroupClusterId = 42L; + long newProjectId = 99L; + long newTestId = 88L; + testEntity.setId(newTestId); + ProjectEntity newProject = new ProjectEntity( + newCourseId, + projectEntity.getName(), + projectEntity.getDescription(), + newGroupClusterId, + projectEntity.getTestId(), + projectEntity.isVisible(), + projectEntity.getMaxScore(), + projectEntity.getDeadline() + ); + newProject.setId(newProjectId); + + when(projectRepository.save(any())).thenReturn(newProject); + + when(testRepository.findById(projectEntity.getTestId())).thenReturn(Optional.of(testEntity)); + doReturn(new CheckResult<>(HttpStatus.OK, "", testEntity)) + .when(commonDatabaseActions).copyTest(testEntity); + + CheckResult result = commonDatabaseActions.copyProject(projectEntity, newCourseId, newGroupClusterId); + assertEquals(HttpStatus.OK, result.getStatus()); + assertEquals(newProject, result.getData()); + assertEquals(newProjectId, result.getData().getId()); + + + + verify(projectRepository, times(1)).save(argThat( + project -> project.getCourseId() == newCourseId && + project.getName().equals(projectEntity.getName()) && + project.getDescription().equals(projectEntity.getDescription()) && + project.getGroupClusterId() == newGroupClusterId && + Objects.equals(project.getTestId(), null) && + project.isVisible() == projectEntity.isVisible() && + Objects.equals(project.getMaxScore(), projectEntity.getMaxScore()) && + project.getDeadline().equals(projectEntity.getDeadline()) + )); + + verify(projectRepository, times(1)).save(argThat( + project -> + project.getCourseId() == newCourseId && + project.getName().equals(projectEntity.getName()) && + project.getDescription().equals(projectEntity.getDescription()) && + project.getGroupClusterId() == newGroupClusterId && + Objects.equals(project.getTestId(), newTestId) && + project.isVisible() == projectEntity.isVisible() && + Objects.equals(project.getMaxScore(), projectEntity.getMaxScore()) && + project.getDeadline().equals(projectEntity.getDeadline()) + )); + + /* CopyTestFails */ + doReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)) + .when(commonDatabaseActions).copyTest(testEntity); + result = commonDatabaseActions.copyProject(projectEntity, newCourseId, newGroupClusterId); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + /* Test not found */ + when(testRepository.findById(projectEntity.getTestId())).thenReturn(Optional.empty()); + result = commonDatabaseActions.copyProject(projectEntity, newCourseId, newGroupClusterId); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatus()); + + /* project has no test */ + reset(projectRepository); + reset(testRepository); + reset(commonDatabaseActions); + when(projectRepository.save(any())).thenReturn(newProject); + projectEntity.setTestId(null); + result = commonDatabaseActions.copyProject(projectEntity, newCourseId, newGroupClusterId); + assertEquals(HttpStatus.OK, result.getStatus()); + + verify(projectRepository, times(1)).save(argThat( + project -> project.getCourseId() == newCourseId && + project.getName().equals(projectEntity.getName()) && + project.getDescription().equals(projectEntity.getDescription()) && + project.getGroupClusterId() == newGroupClusterId && + Objects.equals(project.getTestId(), null) && + project.isVisible() == projectEntity.isVisible() && + Objects.equals(project.getMaxScore(), projectEntity.getMaxScore()) && + project.getDeadline().equals(projectEntity.getDeadline()) + )); + verify(testRepository, times(0)).findById(projectEntity.getTestId()); + verify(commonDatabaseActions, times(0)).copyTest(testEntity); + } + + @Test + public void testCopyTest() { + long newTestId = 9088L; + TestEntity newTest = new TestEntity( + testEntity.getDockerImage(), + testEntity.getDockerTestScript(), + testEntity.getDockerTestTemplate(), + testEntity.getStructureTemplate() + ); + newTest.setId(newTestId); + + when(testRepository.save(argThat( + test -> + test.getDockerImage().equals(testEntity.getDockerImage()) && + test.getDockerTestScript().equals(testEntity.getDockerTestScript()) && + test.getDockerTestTemplate().equals(testEntity.getDockerTestTemplate()) && + test.getStructureTemplate().equals(testEntity.getStructureTemplate()) + ))).thenReturn(newTest); + + CheckResult result = commonDatabaseActions.copyTest(testEntity); + assertEquals(HttpStatus.OK, result.getStatus()); + assertEquals(newTest, result.getData()); + assertEquals(newTestId, result.getData().getId()); + } +} diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/CourseUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/CourseUtilTest.java index 0126606b..eb7bd34d 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/CourseUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/CourseUtilTest.java @@ -3,12 +3,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; -import com.ugent.pidgeon.model.json.CourseJson; -import com.ugent.pidgeon.model.json.CourseMemberRequestJson; -import com.ugent.pidgeon.model.json.UserIdJson; +import com.ugent.pidgeon.json.CourseJson; +import com.ugent.pidgeon.json.CourseMemberRequestJson; import com.ugent.pidgeon.postgre.models.CourseEntity; import com.ugent.pidgeon.postgre.models.CourseUserEntity; import com.ugent.pidgeon.postgre.models.UserEntity; @@ -16,12 +17,14 @@ import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.CourseRepository; import com.ugent.pidgeon.postgre.repository.CourseUserRepository; +import java.time.OffsetDateTime; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -35,6 +38,10 @@ public class CourseUtilTest { @Mock private CourseRepository courseRepository; + @Mock + private UserUtil userUtil; + + @Spy @InjectMocks private CourseUtil courseUtil; @@ -46,10 +53,10 @@ public class CourseUtilTest { @BeforeEach public void setUp() { - user = new UserEntity("name", "surname", "email", UserRole.student, "azureid"); - user.setId(1L); - course = new CourseEntity("name", "description"); - course.setId(1L); + user = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); + user.setId(44L); + course = new CourseEntity("name", "description",2024); + course.setId(9L); course.setJoinKey("key"); cuEnrolled = new CourseUserEntity(1L, 1L, CourseRelation.enrolled); cuAdmin = new CourseUserEntity(1L, 2L, CourseRelation.course_admin); @@ -57,103 +64,426 @@ public void setUp() { } @Test - public void testGetCourseIfAdmin() throws Exception { - when(courseRepository.findById(anyLong())).thenReturn(Optional.of(course)); - when(courseUserRepository.findById(any())).thenReturn(Optional.of(cuAdmin)); - CheckResult result = courseUtil.getCourseIfAdmin(1L, user); - assertEquals(HttpStatus.OK, result.getStatus()); - assertEquals(course, result.getData()); + public void testGetCourseIfAdmin() { + /* All checks succeed */ + doReturn(new CheckResult<>(HttpStatus.OK, "", new Pair(course, CourseRelation.course_admin))) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + + CheckResult check = courseUtil.getCourseIfAdmin(course.getId(), user); + assertEquals(HttpStatus.OK, check.getStatus()); + assertEquals(course, check.getData()); + /* User is not a course admin */ + doReturn(new CheckResult<>(HttpStatus.OK, "", new Pair(course, CourseRelation.enrolled))) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + check = courseUtil.getCourseIfAdmin(course.getId(), user); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* User is not a course admin, but a platform admin */ + user.setRole(UserRole.admin); + check = courseUtil.getCourseIfAdmin(course.getId(), user); + assertEquals(HttpStatus.OK, check.getStatus()); + + /* Get course fails */ + doReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + check = courseUtil.getCourseIfAdmin(course.getId(), user); + assertEquals(HttpStatus.I_AM_A_TEAPOT, check.getStatus()); + } + + @Test + public void testGetCourseIfUserInCourse() { + /* All checks succeed */ + doReturn(new CheckResult<>(HttpStatus.OK, "", course)).when(courseUtil).getCourseIfExists(course.getId()); when(courseUserRepository.findById(any())).thenReturn(Optional.of(cuEnrolled)); - result = courseUtil.getCourseIfAdmin(1L, user); - assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); - assertEquals("User is not an admin of the course", result.getMessage()); + CheckResult> check = courseUtil.getCourseIfUserInCourse(course.getId(), user); + assertEquals(HttpStatus.OK, check.getStatus()); + assertEquals(course, check.getData().getFirst()); + assertEquals(CourseRelation.enrolled, check.getData().getSecond()); + + when(courseUserRepository.findById(any())).thenReturn(Optional.of(cuAdmin)); + check = courseUtil.getCourseIfUserInCourse(course.getId(), user); + assertEquals(CourseRelation.course_admin, check.getData().getSecond()); + + when(courseUserRepository.findById(any())).thenReturn(Optional.of(cuCreator)); + check = courseUtil.getCourseIfUserInCourse(course.getId(), user); + assertEquals(CourseRelation.creator, check.getData().getSecond()); + + /* User isn't in course */ + when(courseUserRepository.findById(any())).thenReturn(Optional.empty()); + check = courseUtil.getCourseIfUserInCourse(course.getId(), user); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* User isn't in course but is admin */ + user.setRole(UserRole.admin); + check = courseUtil.getCourseIfUserInCourse(course.getId(), user); + assertEquals(HttpStatus.OK, check.getStatus()); + + /* Get course fails */ + reset(courseUtil); + doReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)) + .when(courseUtil).getCourseIfExists(course.getId()); + check = courseUtil.getCourseIfUserInCourse(course.getId(), user); + assertEquals(HttpStatus.I_AM_A_TEAPOT, check.getStatus()); } @Test - public void testGetCourseIfExists() throws Exception { - when(courseRepository.findById(anyLong())).thenReturn(Optional.of(course)); - CheckResult check = courseUtil.getCourseIfExists(1L); + public void testGetCourseIfExists() { + reset(courseUtil); + /* All checks succeed */ + when(courseRepository.findById(course.getId())).thenReturn(Optional.of(course)); + CheckResult check = courseUtil.getCourseIfExists(course.getId()); assertEquals(HttpStatus.OK, check.getStatus()); assertEquals(course, check.getData()); - when(courseRepository.findById(anyLong())).thenReturn(Optional.empty()); - check = courseUtil.getCourseIfExists(1L); + /* Course does not exist */ + when(courseRepository.findById(course.getId())).thenReturn(Optional.empty()); + check = courseUtil.getCourseIfExists(course.getId()); assertEquals(HttpStatus.NOT_FOUND, check.getStatus()); - assertEquals("Course not found", check.getMessage()); assertNull(check.getData()); } @Test - public void testCanUpdateUserInCourse() throws Exception { + public void testCanUpdateUserInCourse() { CourseMemberRequestJson request = new CourseMemberRequestJson(); - request.setUserId(2L); - request.setRelation(String.valueOf(CourseRelation.enrolled)); - when(courseRepository.findById(anyLong())).thenReturn(Optional.of(course)); + request.setUserId(5L); + request.setRelation("course_admin"); + /* All checks succeed */ + doReturn(new CheckResult<>(HttpStatus.OK, "", new Pair(course, CourseRelation.creator))) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + when(courseUserRepository.findById(any())).thenReturn(Optional.of(cuAdmin)); - CheckResult checkResult = courseUtil.canUpdateUserInCourse( - 1L, request, user, HttpMethod.PATCH - ); - assertEquals(HttpStatus.OK, checkResult.getStatus()); + + CheckResult check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.PATCH); + assertEquals(HttpStatus.OK, check.getStatus()); + assertEquals(cuAdmin, check.getData()); + + /* User is not creator but trying to add admin */ + doReturn(new CheckResult<>(HttpStatus.OK, "", new Pair(course, CourseRelation.course_admin))) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.PATCH); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* User is not creator but trying to downgrade admin */ + request.setRelation("enrolled"); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.PATCH); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* User is general admin and trying to add admin */ + request.setRelation("course_admin"); + user.setRole(UserRole.admin); + when(courseUserRepository.findById(any())).thenReturn(Optional.of(cuEnrolled)); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.PATCH); + assertEquals(HttpStatus.OK, check.getStatus()); + + /* User is trying to change the creator */ + request.setRelation("creator"); + user.setRole(UserRole.teacher); + doReturn(new CheckResult<>(HttpStatus.OK, "", new Pair(course, CourseRelation.creator))) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.PATCH); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* User is trying to change the creator as admin */ + user.setRole(UserRole.admin); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.PATCH); + assertEquals(HttpStatus.OK, check.getStatus()); + user.setRole(UserRole.teacher); + request.setRelation("enrolled"); + + /* User is trying to change it's own role */ + request.setUserId(user.getId()); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.PATCH); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* User is trying to change it's own role as admin */ + user.setRole(UserRole.admin); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.PATCH); + assertEquals(HttpStatus.OK, check.getStatus()); + user.setRole(UserRole.teacher); + + /* User isn't in course on patch */ + request.setUserId(5L); + when(courseUserRepository.findById(any())).thenReturn(Optional.empty()); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.PATCH); + assertEquals(HttpStatus.BAD_REQUEST, check.getStatus()); + + /* Post everything succeeds */ + when(userUtil.userExists(request.getUserId())).thenReturn(true); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.POST); + assertEquals(HttpStatus.OK, check.getStatus()); + + /* User doesn't exist */ + when(userUtil.userExists(request.getUserId())).thenReturn(false); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.POST); + assertEquals(HttpStatus.NOT_FOUND, check.getStatus()); + + /* User is already in course on POST */ + when(courseUserRepository.findById(any())).thenReturn(Optional.of(cuEnrolled)); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.POST); + assertEquals(HttpStatus.BAD_REQUEST, check.getStatus()); + + /* Invalid relation */ + request.setRelation("invalid"); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.POST); + assertEquals(HttpStatus.BAD_REQUEST, check.getStatus()); + + /* Relation not present */ + request.setRelation(null); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.POST); + assertEquals(HttpStatus.BAD_REQUEST, check.getStatus()); + + /* UserId not present */ + request.setRelation("enrolled"); + request.setUserId(null); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.POST); + assertEquals(HttpStatus.BAD_REQUEST, check.getStatus()); + + /* User is not an admin */ + request.setUserId(5L); + when(courseUserRepository.findById(any())).thenReturn(Optional.empty()); + when(userUtil.userExists(request.getUserId())).thenReturn(true); + doReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(course, CourseRelation.enrolled))) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.POST); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* User is not in course but is admin */ + user.setRole(UserRole.admin); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.POST); + assertEquals(HttpStatus.OK, check.getStatus()); + + /* get course fails */ + doReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + check = courseUtil.canUpdateUserInCourse(course.getId(), request, user, HttpMethod.POST); + assertEquals(HttpStatus.I_AM_A_TEAPOT, check.getStatus()); + } @Test public void testCanLeaveCourse() throws Exception { - when(courseRepository.findById(anyLong())).thenReturn(Optional.of(course)); - when(courseUserRepository.findById(any())).thenReturn(Optional.of(cuAdmin)); - CheckResult checkResult = courseUtil.canLeaveCourse(1L, user); - assertEquals(HttpStatus.OK, checkResult.getStatus()); - assertEquals(CourseRelation.course_admin, checkResult.getData()); + /* All checks succeed */ + doReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(course, CourseRelation.enrolled))) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + CheckResult check = courseUtil.canLeaveCourse(course.getId(), user); + assertEquals(HttpStatus.OK, check.getStatus()); + assertEquals(CourseRelation.enrolled, check.getData()); + + /* Course is archived */ + course.setArchivedAt(OffsetDateTime.now()); + check = courseUtil.canLeaveCourse(course.getId(), user); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* User is course creator */ + doReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(course, CourseRelation.creator))) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + check = courseUtil.canLeaveCourse(course.getId(), user); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* get course fails */ + doReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + check = courseUtil.canLeaveCourse(course.getId(), user); + assertEquals(HttpStatus.I_AM_A_TEAPOT, check.getStatus()); } @Test public void testCanDeleteUser() throws Exception { - when(courseRepository.findById(anyLong())).thenReturn(Optional.of(course)); - when(courseUserRepository.findById(any())).thenReturn(Optional.of(cuAdmin)); - CheckResult checkResult = courseUtil.canDeleteUser( - 1L, 5L, user - ); - assertEquals(HttpStatus.OK, checkResult.getStatus()); - assertEquals(CourseRelation.course_admin, checkResult.getData()); + /* All checks succeed */ + doReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(course, CourseRelation.creator))) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + when(courseUserRepository.findById(argThat( + arg -> arg.getCourseId() == (course.getId()) && + arg.getUserId() == cuEnrolled.getUserId()) + )).thenReturn(Optional.of(cuEnrolled)); + CheckResult check = courseUtil.canDeleteUser(course.getId(), cuEnrolled.getUserId(), user); + assertEquals(HttpStatus.OK, check.getStatus()); + assertEquals(CourseRelation.enrolled, check.getData()); + + /* User is course admin */ + reset(courseUserRepository); + when(courseUserRepository.findById( + argThat(arg -> arg.getCourseId() == (course.getId()) && arg.getUserId() == cuAdmin.getUserId()) + )).thenReturn(Optional.of(cuAdmin)); + check = courseUtil.canDeleteUser(course.getId(), cuAdmin.getUserId(), user); + assertEquals(HttpStatus.OK, check.getStatus()); + + /* User isn't course creator but tries to delete admin */ + doReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(course, CourseRelation.course_admin))) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + check = courseUtil.canDeleteUser(course.getId(), cuAdmin.getUserId(), user); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* User is general admin and tries to delete admin */ + user.setRole(UserRole.admin); + check = courseUtil.canDeleteUser(course.getId(), cuAdmin.getUserId(), user); + assertEquals(HttpStatus.OK, check.getStatus()); + + /* User tries to delete creator */ + reset(courseUserRepository); + when(courseUserRepository.findById( + argThat(arg -> arg.getCourseId() == (course.getId()) && arg.getUserId() == cuCreator.getUserId()) + )).thenReturn(Optional.of(cuCreator)); + check = courseUtil.canDeleteUser(course.getId(), cuCreator.getUserId(), user); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* User tries to delete itself */ + reset(courseUserRepository); + when(courseUserRepository.findById( + argThat(arg -> arg.getCourseId() == (course.getId()) && arg.getUserId() == user.getId()) + )).thenReturn(Optional.of(new CourseUserEntity(1L, 1L, CourseRelation.enrolled))); + check = courseUtil.canDeleteUser(course.getId(), user.getId(), user); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* User is trying to delete non-existing user */ + reset(courseUserRepository); + when(courseUserRepository.findById( + argThat(arg -> arg.getCourseId() == (course.getId()) && arg.getUserId() == cuEnrolled.getUserId()) + )).thenReturn(Optional.empty()); + check = courseUtil.canDeleteUser(course.getId(), cuEnrolled.getUserId(), user); + assertEquals(HttpStatus.NOT_FOUND, check.getStatus()); + + /* User is not an admin */ + user.setRole(UserRole.student); + doReturn(new CheckResult<>(HttpStatus.OK, "", new Pair<>(course, CourseRelation.enrolled))) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + check = courseUtil.canDeleteUser(course.getId(), cuEnrolled.getUserId(), user); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* User is not in course but is admin */ + user.setRole(UserRole.admin); + reset(courseUserRepository); + when(courseUserRepository.findById(argThat( + arg -> arg.getCourseId() == (course.getId()) && + arg.getUserId() == cuEnrolled.getUserId()) + )).thenReturn(Optional.of(cuEnrolled)); + check = courseUtil.canDeleteUser(course.getId(), cuEnrolled.getUserId(), user); + assertEquals(HttpStatus.OK, check.getStatus()); + + /* get course fails */ + doReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "I'm a teapot", null)) + .when(courseUtil).getCourseIfUserInCourse(course.getId(), user); + check = courseUtil.canDeleteUser(course.getId(), cuEnrolled.getUserId(), user); + assertEquals(HttpStatus.I_AM_A_TEAPOT, check.getStatus()); } @Test public void testGetJoinLink() throws Exception { - String link = courseUtil.getJoinLink("key", "1"); - assertEquals("/api/courses/1/join/key", link); - link = courseUtil.getJoinLink(null, "1"); - assertEquals("/api/courses/1/join", link); + /* Link with key */ + String link = courseUtil.getJoinLink("key", "898"); + assertEquals("/api/courses/898/join/key", link); + /* Link without key */ + link = courseUtil.getJoinLink(null, "334"); + assertEquals("/api/courses/334/join", link); } @Test public void testCheckJoinLink() throws Exception { - when(courseRepository.findById(anyLong())).thenReturn(Optional.of(course)); - when(courseUserRepository.findById(any())).thenReturn(Optional.empty()); - CheckResult result = courseUtil.checkJoinLink(1L, "key", user); - assertEquals(HttpStatus.OK, result.getStatus()); - assertEquals(course, result.getData()); + /* All checks succeed */ + when(courseRepository.findById(course.getId())).thenReturn(Optional.of(course)); + when(courseUserRepository.findById(argThat( + arg -> arg.getCourseId() == (course.getId()) && arg.getUserId() == user.getId()) + )).thenReturn(Optional.empty()); - result = courseUtil.checkJoinLink(1L, null, user); - assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); - assertEquals("Course requires a join key. Use /api/courses/1/join/{courseKey}", - result.getMessage()); + /* Course without key */ + course.setJoinKey(null); + CheckResult check = courseUtil.checkJoinLink(course.getId(), null, user); + assertEquals(HttpStatus.OK, check.getStatus()); + assertEquals(course, check.getData()); + + /* Course with key */ + course.setJoinKey("key"); + check = courseUtil.checkJoinLink(course.getId(), "key", user); + assertEquals(HttpStatus.OK, check.getStatus()); + assertEquals(course, check.getData()); + + /* Check fails */ + /* User already in course */ + reset(courseUserRepository); + when(courseUserRepository.findById(argThat( + arg -> arg.getCourseId() == (course.getId()) && arg.getUserId() == user.getId()) + )).thenReturn(Optional.of(cuEnrolled)); + check = courseUtil.checkJoinLink(course.getId(), "key", user); + assertEquals(HttpStatus.BAD_REQUEST, check.getStatus()); + + /* Course with key but no key provided */ + check = courseUtil.checkJoinLink(course.getId(), null, user); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* Course with key but wrong key provided */ + check = courseUtil.checkJoinLink(course.getId(), "wrong", user); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* Course without key but key provided */ + course.setJoinKey(null); + check = courseUtil.checkJoinLink(course.getId(), "key", user); + assertEquals(HttpStatus.FORBIDDEN, check.getStatus()); + + /* Course does not exist */ + when(courseRepository.findById(course.getId())).thenReturn(Optional.empty()); + check = courseUtil.checkJoinLink(course.getId(), "key", user); + assertEquals(HttpStatus.NOT_FOUND, check.getStatus()); } @Test public void testCheckCourseJson() throws Exception { - CourseJson courseJson = new CourseJson("name", "description"); - CheckResult result = courseUtil.checkCourseJson(courseJson); + CourseJson courseJson = new CourseJson( + "name", "description", null, 2024 + ); + /* Creating a course */ + user.setRole(UserRole.teacher); + when(courseUserRepository.findById(argThat( + arg -> arg.getCourseId() == (course.getId()) && arg.getUserId() == user.getId()) + )).thenReturn(Optional.of(cuCreator)); + CheckResult result = courseUtil.checkCourseJson(courseJson, user, null); assertEquals(HttpStatus.OK, result.getStatus()); + /* Updating a course */ + CheckResult result2 = courseUtil.checkCourseJson(courseJson, user, course.getId()); + + /* Name is empty */ + courseJson.setName(""); + result = courseUtil.checkCourseJson(courseJson, user, null); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* name is null */ + courseJson.setName(null); + result = courseUtil.checkCourseJson(courseJson, user, null); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* description is null */ + courseJson.setName("name"); courseJson.setDescription(null); - result = courseUtil.checkCourseJson(courseJson); + result = courseUtil.checkCourseJson(courseJson, user, null); assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); - assertEquals("name and description are required", result.getMessage()); + /* year is null */ courseJson.setDescription("description"); - courseJson.setName(""); - result = courseUtil.checkCourseJson(courseJson); + courseJson.setYear(null); + result = courseUtil.checkCourseJson(courseJson, user, null); assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); - assertEquals("Name cannot be empty", result.getMessage()); + + /* creator can (un)archive course */ + courseJson.setYear(2024); + courseJson.setArchived(true); + result = courseUtil.checkCourseJson(courseJson, user, course.getId()); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* not-creator can't (un)archive course */ + reset(courseUserRepository); + when(courseUserRepository.findById(argThat( + arg -> arg.getCourseId() == (course.getId()) && arg.getUserId() == user.getId()) + )).thenReturn(Optional.of(cuAdmin)); + result = courseUtil.checkCourseJson(courseJson, user, course.getId()); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* User has to be in course to update */ + reset(courseUserRepository); + when(courseUserRepository.findById(argThat( + arg -> arg.getCourseId() == (course.getId()) && arg.getUserId() == user.getId()) + )).thenReturn(Optional.empty()); + result = courseUtil.checkCourseJson(courseJson, user, course.getId()); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); } } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java index 11285871..96daa4d2 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java @@ -1,38 +1,80 @@ package com.ugent.pidgeon.util; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.ugent.pidgeon.controllers.ApiRoutes; +import com.ugent.pidgeon.json.CourseReferenceJson; +import com.ugent.pidgeon.json.CourseWithInfoJson; +import com.ugent.pidgeon.json.CourseWithRelationJson; +import com.ugent.pidgeon.json.GroupClusterJson; +import com.ugent.pidgeon.json.GroupFeedbackJson; +import com.ugent.pidgeon.json.GroupFeedbackJsonWithProject; +import com.ugent.pidgeon.json.GroupJson; +import com.ugent.pidgeon.json.ProjectProgressJson; +import com.ugent.pidgeon.json.ProjectResponseJsonWithStatus; +import com.ugent.pidgeon.json.ProjectStatus; +import com.ugent.pidgeon.json.SubmissionJson; +import com.ugent.pidgeon.json.TestJson; +import com.ugent.pidgeon.json.UserReferenceJson; +import com.ugent.pidgeon.json.UserReferenceWithRelation; import com.ugent.pidgeon.model.ProjectResponseJson; -import com.ugent.pidgeon.model.json.CourseWithInfoJson; -import com.ugent.pidgeon.model.json.GroupClusterJson; -import com.ugent.pidgeon.model.json.GroupJson; -import com.ugent.pidgeon.model.json.SubmissionJson; -import com.ugent.pidgeon.model.json.TestJson; -import com.ugent.pidgeon.model.json.UserReferenceJson; -import com.ugent.pidgeon.postgre.models.*; +import com.ugent.pidgeon.postgre.models.CourseEntity; +import com.ugent.pidgeon.postgre.models.CourseUserEntity; +import com.ugent.pidgeon.postgre.models.FileEntity; +import com.ugent.pidgeon.postgre.models.GroupClusterEntity; +import com.ugent.pidgeon.postgre.models.GroupEntity; +import com.ugent.pidgeon.postgre.models.GroupFeedbackEntity; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.SubmissionEntity; +import com.ugent.pidgeon.postgre.models.TestEntity; +import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.CourseRelation; +import com.ugent.pidgeon.postgre.models.types.DockerTestState; +import com.ugent.pidgeon.postgre.models.types.DockerTestType; import com.ugent.pidgeon.postgre.models.types.UserRole; -import com.ugent.pidgeon.postgre.repository.*; +import com.ugent.pidgeon.postgre.repository.CourseRepository; +import com.ugent.pidgeon.postgre.repository.CourseUserRepository; +import com.ugent.pidgeon.postgre.repository.FileRepository; +import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; +import com.ugent.pidgeon.postgre.repository.GroupRepository; +import com.ugent.pidgeon.postgre.repository.GroupRepository.UserReference; +import com.ugent.pidgeon.postgre.repository.ProjectRepository; +import com.ugent.pidgeon.postgre.repository.SubmissionRepository; +import java.io.File; +import java.io.IOException; import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.Collections; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) public class EntityToJsonConverterTest { @Mock private GroupClusterRepository groupClusterRepository; + @Mock + private ClusterUtil clusterUtil; + @Mock private GroupRepository groupRepository; @@ -48,106 +90,577 @@ public class EntityToJsonConverterTest { @Mock private SubmissionRepository submissionRepository; + @Mock + private FileRepository fileRepository; + + @Spy @InjectMocks private EntityToJsonConverter entityToJsonConverter; - private GroupEntity groupEntity; private GroupClusterEntity groupClusterEntity; + private GroupEntity groupEntity; private UserEntity userEntity; + private UserEntity otherUser; private CourseEntity courseEntity; private ProjectEntity projectEntity; + private GroupFeedbackEntity groupFeedbackEntity; private SubmissionEntity submissionEntity; private TestEntity testEntity; + + private GroupJson groupJson; + private UserReferenceJson userReferenceJson; + private UserReferenceJson otherUserReferenceJson; + private GroupFeedbackJson groupFeedbackJson; + private ProjectResponseJson projectResponseJson; + private CourseReferenceJson courseJson; + @BeforeEach public void setUp() { - groupEntity = new GroupEntity("test group", 1L); - groupEntity.setId(1L); + courseEntity = new CourseEntity("name", "description",2024); + courseEntity.setJoinKey("joinKey"); + courseEntity.setId(9L); + + groupClusterEntity = new GroupClusterEntity( + courseEntity.getId(), + 20, + "clusterName", + 5 + ); + groupClusterEntity.setGroupAmount(5); + groupClusterEntity.setId(9L); + + groupEntity = new GroupEntity( + "groupName", + groupClusterEntity.getId() + ); + groupEntity.setId(4L); + + groupJson = new GroupJson( + 20, + 4L, + "groupName", + ApiRoutes.CLUSTER_BASE_PATH + "/" + groupClusterEntity.getId() + ); + + userEntity = new UserEntity( + "name", + "surname", + "email", + UserRole.student, + "azureId", + "" + ); + userEntity.setId(44L); + userReferenceJson = new UserReferenceJson( + userEntity.getName() + " " + userEntity.getSurname(), + userEntity.getEmail(), + userEntity.getId(), + "" + ); + + otherUser = new UserEntity( + "otherName", + "otherSurname", + "otherEmail", + UserRole.student, + "otherAzureId", + "" + ); + otherUserReferenceJson = new UserReferenceJson( + otherUser.getName() + " " + otherUser.getSurname(), + otherUser.getEmail(), + otherUser.getId(), + "" + ); + + + + testEntity = new TestEntity( + "dockerImageBasic", + "dockerTestScriptBasic", + "dockerTestTemplateBasic", + "structureTemplateBasic" + ); + testEntity.setId(38L); - groupClusterEntity = new GroupClusterEntity(1L, 5, "Test Cluster", 1); - groupClusterEntity.setId(1L); + projectEntity = new ProjectEntity( + courseEntity.getId(), + "projectName", + "projectDescription", + groupClusterEntity.getId(), + testEntity.getId(), + true, + 34, + OffsetDateTime.now() + ); + projectEntity.setId(64); - userEntity = new UserEntity("name", "surname", "email", UserRole.student, "azureid"); - userEntity.setId(1L); + courseJson = new CourseReferenceJson(courseEntity.getName(), "courseUrl", courseEntity.getId(), null); - courseEntity = new CourseEntity(); - courseEntity.setId(1L); - courseEntity.setName("Test Course"); - projectEntity = new ProjectEntity(); - projectEntity.setId(1L); - projectEntity.setVisible(true); - projectEntity.setName("Test Project"); + projectResponseJson = new ProjectResponseJson( + courseJson, + projectEntity.getDeadline(), + projectEntity.getDescription(), + projectEntity.getId(), + projectEntity.getName(), + "SubmissionURL", + "TestURL", + projectEntity.getMaxScore(), + projectEntity.isVisible(), + new ProjectProgressJson(44, 60), + groupEntity.getId(), + groupClusterEntity.getId(), + OffsetDateTime.now() + ); - submissionEntity = new SubmissionEntity(1L, 1L, 1L, OffsetDateTime.now(), true, true); - submissionEntity.setId(1L); + groupFeedbackEntity = new GroupFeedbackEntity( + groupEntity.getId(), + projectEntity.getId(), + 5.0f, + "feedback" + ); - testEntity = new TestEntity(); - testEntity.setId(1L); - testEntity.setDockerImage("Test Docker Image"); + groupFeedbackJson = new GroupFeedbackJson( + groupFeedbackEntity.getScore(), + groupFeedbackEntity.getFeedback(), + groupFeedbackEntity.getGroupId(), + groupFeedbackEntity.getProjectId() + ); + + submissionEntity = new SubmissionEntity( + 22, + 45L, + 99L, + OffsetDateTime.MIN, + true, + true + ); } @Test public void testGroupEntityToJson() { - when(groupClusterRepository.findById(anyLong())).thenReturn(Optional.of(groupClusterEntity)); + userEntity.setStudentNumber("studentNumber"); + when(groupClusterRepository.findById(groupEntity.getClusterId())).thenReturn(Optional.of(groupClusterEntity)); when(groupRepository.findGroupUsersReferencesByGroupId(anyLong())).thenReturn( - Collections.emptyList()); - GroupJson result = entityToJsonConverter.groupEntityToJson(groupEntity); + List.of(new UserReference[]{ + new UserReference() { + @Override + public Long getUserId() { + return userEntity.getId(); + } + + @Override + public String getName() { + return userEntity.getName() + " " + userEntity.getSurname(); + } + + @Override + public String getEmail() { + return userEntity.getEmail(); + } + + @Override + public String getStudentNumber() { + return userEntity.getStudentNumber(); + } + } + + }) + ); + GroupJson result = entityToJsonConverter.groupEntityToJson(groupEntity, false); + assertEquals(groupClusterEntity.getMaxSize(), result.getCapacity()); + assertEquals(groupEntity.getId(), result.getGroupId()); assertEquals(groupEntity.getName(), result.getName()); + assertEquals(ApiRoutes.CLUSTER_BASE_PATH + "/" + groupClusterEntity.getId(), result.getGroupClusterUrl()); + assertEquals(1, result.getMembers().size()); + UserReferenceJson userReferenceJson = result.getMembers().get(0); + assertEquals(userEntity.getId(), userReferenceJson.getUserId()); + assertEquals(userEntity.getName() + " " + userEntity.getSurname(), userReferenceJson.getName()); + assertEquals(userEntity.getEmail(), userReferenceJson.getEmail()); + assertEquals(userEntity.getStudentNumber(), userReferenceJson.getStudentNumber()); + + /* Cluster is individual */ + groupClusterEntity.setMaxSize(1); + result = entityToJsonConverter.groupEntityToJson(groupEntity, false); + assertEquals(1, result.getCapacity()); + assertNull(result.getGroupClusterUrl()); + + /* StudentNumber gets hidden correctly */ + result = entityToJsonConverter.groupEntityToJson(groupEntity, true); + assertNull(result.getMembers().get(0).getStudentNumber()); + + /* Issue when groupClusterEntity is null */ + when(groupClusterRepository.findById(groupEntity.getClusterId())).thenReturn(Optional.empty()); + assertThrows(RuntimeException.class, () -> entityToJsonConverter.groupEntityToJson(groupEntity, false)); + } @Test public void testClusterEntityToClusterJson() { - when(groupRepository.findAllByClusterId(anyLong())).thenReturn( - Collections.singletonList(groupEntity)); - when(groupClusterRepository.findById(anyLong())).thenReturn(Optional.of(groupClusterEntity)); - GroupClusterJson result = entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity); + groupClusterEntity.setLockGroupsAfter(OffsetDateTime.now()); + when(groupRepository.findAllByClusterId(groupClusterEntity.getId())).thenReturn(List.of(groupEntity)); + doReturn(groupJson).when(entityToJsonConverter).groupEntityToJson(groupEntity, false); + + GroupClusterJson result = entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, false); + + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, false); + assertEquals(groupClusterEntity.getId(), result.clusterId()); assertEquals(groupClusterEntity.getName(), result.name()); + assertEquals(groupClusterEntity.getMaxSize(), result.capacity()); + assertEquals(groupClusterEntity.getGroupAmount(), result.groupCount()); + assertEquals(groupClusterEntity.getCreatedAt(), result.createdAt()); + assertEquals(1, result.groups().size()); + assertEquals(groupJson, result.groups().get(0)); + assertEquals(groupClusterEntity.getLockGroupsAfter(), result.lockGroupsAfter()); + assertEquals(ApiRoutes.COURSE_BASE_PATH + "/" + courseEntity.getId(), result.courseUrl()); + + /* Hide studentNumber */ + doReturn(groupJson).when(entityToJsonConverter).groupEntityToJson(groupEntity, true); + + result = entityToJsonConverter.clusterEntityToClusterJson(groupClusterEntity, true); + + verify(entityToJsonConverter, times(1)).groupEntityToJson(groupEntity, true); } @Test public void testUserEntityToUserReference() { - UserReferenceJson result = entityToJsonConverter.userEntityToUserReference(userEntity); + userEntity.setStudentNumber("studentNumber"); + UserReferenceJson result = entityToJsonConverter.userEntityToUserReference(userEntity, false); assertEquals(userEntity.getId(), result.getUserId()); assertEquals(userEntity.getName() + " " + userEntity.getSurname(), result.getName()); + assertEquals(userEntity.getEmail(), result.getEmail()); + assertEquals(userEntity.getStudentNumber(), result.getStudentNumber()); + + /* Hide studentnumber */ + result = entityToJsonConverter.userEntityToUserReference(userEntity, true); + assertNull(result.getStudentNumber()); + } + + @Test + public void testUserEntityToUserReferenceWithRelation() { + + doReturn(userReferenceJson).when(entityToJsonConverter).userEntityToUserReference(userEntity, false); + UserReferenceWithRelation result = entityToJsonConverter.userEntityToUserReferenceWithRelation(userEntity, CourseRelation.creator, false); + assertEquals(userReferenceJson, result.getUser()); + assertEquals(CourseRelation.creator.toString(), result.getRelation()); + + verify(entityToJsonConverter, times(1)).userEntityToUserReference(userEntity, false); + + /* Hide studentnumber */ + doReturn(userReferenceJson).when(entityToJsonConverter).userEntityToUserReference(userEntity, true); + result = entityToJsonConverter.userEntityToUserReferenceWithRelation(userEntity, CourseRelation.creator, true); + verify(entityToJsonConverter, times(1)).userEntityToUserReference(userEntity, true); + + /* Different relations */ + result = entityToJsonConverter.userEntityToUserReferenceWithRelation(userEntity, CourseRelation.course_admin, false); + assertEquals(CourseRelation.course_admin.toString(), result.getRelation()); + + result = entityToJsonConverter.userEntityToUserReferenceWithRelation(userEntity, CourseRelation.enrolled, false); + assertEquals(CourseRelation.enrolled.toString(), result.getRelation()); } @Test public void testCourseEntityToCourseWithInfo() { - when(courseRepository.findTeacherByCourseId(anyLong())).thenReturn(userEntity); - when(courseRepository.findAssistantsByCourseId(anyLong())).thenReturn(Collections.emptyList()); - CourseWithInfoJson result = entityToJsonConverter.courseEntityToCourseWithInfo(courseEntity, - "joinLink"); + String joinLink = "JOIN LINK"; + courseEntity.setArchivedAt(OffsetDateTime.now()); + courseEntity.setCreatedAt(OffsetDateTime.MIN); + + when(courseRepository.findTeacherByCourseId(courseEntity.getId())).thenReturn(userEntity); + when(courseRepository.findAssistantsByCourseId(courseEntity.getId())).thenReturn(List.of(otherUser)); + + doReturn(userReferenceJson).when(entityToJsonConverter).userEntityToUserReference(userEntity, true); + doReturn(otherUserReferenceJson).when(entityToJsonConverter).userEntityToUserReference(otherUser, true); + + CourseWithInfoJson result = entityToJsonConverter.courseEntityToCourseWithInfo(courseEntity, joinLink, false); assertEquals(courseEntity.getId(), result.courseId()); assertEquals(courseEntity.getName(), result.name()); + assertEquals(courseEntity.getDescription(), result.description()); + assertEquals(userReferenceJson, result.teacher()); + assertEquals(List.of(otherUserReferenceJson), result.assistants()); + assertEquals(joinLink, result.joinUrl()); + assertEquals(courseEntity.getJoinKey(), result.joinKey()); + assertEquals(courseEntity.getArchivedAt().toInstant(), result.archivedAt().toInstant()); + assertEquals(courseEntity.getCreatedAt().toInstant(), result.createdAt().toInstant()); + assertEquals(courseEntity.getCourseYear(), result.year()); + + /* Hide key */ + result = entityToJsonConverter.courseEntityToCourseWithInfo(courseEntity, joinLink, true); + assertNull(result.joinKey()); + assertNull(result.joinUrl()); + + } + + @Test + public void testCourseEntityToCourseWithRelation() { + + int userCount = 5; + courseEntity.setArchivedAt(OffsetDateTime.now()); + courseEntity.setCreatedAt(OffsetDateTime.MIN); + + when(courseUserRepository.countUsersInCourse(courseEntity.getId())).thenReturn(userCount); + CourseWithRelationJson result = entityToJsonConverter.courseEntityToCourseWithRelation(courseEntity, CourseRelation.creator); + assertEquals(ApiRoutes.COURSE_BASE_PATH + "/" + courseEntity.getId(), result.url()); + assertEquals(CourseRelation.creator, result.relation()); + assertEquals(courseEntity.getName(), result.name()); + assertEquals(courseEntity.getId(), result.courseId()); + assertEquals(courseEntity.getArchivedAt().toInstant(), result.archivedAt().toInstant()); + assertEquals(userCount, result.memberCount()); + assertEquals(courseEntity.getCreatedAt().toInstant(), result.createdAt().toInstant()); + assertEquals(courseEntity.getCourseYear(), result.year()); + + } + + @Test + public void testGroupFeedbackEntityToJson() { + GroupFeedbackJson result = entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity); + assertEquals(groupFeedbackEntity.getScore(), result.getScore()); + assertEquals(groupFeedbackEntity.getFeedback(), result.getFeedback()); + assertEquals(groupFeedbackEntity.getGroupId(), result.getGroupId()); + assertEquals(groupFeedbackEntity.getProjectId(), result.getProjectId()); + } + + @Test + public void testGroupFeedbackEntityToJsonWithProjec() { + doReturn(groupFeedbackJson).when(entityToJsonConverter).groupFeedbackEntityToJson(groupFeedbackEntity); + GroupFeedbackJsonWithProject result = entityToJsonConverter.groupFeedbackEntityToJsonWithProject(groupFeedbackEntity, projectEntity); + assertEquals(projectEntity.getName(), result.getProjectName()); + assertEquals(ApiRoutes.PROJECT_BASE_PATH + "/" + projectEntity.getId(), result.getProjectUrl()); + assertEquals(projectEntity.getId(), result.getProjectId()); + assertEquals(groupFeedbackJson, result.getGroupFeedback()); + assertEquals(projectEntity.getMaxScore().intValue(), result.getMaxScore()); + + /* No feedback */ + result = entityToJsonConverter.groupFeedbackEntityToJsonWithProject(null, projectEntity); + assertNull(result.getGroupFeedback()); + } + + @Test + public void testProjectEntityToProjectResponseJsonWithStatus() { + submissionEntity.setDockerAccepted(true); + submissionEntity.setStructureAccepted(true); + when(groupRepository.groupIdByProjectAndUser(projectEntity.getId(), userEntity.getId())).thenReturn(groupEntity.getId()); + when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(projectEntity.getId(), groupEntity.getId())).thenReturn(Optional.of(submissionEntity)); + + doReturn(projectResponseJson).when(entityToJsonConverter).projectEntityToProjectResponseJson(projectEntity, courseEntity, userEntity); + ProjectResponseJsonWithStatus result = entityToJsonConverter.projectEntityToProjectResponseJsonWithStatus(projectEntity, courseEntity, userEntity); + assertEquals(projectResponseJson, result.project()); + assertEquals(ProjectStatus.correct.toString(), result.status()); + + /* Check different statuses */ + + submissionEntity.setDockerAccepted(false); + result = entityToJsonConverter.projectEntityToProjectResponseJsonWithStatus(projectEntity, courseEntity, userEntity); + assertEquals(ProjectStatus.incorrect.toString(), result.status()); + + submissionEntity.setDockerAccepted(true); + submissionEntity.setStructureAccepted(false); + result = entityToJsonConverter.projectEntityToProjectResponseJsonWithStatus(projectEntity, courseEntity, userEntity); + assertEquals(ProjectStatus.incorrect.toString(), result.status()); + + when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(projectEntity.getId(), groupEntity.getId())).thenReturn(Optional.empty()); + result = entityToJsonConverter.projectEntityToProjectResponseJsonWithStatus(projectEntity, courseEntity, userEntity); + assertEquals(ProjectStatus.not_started.toString(), result.status()); + + /* User not in group yet */ + when(groupRepository.groupIdByProjectAndUser(projectEntity.getId(), userEntity.getId())).thenReturn(null); + result = entityToJsonConverter.projectEntityToProjectResponseJsonWithStatus(projectEntity, courseEntity, userEntity); + assertEquals(ProjectStatus.no_group.toString(), result.status()); } @Test public void testProjectEntityToProjectResponseJson() { - when(projectRepository.findGroupIdsByProjectId(anyLong())).thenReturn( - Collections.singletonList(1L)); - when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(anyLong(), anyLong())) - .thenReturn(Optional.of(submissionEntity)); - when(courseUserRepository.findById(any())).thenReturn( - Optional.of(new CourseUserEntity(1L, 1L, CourseRelation.enrolled))); - when(groupRepository.groupIdByProjectAndUser(anyLong(), anyLong())).thenReturn(1L); - ProjectResponseJson result = entityToJsonConverter.projectEntityToProjectResponseJson( - projectEntity, courseEntity, userEntity); + GroupEntity secondGroup = new GroupEntity("secondGroup", groupClusterEntity.getId()); + SubmissionEntity secondSubmission = new SubmissionEntity(22, 232L, 90L, OffsetDateTime.MIN, true, true); + CourseUserEntity courseUser = new CourseUserEntity(projectEntity.getCourseId(), userEntity.getId(), CourseRelation.creator); + projectEntity.setVisibleAfter(OffsetDateTime.now()); + when(projectRepository.findGroupIdsByProjectId(projectEntity.getId())).thenReturn(List.of(groupEntity.getId(), secondGroup.getId())); + when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(projectEntity.getId(), groupEntity.getId())).thenReturn(Optional.of(submissionEntity)); + when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(projectEntity.getId(), secondGroup.getId())).thenReturn(Optional.of(secondSubmission)); + when(courseUserRepository.findById(argThat( + id -> id.getCourseId() == projectEntity.getCourseId() && id.getUserId() == userEntity.getId() + ))).thenReturn(Optional.of(courseUser)); + + when(groupRepository.groupIdByProjectAndUser(projectEntity.getId(), userEntity.getId())).thenReturn(null); + when(clusterUtil.isIndividualCluster(projectEntity.getGroupClusterId())).thenReturn(false); + + doReturn(courseJson).when(entityToJsonConverter).courseEntityToCourseReference(courseEntity); + + ProjectResponseJson result = entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, userEntity); + assertEquals(courseJson, result.course()); + assertEquals(projectEntity.getDeadline(), result.deadline()); + assertEquals(projectEntity.getDescription(), result.description()); assertEquals(projectEntity.getId(), result.projectId()); assertEquals(projectEntity.getName(), result.name()); + assertEquals(ApiRoutes.PROJECT_BASE_PATH + "/" + projectEntity.getId() + "/submissions", result.submissionUrl()); + assertEquals(ApiRoutes.TEST_BASE_PATH + "/" + projectEntity.getTestId(), result.testUrl()); + assertEquals(projectEntity.getMaxScore(), result.maxScore()); + assertEquals(projectEntity.isVisible(), result.visible()); + assertEquals(2, result.progress().completed()); + assertEquals(2, result.progress().total()); + assertNull(result.groupId()); // User is a creator/course_admin -> no group + assertEquals(groupClusterEntity.getId(), result.clusterId()); + assertEquals(projectEntity.getVisibleAfter(), result.visibleAfter()); + + + /* TestId is null */ + projectEntity.setTestId(null); + result = entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, userEntity); + assertNull(result.testUrl()); + + /* Individual cluster */ + when(clusterUtil.isIndividualCluster(projectEntity.getGroupClusterId())).thenReturn(true); + result = entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, userEntity); + assertNull(result.clusterId()); + + /* User is enrolled and in group */ + courseUser.setRelation(CourseRelation.enrolled); + when(groupRepository.groupIdByProjectAndUser(projectEntity.getId(), userEntity.getId())).thenReturn(groupEntity.getId()); + result = entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, userEntity); + assertEquals(ApiRoutes.PROJECT_BASE_PATH + "/" + projectEntity.getId() + "/submissions/" + groupEntity.getId(), result.submissionUrl()); + assertEquals(groupEntity.getId(), result.groupId()); + + /* User is enrolled but not in group */ + when(groupRepository.groupIdByProjectAndUser(projectEntity.getId(), userEntity.getId())).thenReturn(null); + result = entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, userEntity); + assertNull(result.submissionUrl()); + + /* One submission is not correct */ + secondSubmission.setDockerAccepted(false); + result = entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, userEntity); + assertEquals(1, result.progress().completed()); + assertEquals(2, result.progress().total()); + + secondSubmission.setDockerAccepted(true); + secondSubmission.setStructureAccepted(false); + result = entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, userEntity); + assertEquals(1, result.progress().completed()); + assertEquals(2, result.progress().total()); + + /* One group didn't make a submission yet */ + when(submissionRepository.findLatestsSubmissionIdsByProjectAndGroupId(projectEntity.getId(), secondGroup.getId())).thenReturn(Optional.empty()); + result = entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, userEntity); + assertEquals(1, result.progress().completed()); + assertEquals(2, result.progress().total()); + + /* Error while getting courseUser */ + reset(courseUserRepository); + when(courseUserRepository.findById(argThat( + id -> id.getCourseId() == projectEntity.getCourseId() && id.getUserId() == userEntity.getId() + ))).thenReturn(Optional.empty()); + assertThrows(RuntimeException.class, () -> entityToJsonConverter.projectEntityToProjectResponseJson(projectEntity, courseEntity, userEntity)); + } + + @Test + public void testCourseEntityToCourseReference() { + CourseReferenceJson result = entityToJsonConverter.courseEntityToCourseReference(courseEntity); + assertEquals(courseEntity.getName(), result.getName()); + assertEquals(ApiRoutes.COURSE_BASE_PATH + "/" + courseEntity.getId(), result.getUrl()); + assertEquals(courseEntity.getId(), result.getCourseId()); + assertNull(result.getArchivedAt()); } @Test public void testGetSubmissionJson() { - SubmissionJson result = entityToJsonConverter.getSubmissionJson(submissionEntity); - assertEquals(submissionEntity.getId(), result.getSubmissionId()); - assertEquals(submissionEntity.getProjectId(), result.getProjectId()); + try (MockedStatic mockedFileHandler = mockStatic(Filehandler.class)) { + /* Create temp file for artifacts */ + File file = File.createTempFile("SELAB2CANDELETEtest", "zip"); + mockedFileHandler.when(() -> Filehandler.getSubmissionArtifactPath(submissionEntity.getProjectId(), submissionEntity.getGroupId(), submissionEntity.getId())) + .thenReturn(file.toPath()); + submissionEntity.setDockerTestState(DockerTestState.running); + submissionEntity.setSubmissionTime(OffsetDateTime.now()); + submissionEntity.setStructureAccepted(true); + submissionEntity.setStructureFeedback("feedback"); + SubmissionJson result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertEquals(submissionEntity.getId(), result.getSubmissionId()); + assertEquals(ApiRoutes.PROJECT_BASE_PATH + "/" + submissionEntity.getProjectId(), + result.getProjectUrl()); + assertEquals(ApiRoutes.GROUP_BASE_PATH + "/" + submissionEntity.getGroupId(), + result.getGroupUrl()); + assertEquals(submissionEntity.getProjectId(), result.getProjectId()); + assertEquals(submissionEntity.getGroupId(), result.getGroupId()); + assertEquals(ApiRoutes.SUBMISSION_BASE_PATH + "/" + submissionEntity.getId() + "/file", + result.getFileUrl()); + assertTrue(result.getStructureAccepted()); + assertEquals(submissionEntity.getSubmissionTime(), result.getSubmissionTime()); + assertEquals(submissionEntity.getStructureFeedback(), result.getStructureFeedback()); + assertNull(result.getDockerFeedback()); + assertEquals(DockerTestState.running.toString(), result.getDockerStatus()); + assertEquals(ApiRoutes.SUBMISSION_BASE_PATH + "/" + submissionEntity.getId() + "/artifacts", + result.getArtifactUrl()); + + /* No artifacts */ + file.delete(); + result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertNull(result.getArtifactUrl()); + + /* Docker finished running */ + submissionEntity.setDockerTestState(DockerTestState.finished); + /* No docker test */ + submissionEntity.setDockerType(DockerTestType.NONE); + result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertEquals(DockerTestState.finished.toString(), result.getDockerStatus()); + assertEquals(DockerTestType.NONE, result.getDockerFeedback().type()); + + /* Simple docker test */ + submissionEntity.setDockerFeedback("dockerFeedback - simple"); + submissionEntity.setDockerAccepted(true); + submissionEntity.setDockerType(DockerTestType.SIMPLE); + result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertEquals(DockerTestType.SIMPLE, result.getDockerFeedback().type()); + assertEquals(submissionEntity.getDockerFeedback(), result.getDockerFeedback().feedback()); + assertTrue(result.getDockerFeedback().allowed()); + + /* Template docker test */ + submissionEntity.setDockerFeedback("dockerFeedback - template"); + submissionEntity.setDockerAccepted(false); + submissionEntity.setDockerType(DockerTestType.TEMPLATE); + result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertEquals(DockerTestType.TEMPLATE, result.getDockerFeedback().type()); + assertEquals(submissionEntity.getDockerFeedback(), result.getDockerFeedback().feedback()); + assertFalse(result.getDockerFeedback().allowed()); + + /* Docker aborted */ + submissionEntity.setDockerTestState(DockerTestState.aborted); + result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertEquals(DockerTestState.aborted.toString(), result.getDockerStatus()); + assertEquals(DockerTestType.TEMPLATE, result.getDockerFeedback().type()); + assertEquals(submissionEntity.getDockerFeedback(), result.getDockerFeedback().feedback()); + assertFalse(result.getDockerFeedback().allowed()); + + /* Group id is null */ + submissionEntity.setGroupId(null); + result = entityToJsonConverter.getSubmissionJson(submissionEntity); + assertNull(result.getGroupUrl()); + } catch (IOException e) { + throw new RuntimeException(e); + } } @Test public void testTestEntityToTestJson() { - TestJson result = entityToJsonConverter.testEntityToTestJson(testEntity, 1L); + testEntity.setExtraFilesId(5L); + when(fileRepository.findById(testEntity.getExtraFilesId())) + .thenReturn(Optional.of(new FileEntity("nameoffiles", "path", 5L))); + + TestJson result = entityToJsonConverter.testEntityToTestJson(testEntity, projectEntity.getId()); + + + assertEquals(ApiRoutes.PROJECT_BASE_PATH + "/" + projectEntity.getId(), result.getProjectUrl()); assertEquals(testEntity.getDockerImage(), result.getDockerImage()); + assertEquals(testEntity.getDockerTestScript(), result.getDockerScript()); + assertEquals(testEntity.getDockerTestTemplate(), result.getDockerTemplate()); + assertEquals(testEntity.getStructureTemplate(), result.getStructureTest()); + assertEquals(ApiRoutes.PROJECT_BASE_PATH + "/" + projectEntity.getId() + "/tests/extrafiles", result.getExtraFilesUrl()); + assertEquals("nameoffiles", result.getExtraFilesName()); + + testEntity.setExtraFilesId(null); + result = entityToJsonConverter.testEntityToTestJson(testEntity, projectEntity.getId()); + assertNull(result.getExtraFilesUrl()); + assertNull(result.getExtraFilesName()); } + + } \ No newline at end of file diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java new file mode 100644 index 00000000..fa2ffafa --- /dev/null +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/FileHandlerTest.java @@ -0,0 +1,482 @@ +package com.ugent.pidgeon.util; + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockMultipartFile; + +@ExtendWith(MockitoExtension.class) +public class FileHandlerTest { + + + static Path tempDir; + + private MockMultipartFile file; + private final String basicZipFileName = "Testfile.zip"; + private byte[] fileContent; + private final Path testFilePath = Path.of("src/test/test-cases/FilehandlerTestFiles"); + + @AfterEach + public void cleanup() throws Exception { + // Cleanup the files + if (Files.exists(tempDir)) { + try (Stream paths = Files.walk(tempDir)) { + paths.map(Path::toFile) + .forEach(File::delete); + } + } + + } + + + @BeforeEach + public void setUp() throws IOException { + tempDir = Files.createTempDirectory("SELAB6CANDELETEtest"); + fileContent = Files.readAllBytes(testFilePath.resolve(basicZipFileName)); + file = new MockMultipartFile( + basicZipFileName, fileContent + ); + } + + @Test + public void testSaveFile() throws Exception { + File savedFile = Filehandler.saveFile(tempDir, file, Filehandler.SUBMISSION_FILENAME); + + assertTrue(savedFile.exists()); + assertEquals(Filehandler.SUBMISSION_FILENAME, savedFile.getName()); + assertEquals(fileContent.length, savedFile.length()); + byte[] savedFileContent = Files.readAllBytes(savedFile.toPath()); + assertEquals(fileContent.length, savedFileContent.length); + } + + @Test + public void testSaveFile_dirDoesntExist() throws Exception { + File savedFile = Filehandler.saveFile(tempDir.resolve("nonexistent"), file, Filehandler.SUBMISSION_FILENAME); + + assertTrue(savedFile.exists()); + assertEquals(Filehandler.SUBMISSION_FILENAME, savedFile.getName()); + assertEquals(fileContent.length, savedFile.length()); + byte[] savedFileContent = Files.readAllBytes(savedFile.toPath()); + assertEquals(fileContent.length, savedFileContent.length); + } + + @Test + public void testSaveFile_errorWhileCreatingDir() throws Exception { + assertThrows(IOException.class, () -> Filehandler.saveFile(Path.of(""), file, Filehandler.SUBMISSION_FILENAME)); + } + + @Test + public void testSaveFile_notAZipFile() { + MockMultipartFile notAZipFile = new MockMultipartFile( + "notAZipFile.txt", "This is not a zip file".getBytes() + ); + assertThrows(IOException.class, () -> Filehandler.saveFile(tempDir, notAZipFile, Filehandler.SUBMISSION_FILENAME)); + } + + @Test + public void testSaveFile_fileEmpty() { + MockMultipartFile emptyFile = new MockMultipartFile( + "emptyFile.txt", new byte[0] + ); + assertThrows(IOException.class, () -> Filehandler.saveFile(tempDir, emptyFile, Filehandler.SUBMISSION_FILENAME)); + } + + @Test + public void testSaveFile_fileNull() { + assertThrows(IOException.class, () -> Filehandler.saveFile(tempDir, null, Filehandler.SUBMISSION_FILENAME)); + } + + @Test + public void testDeleteLocation() throws Exception { + Path testDir = Files.createTempDirectory("SELAB6CANDELETEtest"); + Path tempFile = Files.createTempFile(testDir, "SELAB6CANDELETEtest", ".txt"); + Filehandler.deleteLocation(new File(tempFile.toString())); + assertFalse(Files.exists(testDir)); + } + + @Test + public void testDeleteLocation_parentDirNotEmpty() throws Exception { + Path testDir = Files.createTempDirectory("SELAB6CANDELETEtest"); + Path tempFile = Files.createTempFile(testDir, "SELAB6CANDELETEtest", ".txt"); + Files.createTempFile(testDir, "SELAB6CANDELETEtest2", ".txt"); + Filehandler.deleteLocation(new File(tempFile.toString())); + assertTrue(Files.exists(testDir)); + } + + @Test + public void testDeleteLocation_locationDoesntExist() throws Exception { + Filehandler.deleteLocation(new File("nonexistent")); + } + + @Test + public void testDeleteLocation_errorWhileDeleting() { + // Create a mock File object + File mockDir = mock(File.class); + + when(mockDir.exists()).thenReturn(true); + when(mockDir.delete()).thenReturn(false); + + assertThrows(IOException.class, () -> Filehandler.deleteLocation(mockDir)); + + verify(mockDir).exists(); + verify(mockDir).delete(); + } + + @Test + public void testDeleteLocation_errorWhileDeletingParentDir() { + File mockDir = mock(File.class); + File mockParentDir = mock(File.class); + File[] mockedFiles = new File[0]; + + when(mockDir.exists()).thenReturn(true); + when(mockDir.delete()).thenReturn(true); + when(mockDir.getParentFile()).thenReturn(mockParentDir); + when(mockParentDir.isDirectory()).thenReturn(true); + when(mockParentDir.listFiles()).thenReturn(mockedFiles); + when(mockParentDir.delete()).thenReturn(false); + + assertThrows(IOException.class, () -> Filehandler.deleteLocation(mockDir)); + + verify(mockDir).exists(); + verify(mockDir).delete(); + verify(mockDir).getParentFile(); + verify(mockParentDir).listFiles(); + verify(mockParentDir).delete(); + } + + @Test + public void testDeleteLocation_filesAreNotEmpty() throws IOException { + File mockDir = mock(File.class); + File mockParentDir = mock(File.class); + File[] mockedFiles = new File[1]; + mockedFiles[0] = mock(File.class); + + when(mockDir.exists()).thenReturn(true); + when(mockDir.delete()).thenReturn(true); + when(mockDir.getParentFile()).thenReturn(mockParentDir); + when(mockParentDir.isDirectory()).thenReturn(true); + when(mockParentDir.listFiles()).thenReturn(mockedFiles); + + Filehandler.deleteLocation(mockDir); + + verify(mockDir).exists(); + verify(mockDir).delete(); + verify(mockDir).getParentFile(); + verify(mockParentDir).listFiles(); + } + + @Test + public void testDeleteLocation_filesAreNull() throws IOException { + File mockDir = mock(File.class); + File mockParentDir = mock(File.class); + + when(mockDir.exists()).thenReturn(true); + when(mockDir.delete()).thenReturn(true); + when(mockDir.getParentFile()).thenReturn(mockParentDir); + when(mockParentDir.isDirectory()).thenReturn(true); + when(mockParentDir.listFiles()).thenReturn(null); + + Filehandler.deleteLocation(mockDir); + + verify(mockDir).exists(); + verify(mockDir).delete(); + verify(mockDir).getParentFile(); + verify(mockParentDir).listFiles(); + } + + @Test + public void testDeleteLocation_parentDirIsNotADir() throws IOException { + File mockDir = mock(File.class); + File mockParentDir = mock(File.class); + + when(mockDir.exists()).thenReturn(true); + when(mockDir.delete()).thenReturn(true); + when(mockDir.getParentFile()).thenReturn(mockParentDir); + when(mockParentDir.isDirectory()).thenReturn(false); + + Filehandler.deleteLocation(mockDir); + + verify(mockDir).exists(); + verify(mockDir).delete(); + verify(mockDir).getParentFile(); + verify(mockParentDir).isDirectory(); + } + + @Test + public void testDeleteLocation_parentDirIsNull() throws IOException { + File mockDir = mock(File.class); + + when(mockDir.exists()).thenReturn(true); + when(mockDir.delete()).thenReturn(true); + when(mockDir.getParentFile()).thenReturn(null); + + Filehandler.deleteLocation(mockDir); + + verify(mockDir).exists(); + verify(mockDir).delete(); + verify(mockDir).getParentFile(); + } + + @Test + public void testGetSubmissionPath() { + Path submissionPath = Filehandler.getSubmissionPath(1, 2L, 3); + assertEquals(Path.of(Filehandler.BASEPATH, "projects", "1", "2", "3"), submissionPath); + } + + @Test + public void testGetSubmissionPath_groupIdIsNull() { + Path submissionPath = Filehandler.getSubmissionPath(1, null, 3); + assertEquals(Path.of(Filehandler.BASEPATH, "projects", "1", Filehandler.ADMIN_SUBMISSION_FOLDER, "3"), submissionPath); + } + + @Test + public void testGetSubmissionArtifactPath() { + Path submissionArtifactPath = Filehandler.getSubmissionArtifactPath(1, 2L, 3); + assertEquals(Path.of(Filehandler.BASEPATH, "projects", "1", "2", "3", "artifacts.zip"), submissionArtifactPath); + } + + @Test + + public void testGetTextExtraFilesPath() { + Path textExtraFilesPath = Filehandler.getTestExtraFilesPath(88); + assertEquals(Path.of(Filehandler.BASEPATH, "projects", String.valueOf(88)), textExtraFilesPath); + } + @Test + public void testGetSubmissionArtifactPath_groupIdIsNull() { + Path submissionArtifactPath = Filehandler.getSubmissionArtifactPath(1, null, 3); + assertEquals(Path.of(Filehandler.BASEPATH, "projects", "1", Filehandler.ADMIN_SUBMISSION_FOLDER, "3", "artifacts.zip"), submissionArtifactPath); + + } + + @Test + public void testGetFileAsResource_FileExists() { + try { + File tempFile = Files.createTempFile("SELAB6CANDELETEtestFile", ".txt").toFile(); + + Resource resource = Filehandler.getFileAsResource(tempFile.toPath()); + + assertNotNull(resource); + assertInstanceOf(FileSystemResource.class, resource); + assertEquals(tempFile.getAbsolutePath(), ((FileSystemResource) resource).getFile().getAbsolutePath()); + + assertTrue(tempFile.delete()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void testGetFileAsResource_FileDoesNotExist() { + Resource resource = Filehandler.getFileAsResource(Path.of("nonexistent")); + + assertNull(resource); + } + + @Test + public void testCopyFilesAsZip() throws IOException { + List files = new ArrayList<>(); + File tempFile1 = Files.createTempFile("SELAB6CANDELETEtempFile1", ".txt").toFile(); + File tempFile2 = Files.createTempFile("SELAB6CANDELETEtempFile2", ".txt").toFile(); + + try { + files.add(tempFile1); + files.add(tempFile2); + + File zipFile = tempDir.resolve("files.zip").toFile(); + Filehandler.copyFilesAsZip(files, zipFile.toPath()); + + assertTrue(zipFile.exists()); + + try (ZipFile zip = new ZipFile(zipFile)) { + for (File file : files) { + String entryName = file.getName(); + ZipEntry zipEntry = zip.getEntry(entryName); + assertNotNull(zipEntry, "File " + entryName + " not found in the zip file."); + } + } + + + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void testCopyFilesAsZip_zipFileAlreadyExist() throws IOException { + List files = new ArrayList<>(); + File tempFile1 = Files.createTempFile("SELAB6CANDELETEtempFile1", ".txt").toFile(); + File tempFile2 = Files.createTempFile("SELAB6CANDELETEtempFile2", ".txt").toFile(); + File zipFile = Files.createTempFile(tempDir, "SELAB6CANDELETEfiles", ".zip").toFile(); + + try { + files.add(tempFile1); + files.add(tempFile2); + + assertTrue(zipFile.exists()); + + Filehandler.copyFilesAsZip(files, zipFile.toPath()); + + assertTrue(zipFile.exists()); + + try (ZipFile zip = new ZipFile(zipFile)) { + for (File file : files) { + String entryName = file.getName(); + ZipEntry zipEntry = zip.getEntry(entryName); + assertNotNull(zipEntry, "File " + entryName + " not found in the zip file."); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static File createTempFileWithContent(String prefix, String suffix, int fileSizeInBytes) throws IOException { + Path tempFilePath = Files.createTempFile(prefix, suffix); + + try (FileOutputStream outputStream = new FileOutputStream(tempFilePath.toFile())) { + // Write data to the file until it reaches the desired size + for (int i = 0; i < fileSizeInBytes; i++) { + outputStream.write('A'); // Write a byte to the file (in this case, the letter 'A') + } + } + + return tempFilePath.toFile(); + } + + @Test + public void testCopyFilesAsZip_zipFileAlreadyExistNonWriteable() throws IOException { + List files = new ArrayList<>(); + File tempFile1 = createTempFileWithContent("SELAB6CANDELETEtempFile1", ".txt", 4095); + File tempFile2 = Files.createTempFile("SELAB6CANDELETEtempFile2", ".txt").toFile(); + File zipFile = Files.createTempFile(tempDir, "SELAB6CANDELETEfiles", ".zip").toFile(); + zipFile.setWritable(false); + + try { + files.add(tempFile1); + files.add(tempFile2); + + assertTrue(zipFile.exists()); + + Filehandler.copyFilesAsZip(files, zipFile.toPath()); + + assertTrue(zipFile.exists()); + + try (ZipFile zip = new ZipFile(zipFile)) { + for (File file : files) { + String entryName = file.getName(); + ZipEntry zipEntry = zip.getEntry(entryName); + assertNotNull(zipEntry, "File " + entryName + " not found in the zip file."); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void testGetZipFileAsResponse() throws IOException { + List files = new ArrayList<>(); + File tempFile1 = Files.createTempFile("SELAB6CANDELETEtempFile1", ".txt").toFile(); + File tempFile2 = Files.createTempFile("SELAB6CANDELETEtempFile2", ".txt").toFile(); + + try { + files.add(tempFile1); + files.add(tempFile2); + + File zipFile = tempDir.resolve("files.zip").toFile(); + Filehandler.copyFilesAsZip(files, zipFile.toPath()); + + assertTrue(zipFile.exists()); + + ResponseEntity response = Filehandler.getZipFileAsResponse(zipFile.toPath(), "customfilename.zip"); + + assertNotNull(response); + assertEquals(200, response.getStatusCodeValue()); + assertEquals("attachment; filename=customfilename.zip", response.getHeaders().get("Content-Disposition").get(0)); + assertEquals("application/zip", response.getHeaders().get("Content-Type").get(0)); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void testGetZipFileAsResponse_fileDoesNotExist() { + ResponseEntity response = Filehandler.getZipFileAsResponse(Path.of("nonexistent"), "customfilename.zip"); + + assertNotNull(response); + assertEquals(404, response.getStatusCodeValue()); + } + + @Test + public void testAddExistingZip() throws IOException { + // Create zip file + String zipFileName = "existingZipFile.zip"; + File tempZipFile = Files.createTempFile("SELAB6CANDELETEexistingZip", ".zip").toFile(); + + // Populate the zip file with some content + try (ZipOutputStream tempZipOutputStream = new ZipOutputStream(new FileOutputStream(tempZipFile))) { + ZipEntry entry = new ZipEntry("testFile.txt"); + tempZipOutputStream.putNextEntry(entry); + tempZipOutputStream.write("Test content".getBytes()); + tempZipOutputStream.closeEntry(); + Filehandler.addExistingZip(tempZipOutputStream, zipFileName, tempZipFile); + } + + + + + + // Check if the zip file contains the entry + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(tempZipFile))) { + ZipEntry entry; + boolean found = false; + boolean originalFound = false; + while ((entry = zis.getNextEntry()) != null) { + Logger.getGlobal().info("Entry: " + entry.getName()); + if (entry.getName().equals(zipFileName)) { + found = true; + } else if (entry.getName().equals("testFile.txt")) { + originalFound = true; + } + } + assertTrue(found); + assertTrue(originalFound); + } + } + + + + +} diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/FileUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/FileUtilTest.java index 7480658a..b82a7d16 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/FileUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/FileUtilTest.java @@ -1,23 +1,26 @@ package com.ugent.pidgeon.util; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import com.ugent.pidgeon.postgre.models.FileEntity; import com.ugent.pidgeon.postgre.repository.FileRepository; +import java.io.IOException; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - +@ExtendWith(MockitoExtension.class) public class FileUtilTest { @Mock @@ -26,28 +29,43 @@ public class FileUtilTest { @InjectMocks private FileUtil fileUtil; + private FileEntity fileEntity; + @BeforeEach public void setUp() { - MockitoAnnotations.openMocks(this); + fileEntity = new FileEntity("testName", "testPath", 5L); + fileEntity.setId(2L); } - @Test - public void testSaveFileEntity() throws IOException { - Path filePath = Paths.get("testPath"); - long projectId = 1L; - long userId = 1L; - FileEntity fileEntity = new FileEntity(filePath.getFileName().toString(), filePath.toString(), userId); - when(fileRepository.save(any(FileEntity.class))).thenReturn(fileEntity); - FileEntity result = fileUtil.saveFileEntity(filePath, projectId, userId); - assertEquals(fileEntity, result); - } @Test public void testDeleteFileById() { - long fileId = 1L; - FileEntity fileEntity = new FileEntity("testName", "testPath", 1L); - when(fileRepository.findById(fileId)).thenReturn(java.util.Optional.of(fileEntity)); - CheckResult result = fileUtil.deleteFileById(fileId); - assertEquals(HttpStatus.OK, result.getStatus()); + when(fileRepository.findById(fileEntity.getId())).thenReturn(Optional.of(fileEntity)); + try (MockedStatic mockedFileHandler = Mockito.mockStatic(Filehandler.class)) { + mockedFileHandler.when(() -> Filehandler.deleteLocation(argThat( + path -> path.toString().equals(fileEntity.getPath())) + )).thenAnswer(invocation -> { + // Do nothing + return null; + }); + CheckResult result = fileUtil.deleteFileById(fileEntity.getId()); + assertEquals(HttpStatus.OK, result.getStatus()); + verify(fileRepository, times(1)).delete(fileEntity); + + // Error when file is being deleted + mockedFileHandler.when(() -> Filehandler.deleteLocation(argThat( + path -> path.toString().equals(fileEntity.getPath())) + )).thenThrow(new IOException()); + result = fileUtil.deleteFileById(fileEntity.getId()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatus()); + + // File not found + when(fileRepository.findById(fileEntity.getId())).thenReturn(Optional.empty()); + result = fileUtil.deleteFileById(fileEntity.getId()); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); + + } catch (Exception e) { + e.printStackTrace(); + } } } \ No newline at end of file diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/GroupFeedbackUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/GroupFeedbackUtilTest.java new file mode 100644 index 00000000..4a57aac9 --- /dev/null +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/GroupFeedbackUtilTest.java @@ -0,0 +1,218 @@ +package com.ugent.pidgeon.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +import com.ugent.pidgeon.json.UpdateGroupScoreRequest; +import com.ugent.pidgeon.postgre.models.GroupEntity; +import com.ugent.pidgeon.postgre.models.GroupFeedbackEntity; +import com.ugent.pidgeon.postgre.models.ProjectEntity; +import com.ugent.pidgeon.postgre.models.UserEntity; +import com.ugent.pidgeon.postgre.models.types.UserRole; +import com.ugent.pidgeon.postgre.repository.GroupFeedbackRepository; +import java.time.OffsetDateTime; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; + +@ExtendWith(MockitoExtension.class) +public class GroupFeedbackUtilTest { + @Mock + private ProjectUtil projectUtil; + @Mock + private GroupUtil groupUtil; + @Mock + private GroupFeedbackRepository groupFeedbackRepository; + + @Spy + @InjectMocks + private GroupFeedbackUtil groupFeedbackUtil; + + private GroupFeedbackEntity groupFeedbackEntity; + private UserEntity mockUser; + private ProjectEntity projectEntity; + private GroupEntity groupEntity; + + @BeforeEach + public void setup() { + groupFeedbackEntity = new GroupFeedbackEntity( + 5L, + 10L, + 10.0f, + "Good job!" + ); + mockUser = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); + mockUser.setId(2L); + projectEntity = new ProjectEntity( + 13L, + "projectName", + "projectDescription", + 21L, + 38L, + true, + 34, + OffsetDateTime.now() + ); + projectEntity.setId(groupFeedbackEntity.getProjectId()); + groupEntity = new GroupEntity("test", projectEntity.getGroupClusterId()); + groupEntity.setId(groupFeedbackEntity.getGroupId()); + } + + @Test + public void testGetGroupFeedbackIfExists() { + /* GroupFeedback found */ + when(groupFeedbackRepository.findById(argThat( + id -> id.getGroupId() == groupFeedbackEntity.getGroupId() && id.getProjectId() == groupFeedbackEntity.getProjectId() + ))).thenReturn(java.util.Optional.of(groupFeedbackEntity)); + CheckResult result = groupFeedbackUtil.getGroupFeedbackIfExists(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId()); + assertEquals(HttpStatus.OK, result.getStatus()); + assertEquals(groupFeedbackEntity, result.getData()); + + /* GroupFeedback not found */ + reset(groupFeedbackRepository); + when(groupFeedbackRepository.findById(argThat( + id -> id.getGroupId() == groupFeedbackEntity.getGroupId() && id.getProjectId() == groupFeedbackEntity.getProjectId() + ))).thenReturn(java.util.Optional.empty()); + result = groupFeedbackUtil.getGroupFeedbackIfExists(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId()); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); + assertNull(result.getData()); + } + + @Test + public void testCheckGroupFeedback() { + /* All schecks succeed */ + when(projectUtil.getProjectIfExists(groupFeedbackEntity.getProjectId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", projectEntity)); + when(groupUtil.getGroupIfExists(groupFeedbackEntity.getGroupId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupEntity)); + + CheckResult result = groupFeedbackUtil.checkGroupFeedback(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId()); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Group doesn't belong to project */ + groupEntity.setClusterId(0); + result = groupFeedbackUtil.checkGroupFeedback(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId()); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* Group get fails */ + when(groupUtil.getGroupIfExists(groupFeedbackEntity.getGroupId())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "Group not found", null)); + result = groupFeedbackUtil.checkGroupFeedback(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId()); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + /* Project get fails */ + when(projectUtil.getProjectIfExists(groupFeedbackEntity.getProjectId())).thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "Project not found", null)); + result = groupFeedbackUtil.checkGroupFeedback(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId()); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + } + + @Test + public void testCheckGroupFeedbackUpdate() { + /* All checks succeed: patch/put */ + doReturn(new CheckResult<>(HttpStatus.OK, "", null)).when(groupFeedbackUtil).checkGroupFeedback(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId()); + when(groupUtil.isAdminOfGroup(groupFeedbackEntity.getGroupId(), mockUser)).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + when(groupFeedbackRepository.findById(argThat( + id -> id.getGroupId() == groupFeedbackEntity.getGroupId() && id.getProjectId() == groupFeedbackEntity.getProjectId() + ))).thenReturn(Optional.of(groupFeedbackEntity)); + + CheckResult result = groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), mockUser, HttpMethod.PATCH); + assertEquals(HttpStatus.OK, result.getStatus()); + assertEquals(groupFeedbackEntity, result.getData()); + result = groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), mockUser, HttpMethod.PUT); + assertEquals(HttpStatus.OK, result.getStatus()); + assertEquals(groupFeedbackEntity, result.getData()); + + /* Group already exists: post */ + result = groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), mockUser, HttpMethod.POST); + assertEquals(HttpStatus.CONFLICT, result.getStatus()); + + /* All checks succeed: post */ + reset(groupFeedbackRepository); + when(groupFeedbackRepository.findById(argThat( + id -> id.getGroupId() == groupFeedbackEntity.getGroupId() && id.getProjectId() == groupFeedbackEntity.getProjectId() + ))).thenReturn(Optional.empty()); + + result = groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), mockUser, HttpMethod.POST); + assertEquals(HttpStatus.OK, result.getStatus()); + assertNull(result.getData()); + + /* Group doesn't exist: patch/put */ + result = groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), mockUser, HttpMethod.PATCH); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); + + result = groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), mockUser, HttpMethod.PUT); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); + + /* Admin check fails */ + when(groupUtil.isAdminOfGroup(groupFeedbackEntity.getGroupId(), mockUser)).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "Not an admin", null)); + result = groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), mockUser, HttpMethod.PATCH); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + result = groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), mockUser, HttpMethod.PUT); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + result = groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), mockUser, HttpMethod.POST); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* groupFeedbackCheckFails */ + doReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "Group feedback not found", null)).when(groupFeedbackUtil).checkGroupFeedback(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId()); + result = groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), mockUser, HttpMethod.PATCH); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + result = groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), mockUser, HttpMethod.PUT); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + result = groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), mockUser, HttpMethod.POST); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + } + + @Test + public void testCheckGroupFeedbackUpdateJson() { + /* All checks succeed */ + UpdateGroupScoreRequest updateGroupScoreRequest = new UpdateGroupScoreRequest(); + updateGroupScoreRequest.setScore(Float.valueOf(projectEntity.getMaxScore())); + updateGroupScoreRequest.setFeedback("Good job!"); + when(projectUtil.getProjectIfExists(groupFeedbackEntity.getProjectId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", projectEntity)); + + CheckResult result = groupFeedbackUtil.checkGroupFeedbackUpdateJson(updateGroupScoreRequest, groupFeedbackEntity.getProjectId()); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Score is too high */ + updateGroupScoreRequest.setScore((float) (projectEntity.getMaxScore() + 1)); + result = groupFeedbackUtil.checkGroupFeedbackUpdateJson(updateGroupScoreRequest, groupFeedbackEntity.getProjectId()); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Maxscore is null while score is too high */ + projectEntity.setMaxScore(null); + result = groupFeedbackUtil.checkGroupFeedbackUpdateJson(updateGroupScoreRequest, groupFeedbackEntity.getProjectId()); + assertEquals(HttpStatus.OK, result.getStatus()); + projectEntity.setMaxScore(34); + + /* Score is negative */ + updateGroupScoreRequest.setScore(-1.0f); + result = groupFeedbackUtil.checkGroupFeedbackUpdateJson(updateGroupScoreRequest, groupFeedbackEntity.getProjectId()); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Score is null */ + updateGroupScoreRequest.setScore(null); + result = groupFeedbackUtil.checkGroupFeedbackUpdateJson(updateGroupScoreRequest, groupFeedbackEntity.getProjectId()); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Feedback is null */ + updateGroupScoreRequest.setScore(Float.valueOf(projectEntity.getMaxScore())); + updateGroupScoreRequest.setFeedback(null); + result = groupFeedbackUtil.checkGroupFeedbackUpdateJson(updateGroupScoreRequest, groupFeedbackEntity.getProjectId()); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Project get fails */ + when(projectUtil.getProjectIfExists(groupFeedbackEntity.getProjectId())).thenReturn(new CheckResult<>(HttpStatus.BAD_REQUEST, "Project not found", null)); + result = groupFeedbackUtil.checkGroupFeedbackUpdateJson(updateGroupScoreRequest, groupFeedbackEntity.getProjectId()); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + } + + +} diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/GroupUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/GroupUtilTest.java index 66a5f5f8..aadf82af 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/GroupUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/GroupUtilTest.java @@ -1,7 +1,7 @@ package com.ugent.pidgeon.util; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; import com.ugent.pidgeon.postgre.models.GroupClusterEntity; @@ -18,6 +18,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; @@ -32,161 +33,334 @@ public class GroupUtilTest { private ClusterUtil clusterUtil; @Mock private ProjectUtil projectUtil; + @Mock + private UserUtil userUtil; + @Spy @InjectMocks private GroupUtil groupUtil; private GroupEntity group; - private UserEntity user; + private UserEntity mockUser; private GroupClusterEntity groupCluster; private ProjectEntity project; @BeforeEach public void setup() { - group = new GroupEntity("Groupname", 1L); - group.setId(1L); - user = new UserEntity("name", "surname", "email", UserRole.student, "azureid"); - user.setId(1L); - groupCluster = new GroupClusterEntity(1L, 5, "cluster test", 20); - groupCluster.setId(1L); - project = new ProjectEntity(1L, "name", "description", 1L, 1L, true, 20, OffsetDateTime.now()); - project.setId(1L); + group = new GroupEntity("Groupname", 12L); + group.setId(54L); + mockUser = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); + mockUser.setId(10L); + groupCluster = new GroupClusterEntity(9L, 5, "cluster test", 20); + groupCluster.setId(12L); + project = new ProjectEntity(9L, "name", "description", 12L, null, true, 20, OffsetDateTime.now()); + project.setId(88L); } @Test - public void testGetGroupIfExists() throws Exception { - when(groupRepository.findById(1L)).thenReturn(Optional.of(group)); - CheckResult result = groupUtil.getGroupIfExists(1L); + public void testGetGroupIfExists() { + /* Group exists */ + when(groupRepository.findById(group.getId())).thenReturn(Optional.of(group)); + CheckResult result = groupUtil.getGroupIfExists(group.getId()); assertEquals(HttpStatus.OK, result.getStatus()); assertEquals(group, result.getData()); + /* Group does not exist */ + when(groupRepository.findById(2L)).thenReturn(Optional.empty()); result = groupUtil.getGroupIfExists(2L); assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); - assertEquals("Group not found", result.getMessage()); } @Test - public void testCanGetGroup() throws Exception { - when(groupRepository.userAccessToGroup(1L, 1L)).thenReturn(true); - CheckResult result = groupUtil.canGetGroup(1L, user); + public void testCanGetGroup() { + /* User has access to group */ + when(groupRepository.userAccessToGroup(mockUser.getId(), group.getId())).thenReturn(true); + CheckResult result = groupUtil.canGetGroup(group.getId(), mockUser); assertEquals(HttpStatus.OK, result.getStatus()); - result = groupUtil.canGetGroup(2L, user); + /* User doesn't have access to group */ + when(groupRepository.userAccessToGroup(mockUser.getId(), group.getId())).thenReturn(false); + result = groupUtil.canGetGroup(group.getId(), mockUser); assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); - assertEquals("User does not have access to this group", result.getMessage()); + + /* User has no acces but is admin */ + mockUser.setRole(UserRole.admin); + result = groupUtil.canGetGroup(group.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); } @Test - public void testIsAdminOfGroup() throws Exception { - when(groupRepository.isAdminOfGroup(1L, user.getId())).thenReturn(true); - CheckResult result = groupUtil.isAdminOfGroup(1L, user); + public void testIsAdminOfGroup() { + /* User is admin of group */ + when(groupRepository.isAdminOfGroup(mockUser.getId(), group.getId())).thenReturn(true); + CheckResult result = groupUtil.isAdminOfGroup(group.getId(), mockUser); assertEquals(HttpStatus.OK, result.getStatus()); - result = groupUtil.isAdminOfGroup(2L, user); + /* User is not admin of group */ + when(groupRepository.isAdminOfGroup(mockUser.getId(), group.getId())).thenReturn(false); + result = groupUtil.isAdminOfGroup(group.getId(), mockUser); assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); - assertEquals("User is not an admin of this group", result.getMessage()); + + /* User is admin */ + mockUser.setRole(UserRole.admin); + result = groupUtil.isAdminOfGroup(group.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); } @Test - public void testCanUpdateGroup() throws Exception { - when(groupRepository.findById(1L)).thenReturn(Optional.of(group)); - when(groupRepository.isAdminOfGroup(1L, user.getId())).thenReturn(true); - when(clusterUtil.isIndividualCluster(1L)).thenReturn(false); - CheckResult result = groupUtil.canUpdateGroup(group.getId(), user); + public void testCanUpdateGroup() { + /* All checks succeed */ + doReturn(new CheckResult<>(HttpStatus.OK, "", group)).when(groupUtil).getGroupIfExists(group.getId()); + doReturn(new CheckResult<>(HttpStatus.OK, "", null)).when(groupUtil).isAdminOfGroup(group.getId(), mockUser); + when(clusterUtil.isIndividualCluster(group.getClusterId())).thenReturn(false); + CheckResult result = groupUtil.canUpdateGroup(group.getId(), mockUser); assertEquals(HttpStatus.OK, result.getStatus()); assertEquals(group, result.getData()); - when(clusterUtil.isIndividualCluster(1L)).thenReturn(true); - result = groupUtil.canUpdateGroup(group.getId(), user); + /* Group is individual cluster */ + when(clusterUtil.isIndividualCluster(group.getClusterId())).thenReturn(true); + result = groupUtil.canUpdateGroup(group.getId(), mockUser); assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); - assertEquals("Cannot update individual group", result.getMessage()); - when(groupRepository.isAdminOfGroup(1L, user.getId())).thenReturn(false); - result = groupUtil.canUpdateGroup(group.getId(), user); + /* User is not admin of group */ + doReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "User is not an admin of this group", null)).when(groupUtil).isAdminOfGroup(group.getId(), mockUser); + result = groupUtil.canUpdateGroup(group.getId(), mockUser); assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); - assertEquals("User is not an admin of this group", result.getMessage()); - when(groupRepository.findById(1L)).thenReturn(Optional.empty()); - result = groupUtil.canUpdateGroup(group.getId(), user); + /* Group does not exist */ + doReturn(new CheckResult<>(HttpStatus.NOT_FOUND, "Group not found", null)).when(groupUtil).getGroupIfExists(group.getId()); + result = groupUtil.canUpdateGroup(group.getId(), mockUser); assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); - assertEquals("Group not found", result.getMessage()); } @Test - public void TestCanAddUserToGroup() throws Exception { - when(groupRepository.findById(1L)).thenReturn(Optional.of(group)); - when(groupRepository.isAdminOfGroup(1L, user.getId())).thenReturn(true); - when(groupClusterRepository.userInGroupForCluster(anyLong(), anyLong())).thenReturn(false); - when(groupRepository.userInGroup(anyLong(), anyLong())).thenReturn(false); - when(clusterUtil.getClusterIfExists(group.getClusterId())) - .thenReturn(new CheckResult<>(HttpStatus.OK, "", groupCluster)); - when(groupRepository.countUsersInGroup(group.getId())).thenReturn(1); - when(clusterUtil.isIndividualCluster(groupCluster.getId())).thenReturn(false); - - CheckResult result = groupUtil.canAddUserToGroup(group.getId(), 2L, user); + public void TestCanAddUserToGroup() { + long otherUserId = 5L; + UserEntity otherUser = new UserEntity("othername", "othersurname", "otheremail", UserRole.student, "otherazureid", ""); + /* All checks succeed */ + /* Trying to add yourself to the group */ + when(groupRepository.findById(group.getId())).thenReturn(Optional.of(group)); + when(groupRepository.userAccessToGroup(mockUser.getId(), group.getId())).thenReturn(true); + when(groupClusterRepository.inArchivedCourse(group.getClusterId())).thenReturn(false); + when(userUtil.getUserIfExists(mockUser.getId())).thenReturn(mockUser); + when(groupClusterRepository.userInGroupForCluster(group.getClusterId(), mockUser.getId())).thenReturn(false); + when(groupRepository.userInGroup(group.getId(), mockUser.getId())).thenReturn(false); + when(clusterUtil.getClusterIfExists(group.getClusterId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupCluster)); + when(groupRepository.countUsersInGroup(group.getId())).thenReturn(groupCluster.getMaxSize() - 1); + when(clusterUtil.isIndividualCluster(group.getClusterId())).thenReturn(false); + doReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)). + when(groupUtil).isAdminOfGroup(group.getId(), mockUser); + + CheckResult result = groupUtil.canAddUserToGroup(group.getId(), mockUser.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Trying to join a group when the groups are locked */ + groupCluster.setLockGroupsAfter(OffsetDateTime.now().minusDays(1)); + result = groupUtil.canAddUserToGroup(group.getId(), mockUser.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* Trying to join a group when a locktime is configured but it hasn't passed yet */ + groupCluster.setLockGroupsAfter(OffsetDateTime.now().plusDays(1)); + result = groupUtil.canAddUserToGroup(group.getId(), mockUser.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Trying to add someone else as admin */ + doReturn(new CheckResult<>(HttpStatus.OK, "", null)). + when(groupUtil).isAdminOfGroup(group.getId(), mockUser); + when(userUtil.getUserIfExists(otherUserId)).thenReturn(otherUser); + when(groupClusterRepository.userInGroupForCluster(group.getClusterId(), otherUserId)).thenReturn(false); + when(groupRepository.userInGroup(group.getId(), otherUserId)).thenReturn(false); + doReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)). + when(groupUtil).isAdminOfGroup(group.getId(), otherUser); + result = groupUtil.canAddUserToGroup(group.getId(), otherUserId, mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Adding someone to a group as admin after the locktime has passed */ + groupCluster.setLockGroupsAfter(OffsetDateTime.now().minusDays(1)); + result = groupUtil.canAddUserToGroup(group.getId(), otherUserId, mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + + + /* Group is already full but it's an admin adding someone else */ + when(groupRepository.countUsersInGroup(group.getId())).thenReturn(groupCluster.getMaxSize()); + result = groupUtil.canAddUserToGroup(group.getId(), otherUserId, mockUser); assertEquals(HttpStatus.OK, result.getStatus()); + when(groupRepository.countUsersInGroup(group.getId())).thenReturn(groupCluster.getMaxSize()-1); + + /* User trying to add is admin */ + doReturn(new CheckResult<>(HttpStatus.OK, "", null)). + when(groupUtil).isAdminOfGroup(group.getId(), otherUser); + result = groupUtil.canAddUserToGroup(group.getId(), otherUserId, mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* Cluster is individual */ + when(clusterUtil.isIndividualCluster(group.getClusterId())).thenReturn(true); + result = groupUtil.canAddUserToGroup(group.getId(), otherUserId, mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* Group is already full */ + when(groupRepository.countUsersInGroup(group.getId())).thenReturn(groupCluster.getMaxSize()); + result = groupUtil.canAddUserToGroup(group.getId(), mockUser.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* ClusterEntity is not found */ + when(clusterUtil.getClusterIfExists(group.getClusterId())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + result = groupUtil.canAddUserToGroup(group.getId(), otherUserId, mockUser); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatus()); + + /* User is already in that group */ + when(groupRepository.userInGroup(group.getId(), otherUserId)).thenReturn(true); + result = groupUtil.canAddUserToGroup(group.getId(), otherUserId, mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* User is already in group for cluster */ + when(groupClusterRepository.userInGroupForCluster(group.getClusterId(), otherUserId)).thenReturn(true); + result = groupUtil.canAddUserToGroup(group.getId(), otherUserId, mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* User to add doesn't exist */ + when(userUtil.getUserIfExists(otherUserId)).thenReturn(null); + result = groupUtil.canAddUserToGroup(group.getId(), otherUserId, mockUser); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); + + /* User trying to add a different user while not being admin */ + doReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)). + when(groupUtil).isAdminOfGroup(group.getId(), mockUser); + result = groupUtil.canAddUserToGroup(group.getId(), otherUserId, mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* User trying to join group in archived course */ + when(groupClusterRepository.inArchivedCourse(group.getClusterId())).thenReturn(true); + result = groupUtil.canAddUserToGroup(group.getId(), mockUser.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* User trying to join group they don't have acces too */ + when(groupRepository.userAccessToGroup(mockUser.getId(), group.getId())).thenReturn(false); + result = groupUtil.canAddUserToGroup(group.getId(), mockUser.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* Group not found */ + when(groupRepository.findById(group.getId())).thenReturn(Optional.empty()); + result = groupUtil.canAddUserToGroup(group.getId(), mockUser.getId(), mockUser); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); } @Test public void testCanRemoveUserFromGroup() throws Exception { - when(groupRepository.findById(1L)).thenReturn(Optional.of(group)); - when(groupRepository.isAdminOfGroup(1L, user.getId())).thenReturn(true); - when(groupRepository.userInGroup(1L, 2L)).thenReturn(true); - when(clusterUtil.isIndividualCluster(groupCluster.getId())).thenReturn(false); - CheckResult result = groupUtil.canRemoveUserFromGroup(group.getId(), 2L, user); + /* All checks succeed */ + /* Trying to leave group */ + when(groupRepository.findById(group.getId())).thenReturn(Optional.of(group)); + when(groupClusterRepository.inArchivedCourse(group.getClusterId())).thenReturn(false); + when(groupRepository.userInGroup(group.getId(), mockUser.getId())).thenReturn(true); + when(clusterUtil.isIndividualCluster(group.getClusterId())).thenReturn(false); + when(clusterUtil.getClusterIfExists(group.getClusterId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupCluster)); + + CheckResult result = groupUtil.canRemoveUserFromGroup(group.getId(), mockUser.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Trying to leave group when groups are locked */ + groupCluster.setLockGroupsAfter(OffsetDateTime.now().minusDays(1)); + result = groupUtil.canRemoveUserFromGroup(group.getId(), mockUser.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* Trying to leave group when a locktime is configured but it hasn't passed yet */ + groupCluster.setLockGroupsAfter(OffsetDateTime.now().plusDays(1)); + result = groupUtil.canRemoveUserFromGroup(group.getId(), mockUser.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Getting cluster fails */ + when(clusterUtil.getClusterIfExists(group.getClusterId())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + result = groupUtil.canRemoveUserFromGroup(group.getId(), mockUser.getId(), mockUser); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatus()); + + /* Trying to remove someone else */ + long otherUserId = 5L; + doReturn(new CheckResult<>(HttpStatus.OK, "", null)). + when(groupUtil).isAdminOfGroup(group.getId(), mockUser); + when(groupRepository.userInGroup(group.getId(), otherUserId)).thenReturn(true); + + result = groupUtil.canRemoveUserFromGroup(group.getId(), otherUserId, mockUser); assertEquals(HttpStatus.OK, result.getStatus()); - when(clusterUtil.isIndividualCluster(groupCluster.getId())).thenReturn(true); - result = groupUtil.canRemoveUserFromGroup(group.getId(), 2L, user); + /* Individual cluster */ + when(clusterUtil.isIndividualCluster(group.getClusterId())).thenReturn(true); + result = groupUtil.canRemoveUserFromGroup(group.getId(), otherUserId, mockUser); assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); - assertEquals("Cannot remove user from individual group", result.getMessage()); - when(groupRepository.userInGroup(1L, 2L)).thenReturn(false); - result = groupUtil.canRemoveUserFromGroup(group.getId(), 2L, user); + /* User is not in group */ + when(groupRepository.userInGroup(group.getId(), otherUserId)).thenReturn(false); + result = groupUtil.canRemoveUserFromGroup(group.getId(), otherUserId, mockUser); assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); - assertEquals("User is not in the group", result.getMessage()); - when(groupRepository.isAdminOfGroup(1L, user.getId())).thenReturn(false); - result = groupUtil.canRemoveUserFromGroup(group.getId(), 2L, user); + /* Trying to leave group in archived course */ + when(groupClusterRepository.inArchivedCourse(group.getClusterId())).thenReturn(true); + result = groupUtil.canRemoveUserFromGroup(group.getId(), mockUser.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* Trying to add someone else while not admin */ + doReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)). + when(groupUtil).isAdminOfGroup(group.getId(), mockUser); + result = groupUtil.canRemoveUserFromGroup(group.getId(), otherUserId, mockUser); assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); - assertEquals("User is not an admin of this group", result.getMessage()); - when(groupRepository.findById(1L)).thenReturn(Optional.empty()); - result = groupUtil.canRemoveUserFromGroup(group.getId(), 2L, user); + /* Group not found */ + when(groupRepository.findById(group.getId())).thenReturn(Optional.empty()); + result = groupUtil.canRemoveUserFromGroup(group.getId(), mockUser.getId(), mockUser); assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); - assertEquals("Group not found", result.getMessage()); } @Test public void testCanGetProjectGroupData() throws Exception { - when(projectUtil.getProjectIfExists(project.getId())) - .thenReturn(new CheckResult<>(HttpStatus.OK, "", project)); - when(groupRepository.findByIdAndClusterId(group.getId(), project.getGroupClusterId())) - .thenReturn(Optional.of(group)); - when(groupRepository.userInGroup(group.getId(), user.getId())).thenReturn(true); - when(projectUtil.isProjectAdmin(project.getId(), user)) - .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - CheckResult result = groupUtil.canGetProjectGroupData(group.getId(), project.getId(), - user); - assertEquals(HttpStatus.OK, result.getStatus()); - - when(groupRepository.userInGroup(group.getId(), user.getId())).thenReturn(false); - when(projectUtil.isProjectAdmin(project.getId(), user)) - .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - result = groupUtil.canGetProjectGroupData(group.getId(), project.getId(), user); - assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); - assertEquals("User does not have access to the submissions of the group", result.getMessage()); - - when(groupRepository.findByIdAndClusterId(group.getId(), project.getGroupClusterId())) - .thenReturn(Optional.empty()); - result = groupUtil.canGetProjectGroupData(group.getId(), project.getId(), user); + /* All checks succeed */ + when(projectUtil.getProjectIfExists(project.getId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", project)); + when(groupRepository.findByIdAndClusterId(group.getId(), project.getGroupClusterId())).thenReturn(Optional.of(group)); + + /* User in the group */ + when(groupRepository.userInGroup(group.getId(), mockUser.getId())).thenReturn(true); + when(projectUtil.isProjectAdmin(project.getId(), mockUser)).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + CheckResult result = groupUtil.canGetProjectGroupData(group.getId(), project.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* User not in group but project admin */ + when(groupRepository.userInGroup(group.getId(), mockUser.getId())).thenReturn(false); + when(projectUtil.isProjectAdmin(project.getId(), mockUser)).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + result = groupUtil.canGetProjectGroupData(group.getId(), project.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* User not in group but general admin */ + when(groupRepository.userInGroup(group.getId(), mockUser.getId())).thenReturn(false); + when(projectUtil.isProjectAdmin(project.getId(), mockUser)).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); + mockUser.setRole(UserRole.admin); + result = groupUtil.canGetProjectGroupData(group.getId(), project.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* User not in group and not admin */ + mockUser.setRole(UserRole.student); + result = groupUtil.canGetProjectGroupData(group.getId(), project.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* Group not part of the project */ + when(groupRepository.findByIdAndClusterId(group.getId(), project.getGroupClusterId())).thenReturn(Optional.empty()); + result = groupUtil.canGetProjectGroupData(group.getId(), project.getId(), mockUser); assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); - assertEquals("Group not part of the project", result.getMessage()); - when(projectUtil.getProjectIfExists(project.getId())) - .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", project)); - result = groupUtil.canGetProjectGroupData(group.getId(), project.getId(), user); + /* Project not found */ + when(projectUtil.getProjectIfExists(project.getId())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); + result = groupUtil.canGetProjectGroupData(group.getId(), project.getId(), mockUser); assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); - assertEquals("", result.getMessage()); + + /* Check if groupId is null (eg: adminsubmission) */ + /* User is admin of project */ + when(projectUtil.getProjectIfExists(project.getId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", project)); + when(projectUtil.isProjectAdmin(project.getId(), mockUser)).thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + result = groupUtil.canGetProjectGroupData(null, project.getId(), mockUser); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* User is not admin of project */ + when(projectUtil.isProjectAdmin(project.getId(), mockUser)).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); + result = groupUtil.canGetProjectGroupData(null, project.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); } -} + + + } diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/PairTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/PairTest.java new file mode 100644 index 00000000..c046313f --- /dev/null +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/PairTest.java @@ -0,0 +1,14 @@ +package com.ugent.pidgeon.util; + +import org.junit.jupiter.api.Test; + +public class PairTest { + + @Test + public void testPair() { + Pair pair = new Pair<>("test", 1); + assert(pair.getFirst().equals("test")); + assert(pair.getSecond().equals(1)); + } + +} diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/ProjectUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/ProjectUtilTest.java index 7a64a4da..dfdf0d29 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/ProjectUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/ProjectUtilTest.java @@ -1,23 +1,27 @@ package com.ugent.pidgeon.util; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import com.ugent.pidgeon.json.ProjectJson; import com.ugent.pidgeon.postgre.models.ProjectEntity; import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.ProjectRepository; -import com.ugent.pidgeon.model.json.ProjectJson; +import java.time.OffsetDateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import java.time.OffsetDateTime; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - +@ExtendWith(MockitoExtension.class) public class ProjectUtilTest { @Mock @@ -26,217 +30,195 @@ public class ProjectUtilTest { @Mock private ClusterUtil clusterUtil; + @Spy @InjectMocks private ProjectUtil projectUtil; + private ProjectEntity projectEntity; + private UserEntity mockUser; + @BeforeEach public void setUp() { - MockitoAnnotations.openMocks(this); + projectEntity = new ProjectEntity( + 99L, + "projectName", + "projectDescription", + 69L, + 38L, + true, + 34, + OffsetDateTime.now() + ); + projectEntity.setId(64); + + mockUser = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); + mockUser.setId(10L); } @Test public void testUserPartOfProject() { - long projectId = 1L; - long userId = 1L; - when(projectRepository.userPartOfProject(projectId, userId)).thenReturn(true); - boolean result = projectUtil.userPartOfProject(projectId, userId); - assertEquals(true, result); - } + /* User in project */ + when(projectRepository.userPartOfProject(projectEntity.getId(), mockUser.getId())).thenReturn(true); + assertTrue(projectUtil.userPartOfProject(projectEntity.getId(), mockUser.getId())); - @Test - public void testGetProjectIfExists() { - long projectId = 1L; - ProjectEntity projectEntity = new ProjectEntity(); - when(projectRepository.findById(projectId)).thenReturn(java.util.Optional.of(projectEntity)); - CheckResult result = projectUtil.getProjectIfExists(projectId); - assertEquals(HttpStatus.OK, result.getStatus()); + /* User not in project */ + when(projectRepository.userPartOfProject(projectEntity.getId(), mockUser.getId())).thenReturn(false); + assertFalse(projectUtil.userPartOfProject(projectEntity.getId(), mockUser.getId())); } @Test - public void testGetProjectIfExistsNotFound() { - long projectId = 1L; - when(projectRepository.findById(projectId)).thenReturn(java.util.Optional.empty()); - CheckResult result = projectUtil.getProjectIfExists(projectId); - assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); - } + public void testGetProjectIfExists() { + /* Project found */ + when(projectRepository.findById(projectEntity.getId())).thenReturn(java.util.Optional.of(projectEntity)); + CheckResult checkResult = projectUtil.getProjectIfExists(projectEntity.getId()); + assertEquals(HttpStatus.OK, checkResult.getStatus()); + assertEquals(projectEntity, checkResult.getData()); + /* Project not found */ + when(projectRepository.findById(projectEntity.getId())).thenReturn(java.util.Optional.empty()); + checkResult = projectUtil.getProjectIfExists(projectEntity.getId()); + assertEquals(HttpStatus.NOT_FOUND, checkResult.getStatus()); + } @Test public void testIsProjectAdmin() { - long projectId = 1L; - UserEntity user = new UserEntity(); - user.setId(1L); - user.setRole(UserRole.admin); - when(projectRepository.adminOfProject(projectId, user.getId())).thenReturn(true); - CheckResult result = projectUtil.isProjectAdmin(projectId, user); - assertEquals(HttpStatus.OK, result.getStatus()); - } + /* User is admin */ + when(projectRepository.adminOfProject(projectEntity.getId(), mockUser.getId())).thenReturn(true); + CheckResult checkResult = projectUtil.isProjectAdmin(projectEntity.getId(), mockUser); + assertEquals(HttpStatus.OK, checkResult.getStatus()); + /* User is not admin */ + when(projectRepository.adminOfProject(projectEntity.getId(), mockUser.getId())).thenReturn(false); + checkResult = projectUtil.isProjectAdmin(projectEntity.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, checkResult.getStatus()); - @Test - public void testIsProjectAdminForbidden() { - long projectId = 1L; - UserEntity user = new UserEntity(); - user.setId(1L); - user.setRole(UserRole.student); - when(projectRepository.adminOfProject(projectId, user.getId())).thenReturn(false); - CheckResult result = projectUtil.isProjectAdmin(projectId, user); - assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + /* User is general admin */ + mockUser.setRole(UserRole.admin); + checkResult = projectUtil.isProjectAdmin(projectEntity.getId(), mockUser); + assertEquals(HttpStatus.OK, checkResult.getStatus()); } @Test public void testGetProjectIfAdmin() { - long projectId = 1L; - UserEntity user = new UserEntity(); - user.setId(1L); - user.setRole(UserRole.admin); - ProjectEntity projectEntity = new ProjectEntity(); - when(projectRepository.findById(projectId)).thenReturn(java.util.Optional.of(projectEntity)); - when(projectRepository.adminOfProject(projectId, user.getId())).thenReturn(true); - CheckResult result = projectUtil.getProjectIfAdmin(projectId, user); - assertEquals(HttpStatus.OK, result.getStatus()); - } + /* All checks succeed */ + doReturn(new CheckResult<>(HttpStatus.OK, "", projectEntity)).when(projectUtil).getProjectIfExists(projectEntity.getId()); + when(projectRepository.adminOfProject(projectEntity.getId(), mockUser.getId())).thenReturn(true); - @Test - public void testGetProjectIfAdminForbidden() { - long projectId = 1L; - UserEntity user = new UserEntity(); - user.setId(1L); - user.setRole(UserRole.student); - ProjectEntity projectEntity = new ProjectEntity(); - when(projectRepository.findById(projectId)).thenReturn(java.util.Optional.of(projectEntity)); - when(projectRepository.adminOfProject(projectId, user.getId())).thenReturn(false); - CheckResult result = projectUtil.getProjectIfAdmin(projectId, user); - assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); - } + CheckResult checkResult = projectUtil.getProjectIfAdmin(projectEntity.getId(), mockUser); + assertEquals(HttpStatus.OK, checkResult.getStatus()); - @Test - public void testCheckProjectJson() { - long courseId = 1L; - ProjectJson projectJson = new ProjectJson(); - projectJson.setName("Test Project"); - projectJson.setDescription("This is a test project."); - projectJson.setMaxScore(100); - projectJson.setGroupClusterId(1L); - projectJson.setDeadline(OffsetDateTime.now().plusDays(1)); - when(clusterUtil.partOfCourse(projectJson.getGroupClusterId(), courseId)).thenReturn( - new CheckResult<>(HttpStatus.OK, "", null)); - CheckResult result = projectUtil.checkProjectJson(projectJson, courseId); - assertEquals(HttpStatus.OK, result.getStatus()); - } + /* User is not admin */ + when(projectRepository.adminOfProject(projectEntity.getId(), mockUser.getId())).thenReturn(false); + checkResult = projectUtil.getProjectIfAdmin(projectEntity.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, checkResult.getStatus()); - @Test - public void testCheckProjectJsonNullName() { - long courseId = 1L; - ProjectJson projectJson = new ProjectJson(); - projectJson.setDescription("This is a test project."); - projectJson.setMaxScore(100); - projectJson.setGroupClusterId(1L); - projectJson.setDeadline(OffsetDateTime.now().plusDays(1)); - when(clusterUtil.partOfCourse(projectJson.getGroupClusterId(), courseId)).thenReturn( - new CheckResult<>(HttpStatus.OK, "", null)); - CheckResult result = projectUtil.checkProjectJson(projectJson, courseId); - assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); - } + /* User is not project admin but admin role */ + mockUser.setRole(UserRole.admin); + checkResult = projectUtil.getProjectIfAdmin(projectEntity.getId(), mockUser); + assertEquals(HttpStatus.OK, checkResult.getStatus()); - @Test - public void testCheckProjectJsonNullDescription() { - long courseId = 1L; - ProjectJson projectJson = new ProjectJson(); - projectJson.setName("Test Project"); - projectJson.setMaxScore(100); - projectJson.setGroupClusterId(1L); - projectJson.setDeadline(OffsetDateTime.now().plusDays(1)); - when(clusterUtil.partOfCourse(projectJson.getGroupClusterId(), courseId)).thenReturn( - new CheckResult<>(HttpStatus.OK, "", null)); - CheckResult result = projectUtil.checkProjectJson(projectJson, courseId); - assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + /* Project not found */ + doReturn(new CheckResult<>(HttpStatus.NOT_FOUND, "Project not found", null)).when(projectUtil).getProjectIfExists(projectEntity.getId()); + checkResult = projectUtil.getProjectIfAdmin(projectEntity.getId(), mockUser); + assertEquals(HttpStatus.NOT_FOUND, checkResult.getStatus()); } @Test - public void testCheckProjectJsonNullMaxScore() { - long courseId = 1L; - ProjectJson projectJson = new ProjectJson(); - projectJson.setName("Test Project"); - projectJson.setDescription("This is a test project."); - projectJson.setGroupClusterId(1L); + public void testCheckProjectJson() { + ProjectJson projectJson = new ProjectJson( + "UpdateProjectName", + "UpdateProjectDescription", + 69L, + true, + 34, + OffsetDateTime.now().plusDays(1) + ); + + /* All checks succeed */ + when(clusterUtil.partOfCourse(projectJson.getGroupClusterId(), projectEntity.getCourseId())) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + + CheckResult checkResult = projectUtil.checkProjectJson(projectJson, projectEntity.getCourseId()); + assertEquals(HttpStatus.OK, checkResult.getStatus()); + + /* projectJson maxScore is negative */ + projectJson.setMaxScore(-1); + checkResult = projectUtil.checkProjectJson(projectJson, projectEntity.getCourseId()); + assertEquals(HttpStatus.BAD_REQUEST, checkResult.getStatus()); + + /* projectJson maxScore is zero */ + projectJson.setMaxScore(0); + checkResult = projectUtil.checkProjectJson(projectJson, projectEntity.getCourseId()); + assertEquals(HttpStatus.BAD_REQUEST, checkResult.getStatus()); + + /* projectJson no max score */ + projectJson.setMaxScore(null); + checkResult = projectUtil.checkProjectJson(projectJson, projectEntity.getCourseId()); + assertEquals(HttpStatus.OK, checkResult.getStatus()); + + /* projectJson deadline is already passed */ + projectJson.setDeadline(OffsetDateTime.now().minusDays(1)); + checkResult = projectUtil.checkProjectJson(projectJson, projectEntity.getCourseId()); + assertEquals(HttpStatus.BAD_REQUEST, checkResult.getStatus()); + + /* Cluster not part of course */ + when(clusterUtil.partOfCourse(projectJson.getGroupClusterId(), projectEntity.getCourseId())) + .thenReturn(new CheckResult<>(HttpStatus.NOT_FOUND, "Cluster not part of course", null)); + checkResult = projectUtil.checkProjectJson(projectJson, projectEntity.getCourseId()); + assertEquals(HttpStatus.NOT_FOUND, checkResult.getStatus()); + + /* name is blank */ + projectJson.setName(""); + checkResult = projectUtil.checkProjectJson(projectJson, projectEntity.getCourseId()); + assertEquals(HttpStatus.BAD_REQUEST, checkResult.getStatus()); + + /* deadline is null */ + projectJson.setDeadline(null); + checkResult = projectUtil.checkProjectJson(projectJson, projectEntity.getCourseId()); + assertEquals(HttpStatus.BAD_REQUEST, checkResult.getStatus()); + + /* groupClusterId is null */ projectJson.setDeadline(OffsetDateTime.now().plusDays(1)); - when(clusterUtil.partOfCourse(projectJson.getGroupClusterId(), courseId)).thenReturn( - new CheckResult<>(HttpStatus.OK, "", null)); - CheckResult result = projectUtil.checkProjectJson(projectJson, courseId); - assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); - } + projectJson.setGroupClusterId(null); + checkResult = projectUtil.checkProjectJson(projectJson, projectEntity.getCourseId()); + assertEquals(HttpStatus.BAD_REQUEST, checkResult.getStatus()); + /* description is null */ + projectJson.setDescription(null); + checkResult = projectUtil.checkProjectJson(projectJson, projectEntity.getCourseId()); + assertEquals(HttpStatus.BAD_REQUEST, checkResult.getStatus()); - @Test - public void testCheckProjectJsonNullDeadline() { - long courseId = 1L; - ProjectJson projectJson = new ProjectJson(); - projectJson.setName("Test Project"); - projectJson.setDescription("This is a test project."); - projectJson.setMaxScore(100); - projectJson.setGroupClusterId(1L); - when(clusterUtil.partOfCourse(projectJson.getGroupClusterId(), courseId)).thenReturn( - new CheckResult<>(HttpStatus.OK, "", null)); - CheckResult result = projectUtil.checkProjectJson(projectJson, courseId); - assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); - } - - @Test - public void testCheckProjectDeadlinePast() { - long courseId = 1L; - ProjectJson projectJson = new ProjectJson(); - projectJson.setName("Test Project"); - projectJson.setDescription("This is a test project."); - projectJson.setMaxScore(100); - projectJson.setGroupClusterId(1L); - projectJson.setDeadline(OffsetDateTime.now().minusDays(1)); - when(clusterUtil.partOfCourse(projectJson.getGroupClusterId(), courseId)).thenReturn( - new CheckResult<>(HttpStatus.OK, "", null)); - CheckResult result = projectUtil.checkProjectJson(projectJson, courseId); - assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + /* name is null */ + projectJson.setName(null); + checkResult = projectUtil.checkProjectJson(projectJson, projectEntity.getCourseId()); + assertEquals(HttpStatus.BAD_REQUEST, checkResult.getStatus()); } @Test public void testCanGetProject() { - long projectId = 1L; - UserEntity user = new UserEntity(); - user.setId(1L); - user.setRole(UserRole.admin); - ProjectEntity projectEntity = new ProjectEntity(); - when(projectRepository.findById(projectId)).thenReturn(java.util.Optional.of(projectEntity)); - when(projectRepository.userPartOfProject(projectId, user.getId())).thenReturn(true); - when(projectRepository.adminOfProject(projectId, user.getId())).thenReturn(true); - CheckResult result = projectUtil.canGetProject(projectId, user); - assertEquals(HttpStatus.OK, result.getStatus()); - } + /* User is student */ + when(projectRepository.findById(projectEntity.getId())).thenReturn(java.util.Optional.of(projectEntity)); + when(projectRepository.userPartOfProject(projectEntity.getId(), mockUser.getId())).thenReturn(true); - @Test - public void testCanGetProjectForbidden() { - long projectId = 1L; - UserEntity user = new UserEntity(); - user.setId(1L); - user.setRole(UserRole.student); - ProjectEntity projectEntity = new ProjectEntity(); - when(projectRepository.findById(projectId)).thenReturn(java.util.Optional.of(projectEntity)); - when(projectRepository.userPartOfProject(projectId, user.getId())).thenReturn(false); - when(projectRepository.adminOfProject(projectId, user.getId())).thenReturn(false); - CheckResult result = projectUtil.canGetProject(projectId, user); - assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); - } + CheckResult checkResult = projectUtil.canGetProject(projectEntity.getId(), mockUser); + assertEquals(HttpStatus.OK, checkResult.getStatus()); - @Test - public void testCanGetProjectNotFound() { - long projectId = 1L; - UserEntity user = new UserEntity(); - user.setId(1L); - user.setRole(UserRole.admin); - when(projectRepository.findById(projectId)).thenReturn(java.util.Optional.empty()); - CheckResult result = projectUtil.canGetProject(projectId, user); - assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); - } + /* User is admin */ + when(projectRepository.userPartOfProject(projectEntity.getId(), mockUser.getId())).thenReturn(false); + mockUser.setRole(UserRole.admin); + checkResult = projectUtil.canGetProject(projectEntity.getId(), mockUser); + assertEquals(HttpStatus.OK, checkResult.getStatus()); + /* User is not part of project */ + mockUser.setRole(UserRole.student); + checkResult = projectUtil.canGetProject(projectEntity.getId(), mockUser); + assertEquals(HttpStatus.FORBIDDEN, checkResult.getStatus()); + /* Project not found */ + when(projectRepository.findById(projectEntity.getId())).thenReturn(java.util.Optional.empty()); + checkResult = projectUtil.canGetProject(projectEntity.getId(), mockUser); + assertEquals(HttpStatus.NOT_FOUND, checkResult.getStatus()); + } } \ No newline at end of file diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/StringMatcherTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/StringMatcherTest.java new file mode 100644 index 00000000..c3be6e06 --- /dev/null +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/StringMatcherTest.java @@ -0,0 +1,41 @@ +package com.ugent.pidgeon.util; + +import org.junit.jupiter.api.Test; + +public class StringMatcherTest { + + @Test + public void testIsValidEmail() { + assert (StringMatcher.isValidEmail("name.surname@UGent.be")); + assert (StringMatcher.isValidEmail("namesurname@UGent.be")); + + } + + @Test + public void testIsValidEmailNoEndPart() { + assert (!StringMatcher.isValidEmail("name.surname@UGent")); + } + + @Test + public void testIsValidEmailNoAt() { + assert (!StringMatcher.isValidEmail("name.surnameUGent.be")); + } + + @Test + public void testIsValidEmailNoStartPart() { + assert (!StringMatcher.isValidEmail("@UGent.be")); + } + + @Test + public void testIsValidEmailNoDot() { + assert (!StringMatcher.isValidEmail("name.surname@UGentbe")); + + } + + + + + + + +} diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java index b2b83551..8667740c 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java @@ -1,11 +1,19 @@ package com.ugent.pidgeon.util; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.when; + +import com.ugent.pidgeon.postgre.models.GroupEntity; import com.ugent.pidgeon.postgre.models.ProjectEntity; import com.ugent.pidgeon.postgre.models.SubmissionEntity; import com.ugent.pidgeon.postgre.models.UserEntity; +import com.ugent.pidgeon.postgre.models.types.UserRole; +import com.ugent.pidgeon.postgre.repository.GroupClusterRepository; import com.ugent.pidgeon.postgre.repository.GroupRepository; import com.ugent.pidgeon.postgre.repository.SubmissionRepository; import java.time.OffsetDateTime; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -14,13 +22,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) public class SubmissionUtilTest { @@ -33,6 +34,9 @@ public class SubmissionUtilTest { @Mock private SubmissionRepository submissionRepository; + @Mock + private GroupClusterRepository groupClusterRepository; + @Mock private GroupUtil groupUtil; @@ -42,51 +46,155 @@ public class SubmissionUtilTest { private SubmissionEntity submissionEntity; private ProjectEntity projectEntity; private UserEntity userEntity; + private GroupEntity groupEntity; @BeforeEach public void setUp() { - submissionEntity = new SubmissionEntity(); - submissionEntity.setId(1L); - projectEntity = new ProjectEntity(); - projectEntity.setId(1L); - userEntity = new UserEntity(); - userEntity.setId(1L); + submissionEntity = new SubmissionEntity( + 22, + 45L, + 99L, + OffsetDateTime.MIN, + true, + true + ); + submissionEntity.setId(78L); + projectEntity = new ProjectEntity( + 99L, + "projectName", + "projectDescription", + 2L, + 100L, + true, + 34, + OffsetDateTime.now() + ); + projectEntity.setId(64); + userEntity = new UserEntity( + "name", + "surname", + "email", + UserRole.student, + "azureId", + "" + ); + userEntity.setId(44L); + + groupEntity = new GroupEntity( + "groupName", + projectEntity.getGroupClusterId() + ); + groupEntity.setId(4L); + } @Test public void testCanGetSubmission() { - when(submissionRepository.findById(anyLong())).thenReturn(Optional.of(submissionEntity)); - when(groupUtil.canGetProjectGroupData(anyLong(), anyLong(), any(UserEntity.class))) + /* All checks succeed */ + when(submissionRepository.findById(submissionEntity.getId())).thenReturn(Optional.of(submissionEntity)); + when(groupUtil.canGetProjectGroupData(submissionEntity.getGroupId(), submissionEntity.getProjectId(), userEntity)) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - assertEquals(submissionEntity, submissionUtil.canGetSubmission(1L, userEntity).getData()); - when(groupUtil.canGetProjectGroupData(anyLong(), anyLong(), any(UserEntity.class))) - .thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "User does not have access to this submission", null)); - assertNull(submissionUtil.canGetSubmission(1L, userEntity).getData()); + CheckResult result = submissionUtil.canGetSubmission(submissionEntity.getId(), userEntity); + assertEquals(HttpStatus.OK, result.getStatus()); + assertEquals(submissionEntity, result.getData()); + + /* User does not have access to the submission */ + when(groupUtil.canGetProjectGroupData(submissionEntity.getGroupId(), submissionEntity.getProjectId(), userEntity)) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "User does not have access to get this submission", null)); + result = submissionUtil.canGetSubmission(submissionEntity.getId(), userEntity); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + /* Submission not found */ + when(submissionRepository.findById(submissionEntity.getId())).thenReturn(Optional.empty()); + result = submissionUtil.canGetSubmission(submissionEntity.getId(), userEntity); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); } @Test public void testCanDeleteSubmission() { - when(submissionRepository.findById(anyLong())).thenReturn(Optional.of(submissionEntity)); - when(projectUtil.isProjectAdmin(anyLong(), any(UserEntity.class))) + /* All checks succeed */ + when(submissionRepository.findById(submissionEntity.getId())).thenReturn(Optional.of(submissionEntity)); + when(projectUtil.isProjectAdmin(submissionEntity.getProjectId(), userEntity)) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - assertEquals(submissionEntity, submissionUtil.canDeleteSubmission(1L, userEntity).getData()); - when(projectUtil.isProjectAdmin(anyLong(), any(UserEntity.class))) - .thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "User does not have access to delete this submission", null)); - assertNull(submissionUtil.canDeleteSubmission(1L, userEntity).getData()); - } + CheckResult result = submissionUtil.canDeleteSubmission(submissionEntity.getId(), userEntity); + assertEquals(HttpStatus.OK, result.getStatus()); + assertEquals(submissionEntity, result.getData()); + + /* User does not have access to delete the submission */ + when(projectUtil.isProjectAdmin(submissionEntity.getProjectId(), userEntity)) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "User does not have access to delete this submission", null)); + result = submissionUtil.canDeleteSubmission(submissionEntity.getId(), userEntity); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + /* Submission not found */ + when(submissionRepository.findById(submissionEntity.getId())).thenReturn(Optional.empty()); + result = submissionUtil.canDeleteSubmission(submissionEntity.getId(), userEntity); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); + } + @Test public void testCheckOnSubmit() { - ProjectEntity projectEntity = new ProjectEntity(); - projectEntity.setId(1L); + /* All checks succeed */ projectEntity.setDeadline(OffsetDateTime.now().plusDays(1)); - CheckResult projectCheck = new CheckResult<>(HttpStatus.OK, "", projectEntity); - when(projectUtil.getProjectIfExists(anyLong())).thenReturn(projectCheck); - when(groupRepository.groupIdByProjectAndUser(anyLong(), anyLong())).thenReturn(1L); - when(projectUtil.userPartOfProject(anyLong(), anyLong())).thenReturn(true); - when(projectUtil.getProjectIfExists(anyLong())).thenReturn(new CheckResult<>(HttpStatus.OK, "", projectEntity)); - assertEquals(1L, submissionUtil.checkOnSubmit(1L, userEntity).getData()); + when(projectUtil.userPartOfProject(projectEntity.getId(), userEntity.getId())).thenReturn(true); + when(groupRepository.groupIdByProjectAndUser(projectEntity.getId(), userEntity.getId())).thenReturn(groupEntity.getId()); + when(groupUtil.getGroupIfExists(groupEntity.getId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", groupEntity)); + when(groupClusterRepository.inArchivedCourse(groupEntity.getClusterId())).thenReturn(false); + + when(projectUtil.getProjectIfExists(projectEntity.getId())).thenReturn(new CheckResult<>(HttpStatus.OK, "", projectEntity)); + CheckResult result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* User not part of group but admin */ + when(groupRepository.groupIdByProjectAndUser(projectEntity.getId(), userEntity.getId())).thenReturn(null); + when(projectUtil.isProjectAdmin(projectEntity.getId(), userEntity)) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.OK, result.getStatus()); + assertNull(result.getData()); + + /* Deadline passed when user is admin, should still be allowed */ + projectEntity.setDeadline(OffsetDateTime.now().minusDays(1)); + result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* User not part of group and not admin */ + when(projectUtil.isProjectAdmin(projectEntity.getId(), userEntity)) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "User is not part of a group for this project", null)); + result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + when(groupRepository.groupIdByProjectAndUser(projectEntity.getId(), userEntity.getId())).thenReturn(groupEntity.getId()); + + /* Deadline passed */ + projectEntity.setDeadline(OffsetDateTime.now().minusDays(1)); + result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + + /* GroupCluster in archived course */ + when(groupClusterRepository.inArchivedCourse(groupEntity.getClusterId())).thenReturn(true); + result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* Group not found */ + when(groupUtil.getGroupIfExists(groupEntity.getId())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "Group not found", null)); + result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + + /* User not part of project */ + when(projectUtil.userPartOfProject(projectEntity.getId(), userEntity.getId())).thenReturn(false); + result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* Project not found */ + when(projectUtil.getProjectIfExists(projectEntity.getId())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "Project not found", null)); + result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); } + + } \ No newline at end of file diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java new file mode 100644 index 00000000..c71c0bb5 --- /dev/null +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/TestRunnerTest.java @@ -0,0 +1,161 @@ +package com.ugent.pidgeon.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.ugent.pidgeon.model.submissionTesting.DockerOutput; +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; +import com.ugent.pidgeon.model.submissionTesting.DockerTemplateTestOutput; +import com.ugent.pidgeon.model.submissionTesting.DockerTestOutput; +import com.ugent.pidgeon.model.submissionTesting.SubmissionTemplateModel; +import com.ugent.pidgeon.model.submissionTesting.SubmissionTemplateModel.SubmissionResult; +import com.ugent.pidgeon.postgre.models.TestEntity; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.ZipFile; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class TestRunnerTest { + + @Mock + private SubmissionTemplateModel structureModel; + @Mock + private DockerSubmissionTestModel dockerModel; + @Mock + private ZipFile file; + @Mock + private File artifactFile; + + + + + private List artifacts; + + private TestEntity testEntity; + private SubmissionResult submissionResult; + private DockerTestOutput dockerTestOutput; + private DockerTemplateTestOutput dockerTemplateTestOutput; + private final long projectId = 876L; + + @BeforeEach + public void setUp() { + testEntity = new TestEntity( + "dockerImageBasic", + "dockerTestScriptBasic", + "dockerTestTemplateBasic", + "structureTemplateBasic" + ); + testEntity.setId(38L); + + submissionResult = new SubmissionResult( + true, "submissionResultBasic" + ); + + dockerTestOutput = new DockerTestOutput( + List.of("logs"), true + ); + + dockerTemplateTestOutput = new DockerTemplateTestOutput( + Collections.emptyList(), true + ); + + artifacts = List.of(artifactFile); + } + + @Test + public void testRunStructureTest() throws IOException { + /* The test exists */ + when(structureModel.checkSubmission(file)).thenReturn(submissionResult); + SubmissionResult result = new TestRunner().runStructureTest(file, testEntity, structureModel); + assertEquals(submissionResult, result); + verify(structureModel).parseSubmissionTemplate(testEntity.getStructureTemplate()); + + /* Structure template is null */ + testEntity.setStructureTemplate(null); + result = new TestRunner().runStructureTest(file, testEntity, structureModel); + assertNull(result); + + /* Test entity is null */ + result = new TestRunner().runStructureTest(file, null, structureModel); + assertNull(result); + } + + @Test + public void testRunDockerTest() throws IOException { + Path outputPath = Path.of("outputPath"); + Path extraFilesPath = Path.of("extraFilesPath"); + Path extraFilesPathResolved = extraFilesPath.resolve(Filehandler.EXTRA_TESTFILES_FILENAME); + + try (MockedStatic filehandler = org.mockito.Mockito.mockStatic(Filehandler.class)) { + + AtomicInteger filehandlerCalled = new AtomicInteger(); + filehandlerCalled.set(0); + filehandler.when(() -> Filehandler.copyFilesAsZip(artifacts, outputPath)).thenAnswer( + invocation -> { + filehandlerCalled.getAndIncrement(); + return null; + }); + filehandler.when(() -> Filehandler.getTestExtraFilesPath(projectId)).thenReturn(extraFilesPath); + when(dockerModel.runSubmissionWithTemplate(testEntity.getDockerTestScript(), testEntity.getDockerTestTemplate())) + .thenReturn(dockerTemplateTestOutput); + when(dockerModel.getArtifacts()).thenReturn(artifacts); + + DockerOutput result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel, projectId); + assertEquals(dockerTemplateTestOutput, result); + + verify(dockerModel, times(1)).addZipInputFiles(file); + verify(dockerModel, times(1)).cleanUp(); + assertEquals(1, filehandlerCalled.get()); + + /* artifacts are empty */ + when(dockerModel.getArtifacts()).thenReturn(Collections.emptyList()); + result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel, projectId); + assertEquals(dockerTemplateTestOutput, result); + verify(dockerModel, times(2)).addZipInputFiles(file); + verify(dockerModel, times(2)).cleanUp(); + assertEquals(1, filehandlerCalled.get()); + + /* aritifacts are null */ + when(dockerModel.getArtifacts()).thenReturn(null); + result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel, projectId); + assertEquals(dockerTemplateTestOutput, result); + verify(dockerModel, times(3)).addZipInputFiles(file); + verify(dockerModel, times(3)).cleanUp(); + assertEquals(1, filehandlerCalled.get()); + + /* No template */ + testEntity.setDockerTestTemplate(null); + when(dockerModel.runSubmission(testEntity.getDockerTestScript())).thenReturn(dockerTestOutput); + result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel, projectId); + assertEquals(dockerTestOutput, result); + verify(dockerModel, times(4)).addZipInputFiles(file); + verify(dockerModel, times(4)).cleanUp(); + + /* Error gets thrown */ + when(dockerModel.runSubmission(testEntity.getDockerTestScript())).thenThrow(new RuntimeException("Error")); + assertThrows(Exception.class, () -> new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel, projectId)); + verify(dockerModel, times(5)).cleanUp(); + + /* No script */ + testEntity.setDockerTestScript(null); + result = new TestRunner().runDockerTest(file, testEntity, outputPath, dockerModel, projectId); + assertNull(result); + } + + + } +} diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java index 7ad5e4d3..b87f9ca9 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java @@ -1,26 +1,32 @@ package com.ugent.pidgeon.util; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; +import com.ugent.pidgeon.model.submissionTesting.SubmissionTemplateModel; import com.ugent.pidgeon.postgre.models.ProjectEntity; import com.ugent.pidgeon.postgre.models.TestEntity; import com.ugent.pidgeon.postgre.models.UserEntity; +import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.TestRepository; +import java.time.OffsetDateTime; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.web.multipart.MultipartFile; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class TestUtilTest { @@ -31,6 +37,7 @@ public class TestUtilTest { @Mock private ProjectUtil projectUtil; + @Spy @InjectMocks private TestUtil testUtil; @@ -40,59 +47,395 @@ public class TestUtilTest { @BeforeEach public void setUp() { - testEntity = new TestEntity(); - testEntity.setId(1L); - projectEntity = new ProjectEntity(); - projectEntity.setId(1L); - userEntity = new UserEntity(); - userEntity.setId(1L); + projectEntity = new ProjectEntity( + 99L, + "projectName", + "projectDescription", + 2L, + 100L, + true, + 34, + OffsetDateTime.now() + ); + projectEntity.setId(64); + userEntity = new UserEntity( + "name", + "surname", + "email", + UserRole.student, + "azureId", + "" + ); + userEntity.setId(44L); + testEntity = new TestEntity( + "dockerImageBasic", + "dockerTestScriptBasic", + "dockerTestTemplateBasic", + "structureTemplateBasic" + ); + testEntity.setId(38L); } @Test public void testGetTestIfExists() { - when(testRepository.findByProjectId(anyLong())).thenReturn(Optional.of(testEntity)); - assertEquals(testEntity, testUtil.getTestIfExists(1L)); + /* TestEntity exists */ + when(testRepository.findByProjectId(projectEntity.getId())).thenReturn(Optional.of(testEntity)); + assertEquals(testEntity, testUtil.getTestIfExists(projectEntity.getId())); - when(testRepository.findByProjectId(anyLong())).thenReturn(Optional.empty()); - assertNull(testUtil.getTestIfExists(1L)); + /* TestEntity does not exist */ + when(testRepository.findByProjectId(projectEntity.getId())).thenReturn(Optional.empty()); + assertNull(testUtil.getTestIfExists(projectEntity.getId())); } @Test public void testCheckForTestUpdate() { - // Mock the projectUtil.getProjectIfAdmin method to return a CheckResult with HttpStatus.OK - when(projectUtil.getProjectIfAdmin(anyLong(), any(UserEntity.class))) + String dockerImage = "dockerImage"; + String dockerScript = "dockerScript"; + String dockerTemplate = "@dockerTemplate\nExpectedOutput"; + String structureTemplate = "src/\n\tindex.js\n"; + HttpMethod httpMethod = HttpMethod.POST; + + when(projectUtil.getProjectIfAdmin(projectEntity.getId(), userEntity)) .thenReturn(new CheckResult<>(HttpStatus.OK, "", projectEntity)); - // Mock the testRepository.findByProjectId method to return an Optional of testEntity - when(testRepository.findByProjectId(anyLong())).thenReturn(Optional.of(testEntity)); + doReturn(testEntity).when(testUtil).getTestIfExists(projectEntity.getId()); - // Create a mock MultipartFile - MultipartFile mockFile = mock(MultipartFile.class); + try (MockedStatic mockedTestModel = mockStatic(DockerSubmissionTestModel.class); + MockedStatic mockedTemplateModel = mockStatic(SubmissionTemplateModel.class) + ) { + mockedTestModel.when(() -> DockerSubmissionTestModel.imageExists(dockerImage)).thenReturn(true); + mockedTestModel.when(() -> DockerSubmissionTestModel.tryTemplate(dockerTemplate)).then( + invocation -> null); + mockedTemplateModel.when(() -> SubmissionTemplateModel.tryTemplate(structureTemplate)).then( + invocation -> null); - // Call the checkForTestUpdate method - CheckResult> result = testUtil.checkForTestUpdate(1L, - userEntity, "dockerImage", mockFile, mockFile, HttpMethod.POST); + projectEntity.setTestId(null); + CheckResult> result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + dockerScript, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.OK, result.getStatus()); + assertEquals(testEntity, result.getData().getFirst()); + assertEquals(projectEntity, result.getData().getSecond()); - // Assert the result - assertEquals(HttpStatus.OK, result.getStatus()); - assertEquals(testEntity, result.getData().getFirst()); - assertEquals(projectEntity, result.getData().getSecond()); + /* TestEntity not found and method is post */ + doReturn(null).when(testUtil).getTestIfExists(projectEntity.getId()); + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + dockerScript, + dockerTemplate, + null, + HttpMethod.POST + ); + assertEquals(HttpStatus.OK, result.getStatus()); + doReturn(testEntity).when(testUtil).getTestIfExists(projectEntity.getId()); + + + /* Not a valid docker template */ + mockedTestModel.when(() -> DockerSubmissionTestModel.tryTemplate(dockerTemplate)) + .thenThrow(new IllegalArgumentException("Invalid template")); + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + dockerScript, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + mockedTestModel.when(() -> DockerSubmissionTestModel.tryTemplate(dockerTemplate)).then( + invocation -> null); + + /* Invalid structure template */ + mockedTemplateModel.when(() -> SubmissionTemplateModel.tryTemplate(structureTemplate)) + .thenThrow(new IllegalArgumentException("Invalid template")); + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + dockerScript, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + mockedTemplateModel.when(() -> SubmissionTemplateModel.tryTemplate(structureTemplate)). + then(invocation -> null); + + + /* Method is patch and no template provided */ + projectEntity.setTestId(testEntity.getId()); + httpMethod = HttpMethod.PATCH; + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + dockerScript, + null, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Method is patch and script is null while test has a dockerImage */ + testEntity.setDockerTestScript(null); + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + null, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + testEntity.setDockerTestScript(dockerScript); + + /* Method is patch and script is null but test already has a script */ + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + null, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Method is patch and image is null while test has a dockerScript */ + testEntity.setDockerImage(null); + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + null, + dockerScript, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + testEntity.setDockerImage(dockerImage); + + /* Method is patch and image is null but test already has an image */ + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + null, + dockerScript, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Patch method with everything present in request, nothing in test */ + testEntity.setDockerImage(null); + testEntity.setDockerTestScript(null); + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + dockerScript, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.OK, result.getStatus()); + testEntity.setDockerImage(dockerImage); + testEntity.setDockerTestScript(dockerScript); + + /* Method not patch and template provided without script */ + httpMethod = HttpMethod.PUT; + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + null, + null, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Method not patch and no args provided */ + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + null, + null, + null, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* Invalid dockerImage */ + when(DockerSubmissionTestModel.imageExists(dockerImage)).thenReturn(false); + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + dockerScript, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + when(DockerSubmissionTestModel.imageExists(dockerImage)).thenReturn(true); + + /* dockerImage without script */ + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + null, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* dockerScript without image */ + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + null, + dockerScript, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Method is post and test already exists */ + projectEntity.setTestId(99L); + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + dockerScript, + dockerTemplate, + structureTemplate, + HttpMethod.POST + ); + assertEquals(HttpStatus.CONFLICT, result.getStatus()); + + /* Method is delete and test is found */ + httpMethod = HttpMethod.DELETE; + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + null, + null, + null, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* TestEntity not found and method is not post */ + doReturn(null).when(testUtil).getTestIfExists(projectEntity.getId()); + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + dockerScript, + dockerTemplate, + structureTemplate, + HttpMethod.PATCH + ); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); + + + /* Project check fails */ + when(projectUtil.getProjectIfAdmin(projectEntity.getId(), userEntity)) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "Project not found", null)); + result = testUtil.checkForTestUpdate( + projectEntity.getId(), + userEntity, + dockerImage, + dockerScript, + dockerTemplate, + structureTemplate, + httpMethod + ); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + } } @Test public void testGetTestIfAdmin() { - // Mock the testRepository.findByProjectId method to return an Optional of testEntity - when(testRepository.findByProjectId(anyLong())).thenReturn(Optional.of(testEntity)); + /* TestEntity exists */ + doReturn(testEntity).when(testUtil).getTestIfExists(projectEntity.getId()); + when(projectUtil.isProjectAdmin(projectEntity.getId(), userEntity)) + .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); + + CheckResult result = testUtil.getTestIfAdmin(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.OK, result.getStatus()); + + /* User not admin */ + when(projectUtil.isProjectAdmin(projectEntity.getId(), userEntity)) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "User is not an admin", null)); + result = testUtil.getTestIfAdmin(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + /* TestEntity not found */ + doReturn(null).when(testUtil).getTestIfExists(projectEntity.getId()); + result = testUtil.getTestIfAdmin(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); - // Mock the projectUtil.isProjectAdmin method to return a CheckResult with HttpStatus.OK - when(projectUtil.isProjectAdmin(anyLong(), any(UserEntity.class))) + } + + @Test + public void testGetTestWithAdminStatus() { + doReturn(testEntity).when(testUtil).getTestIfExists(projectEntity.getId()); + when(projectUtil.userPartOfProject(projectEntity.getId(), userEntity.getId())).thenReturn(true); + when(projectUtil.isProjectAdmin(projectEntity.getId(), userEntity)) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); - // Call the getTestIfAdmin method - CheckResult result = testUtil.getTestIfAdmin(1L, userEntity); + CheckResult> result = testUtil.getTestWithAdminStatus(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.OK, result.getStatus()); + assertTrue(result.getData().getSecond()); + + /* User not admin */ + when(projectUtil.isProjectAdmin(projectEntity.getId(), userEntity)) + .thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "User is not an admin", null)); + result = testUtil.getTestWithAdminStatus(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.OK, result.getStatus()); + assertFalse(result.getData().getSecond()); - // Assert the result + /* User not admin but general admin */ + userEntity.setRole(UserRole.admin); + result = testUtil.getTestWithAdminStatus(projectEntity.getId(), userEntity); assertEquals(HttpStatus.OK, result.getStatus()); - assertEquals(testEntity, result.getData()); + assertTrue(result.getData().getSecond()); + + /* Project admin check returns unexpected status */ + when(projectUtil.isProjectAdmin(projectEntity.getId(), userEntity)) + .thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "Unexpected error", null)); + result = testUtil.getTestWithAdminStatus(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.I_AM_A_TEAPOT, result.getStatus()); + + /* User not part of project */ + when(projectUtil.userPartOfProject(projectEntity.getId(), userEntity.getId())).thenReturn(false); + result = testUtil.getTestWithAdminStatus(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + + /* TestEntity not found */ + doReturn(null).when(testUtil).getTestIfExists(projectEntity.getId()); + result = testUtil.getTestWithAdminStatus(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); } + + } \ No newline at end of file diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/UserUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/UserUtilTest.java index 4f61e535..66c8fc2e 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/UserUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/UserUtilTest.java @@ -1,29 +1,32 @@ package com.ugent.pidgeon.util; -import com.ugent.pidgeon.model.json.UserUpdateJson; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import com.ugent.pidgeon.json.UserUpdateJson; import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.UserRepository; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) public class UserUtilTest { @Mock private UserRepository userRepository; + @Spy @InjectMocks private UserUtil userUtil; @@ -31,26 +34,30 @@ public class UserUtilTest { @BeforeEach public void setUp() { - user = new UserEntity("name", "surname", "email", UserRole.student, "azureid"); - user.setId(1L); + user = new UserEntity("name", "surname", "email", UserRole.student, "azureid", ""); + user.setId(87L); } @Test public void testUserExists() { - when(userRepository.existsById(anyLong())).thenReturn(true); - assertTrue(userUtil.userExists(1L)); + /* The user exists */ + when(userRepository.existsById(user.getId())).thenReturn(true); + assertTrue(userUtil.userExists(user.getId())); - when(userRepository.existsById(anyLong())).thenReturn(false); - assertFalse(userUtil.userExists(1L)); + /* The user does not exist */ + when(userRepository.existsById(user.getId())).thenReturn(false); + assertFalse(userUtil.userExists(user.getId())); } @Test public void testGetUserIfExists() { - when(userRepository.findById(anyLong())).thenReturn(Optional.of(user)); - assertEquals(user, userUtil.getUserIfExists(1L)); + /* The user exists */ + when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); + assertEquals(user, userUtil.getUserIfExists(user.getId())); - when(userRepository.findById(anyLong())).thenReturn(Optional.empty()); - assertNull(userUtil.getUserIfExists(1L)); + /* The user does not exist */ + when(userRepository.findById(user.getId())).thenReturn(Optional.empty()); + assertNull(userUtil.getUserIfExists(user.getId())); } @Test @@ -61,14 +68,61 @@ public void testCheckForUserUpdateJson() { json.setEmail("newEmail@example.com"); json.setRole("student"); - when(userRepository.findById(anyLong())).thenReturn(Optional.of(user)); - CheckResult result = userUtil.checkForUserUpdateJson(1L, json); + /* All checks succeed */ + when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); + CheckResult result = userUtil.checkForUserUpdateJson(user.getId(), json); assertEquals(HttpStatus.OK, result.getStatus()); assertEquals(user, result.getData()); + /* Not a valid email */ json.setEmail("invalidEmail"); - result = userUtil.checkForUserUpdateJson(1L, json); + result = userUtil.checkForUserUpdateJson(user.getId(), json); assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); - assertEquals("Email is not valid", result.getMessage()); + json.setEmail("newEmail@example.com"); + + /* Surname is blank */ + json.setSurname(""); + result = userUtil.checkForUserUpdateJson(user.getId(), json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Name is blank */ + json.setSurname("newSurname"); + json.setName(""); + result = userUtil.checkForUserUpdateJson(user.getId(), json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Role is not valid */ + json.setName("newName"); + json.setRole("invalidRole"); + result = userUtil.checkForUserUpdateJson(user.getId(), json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Role is null */ + json.setRole(null); + result = userUtil.checkForUserUpdateJson(user.getId(), json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Email is null */ + json.setRole("student"); + json.setEmail(null); + result = userUtil.checkForUserUpdateJson(user.getId(), json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Surname is null */ + json.setEmail("email.email@email.email"); + json.setSurname(null); + result = userUtil.checkForUserUpdateJson(user.getId(), json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* Name is null */ + json.setSurname("newSurname"); + json.setName(null); + result = userUtil.checkForUserUpdateJson(user.getId(), json); + assertEquals(HttpStatus.BAD_REQUEST, result.getStatus()); + + /* User not found */ + when(userRepository.findById(user.getId())).thenReturn(Optional.empty()); + result = userUtil.checkForUserUpdateJson(user.getId(), json); + assertEquals(HttpStatus.NOT_FOUND, result.getStatus()); } } \ No newline at end of file diff --git a/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip b/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip new file mode 100644 index 00000000..b95a2782 Binary files /dev/null and b/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip differ diff --git a/backend/app/src/test/test-cases/DockerSubmissionTestTest/helloworld.zip b/backend/app/src/test/test-cases/DockerSubmissionTestTest/helloworld.zip new file mode 100644 index 00000000..54fb8fc7 Binary files /dev/null and b/backend/app/src/test/test-cases/DockerSubmissionTestTest/helloworld.zip differ diff --git a/backend/app/src/test/test-cases/FileStructureTestCases/allowAll/template.txt b/backend/app/src/test/test-cases/FileStructureTestCases/allowAll/template.txt index 9abb766c..3f5e9c7c 100644 --- a/backend/app/src/test/test-cases/FileStructureTestCases/allowAll/template.txt +++ b/backend/app/src/test/test-cases/FileStructureTestCases/allowAll/template.txt @@ -1 +1 @@ -.* \ No newline at end of file +\.* \ No newline at end of file diff --git a/backend/app/src/test/test-cases/FileStructureTestCases/denyAllFiles/template.txt b/backend/app/src/test/test-cases/FileStructureTestCases/denyAllFiles/template.txt index a75f68df..ff348fc9 100644 --- a/backend/app/src/test/test-cases/FileStructureTestCases/denyAllFiles/template.txt +++ b/backend/app/src/test/test-cases/FileStructureTestCases/denyAllFiles/template.txt @@ -1 +1 @@ --. \ No newline at end of file +-\. \ No newline at end of file diff --git a/backend/app/src/test/test-cases/FilehandlerTestFiles/Testfile.zip b/backend/app/src/test/test-cases/FilehandlerTestFiles/Testfile.zip new file mode 100644 index 00000000..942dc031 Binary files /dev/null and b/backend/app/src/test/test-cases/FilehandlerTestFiles/Testfile.zip differ diff --git a/backend/database/populate_database.sql b/backend/database/populate_database.sql index 28ff6cd6..c5644482 100644 --- a/backend/database/populate_database.sql +++ b/backend/database/populate_database.sql @@ -10,65 +10,36 @@ INSERT INTO users (name, surname, email, azure_id, role) VALUES ('Charlie', 'Davis', 'charlie.davis@example.com', 'token_5', 'teacher'); -- Inserting into `courses` -INSERT INTO courses (course_id,course_name, description) VALUES - (1,'Math 101', 'Introduction to Mathematics'), - (2,'Science 101', 'Basics of Scientific Method'), - (3,'History 101', 'World History Overview'), - (4,'Computer Science 101', 'Introduction to Computing'), - (5,'English 101', 'English Literature'); +INSERT INTO courses (course_id,course_name, description, course_year) VALUES + (1,'Math 101', 'Introduction to Mathematics',2023), + (2,'Science 101', 'Basics of Scientific Method',2023), + (3,'History 101', 'World History Overview',2023), + (4,'Computer Science 101', 'Introduction to Computing',2023), + (5,'English 101', 'English Literature',2023); -- Inserting into `course_users` -- Assume course_id and user_id start from 1 and match accordingly INSERT INTO course_users (course_id, user_id, course_relation) VALUES - (1, 1, 'enrolled'), - (2, 1, 'enrolled'), - (3, 2, 'creator'), - (4, 3, 'course_admin'), - (5, 4, 'enrolled'); - --- Inserting into `files` --- Assume files are uploaded by different users -INSERT INTO files (file_path, file_name, uploaded_by) VALUES - ('/path/to/file1', 'file1.txt', 1), - ('/path/to/file2', 'file2.txt', 2), - ('/path/to/file3', 'file3.txt', 3), - ('/path/to/file4', 'file4.txt', 4), - ('/path/to/file5', 'file5.txt', 1), - ('/path/to/file6', 'file6.txt', 2), - ('/path/to/file7', 'file7.txt', 3), - ('/path/to/file8', 'file8.txt', 4), - ('/path/to/file9', 'file9.txt', 1), - ('/path/to/file10', 'file10.txt', 2), - ('/path/to/file11', 'file11.txt', 3), - ('/path/to/file12', 'file12.txt', 4), - ('/path/to/file13', 'file13.txt', 5), - ('/path/to/file14', 'file14.txt', 4), - ('/path/to/file15', 'file15.txt', 5), - ('/path/to/file16', 'file16.txt', 1), - ('/path/to/file17', 'file17.txt', 2), - ('/path/to/file18', 'file18.txt', 3), - ('/path/to/file19', 'file19.txt', 4), - ('/path/to/file20', 'file20.txt', 1), - ('/path/to/file21', 'file21.txt', 2), - ('/path/to/file22', 'file22.txt', 3); - - --- Assume tests are created before projects for foreign key constraints --- Inserting into `tests` -INSERT INTO tests (docker_image, docker_test, structure_test_id) VALUES - ('docker/image1', 16, 17), - ('docker/image2', 5, 6), - ('docker/image3', 8, 9), - ('docker/image4', 12, 13), - ('docker/image5', 14, 15); + (1, 1, 'creator'), + (2, 1, 'enrolled'), + (2, 2, 'creator'), + (3, 2, 'creator'), + (4, 3, 'creator'), + (5, 4, 'creator'); + -- Inserting into `group_clusters` INSERT INTO group_clusters (course_id, cluster_name, max_size, group_amount) VALUES - (1, 'Project: priemgetallen', 4, 20), - (2, 'Analyse van alkanen', 3, 10), - (3, 'Groepswerk industriële revolutie', 5, 13), - (4, 'Linux practica', 2, 100), - (5, 'Review: A shaskespeare story', 3, 30); + (1, 'Project: priemgetallen', 4, 0), + (2, 'Analyse van alkanen', 3, 0), + (3, 'Groepswerk industriële revolutie', 5, 0), + (4, 'Linux practica', 2, 0), + (5, 'Review: A shaskespeare story', 3, 0), + (1, 'Students', 1, 0), + (2, 'Students', 1, 0), + (3, 'Students', 1, 0), + (4, 'Students', 1, 0), + (5, 'Students', 1, 0); -- Inserting into `groups` INSERT INTO groups (group_name, group_cluster) VALUES @@ -76,16 +47,13 @@ INSERT INTO groups (group_name, group_cluster) VALUES ('Group 2', 2), ('Group 3', 3), ('Group 4', 4), - ('Group 5', 5); + ('Group 5', 5), + ('Naam van degene die het script heeft uitgevoerd', 7); -- Inserting into `group_users` -- Linking users to groups, assuming group_id and user_id start from 1 INSERT INTO group_users (group_id, user_id) VALUES - (1, 1), - (2, 2), - (3, 3), - (4, 4), - (5, 5); + (6, 1); @@ -93,8 +61,8 @@ INSERT INTO group_users (group_id, user_id) VALUES -- Linking solutions to projects and groups INSERT INTO projects (course_id, test_id, project_name, description, group_cluster_id, max_score, deadline) VALUES - (1, 1, 'Math project 1', 'Solve equations', 1, 20, '2024-03-20 09:00:00+02'), - (2, 2, 'Science Lab 1', 'Aparte reeks met enkel de opdracht-oefening zodat de interface van Dodona de deadline duidelijk maakt. Deze opdracht komt uit de hoofdstuk 5 en de eerdere oefeningen uit die reeks zullen je helpen tot een juiste oplossing te komen. + (1, null, 'Math project 1', 'Solve equations', 1, 20, '2024-03-20 09:00:00+02'), + (2, null, 'Science Lab 1', 'Aparte reeks met enkel de opdracht-oefening zodat de interface van Dodona de deadline duidelijk maakt. Deze opdracht komt uit de hoofdstuk 5 en de eerdere oefeningen uit die reeks zullen je helpen tot een juiste oplossing te komen. Jullie oplossing wordt geëvalueerd op basis van uitvoeringstijd (**50%**), geheugengebruik (25%) en codestijl (25%). @@ -140,36 +108,8 @@ def global_multiple_alignment(infile: str | Path, output: str | Path | None = No 212 156 65 -16 52 23 67 155 96 0 ```', 2, 6, '2024-06-22 12:00:00+02'), - (3, 3, 'History Essay 1', 'Discuss historical event', 3, NULL, '2024-03-22 12:00:00+02'), - (4, 4, 'Programming Assignment 1', 'Write code', 4, 4, '2024-03-23 14:45:00+02'), - (5, 5, 'Literature Analysis', 'Analyze text', 5, 10, '2024-03-24 10:00:00+02'); - - --- Inserting into `group_grades` --- Assign grades to group solutions -INSERT INTO group_feedback(group_id, project_id, grade, feedback) VALUES - (1, 1, 95.0, ''), - (2, 2, 88.5, ''), - (3, 3, NULL, ''), - (4, 4, 89.0, ''), - (5, 5, 94.5, ''); - - -INSERT INTO submissions ( - project_id, - group_id, - file_id, - structure_accepted, - docker_accepted, - structure_feedback, - docker_feedback -) VALUES - (1, 1, 1, true, true, NULL, NULL), - (2, 2, 2, false, true, 'ERROR: .....', NULL), - (3, 3, 3, true, false, NULL, 'Docker configuration needs improvement'), - (4, 4, 4, false, false, 'Structure needs improvement', 'Docker configuration needs improvement'); - - - --- Makes user with id 1 the creator of courses -UPDATE course_users SET course_relation = 'creator' WHERE user_id = 1 + (3, null, 'History Essay 1', 'Discuss historical event', 3, NULL, '2024-03-22 12:00:00+02'), + (4, null, 'Programming Assignment 1', 'Write code', 4, 4, '2024-03-23 14:45:00+02'), + (5, null, 'Literature Analysis', 'Analyze text', 5, 10, '2024-03-24 10:00:00+02'), + (1, null, 'Individueel project', 'Beschrijving voor individueel project', 6, 20, '2024-05-22 12:00:00+02'), + (2, null, 'Individueel project', 'Beschrijving voor individueel project', 7, 20, '2024-05-22 12:00:00+02'); diff --git a/backend/database/start_database.sql b/backend/database/start_database.sql index baf9bb48..9c15a5be 100644 --- a/backend/database/start_database.sql +++ b/backend/database/start_database.sql @@ -3,55 +3,63 @@ CREATE SCHEMA public; -- Users table to store information about users CREATE TABLE users ( - user_id SERIAL PRIMARY KEY, - name VARCHAR(50) NOT NULL, - surname VARCHAR(50) NOT NULL, - email VARCHAR(100) UNIQUE NOT NULL, - azure_id VARCHAR(255) NOT NULL, - role VARCHAR(50) NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + user_id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL, + surname VARCHAR(50) NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + azure_id VARCHAR(255) NOT NULL, + role VARCHAR(50) NOT NULL, + studentnumber VARCHAR(50), + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Courses table to store information about courses CREATE TABLE courses ( - course_id SERIAL PRIMARY KEY, - course_name VARCHAR(100) NOT NULL, - description TEXT, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - join_key TEXT + course_id SERIAL PRIMARY KEY, + course_name VARCHAR(100) NOT NULL, + description TEXT, + course_year INTEGER, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + archived_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + join_key TEXT ); -- Linking table to associate users with courses and define their role in the course CREATE TABLE course_users ( - course_id INT REFERENCES courses(course_id), - user_id INT REFERENCES users(user_id), - course_relation VARCHAR(50) NOT NULL, - PRIMARY KEY (course_id, user_id) + course_id INT REFERENCES courses(course_id), + user_id INT REFERENCES users(user_id), + course_relation VARCHAR(50) NOT NULL, + PRIMARY KEY (course_id, user_id) ); CREATE TABLE group_clusters ( - group_cluster_id SERIAL PRIMARY KEY, - course_id INT REFERENCES courses(course_id), - max_size INT NOT NULL, - cluster_name VARCHAR(100) NOT NULL, - group_amount INT NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + group_cluster_id SERIAL PRIMARY KEY, + course_id INT REFERENCES courses(course_id), + max_size INT NOT NULL, + cluster_name VARCHAR(100) NOT NULL, + group_amount INT NOT NULL, + lock_groups_after TIMESTAMP WITH TIME ZONE DEFAULT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Files table to store file information CREATE TABLE files ( - file_id SERIAL PRIMARY KEY, - file_path VARCHAR(512) NOT NULL, - file_name VARCHAR(512) NOT NULL, - uploaded_by INT REFERENCES users(user_id) + file_id SERIAL PRIMARY KEY, + file_path VARCHAR(512) NOT NULL, + file_name VARCHAR(512) NOT NULL, + uploaded_by INT REFERENCES users(user_id) ); -- A id for the docker test and an id for the file test id +-- docker test is enabled if script is not null +-- docker test is in simple mode if template is null CREATE TABLE tests ( - test_id SERIAL PRIMARY KEY, - docker_image VARCHAR(256), - docker_test INT REFERENCES files(file_id), - structure_test_id INT REFERENCES files(file_id) + test_id SERIAL PRIMARY KEY, + docker_image VARCHAR(256), + docker_test_script TEXT, + docker_test_template TEXT, + structure_template TEXT, + extra_files INT REFERENCES files(file_id) ); @@ -60,33 +68,34 @@ CREATE TABLE tests ( -- test_id points to internal test, possibly a docker or a file structure test. CREATE TABLE projects ( - project_id SERIAL PRIMARY KEY, - course_id INT REFERENCES courses(course_id), - project_name VARCHAR(100) NOT NULL, - description TEXT, - group_cluster_id INT REFERENCES group_clusters(group_cluster_id), - deadline TIMESTAMP WITH TIME ZONE NOT NULL, - test_id INT REFERENCES tests(test_id), - visible BOOLEAN DEFAULT false NOT NULL, - max_score INT + project_id SERIAL PRIMARY KEY, + course_id INT REFERENCES courses(course_id), + project_name VARCHAR(100) NOT NULL, + description TEXT, + group_cluster_id INT REFERENCES group_clusters(group_cluster_id), + deadline TIMESTAMP WITH TIME ZONE NOT NULL, + test_id INT REFERENCES tests(test_id), + visible BOOLEAN DEFAULT false NOT NULL, + max_score INT, + visible_after TIMESTAMP WITH TIME ZONE DEFAULT NULL ); -- Groups table to manage groups of students CREATE TABLE groups ( - group_id SERIAL PRIMARY KEY, - group_name VARCHAR(100) NOT NULL, - group_cluster INT REFERENCES group_clusters(group_cluster_id) + group_id SERIAL PRIMARY KEY, + group_name VARCHAR(100) NOT NULL, + group_cluster INT REFERENCES group_clusters(group_cluster_id) ); -- Group grades table to store grades for groups in projects CREATE TABLE group_feedback ( - group_id INT REFERENCES groups(group_id), - project_id INT REFERENCES projects(project_id), - grade FLOAT, - feedback TEXT, - PRIMARY KEY (group_id, project_id) + group_id INT REFERENCES groups(group_id), + project_id INT REFERENCES projects(project_id), + grade FLOAT, + feedback TEXT, + PRIMARY KEY (group_id, project_id) ); @@ -95,15 +104,17 @@ CREATE TABLE group_feedback ( -- Solo projects are done with group clusters with 1 person. CREATE TABLE submissions ( - submission_id SERIAL PRIMARY KEY, - project_id INT REFERENCES projects(project_id), - group_id INT REFERENCES groups(group_id), - file_id INT REFERENCES files(file_id), - structure_accepted BOOLEAN NOT NULL, - docker_accepted BOOLEAN NOT NULL, - structure_feedback TEXT, - docker_feedback TEXT, - submission_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + submission_id SERIAL PRIMARY KEY, + project_id INT REFERENCES projects(project_id), + group_id INT REFERENCES groups(group_id), + file_id INT REFERENCES files(file_id), + structure_accepted BOOLEAN NOT NULL, + docker_accepted BOOLEAN NOT NULL, + structure_feedback TEXT, + docker_feedback TEXT, + docker_test_state VARCHAR(10) DEFAULT 'running', + docker_type VARCHAR(10) DEFAULT 'simple', + submission_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); @@ -112,7 +123,7 @@ CREATE TABLE submissions ( -- Linking table to associate users with groups CREATE TABLE group_users ( - group_id INT REFERENCES groups(group_id), - user_id INT REFERENCES users(user_id), - PRIMARY KEY (group_id, user_id) + group_id INT REFERENCES groups(group_id), + user_id INT REFERENCES users(user_id), + PRIMARY KEY (group_id, user_id) ); diff --git a/backend/web-bff/App/app.js b/backend/web-bff/App/app.js index d4031e83..22211508 100644 --- a/backend/web-bff/App/app.js +++ b/backend/web-bff/App/app.js @@ -1,66 +1,130 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ +require('dotenv').config({path:".env"}); + +const path = require('path'); +const express = require('express'); +const session = require('express-session'); +const MongoStore = require('connect-mongo'); +const createError = require('http-errors'); +const logger = require('morgan'); -require('dotenv').config(); +const rateLimit = require('express-rate-limit') -var path = require('path'); -var express = require('express'); -var session = require('express-session'); -var createError = require('http-errors'); -var cookieParser = require('cookie-parser'); -var logger = require('morgan'); +const cors = require('cors') -var indexRouter = require('./routes/index'); -var usersRouter = require('./routes/users'); -var authRouter = require('./routes/auth'); +const indexRouter = require('./routes/index'); +const usersRouter = require('./routes/users'); +const authRouter = require('./routes/auth'); +const apiRouter = require('./routes/api'); -// initialize express -var app = express(); +/** + * Initialize express + */ +const app = express(); +const DEVELOPMENT = process.env.ENVIRONMENT === "development"; /** - * Using express-session middleware for persistent user session. Be sure to - * familiarize yourself with available options. Visit: https://www.npmjs.com/package/express-session + * Using cookie-session middleware for persistent user session. */ -app.use(session({ - secret: process.env.EXPRESS_SESSION_SECRET, - resave: false, - saveUninitialized: false, - cookie: { - httpOnly: true, - secure: false, // set this to true on production - } +const connection_string = `mongodb://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}` + + +if (DEVELOPMENT) { + // Use in memory storage for development purposes. + // Keep in mind that when the server shuts down, so does the session information. + app.use(session({ + name: 'pigeon session', + secret: process.env.EXPRESS_SESSION_SECRET, + resave: false, + saveUninitialized: false, + // expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days + cookie: { + httpOnly: true, + secure: false, // make sure this is true in production + maxAge: 7 * 24 * 60 * 60 * 1000, + }, + //store: MongoStore.create( + // {mongoUrl: connection_string}) + + })); +} else { + // When using production mode, please make sure a mongodb instance is running and accepting connections + // on port PORT. Also make sure the user exists. + app.set('trust proxy', 1) + app.use(session({ + name: 'pigeon session', + secret: process.env.EXPRESS_SESSION_SECRET, + resave: false, + saveUninitialized: false, + // expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days + cookie: { + httpOnly: true, + secure: true, // make sure this is true in production + maxAge: 7 * 24 * 60 * 60 * 1000, + }, + store: MongoStore.create( + {mongoUrl: connection_string}) })); +} + + +/** + * Initialize the rate limiter. + * + */ +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 4000, +}); -// view engine setup +app.use(limiter); + +/** + * Initialize the cors protection. + * Requests from our frontend are allowed. + */ +const corsOptions = { + origin: [/localhost/, "https://sel2-6.ugent.be/"], + optionsSuccessStatus: 200, + credentials: true, +} +app.use('*', cors(corsOptions)); + + +// view engine setup for debugging app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'hbs'); app.use(logger('dev')); app.use(express.json()); -app.use(cookieParser()); -app.use(express.urlencoded({ extended: false })); +app.use(express.urlencoded({extended: false})); app.use(express.static(path.join(__dirname, 'public'))); +/** + * Make our routes accessible. + */ app.use('/', indexRouter); -app.use('/users', usersRouter); -app.use('/auth', authRouter); +app.use('/web/users', usersRouter); +app.use('/web/auth', authRouter); +app.use('/web/api', apiRouter) -// catch 404 and forward to error handler +/** + * Catch 404 and forward to error handler. + */ app.use(function (req, res, next) { next(createError(404)); }); -// error handler +/** + * Error handler. + */ app.use(function (err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; + res.locals.error = DEVELOPMENT ? err : {}; // render the error page res.status(err.status || 500); res.render('error'); }); -module.exports = app; \ No newline at end of file +module.exports = app; diff --git a/backend/web-bff/App/auth/AuthProvider.js b/backend/web-bff/App/auth/AuthProvider.js index f2fb8b8f..c73d5938 100644 --- a/backend/web-bff/App/auth/AuthProvider.js +++ b/backend/web-bff/App/auth/AuthProvider.js @@ -105,13 +105,13 @@ class AuthProvider { req.session.idToken = tokenResponse.idToken; req.session.account = tokenResponse.account; - res.redirect(options.successRedirect); + next(); } catch (error) { if (error instanceof msal.InteractionRequiredAuthError) { return this.login({ scopes: options.scopes || [], redirectUri: options.redirectUri, - successRedirect: options.successRedirect || '/', + successRedirect: '/', })(req, res, next); } @@ -125,7 +125,6 @@ class AuthProvider { if (!req.body || !req.body.state) { return next(new Error('Error: response not found')); } - const authCodeRequest = { ...req.session.authCodeRequest, code: req.body.code, @@ -145,7 +144,7 @@ class AuthProvider { req.session.idToken = tokenResponse.idToken; req.session.account = tokenResponse.account; req.session.isAuthenticated = true; - + const state = JSON.parse(this.cryptoProvider.base64Decode(req.body.state)); res.redirect(state.successRedirect); } catch (error) { @@ -270,4 +269,4 @@ class AuthProvider { const authProvider = new AuthProvider(msalConfig); -module.exports = authProvider; \ No newline at end of file +module.exports = authProvider; diff --git a/backend/web-bff/App/authConfig.js b/backend/web-bff/App/authConfig.js index 26481118..80055b96 100644 --- a/backend/web-bff/App/authConfig.js +++ b/backend/web-bff/App/authConfig.js @@ -3,12 +3,10 @@ * Licensed under the MIT License. */ -require('dotenv').config({ path: '.env.dev' }); +require('dotenv').config({ path: '.env' }); /** * Configuration object to be passed to MSAL instance on creation. - * For a full list of MSAL Node configuration parameters, visit: - * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md */ const msalConfig = { auth: { @@ -26,14 +24,16 @@ const msalConfig = { } } } - +/** + * Environment constants. + */ const REDIRECT_URI = process.env.REDIRECT_URI; -const POST_LOGOUT_REDIRECT_URI = process.env.POST_LOGOUT_REDIRECT_URI; +const FRONTEND_URI = process.env.FRONTEND_URI; const BACKEND_API_ENDPOINT = process.env.BACKEND_API_ENDPOINT; module.exports = { msalConfig, REDIRECT_URI, - POST_LOGOUT_REDIRECT_URI, + FRONTEND_URI, BACKEND_API_ENDPOINT -}; \ No newline at end of file +}; diff --git a/backend/web-bff/App/bin/www.js b/backend/web-bff/App/bin/www.js index 092eb15d..998c7a0d 100644 --- a/backend/web-bff/App/bin/www.js +++ b/backend/web-bff/App/bin/www.js @@ -4,22 +4,22 @@ * Module dependencies. */ -var app = require('../app'); -var debug = require('debug')('msal:server'); -var http = require('http'); +const app = require('../app'); +const debug = require('debug')('msal:server'); +const http = require('http'); + /** * Get port from environment and store in Express. */ - -var port = normalizePort(process.env.PORT || '3000'); +const port = normalizePort(process.env.PORT || '3000'); app.set('port', port); /** * Create HTTP server. */ -var server = http.createServer(app); +const server = http.createServer(app); /** * Listen on provided port, on all network interfaces. @@ -30,35 +30,30 @@ server.on('error', onError); server.on('listening', onListening); /** - * Normalize a port into a number, string, or false. + * Parse port into a number, string, or false. */ - function normalizePort(val) { - var port = parseInt(val, 10); - + let port = parseInt(val, 10); if (isNaN(port)) { // named pipe return val; } - if (port >= 0) { // port number return port; } - return false; } /** * Event listener for HTTP server "error" event. */ - function onError(error) { if (error.syscall !== 'listen') { throw error; } - var bind = typeof port === 'string' + let bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; @@ -80,10 +75,9 @@ function onError(error) { /** * Event listener for HTTP server "listening" event. */ - function onListening() { - var addr = server.address(); - var bind = typeof addr === 'string' + let addr = server.address(); + let bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); diff --git a/backend/web-bff/App/fetch.js b/backend/web-bff/App/fetch.js index b3a2f6f8..5587ce1e 100644 --- a/backend/web-bff/App/fetch.js +++ b/backend/web-bff/App/fetch.js @@ -2,9 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ - -var axios = require('axios'); -const https = require('https'); +const axios = require('axios'); const {BACKEND_API_ENDPOINT} = require("./authConfig"); @@ -12,30 +10,42 @@ const {BACKEND_API_ENDPOINT} = require("./authConfig"); * Attaches a given access token to a Backend API Call * @param endpoint REST API endpoint to call * @param accessToken raw access token string + * @param method The http method for the call. Choice out of 'GET', 'PUT', etc... + * @param body body of request + * @param headers headers of request */ -async function fetch(endpoint, accessToken) { +async function fetch(endpoint, accessToken, method, body, headers) { + let methods = ["GET", "POST", "PATCH", "PUT", "DELETE"] + if (!(methods.includes(method))) { + throw new Error('Not a valid HTTP method'); + } const url = new URL(endpoint, BACKEND_API_ENDPOINT) - console.log(accessToken) - const headers = { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", + const authHeaders = { + "Authorization": `Bearer ${accessToken}`, } + const finalHeaders = { ...headers, ...authHeaders } const config= { - method: "GET", + method: method, url: url.toString(), - headers: headers, + data: body, + headers: finalHeaders, } - console.log(`request made to ${BACKEND_API_ENDPOINT}/${endpoint} at: ` + new Date().toString()); + console.log(`${method} request made to ${BACKEND_API_ENDPOINT}/${endpoint} at: ` + new Date().toString()); try { - - const response = await axios(config); - return await response.data; + const res = await axios(config) + return {code: res.status, data: res.data} } catch (error) { - throw new Error(error); + if (error.response) { + return {code: error.response.status, data: error.response.data} + } else { + throw Error(error); + } } + + } -module.exports = fetch; \ No newline at end of file +module.exports = fetch; diff --git a/backend/web-bff/App/package-lock.json b/backend/web-bff/App/package-lock.json new file mode 100644 index 00000000..db535811 --- /dev/null +++ b/backend/web-bff/App/package-lock.json @@ -0,0 +1,1976 @@ +{ + "name": "web-bff", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web-bff", + "version": "1.0.0", + "dependencies": { + "@azure/msal-node": "^2.6.4", + "axios": "^1.6.8", + "busboy": "^1.6.0", + "connect-mongo": "^5.1.0", + "cookie-parser": "^1.4.6", + "cookie-session": "^2.1.0", + "cors": "^2.8.5", + "csurf": "^1.11.0", + "debug": "^4.3.4", + "dotenv": "^16.4.1", + "express": "^4.19.1", + "express-rate-limit": "^7.2.0", + "express-session": "^1.18.0", + "form-data": "^4.0.0", + "hbs": "^4.2.0", + "helmet": "^7.1.0", + "hpp": "^0.2.3", + "http-errors": "^2.0.0", + "morgan": "^1.10.0" + }, + "devDependencies": { + "nodemon": "^3.1.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.9.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.9.0.tgz", + "integrity": "sha512-yzBPRlWPnTBeixxLNI3BBIgF5/bHpbhoRVuuDBnYjCyWRavaPUsKAHUDYLqpGkBLDciA6TCc6GOxN4/S3WiSxg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.7.0.tgz", + "integrity": "sha512-wXD8LkUvHICeSWZydqg6o8Yvv+grlBEcmLGu+QEI4FcwFendbTEZrlSygnAXXSOCVaGAirWLchca35qrgpO6Jw==", + "dependencies": { + "@azure/msal-common": "14.9.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.5.tgz", + "integrity": "sha512-XLNOMH66KhJzUJNwT/qlMnS4WsNDWD5ASdyaSH3EtK+F4r/CFGa3jT4GNi4mfOitGvWXtdLgQJkQjxSVrio+jA==", + "peer": true, + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "peer": true + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.4.tgz", + "integrity": "sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==", + "peer": true, + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.6.0.tgz", + "integrity": "sha512-BVINv2SgcMjL4oYbBuCQTpE3/VKOSxrOA8Cj/wQP7izSzlBGVomdm+TcUd0Pzy0ytLSSDweCKQ6X3f5veM5LQA==", + "peer": true, + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect-mongo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz", + "integrity": "sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==", + "dependencies": { + "debug": "^4.3.1", + "kruptein": "^3.0.0" + }, + "engines": { + "node": ">=12.9.0" + }, + "peerDependencies": { + "express-session": "^1.17.1", + "mongodb": ">= 5.1.0 < 7" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-session": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.1.0.tgz", + "integrity": "sha512-u73BDmR8QLGcs+Lprs0cfbcAPKl2HnPcjpwRXT41sEV4DRJ2+W0vJEEZkG31ofkx+HZflA70siRIjiTdIodmOQ==", + "dependencies": { + "cookies": "0.9.1", + "debug": "3.2.7", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cookie-session/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "dependencies": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csurf": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", + "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", + "deprecated": "Please use another csrf package", + "dependencies": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "csrf": "3.1.0", + "http-errors": "~1.7.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/csurf/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/csurf/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-rate-limit": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.2.0.tgz", + "integrity": "sha512-T7nul1t4TNyfZMJ7pKRKkdeVJWa2CqB8NA1P8BwYaoDI5QSBZARv5oMS43J7b7I5P+4asjVXjb7ONuwDKucahg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "4 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "dependencies": { + "cookie": "0.6.0", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/express/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==" + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hbs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hbs/-/hbs-4.2.0.tgz", + "integrity": "sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg==", + "dependencies": { + "handlebars": "4.7.7", + "walk": "2.3.15" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/helmet": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz", + "integrity": "sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/hpp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", + "integrity": "sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw==", + "dependencies": { + "lodash": "^4.17.12", + "type-is": "^1.6.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/kruptein": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.6.tgz", + "integrity": "sha512-EQJjTwAJfQkC4NfdQdo3HXM2a9pmBm8oidzH270cYu1MbgXPNPMJuldN7OPX+qdhPO5rw4X3/iKz0BFBfkXGKA==", + "dependencies": { + "asn1.js": "^5.4.1" + }, + "engines": { + "node": ">8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "peer": true + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mongodb": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.5.0.tgz", + "integrity": "sha512-Fozq68InT+JKABGLqctgtb8P56pRrJFkbhW0ux+x1mdHeyinor8oNzJqwLjV/t5X5nJGfTlluxfyMnOXNggIUA==", + "peer": true, + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.4.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.0.tgz", + "integrity": "sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==", + "peer": true, + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/nodemon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", + "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "peer": true, + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "peer": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walk": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz", + "integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==", + "dependencies": { + "foreachasync": "^3.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "peer": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/backend/web-bff/App/package.json b/backend/web-bff/App/package.json index 1deeab35..0d047121 100644 --- a/backend/web-bff/App/package.json +++ b/backend/web-bff/App/package.json @@ -2,18 +2,31 @@ "name": "web-bff", "version": "1.0.0", "scripts": { - "start": "node ./bin/www" + "start": "node ./bin/www", + "dev": "nodemon ./bin/www" }, "dependencies": { "@azure/msal-node": "^2.6.4", "axios": "^1.6.8", + "busboy": "^1.6.0", + "connect-mongo": "^5.1.0", "cookie-parser": "^1.4.6", + "cookie-session": "^2.1.0", + "cors": "^2.8.5", + "csurf": "^1.11.0", "debug": "^4.3.4", "dotenv": "^16.4.1", "express": "^4.19.1", + "express-rate-limit": "^7.2.0", "express-session": "^1.18.0", + "form-data": "^4.0.0", "hbs": "^4.2.0", + "helmet": "^7.1.0", + "hpp": "^0.2.3", "http-errors": "^2.0.0", "morgan": "^1.10.0" + }, + "devDependencies": { + "nodemon": "^3.1.0" } } diff --git a/backend/web-bff/App/routes/api.js b/backend/web-bff/App/routes/api.js new file mode 100644 index 00000000..63063889 --- /dev/null +++ b/backend/web-bff/App/routes/api.js @@ -0,0 +1,40 @@ +const authProvider = require('../auth/AuthProvider'); + +const express = require('express'); +const router = express.Router(); + +const fetch = require('../fetch'); + +const {BACKEND_API_ENDPOINT, msalConfig, REDIRECT_URI} = require('../authConfig'); +const isAuthenticated = require('../util/isAuthenticated'); +const handleMultipart = require('../util/handleMultipart'); + +/** + * Route that captures every method and route starting with /web/api. + * An access token is acquired and provided in the authorization header to the backend. + * The response is sent back to the frontend. + * + * @route /web/api/* + */ +router.all('/*', + isAuthenticated("/web/auth/signin"), + authProvider.acquireToken({ + scopes: [msalConfig.auth.clientId + "/.default"], + redirectUri: REDIRECT_URI + }), + async function (req, res, next) { + const contentType = req.headers['content-type']; + if (contentType && contentType.includes('multipart/form-data')) { + handleMultipart(req, res, next); + } else { + try { + const response = await fetch("api" + req.url, req.session.accessToken, req.method, req.body, req.headers) + res.status(response.code).send(response.data) + } catch (error) { + next(error); + } + } + } +) + +module.exports = router; \ No newline at end of file diff --git a/backend/web-bff/App/routes/auth.js b/backend/web-bff/App/routes/auth.js index de43525e..d90f8041 100644 --- a/backend/web-bff/App/routes/auth.js +++ b/backend/web-bff/App/routes/auth.js @@ -1,30 +1,32 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -var express = require('express'); +const express = require('express'); const authProvider = require('../auth/AuthProvider'); -const { REDIRECT_URI, POST_LOGOUT_REDIRECT_URI, msalConfig} = require('../authConfig'); +const { REDIRECT_URI, FRONTEND_URI, msalConfig} = require('../authConfig'); const router = express.Router(); +/** + * Route that starts the authentication flow for msal. + * + * @route GET /web/auth/singin + * + * On successful login the user is redirected to the frontend. + */ router.get('/signin', authProvider.login({ - scopes: [], - redirectUri: REDIRECT_URI, - successRedirect: '/' -})); - -router.get('/acquireToken', authProvider.acquireToken({ scopes: [msalConfig.auth.clientId + "/.default"], redirectUri: REDIRECT_URI, - successRedirect: '/users/profile' + successRedirect: FRONTEND_URI, })); + +/** + * Route that starts the logout flow for msal. + * + * @route GET /web/auth/signout + */ router.get('/signout', authProvider.logout({ - postLogoutRedirectUri: POST_LOGOUT_REDIRECT_URI + postLogoutRedirectUri: FRONTEND_URI })); module.exports = router; \ No newline at end of file diff --git a/backend/web-bff/App/routes/index.js b/backend/web-bff/App/routes/index.js index e6bd1ddb..30de7951 100644 --- a/backend/web-bff/App/routes/index.js +++ b/backend/web-bff/App/routes/index.js @@ -3,10 +3,15 @@ * Licensed under the MIT License. */ -var express = require('express'); +const express = require('express'); const authProvider = require("../auth/AuthProvider"); -var router = express.Router(); +const router = express.Router(); +/** + * Index route for debugging purposes. + * + * @route GET / + */ router.get('/', function (req, res, next) { res.render('index', { title: 'MSAL Node & Express Web App', @@ -15,6 +20,12 @@ router.get('/', function (req, res, next) { }); }); +/** + * Index route that handles a correct login in the msal library. + * This route must be /, this is configured in the application request. + * + * @route POST / + */ router.post('/', authProvider.handleRedirect()); module.exports = router; \ No newline at end of file diff --git a/backend/web-bff/App/routes/users.js b/backend/web-bff/App/routes/users.js index ce01f651..c7ae0617 100644 --- a/backend/web-bff/App/routes/users.js +++ b/backend/web-bff/App/routes/users.js @@ -3,39 +3,81 @@ * Licensed under the MIT License. */ -var express = require('express'); -var router = express.Router(); +const express = require('express'); +const router = express.Router(); -var fetch = require('../fetch'); -var { BACKEND_API_ENDPOINT } = require('../authConfig'); +const isAuthenticated = require('../util/isAuthenticated') -// custom middleware to check auth state -function isAuthenticated(req, res, next) { - if (!req.session.isAuthenticated) { - return res.redirect('/auth/signin'); // redirect to sign-in route - } - - next(); -} +const { BACKEND_API_ENDPOINT, msalConfig, REDIRECT_URI} = require('../authConfig'); +const authProvider = require("../auth/AuthProvider"); +/** + * This route shows all id token claims for debugging purposes. + * + * @route GET /web/users/id + * + * Renders html page with id token claims. + */ router.get('/id', - isAuthenticated, // check if user is authenticated + isAuthenticated('/web/auth/signin'), // check if user is authenticated async function (req, res, next) { res.render('id', { idTokenClaims: req.session.account.idTokenClaims }); } ); -router.get('/profile', - isAuthenticated, // check if user is authenticated +/** + * This route returns an object containing info about the authentication status. + * + * @route GET /web/users/id + * + * @returns + * isAuthenticated: boolean, + * account: { + * name: string + * } + */ +router.get('/isAuthenticated', + async function (req, res, next) { try { - const response = await fetch("api/test", req.session.accessToken); - res.render('profile', { profile: response }); - } catch (error) { + if (req.session.isAuthenticated) { + res.send({ + isAuthenticated: true, + account: { + name: req.session.account?.name + } + }); + } else { + res.send({ + isAuthenticated: false, + account: null + }) + } + } catch(error) { next(error); } } ); +/** + * This route renders a page containing the accessToken for debugging purposes. + * + * @route GET /web/users/token + */ +if (process.env.ENVIRONMENT === 'development') { + router.get('/token', + isAuthenticated('/web/auth/signin'), + authProvider.acquireToken({ + scopes: [msalConfig.auth.clientId + "/.default"], + redirectUri: REDIRECT_URI + }), + async function (req, res, next) { + res.render('token', {accessToken: req.session.accessToken}); + } + ) +} + + + module.exports = router; \ No newline at end of file diff --git a/backend/web-bff/App/util/handleMultipart.js b/backend/web-bff/App/util/handleMultipart.js new file mode 100644 index 00000000..783cd402 --- /dev/null +++ b/backend/web-bff/App/util/handleMultipart.js @@ -0,0 +1,34 @@ +const busboy = require('busboy'); +const FormData = require('form-data'); +const fetch = require('../fetch'); + +function handleMultipart(req, res, next) { + console.log("multipart") + + const bb = busboy({headers: req.headers}); + const form = new FormData(); + + bb.on('file', (name, file, info) => { + const {filename, encoding, mimetype} = info; + file.on('data', (data) => { + form.append(name, data, {filename, contentType: mimetype}); + }); + }); + + bb.on('field', (fieldname, val) => { + form.append(fieldname, val); + }); + + bb.on('close', async () => { + try { + const response = await fetch("api" + req.url, req.session.accessToken, req.method, form, form.getHeaders()) + res.status(response.code).send(response.data); + } catch (error) { + next(error); + } + }); + + req.pipe(bb); +} + +module.exports = handleMultipart; \ No newline at end of file diff --git a/backend/web-bff/App/util/isAuthenticated.js b/backend/web-bff/App/util/isAuthenticated.js new file mode 100644 index 00000000..50de4e61 --- /dev/null +++ b/backend/web-bff/App/util/isAuthenticated.js @@ -0,0 +1,22 @@ +/** + * This route checks if the user is authenticated. + * If not, the user is redirected to the supplied route. + * + * @param redirectPath supplied redirect route + * @returns {(function(*, *, *): (*|undefined))|*} + * + * returns a function that takes 3 arguments: req, res, next to be used as express middleware. + */ + +function isAuthenticated(redirectPath) { + return (req, res, next) => { + // If not authenticated, redirect + if (!req.session.isAuthenticated) { + return res.redirect(redirectPath); + } + // If authenticated, execute next function in middleware. + next(); + } +} + +module.exports = isAuthenticated; \ No newline at end of file diff --git a/backend/web-bff/App/views/index.hbs b/backend/web-bff/App/views/index.hbs index 9f23656c..33fa9826 100644 --- a/backend/web-bff/App/views/index.hbs +++ b/backend/web-bff/App/views/index.hbs @@ -1,12 +1,11 @@

{{title}}

{{#if isAuthenticated }}

Hi {{username}}!

- View ID token claims + View ID token claims
- Acquire a token to call the Microsoft Graph API -
- Sign out + view token + Sign out {{else}}

Welcome to {{title}}

- Sign in + Sign in {{/if}} \ No newline at end of file diff --git a/backend/web-bff/App/views/layout.hbs b/backend/web-bff/App/views/layout.hbs index c335d0a4..5ce79239 100644 --- a/backend/web-bff/App/views/layout.hbs +++ b/backend/web-bff/App/views/layout.hbs @@ -4,6 +4,16 @@ {{title}} + diff --git a/backend/web-bff/App/views/profile.hbs b/backend/web-bff/App/views/profile.hbs deleted file mode 100644 index a602c9d5..00000000 --- a/backend/web-bff/App/views/profile.hbs +++ /dev/null @@ -1,14 +0,0 @@ -

Backend API

-

/ endpoint response

- - - {{#each profile}} - - - - - {{/each}} - -
{{@key}}{{this}}
-
-Go back \ No newline at end of file diff --git a/backend/web-bff/App/views/token.hbs b/backend/web-bff/App/views/token.hbs new file mode 100644 index 00000000..ab04c4a5 --- /dev/null +++ b/backend/web-bff/App/views/token.hbs @@ -0,0 +1,7 @@ +

Backend API

+

/token endpoint response

+ +
+Go back \ No newline at end of file diff --git a/backend/web-bff/Dockerfile b/backend/web-bff/Dockerfile new file mode 100644 index 00000000..b8607e0b --- /dev/null +++ b/backend/web-bff/Dockerfile @@ -0,0 +1,11 @@ +FROM node:21-bookworm + +WORKDIR /express-web-bff + +COPY App/package*.json ./ + +RUN npm install + +COPY App/ . + +CMD npm start diff --git a/backend/web-bff/temp-frontend/.eslintrc.cjs b/backend/web-bff/temp-frontend/.eslintrc.cjs new file mode 100644 index 00000000..d6c95379 --- /dev/null +++ b/backend/web-bff/temp-frontend/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/backend/web-bff/temp-frontend/.gitignore b/backend/web-bff/temp-frontend/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/backend/web-bff/temp-frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/backend/web-bff/temp-frontend/README.md b/backend/web-bff/temp-frontend/README.md new file mode 100644 index 00000000..b2224872 --- /dev/null +++ b/backend/web-bff/temp-frontend/README.md @@ -0,0 +1,3 @@ +# Testing frontend + +Our actual frontend is a bit too complex to test basic features in. This is why this small react project exists. \ No newline at end of file diff --git a/backend/web-bff/temp-frontend/index.html b/backend/web-bff/temp-frontend/index.html new file mode 100644 index 00000000..e4b78eae --- /dev/null +++ b/backend/web-bff/temp-frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/backend/web-bff/temp-frontend/package-lock.json b/backend/web-bff/temp-frontend/package-lock.json new file mode 100644 index 00000000..9000963f --- /dev/null +++ b/backend/web-bff/temp-frontend/package-lock.json @@ -0,0 +1,4223 @@ +{ + "name": "temp-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "temp-frontend", + "version": "0.0.0", + "dependencies": { + "axios": "^1.6.8", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.23.1" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/compat-data": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/generator": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-module-transforms": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-simple-access": "^7.24.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-simple-access": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/helpers": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/highlight": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/traverse": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/core/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/@babel/core/node_modules/caniuse-lite": { + "version": "1.0.30001617", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz", + "integrity": "sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/@babel/core/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/core/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/electron-to-chromium": { + "version": "1.4.763", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.763.tgz", + "integrity": "sha512-k4J8NrtJ9QrvHLRo8Q18OncqBCB7tIUyqxRcJnlonQ0ioHKYB988GcDFF3ZePmnb8eHEopDs/wPHR/iGAFgoUQ==", + "dev": true + }, + "node_modules/@babel/core/node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/core/node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/@babel/core/node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/core/node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/@babel/core/node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/core/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/core/node_modules/update-browserslist-db": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz", + "integrity": "sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/@babel/core/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.5.tgz", + "integrity": "sha512-RtCJoUO2oYrYwFPtR1/jkoBEcFuI1ae9a9IMxeyAVa3a1Ap4AnxmyIKG2b2FaJKqkidw/0cxRbWN+HOs6ZWd1w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self/node_modules/@babel/helper-plugin-utils": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.1.tgz", + "integrity": "sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source/node_modules/@babel/helper-plugin-utils": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz", + "integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz", + "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/type-utils": "7.8.0", + "@typescript-eslint/utils": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", + "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", + "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/@typescript-eslint/parser/node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", + "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", + "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/utils": "7.8.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", + "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/@typescript-eslint/type-utils/node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", + "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.15", + "@types/semver": "^7.5.8", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "semver": "^7.6.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@typescript-eslint/utils/node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", + "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/@typescript-eslint/utils/node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", + "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.8.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", + "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.5", + "@babel/plugin-transform-react-jsx-self": "^7.23.3", + "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", + "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint/node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/eslint/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/eslint/node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/eslint/node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "node_modules/eslint/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/eslint/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eslint/node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/eslint/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/eslint/node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/eslint/node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/eslint/node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/eslint/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/eslint/node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/eslint/node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/eslint/node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/eslint/node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/eslint/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/eslint/node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/eslint/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/eslint/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint/node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/eslint/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/eslint/node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/eslint/node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint/node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/eslint/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint/node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/eslint/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/eslint/node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/eslint/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint/node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/eslint/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loose-envify/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "dependencies": { + "@remix-run/router": "1.16.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "dependencies": { + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", + "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-android-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", + "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", + "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", + "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", + "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", + "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", + "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", + "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", + "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", + "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", + "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", + "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", + "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", + "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", + "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", + "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vite/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/vite/node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", + "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/vite/node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/backend/web-bff/temp-frontend/package.json b/backend/web-bff/temp-frontend/package.json new file mode 100644 index 00000000..af51b155 --- /dev/null +++ b/backend/web-bff/temp-frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "temp-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --cors true", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.23.1", + "axios": "^1.6.8" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } +} diff --git a/backend/web-bff/temp-frontend/public/vite.svg b/backend/web-bff/temp-frontend/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/backend/web-bff/temp-frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/web-bff/temp-frontend/src/App.css b/backend/web-bff/temp-frontend/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/backend/web-bff/temp-frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/backend/web-bff/temp-frontend/src/App.tsx b/backend/web-bff/temp-frontend/src/App.tsx new file mode 100644 index 00000000..3e8022d9 --- /dev/null +++ b/backend/web-bff/temp-frontend/src/App.tsx @@ -0,0 +1,46 @@ +import {useEffect, useState} from 'react' +import axios from 'axios' +import {Link} from "react-router-dom" + +import './App.css' + + +function App() { + + const [auth, setAuth] + = useState<{ isAuthenticated:boolean, account: { name:string } | null} | null>(null) + + useEffect(() => { + axios.get('http://localhost:3000/web/users/isAuthenticated', {withCredentials: true}).then(({data}) => { + console.log(data) + setAuth(data); + }) + }) + + if (auth === null) { + return ( + <> +

Loading...

+ + ) + + } else if (auth.isAuthenticated) { + return ( + <> +

Logged in!

+

You are logged in as {auth && auth.account?.name ? auth.account.name : null}

+ + ) + } else { + return ( + <> +

Welcome, please login

+ Login + + ) + } + + +} + +export default App diff --git a/backend/web-bff/temp-frontend/src/assets/react.svg b/backend/web-bff/temp-frontend/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/backend/web-bff/temp-frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/web-bff/temp-frontend/src/index.css b/backend/web-bff/temp-frontend/src/index.css new file mode 100644 index 00000000..6119ad9a --- /dev/null +++ b/backend/web-bff/temp-frontend/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/backend/web-bff/temp-frontend/src/main.tsx b/backend/web-bff/temp-frontend/src/main.tsx new file mode 100644 index 00000000..a561b2f8 --- /dev/null +++ b/backend/web-bff/temp-frontend/src/main.tsx @@ -0,0 +1,11 @@ + +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' +import {BrowserRouter} from "react-router-dom"; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/backend/web-bff/temp-frontend/src/vite-env.d.ts b/backend/web-bff/temp-frontend/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/backend/web-bff/temp-frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/backend/web-bff/temp-frontend/tsconfig.json b/backend/web-bff/temp-frontend/tsconfig.json new file mode 100644 index 00000000..a7fc6fbf --- /dev/null +++ b/backend/web-bff/temp-frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/backend/web-bff/temp-frontend/tsconfig.node.json b/backend/web-bff/temp-frontend/tsconfig.node.json new file mode 100644 index 00000000..97ede7ee --- /dev/null +++ b/backend/web-bff/temp-frontend/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/backend/web-bff/temp-frontend/vite.config.ts b/backend/web-bff/temp-frontend/vite.config.ts new file mode 100644 index 00000000..5a33944a --- /dev/null +++ b/backend/web-bff/temp-frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml new file mode 100644 index 00000000..fd0d001a --- /dev/null +++ b/docker-compose.prod.yaml @@ -0,0 +1,93 @@ +services: + frontend: + container_name : nginx_container + image: nginx:latest + ports: + - 80:80 + - 443:443 + restart: always + volumes: + - ./nginx/conf/:/etc/nginx/conf.d/:ro + - ./certbot/conf/:/etc/nginx/ssl/:ro + - ./frontend/build/:/usr/share/nginx/html/build/:rw + networks: + docker_network: + ipv4_address: 10.5.0.3 + backend: + container_name: spring_container + build: backend/app/ + depends_on: + - db + ports: + - 8080:8080 + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/postgres + - SPRING_DATASOURCE_USERNAME=${PGU} + - SPRING_DATASOURCE_PASSWORD=${PGP} + restart: always + volumes: + - ./backend/app/data:/data:rw + networks: + docker_network: + ipv4_address: 10.5.0.9 + db: + container_name: db + image: 'postgres:latest' + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${PGU} + - POSTGRES_PASSWORD=${PGP} + - POSTGRES_DB=postgres + ports: + - 5432:5432 + networks: + docker_network: + ipv4_address: 10.5.0.8 + dind: + container_name: dind + image: docker:dind + privileged: true + volumes: + - /var/lib/docker + ports: + - 2375:2375 + networks: + docker_network: + ipv4_address: 10.5.0.4 + + + pgadmin: + container_name: container-pgadmin + image: dpage/pgadmin4 + depends_on: + - db + ports: + - "5050:80" + environment: + PGADMIN_DEFAULT_EMAIL: admin@admin.com + PGADMIN_DEFAULT_PASSWORD: root + restart: unless-stopped + networks: + docker_network: + ipv4_address: 10.5.0.13 + certbot: + image: certbot/certbot:latest + volumes: + - ./certbot/www/:/var/www/certbot/:rw + - ./certbot/conf/:/etc/letsencrypt/:rw + networks: + docker_network: + ipv4_address: 10.5.0.14 +volumes: + postgres-data: +secrets: + db-password: + file: backend/db/password.txt +networks: + docker_network: + driver: bridge + ipam: + config: + - subnet: 10.5.0.0/16 + gateway: 10.5.0.1 diff --git a/docker-compose.yaml b/docker-compose.yaml index f19e3b71..f9541be1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,22 +1,10 @@ services: - frontend: - container_name : nginx_container - image: nginx:latest - ports: - - 80:80 - - 443:443 - restart: always - volumes: - - ./nginx/conf/:/etc/nginx/conf.d/:ro - - ./certbot/conf/:/etc/nginx/ssl/:ro - - ./frontend/build/:/usr/share/nginx/html/build/:rw - networks: - - docker_network backend: container_name: spring_container build: backend/app/ depends_on: - db + - dind ports: - 8080:8080 environment: @@ -27,7 +15,9 @@ services: volumes: - ./backend/app/data:/data:rw networks: - - docker_network + docker_network: + ipv4_address: 10.5.0.9 + db: container_name: db image: 'postgres:latest' @@ -40,9 +30,18 @@ services: ports: - 5432:5432 networks: - - docker_network - + docker_network: + ipv4_address: 10.5.0.8 + dind: + container_name: dind + image: docker:dind + privileged: true + ports: + - 2375:2375 + networks: + docker_network: + ipv4_address: 10.5.0.4 pgadmin: container_name: container-pgadmin image: dpage/pgadmin4 @@ -55,22 +54,15 @@ services: PGADMIN_DEFAULT_PASSWORD: root restart: unless-stopped networks: - - docker_network - certbot: - image: certbot/certbot:latest - volumes: - - ./certbot/www/:/var/www/certbot/:rw - - ./certbot/conf/:/etc/letsencrypt/:rw - networks: - - docker_network + docker_network: + ipv4_address: 10.5.0.13 volumes: postgres-data: -secrets: - db-password: - file: backend/db/password.txt networks: docker_network: driver: bridge ipam: - config: - - subnet: 172.16.0.0/24 + config: + - subnet: 10.5.0.0/16 + gateway: 10.5.0.1 + diff --git a/envBuilder.py b/envBuilder.py new file mode 100644 index 00000000..9b05ba87 --- /dev/null +++ b/envBuilder.py @@ -0,0 +1,54 @@ + + +class envBuilder(): + def __init__(self): + self.env = {} + self.javaEnv = {'client-secret': ['azure.activedirectory.b2c.client-secret'],'client-id':['azure.activedirectory.client-id'],'tenant-id':['azure.activedirectory.tenant-id'],'PGP':['spring.datasource.password'],'PGU':['spring.datasource.username']} + self.expressEnv = {'URI':['REDIRECT_URI','FRONTEND_URI','BACKEND_API_ENDPOINT'], + 'client-id':['CLIENT_ID'],'client-secret':['CLIENT_SECRET'], + 'tenant-id':['TENANT_ID'],'PGP':['DB_PASSWORD'],'PGU':['DB_USER'],'DB_HOST':['DB_HOST'], + 'DB_PORT':['DB_PORT'],'DB_NAME':['DB_NAME'],'EXPRESS_SESSION_SECRET':['EXPRESS_SESSION_SECRET']} + self.javaEnvLocation = 'backend/app/src/main/resources/application-secrets.properties' + self.expressEnvLocation = 'backend/web-bff/App/.env' + def readEnv(self): + with open('.env', 'r') as file: + for line in file: + [key, value] = line.split('=') + self.env[key] = value + + def javaBuilder(self): + with open(self.javaEnvLocation, 'a+') as file: + for key in self.javaEnv: + if key in self.env: + value = self.env[key] + if value == '': + print(f'{key} is empty') + else: + for envName in self.javaEnv[key]: + file.seek(0) + if sum(line.count(f'{envName}') for line in file) == 0: + file.write(f'{envName}={value}\n') + else : + print(f'{key} not found in .env file') + + def expressBuilder(self): + with open(self.expressEnvLocation, 'a+') as file: + for key in self.expressEnv: + if key in self.env: + value = self.env[key] + if value == '': + print(f'{key} is empty') + else: + for envName in self.expressEnv[key]: + file.seek(0) + if sum(line.count(f'{envName}') for line in file) == 0: + file.write(f'{envName}={value}\n') + else : + print(f'{key} not found in .env file') + + +if __name__ == '__main__': + env = envBuilder() + env.readEnv() + env.javaBuilder() + env.expressBuilder() diff --git a/frontend/.babelrc b/frontend/.babelrc new file mode 100644 index 00000000..522c7e1c --- /dev/null +++ b/frontend/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "current" + } + } + ] + ] +} \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index 448863b5..a851d9f2 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -4,6 +4,8 @@ /node_modules /.pnp .pnp.js +package-lock.json +/package-lock.json # testing /coverage @@ -22,3 +24,4 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +startBackend.sh \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index cc946687..00000000 --- a/frontend/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Getting Started with React - -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.\ -You will also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts new file mode 100644 index 00000000..49ac4f86 --- /dev/null +++ b/frontend/cypress.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "cypress"; +import viteConfig from "./vite.config"; + +export default defineConfig({ + component: { + devServer: { + framework: "react", + bundler: "vite", + // optionally pass in vite config + viteConfig: viteConfig, + // or a function - the result is merged with + // any `vite.config` file that is detected + }, + }, + + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + viewportWidth: 1920, + viewportHeight: 1080, + }, +}); diff --git a/frontend/cypress/components/CourseAdminBtn.cy.tsx b/frontend/cypress/components/CourseAdminBtn.cy.tsx new file mode 100644 index 00000000..7e924574 --- /dev/null +++ b/frontend/cypress/components/CourseAdminBtn.cy.tsx @@ -0,0 +1,13 @@ +import CourseAdminBtn from '../../src/pages/course/components/tabExtraBtn/CourseAdminBtn' +import {BrowserRouter} from "react-router-dom"; + +Cypress.on('uncaught:exception', (err, runnable) => { + return false +}) + +describe('', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/CourseCard.cy.tsx b/frontend/cypress/components/CourseCard.cy.tsx new file mode 100644 index 00000000..16bb058c --- /dev/null +++ b/frontend/cypress/components/CourseCard.cy.tsx @@ -0,0 +1,69 @@ +import CourseCard from '../../src/pages/index/components/CourseCard' +import {BrowserRouter} from "react-router-dom"; +import {ApiRoutes, CourseRelation, DockerFeedback, DockerStatus, Timestamp} from "../../src/@types/requests"; + +const mockProjects = [ + {course: {name: "test course 1", url: "test course 1 url", courseId: 1908}, + deadline: "NOW", + description: "do something", + clusterId: null, + projectId: 35, + name: "test project 1", + submissionUrl: null, + testsUrl: "test project 1 url", + maxScore: null, + visible: true, + progress: { + completed: 5, + total: 10, + }, + groupId: null, + }, + {course: {name: "test course 1", url: "test course 1 url", courseId: 1908}, + deadline: "NOW", + description: "do something", + clusterId: null, + projectId: 36, + name: "test project 2", + submissionUrl: null, + testsUrl: "test project 2 url", + maxScore: null, + visible: true, + progress: { + completed: 0, + total: 10, + }, + groupId: null, + } + ] + + +const mockCourse = { + courseId: 1908, + name: "test course 1", + relation: "enrolled" as CourseRelation, + memberCount: 20, + archivedAt: null, + year: 2023, + url: "test course 1 url" +} + +Cypress.on('uncaught:exception', (err: any, runnable: any) => { + return false +}) + +describe('', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get('.ant-card-head').should("contain.text", "test course 1") + cy.get('.ant-list-items > :nth-child(1)').should("contain.text", "test project 1") + .and("contain.text", "50%") + cy.get('.ant-list-items > :nth-child(2)').should("contain.text", "test project 2") + .and("contain.text", "0%") + cy.get(':nth-child(1) > :nth-child(1) > :nth-child(1) > .ant-statistic > .ant-statistic-content') + .should("contain.text", "20") + cy.get(':nth-child(2) > :nth-child(1) > :nth-child(1) > .ant-statistic > .ant-statistic-content') + .should("contain.text", "2") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/CourseSection.cy.tsx b/frontend/cypress/components/CourseSection.cy.tsx new file mode 100644 index 00000000..6dc81202 --- /dev/null +++ b/frontend/cypress/components/CourseSection.cy.tsx @@ -0,0 +1,13 @@ +import CourseSection from '../../src/pages/index/components/CourseSection' +import {BrowserRouter} from "react-router-dom"; + +Cypress.on('uncaught:exception', (err: any, runnable: any) => { + return false +}) + +describe('', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/CoursesList.cy.tsx b/frontend/cypress/components/CoursesList.cy.tsx new file mode 100644 index 00000000..07690b62 --- /dev/null +++ b/frontend/cypress/components/CoursesList.cy.tsx @@ -0,0 +1,42 @@ +import CoursesList from '../../src/pages/courses/components/CoursesList' +import {CourseRelation} from "../../src/@types/requests"; +import {BrowserRouter} from "react-router-dom"; + +const mockCourses = [ + { + courseId: 1, + name: "Test course 1", + relation: "enrolled" as CourseRelation, + memberCount: 1908, + archivedAt: null, + year: 2023, + url: "Test course 1 url" + },{ + courseId: 2, + name: "Test course 2", + relation: "enrolled" as CourseRelation, + memberCount: 35, + archivedAt: null, + year: 2023, + url: "Test course 2 url" + } +] + +describe('CoursesList', () => { + it('renders with no courses', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("body").should("contain.text", "courses.noCourses") + }) + + it('renders with courses', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("body").should("not.contain.text", "courses.noCourses") + .and("contain.text", "Test course 1") + .and("contain.text", "Test course 2") + .and("contain.text", "2023 - 2024") + .and("contain.text", "1908 courses.members") + .and("contain.text", "35 courses.members") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/EditProject.cy.tsx b/frontend/cypress/components/EditProject.cy.tsx new file mode 100644 index 00000000..9d374e9c --- /dev/null +++ b/frontend/cypress/components/EditProject.cy.tsx @@ -0,0 +1,9 @@ +import EditProject from '../../src/pages/editProject/EditProject' +import {BrowserRouter} from "react-router-dom"; + +describe('', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/EditRole.cy.tsx b/frontend/cypress/components/EditRole.cy.tsx new file mode 100644 index 00000000..689f4db2 --- /dev/null +++ b/frontend/cypress/components/EditRole.cy.tsx @@ -0,0 +1,10 @@ +import EditRole from '../../src/pages/editRole/EditRole' + +describe('EditRole', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("body").should("contain.text", "editRole.name") + .and("contain.text", "editRole.searchTutorial") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/ExtraTabBtn.cy.tsx b/frontend/cypress/components/ExtraTabBtn.cy.tsx new file mode 100644 index 00000000..d3b91d25 --- /dev/null +++ b/frontend/cypress/components/ExtraTabBtn.cy.tsx @@ -0,0 +1,14 @@ +import ExtraTabBtn from '../../src/pages/course/components/tabExtraBtn/ExtraTabBtn' + +Cypress.on('uncaught:exception', (err, runnable) => { + // returning false here prevents Cypress from + // failing the test + return false +}) + +describe('', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/GradesCard.cy.tsx b/frontend/cypress/components/GradesCard.cy.tsx new file mode 100644 index 00000000..a66d5a53 --- /dev/null +++ b/frontend/cypress/components/GradesCard.cy.tsx @@ -0,0 +1,12 @@ +import GradesCard from '../../src/pages/course/components/gradesTab/GradesCard' + +Cypress.on('uncaught:exception', (err:any, runnable:any) => { + + return false +}) +describe('GradesCard', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/GradesList.cy.tsx b/frontend/cypress/components/GradesList.cy.tsx new file mode 100644 index 00000000..790a8a04 --- /dev/null +++ b/frontend/cypress/components/GradesList.cy.tsx @@ -0,0 +1,23 @@ +import GradesList from '../../src/pages/course/components/gradesTab/GradesList' +import {CourseGradesType} from "../../src/pages/course/components/gradesTab/GradesCard"; +import {BrowserRouter} from "react-router-dom"; + +const mockGrades:CourseGradesType[] = [{ + projectName: "Test Project", + projectUrl: "Project URL", + projectId: 1908, + maxScore: 100, + groupFeedback: {score: 95, feedback: "Goed gedaan", groupId:1, projectId:1908} +}] + +describe('GradesList', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount() + cy.get("body").should("contain.text", "Feedback") + cy.get(".ant-list-item").should("exist") + .and("contain.text", "Test Project") + .and("contain.text", "Goed gedaan") + .and("contain.text", "95 / 100") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/GroupClusterModalContent.cy.tsx b/frontend/cypress/components/GroupClusterModalContent.cy.tsx new file mode 100644 index 00000000..9d29e1e4 --- /dev/null +++ b/frontend/cypress/components/GroupClusterModalContent.cy.tsx @@ -0,0 +1,14 @@ +import GroupClusterModalContent from '../../src/pages/projectCreate/components/GroupClusterModalContent' + +describe('GroupClusterModalContent', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount( {}} onClose={()=>{}}/>).should("exist") + cy.get("#name").type("TEST") + cy.get("body").should("contain.text", "project.change.clusterName") + .and("contain.text", "project.change.amountOfGroups") + .and("contain.text", "project.change.groupSize") + cy.get(':nth-child(1) > .ant-btn').should("be.visible") + cy.get(':nth-child(2) > .ant-btn').should("be.visible") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/GroupInfoModal.cy.tsx b/frontend/cypress/components/GroupInfoModal.cy.tsx new file mode 100644 index 00000000..f2f0efc9 --- /dev/null +++ b/frontend/cypress/components/GroupInfoModal.cy.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import GroupInfoModal from '../../src/pages/course/components/groupTab/GroupInfoModal' +import {ApiRoutes, } from "../../src/@types/requests.d"; + +Cypress.on('uncaught:exception', (err:any, runnable:any) => { + return false +}) + +const mockGroup = { + groupId: 1, + capacity: 5, + name: "Test group", + groupClusterUrl: ApiRoutes.CLUSTER as ApiRoutes.CLUSTER, + members:[{email: "test email", name: "TEST USER", userId: 1908}] +} + +describe('GroupInfoModal', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount( {return}} + removeUserFromGroup={(userId, groupId) => {return}} + />).should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/GroupProgress.cy.tsx b/frontend/cypress/components/GroupProgress.cy.tsx new file mode 100644 index 00000000..3f877a77 --- /dev/null +++ b/frontend/cypress/components/GroupProgress.cy.tsx @@ -0,0 +1,10 @@ +import GroupProgress from '../../src/pages/index/components/GroupProgress' + +describe('GroupProgress', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("body").should("be.visible") + .and("contain.text", "50%") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/GroupsCard.cy.tsx b/frontend/cypress/components/GroupsCard.cy.tsx new file mode 100644 index 00000000..1954e33b --- /dev/null +++ b/frontend/cypress/components/GroupsCard.cy.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import GroupsCard from '../../src/pages/course/components/groupTab/GroupsCard' + + +describe('', () => { + it('loads', () => { + // see: https://on.cypress.io/mounting-react + cy.mount() + cy.get("body").should("contain.text", "Loading") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/HorizontalCourseScroll.cy.tsx b/frontend/cypress/components/HorizontalCourseScroll.cy.tsx new file mode 100644 index 00000000..a53a26f7 --- /dev/null +++ b/frontend/cypress/components/HorizontalCourseScroll.cy.tsx @@ -0,0 +1,26 @@ +import HorizontalCourseScroll from '../../src/pages/index/components/HorizontalCourseScroll' +import {BrowserRouter} from "react-router-dom"; + +describe('HorizontalCourseScroll', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount( + {}} + showMore={true} + showPlus={true} + allOptions={true} + /> + ).should("exist") + cy.get("body").should("contain.text", "test horizontal scroll") + .and("contain.text", "home.moreCourses") + cy.get(':nth-child(1) > .ant-card > .ant-card-body').should("be.visible") + cy.get(':nth-child(2) > .ant-card > .ant-card-body').should("be.visible") + cy.get(':nth-child(3) > .ant-card > .ant-card-body').should("not.be.visible") + cy.viewport(2560, 1440) + cy.get(':nth-child(3) > .ant-card > .ant-card-body').should("be.visible") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/InformationTab.cy.tsx b/frontend/cypress/components/InformationTab.cy.tsx new file mode 100644 index 00000000..4152cda4 --- /dev/null +++ b/frontend/cypress/components/InformationTab.cy.tsx @@ -0,0 +1,12 @@ +import InformationTab from '../../src/pages/course/components/informationTab/InformationTab' + +Cypress.on('uncaught:exception', (err, runnable) => { + return false +}) + +describe('InformationTab', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount() + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/LanguageDropdown.cy.tsx b/frontend/cypress/components/LanguageDropdown.cy.tsx new file mode 100644 index 00000000..c090c931 --- /dev/null +++ b/frontend/cypress/components/LanguageDropdown.cy.tsx @@ -0,0 +1,19 @@ +import LanguageDropdown from '../../src/components/LanguageDropdown' + +describe('', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount() + cy.get('.Dropdown').should("exist") + }) + + it('functions', () => { + // see: https://on.cypress.io/mounting-react + cy.mount() + cy.get("body").should("not.contain.text", "English") + .and("not.contain.text", "Nederlands") + cy.get('.Dropdown').trigger('mouseover') + cy.get("body").should("contain.text", "English") + .and("contain.text", "Nederlands") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/LeaveCourseButton.cy.tsx b/frontend/cypress/components/LeaveCourseButton.cy.tsx new file mode 100644 index 00000000..bb6acc90 --- /dev/null +++ b/frontend/cypress/components/LeaveCourseButton.cy.tsx @@ -0,0 +1,15 @@ +import LeaveCourseButton from '../../src/pages/course/components/tabExtraBtn/LeaveCourseButton' +import {BrowserRouter} from "react-router-dom"; + +describe('', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get('.ant-btn').should("contain.text", "course.leave") + cy.get('.ant-btn').click() + cy.get("body").should("contain.text", "course.leave") + .and("contain.text", "course.leaveConfirm") + .and("contain.text", "OK") + .and("contain.text", "Cancel") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/MemberCardMembersCard.cy.tsx b/frontend/cypress/components/MemberCardMembersCard.cy.tsx new file mode 100644 index 00000000..83f51876 --- /dev/null +++ b/frontend/cypress/components/MemberCardMembersCard.cy.tsx @@ -0,0 +1,11 @@ +import MembersCard from '../../src/pages/course/components/membersTab/MemberCard' + +Cypress.on('uncaught:exception', (err, runnable) => { + return false +}) +describe('', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/MembersList.cy.tsx b/frontend/cypress/components/MembersList.cy.tsx new file mode 100644 index 00000000..701c0e9a --- /dev/null +++ b/frontend/cypress/components/MembersList.cy.tsx @@ -0,0 +1,12 @@ +import MembersList from '../../src/pages/course/components/membersTab/MembersList' + +Cypress.on('uncaught:exception', (err, runnable) => { + return false +}) + +describe('', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/Navbar.cy.tsx b/frontend/cypress/components/Navbar.cy.tsx new file mode 100644 index 00000000..892adae4 --- /dev/null +++ b/frontend/cypress/components/Navbar.cy.tsx @@ -0,0 +1,13 @@ +import Navbar from '../../src/pages/index/landing/Navbar' +import {BrowserRouter} from "react-router-dom"; + +describe('Navbar', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount( {}}/>).should("exist") + cy.get(".navbar").should("be.visible") + .and("contain.text", "Pigeonhole") + cy.get(".landing-page-btn").should("be.visible") + cy.get(".white-color").should("be.visible") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/ProfileCard.cy.tsx b/frontend/cypress/components/ProfileCard.cy.tsx new file mode 100644 index 00000000..453b549e --- /dev/null +++ b/frontend/cypress/components/ProfileCard.cy.tsx @@ -0,0 +1,22 @@ +import ProfileCard from '../../src/pages/profile/components/ProfileCard' +import {User} from "../../src/providers/UserProvider"; +import {BrowserRouter} from "react-router-dom"; + +const mockUser: User = { + courseUrl:"courseURL", + projects_url: "projectsURL", + url : "URL", + role: "student", + email: "email", + id: 1, + name: "name", + surname: "surname" +} + +describe('ProfileCard', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount() + cy.get("body").should("contain.text", "name surname") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/ProjectCard.cy.tsx b/frontend/cypress/components/ProjectCard.cy.tsx new file mode 100644 index 00000000..59ddb61f --- /dev/null +++ b/frontend/cypress/components/ProjectCard.cy.tsx @@ -0,0 +1,12 @@ +import ProjectCard from '../../src/pages/index/components/ProjectCard' +import {BrowserRouter} from "react-router-dom"; +Cypress.on('uncaught:exception', (err: any, runnable: any) => { + + return false +}) +describe('', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/ProjectStatusTag.cy.tsx b/frontend/cypress/components/ProjectStatusTag.cy.tsx new file mode 100644 index 00000000..82bb2696 --- /dev/null +++ b/frontend/cypress/components/ProjectStatusTag.cy.tsx @@ -0,0 +1,21 @@ +import ProjectStatusTag from '../../src/pages/index/components/ProjectStatusTag' + +describe('ProjectStatusTag', () => { + it('renders when correct', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("body").should("contain.text", "home.projects.status.completed") + }) + it('renders when incorrect', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("body").should("contain.text", "home.projects.status.failed") + .and("not.contain.text", "home.projects.status.completed") + }) + it('renders when not started', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("body").should("contain.text", "home.projects.status.notStarted") + .and("not.contain.text", "home.projects.status.completed") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/ProjectTable.cy.tsx b/frontend/cypress/components/ProjectTable.cy.tsx new file mode 100644 index 00000000..49e2c867 --- /dev/null +++ b/frontend/cypress/components/ProjectTable.cy.tsx @@ -0,0 +1,67 @@ +import ProjectTable from '../../src/pages/index/components/ProjectTable' + +Cypress.on('uncaught:exception', (err: any, runnable: any) => { + + return false +}) + +const mockProjects = [ + {course: {name: "test course 1", url: "test course 1 url", courseId: 1908}, + deadline: "2024/05/28", + description: "do something", + clusterId: null, + projectId: 35, + name: "test project 1", + submissionUrl: null, + testsUrl: "test project 1 url", + maxScore: null, + visible: true, + progress: { + completed: 5, + total: 10, + }, + groupId: null, + }, + {course: {name: "test course 1", url: "test course 1 url", courseId: 1908}, + deadline: "2024/06/03", + description: "do something", + clusterId: null, + projectId: 36, + name: "test project 2", + submissionUrl: null, + testsUrl: "test project 2 url", + maxScore: null, + visible: true, + progress: { + completed: 0, + total: 10, + }, + groupId: null, + } +] + +describe('ProjectTable', () => { + it('renders loading correctly', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get('.ant-spin-dot').should("be.visible") + }) + it('renders projects', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get('.ant-spin-dot').should("not.exist") + cy.get("body").should("not.contain.text", "home.projects.noProjects") + }) + it('renders no projects', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get('.ant-spin-dot').should("not.exist") + cy.get("body").should("contain.text", "home.projects.noProjects") + .and("contain.text", "home.projects.name") + .and("contain.text", "home.projects.course") + .and("contain.text", "home.projects.projectStatus") + .and("contain.text", "home.projects.deadline") + .and("contain.text", "home.projects.groupProgress") + }) + +}) \ No newline at end of file diff --git a/frontend/cypress/components/ScoreTabScoreCard.cy.tsx b/frontend/cypress/components/ScoreTabScoreCard.cy.tsx new file mode 100644 index 00000000..cf4b9e2a --- /dev/null +++ b/frontend/cypress/components/ScoreTabScoreCard.cy.tsx @@ -0,0 +1,8 @@ +import ScoreCard from '../../src/pages/project/components/ScoreTab' + +describe('ScoreCard', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/SettingsCard.cy.tsx b/frontend/cypress/components/SettingsCard.cy.tsx new file mode 100644 index 00000000..f6f80813 --- /dev/null +++ b/frontend/cypress/components/SettingsCard.cy.tsx @@ -0,0 +1,13 @@ +import SettingsCard from '../../src/pages/course/components/settingsTab/SettingsCard' +import {BrowserRouter} from "react-router-dom"; + +Cypress.on('uncaught:exception', (err, runnable) => { + return false +}) + +describe('', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount( ).should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/Sidebar.cy.tsx b/frontend/cypress/components/Sidebar.cy.tsx new file mode 100644 index 00000000..24ddb759 --- /dev/null +++ b/frontend/cypress/components/Sidebar.cy.tsx @@ -0,0 +1,19 @@ +import Sidebar from '../../src/components/layout/sidebar/Sidebar' +import {BrowserRouter} from "react-router-dom"; + +describe('', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount( ) + cy.get("Button").should("exist") + }) + + it("functions", () => { + cy.mount( ) + cy.get("body").should("not.contain.text", "Profile") + .and("not.contain.text", "home.allCourses") + cy.get("Button").click() + cy.get("body").should("contain.text", "Profile") + .and("contain.text", "home.allCourses") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/SubmissionList.cy.tsx b/frontend/cypress/components/SubmissionList.cy.tsx new file mode 100644 index 00000000..3fcfa682 --- /dev/null +++ b/frontend/cypress/components/SubmissionList.cy.tsx @@ -0,0 +1,12 @@ +import SubmissionList from '../../src/pages/project/components/SubmissionList' + +describe('SubmissionList', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("body").should("contain.text", "project.noSubmissions") + .and("contain.text", "project.submission") + .and("contain.text", "project.submissionTime") + .and("contain.text", "project.status") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/SubmissionStatusTag.cy.tsx b/frontend/cypress/components/SubmissionStatusTag.cy.tsx new file mode 100644 index 00000000..9d2697d1 --- /dev/null +++ b/frontend/cypress/components/SubmissionStatusTag.cy.tsx @@ -0,0 +1,25 @@ +import SubmissionStatusTag, {SubmissionStatus} from '../../src/pages/project/components/SubmissionStatusTag' + +describe('SubmissionStatusTag', () => { + it('renders when passed', () => { + // see: https://on.cypress.io/mounting-react + cy.mount() + cy.get("body").should("contain.text", "project.passed") + .and("not.contain.text", "project.testFailed") + .and("not.contain.text", "project.notSubmitted") + }) + it('renders when failed', () => { + // see: https://on.cypress.io/mounting-react + cy.mount() + cy.get("body").should("contain.text", "project.testFailed") + .and("not.contain.text", "project.passed") + .and("not.contain.text", "project.notSubmitted") + }) + it('renders when not submitted', () => { + // see: https://on.cypress.io/mounting-react + cy.mount() + cy.get("body").should("not.contain.text", "project.passed") + .and("not.contain.text", "project.testFailed") + .and("contain.text", "project.notSubmitted") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/SubmissionTab.cy.tsx b/frontend/cypress/components/SubmissionTab.cy.tsx new file mode 100644 index 00000000..f3020cfe --- /dev/null +++ b/frontend/cypress/components/SubmissionTab.cy.tsx @@ -0,0 +1,9 @@ +import SubmissionTab from '../../src/pages/project/components/SubmissionTab' + +describe('SubmissionTab', () => { + it('renders when loading', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get(".ant-spin-dot").should("be.visible") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/SubmissionsTable.cy.tsx b/frontend/cypress/components/SubmissionsTable.cy.tsx new file mode 100644 index 00000000..dcd562cc --- /dev/null +++ b/frontend/cypress/components/SubmissionsTable.cy.tsx @@ -0,0 +1,15 @@ +import SubmissionsTable from '../../src/pages/project/components/SubmissionsTable' + +describe('SubmissionsTable', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount( {}}/>).should("exist") + cy.get("body").should("contain.text", "project.noSubmissions") + .and("contain.text", "project.userName") + .and("contain.text", "project.submission") + .and("contain.text", "project.submissionTime") + .and("contain.text", "project.status") + .and("contain.text", "Score") + .and("contain.text", "Download") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/components/SubmitForm.cy.tsx b/frontend/cypress/components/SubmitForm.cy.tsx new file mode 100644 index 00000000..a594b704 --- /dev/null +++ b/frontend/cypress/components/SubmitForm.cy.tsx @@ -0,0 +1,15 @@ +import SubmitForm from '../../src/pages/submit/components/SubmitForm' + +describe('SubmitForm', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("body").should("contain.text", "project.uploadAreaTitle") + .and("contain.text", "project.uploadAreaSubtitle") + }) + it('opens a file window when clicked', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get('.ant-upload-drag-container').click() + }); +}) \ No newline at end of file diff --git a/frontend/cypress/components/UserList.cy.tsx b/frontend/cypress/components/UserList.cy.tsx new file mode 100644 index 00000000..b271d602 --- /dev/null +++ b/frontend/cypress/components/UserList.cy.tsx @@ -0,0 +1,30 @@ +import UserList from '../../src/pages/editRole/components/UserList' +import {UsersType} from "../../src/pages/editRole/EditRole"; + +const mockUsers : UsersType[] = [ + { + name: "Test user 1", userId: 1908, url: "test/user1", email: "user1@test.com", role: "student" + }, + { + name: "Test user 2", userId: 35, url: "test/user2", email: "user2@test.com", role: "teacher" + } +] + +describe('UserList', () => { + it('renders without users', () => { + // see: https://on.cypress.io/mounting-react + cy.mount( {return}}/>) + .should("exist") + cy.get("body").should("contain.text", "editRole.noUsersFound") + }) + + it('renders with users', () => { + cy.mount( {return}}/>) + .should("exist") + cy.get("body").should("not.contain.text", "No data") + .and("contain.text", "Test user 1") + .and("contain.text", "Test user 2") + cy.get('.ant-list-items > :nth-child(1)').should("contain.text", "editRole.student") + cy.get('.ant-list-items > :nth-child(2)').should("contain.text", "editRole.teacher") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/e2e/landingpage.cy.js b/frontend/cypress/e2e/landingpage.cy.js new file mode 100644 index 00000000..b6a4aa12 --- /dev/null +++ b/frontend/cypress/e2e/landingpage.cy.js @@ -0,0 +1,53 @@ + +describe('landing page', () => { + beforeEach(() => { + cy.visit('http://localhost:3001') + }) + + it('contains correct classes', () => { + // Een '.' betekent dat je een element selecteert op basis van html class + cy.get(".landing-page") + .should("exist") + .find(".landing-page-btn") + .should("exist") + + cy.contains('Pigeonhole') + .should('exist') + + cy.get('.navbar') + .should('exist') + .find(".landing-page-btn") + .should("exist") + }) + + it('contains all the cute logos', () => { + cy.get(".landing-page") + .find(".logo-item, .ugent-logo, .code-logo, .js-logo, .docker-logo, .py-logo, .c-logo, .blob-image") + .should("exist") + .and("be.visible") + + }) + + it('has a title and subtitle', () => { + cy.get('.landing-title').should('exist'); + cy.contains('UGent').should('exist') + cy.contains('projecten').should('exist') + }); + + + it('can navigate to the login page', () => { + cy.get('.landing-page-btn').first().click() + + // Controleer of het nieuwe venster is geopend + cy.window().should('exist') + }); + + it('is responsive', () => { + //test de pagina op een kleiner scherm. De logos en afbeeldingen zijn niet meer zichtbaar dan + cy.viewport(800, 550) + cy.get('.landing-page').should('exist') + .find(".logo-item, .ugent-logo, .code-logo, .js-logo, .docker-logo, .py-logo, .c-logo, .blob-image") + .should("exist") + .and("not.be.visible") + }); +}) \ No newline at end of file diff --git a/frontend/cypress/fixtures/course.json b/frontend/cypress/fixtures/course.json new file mode 100644 index 00000000..c97daee8 --- /dev/null +++ b/frontend/cypress/fixtures/course.json @@ -0,0 +1,9 @@ +{ + "name": "testCourse", + "description": "testDescription", + "courseId": 1, + "teacher": {"name": "teacherName", "surname": "teacherSurname", "url": "URL"}, + "year": 2023, + "createdAt": "now", + "assistents": [] +} \ No newline at end of file diff --git a/frontend/cypress/pages/Course.cy.tsx b/frontend/cypress/pages/Course.cy.tsx new file mode 100644 index 00000000..9f9d412d --- /dev/null +++ b/frontend/cypress/pages/Course.cy.tsx @@ -0,0 +1,13 @@ +import Course from '../../src/pages/course/Course' +import {BrowserRouter} from "react-router-dom"; + +Cypress.on('uncaught:exception', (err, runnable) => { + return false +}) + +describe('', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/pages/Courses.cy.tsx b/frontend/cypress/pages/Courses.cy.tsx new file mode 100644 index 00000000..c3c9b5f3 --- /dev/null +++ b/frontend/cypress/pages/Courses.cy.tsx @@ -0,0 +1,13 @@ +import Courses from '../../src/pages/courses/Courses' +import {BrowserRouter} from "react-router-dom"; + +describe('Courses', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get('.ant-card-head').should("contain.text", "courses.courses") + .and("contain.text", "courses.sortAscending") + cy.get('.ant-input').should("be.visible") + cy.get('.ant-card-body').should("contain.text", "courses.noCourses") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/pages/Error.cy.tsx b/frontend/cypress/pages/Error.cy.tsx new file mode 100644 index 00000000..b1fe3cab --- /dev/null +++ b/frontend/cypress/pages/Error.cy.tsx @@ -0,0 +1,12 @@ +import Error from '../../src/pages/error/Error' +import {BrowserRouter} from "react-router-dom"; + + +describe('ErrorPage', () => { + it('renders', () => { + cy.mount() + cy.get("Button").should("exist") + cy.get("body").should("contain.text", "404") + cy.get("body").should("contain.text", "TEST") + }); +}); \ No newline at end of file diff --git a/frontend/cypress/pages/Home.cy.tsx b/frontend/cypress/pages/Home.cy.tsx new file mode 100644 index 00000000..fd0d27d7 --- /dev/null +++ b/frontend/cypress/pages/Home.cy.tsx @@ -0,0 +1,44 @@ +import Home from '../../src/pages/index/Home' +import {BrowserRouter} from "react-router-dom"; + +Cypress.on('uncaught:exception', (err: any, runnable: any) => { + return false +}) + +describe('Home', () => { + beforeEach(() => { + cy.mount().should("exist") + cy.viewport(1000, 600) + }) + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.get("body").should("contain.text", "home.yourCourses") + cy.get(':nth-child(1) > .ant-card > .ant-card-body').should("be.visible") + cy.get(':nth-child(2) > .ant-card > .ant-card-body').should("be.visible") + cy.get(':nth-child(3) > .ant-card > .ant-card-body').should("be.visible") + }) + + it('shows projects by default', () => { + // see: https://on.cypress.io/mounting-react + cy.get("body").should("contain.text", "home.yourProjects") + cy.get(".projectTable").should("be.visible") + cy.get(".timeline").should("not.exist") + cy.get(".calendar").should("not.exist") + }) + it('can show a timeline', () => { + // see: https://on.cypress.io/mounting-react + cy.get("body").should("contain.text", "home.yourProjects") + cy.get(':nth-child(2) > .ant-segmented-item-label').click() + cy.get(".projectTable").should("not.exist") + cy.get(".timeline").should("be.visible") + cy.get(".calendar").should("not.exist") + }) + it('can show a calendar', () => { + // see: https://on.cypress.io/mounting-react + cy.get("body").should("contain.text", "home.yourProjects") + cy.get(':nth-child(3) > .ant-segmented-item-label').click() + cy.get(".projectTable").should("not.exist") + cy.get(".timeline").should("not.exist") + cy.get(".calendar").should("be.visible") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/pages/Profile.cy.tsx b/frontend/cypress/pages/Profile.cy.tsx new file mode 100644 index 00000000..5413530b --- /dev/null +++ b/frontend/cypress/pages/Profile.cy.tsx @@ -0,0 +1,13 @@ +import Profile from '../../src/pages/profile/Profile' +import {User} from "../../src/providers/UserProvider"; + +Cypress.on('uncaught:exception', (err: any, runnable: any) => { + return false +}) + +describe('Profile', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/pages/Project.cy.tsx b/frontend/cypress/pages/Project.cy.tsx new file mode 100644 index 00000000..921cd8c2 --- /dev/null +++ b/frontend/cypress/pages/Project.cy.tsx @@ -0,0 +1,12 @@ +import Project from '../../src/pages/project/Project' + +Cypress.on('uncaught:exception', (err: any, runnable: any) => { + return false +}) + +describe('Project', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/pages/ProjectCreate.cy.tsx b/frontend/cypress/pages/ProjectCreate.cy.tsx new file mode 100644 index 00000000..7c5aa64d --- /dev/null +++ b/frontend/cypress/pages/ProjectCreate.cy.tsx @@ -0,0 +1,48 @@ +import ProjectCreate from '../../src/pages/projectCreate/ProjectCreate' +import {BrowserRouter} from "react-router-dom"; + +describe('ProjectCreate', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("body").should("contain.text", "project.change.title") + }) + + it('shows message for empty required fields', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("body").should("not.contain.text", "project.change.nameMessage") + cy.get("#name").click() + cy.get("body").click() + cy.get("body").should("contain.text", "project.change.nameMessage") + cy.get("#name").type("Test name") + cy.get("body").click() + cy.get("body").should("not.contain.text", "project.change.nameMessage") + }) + + it('shows the description in the preview', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("#description").type("Test description") + cy.get('[data-node-key="preview"]').click() + cy.get("body").should("contain.text", "Test description") + }) + + it('can switch between tabs', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("#name").should("be.visible") + cy.get(".ant-select-selector").should("not.be.visible") + cy.get("#structureTest").should("not.be.visible") + cy.get("#rc-tabs-6-tab-groups").click() + cy.get("#name").should("not.be.visible") + cy.get(".ant-select-selector").should("be.visible") + cy.get("#structureTest").should("not.be.visible") + // idk why maar als ik 1maal klik dan switcht hij niet + cy.get('#rc-tabs-6-tab-structure').click() + cy.get('#rc-tabs-6-tab-structure').click() + cy.get("#name").should("not.be.visible") + cy.get(".ant-select-selector").should("not.be.visible") + cy.get("#structureTest").should("be.visible") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/pages/Submission.cy.tsx b/frontend/cypress/pages/Submission.cy.tsx new file mode 100644 index 00000000..4cd30abc --- /dev/null +++ b/frontend/cypress/pages/Submission.cy.tsx @@ -0,0 +1,9 @@ +import Submission from '../../src/pages/submission/Submission' + +describe('Submission', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get(".ant-spin-dot").should("be.visible") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/pages/Submit.cy.tsx b/frontend/cypress/pages/Submit.cy.tsx new file mode 100644 index 00000000..7ea208a6 --- /dev/null +++ b/frontend/cypress/pages/Submit.cy.tsx @@ -0,0 +1,16 @@ +import Submit from '../../src/pages/submit/Submit' +import {BrowserRouter} from "react-router-dom"; + +describe('Submit', () => { + it('renders', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get("body").should("contain.text", "project.files") + }) + it('disables submit button by default', () => { + // see: https://on.cypress.io/mounting-react + cy.mount().should("exist") + cy.get('.ant-btn-default').should("be.visible").and("not.be.disabled") + cy.get('.ant-btn-primary').should("be.visible").and("be.disabled") + }) +}) \ No newline at end of file diff --git a/frontend/cypress/support/commands.ts b/frontend/cypress/support/commands.ts new file mode 100644 index 00000000..698b01a4 --- /dev/null +++ b/frontend/cypress/support/commands.ts @@ -0,0 +1,37 @@ +/// +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } \ No newline at end of file diff --git a/frontend/cypress/support/component-index.html b/frontend/cypress/support/component-index.html new file mode 100644 index 00000000..2cea6bdc --- /dev/null +++ b/frontend/cypress/support/component-index.html @@ -0,0 +1,12 @@ + + + + + + + Components App + + +
+ + \ No newline at end of file diff --git a/frontend/cypress/support/component.ts b/frontend/cypress/support/component.ts new file mode 100644 index 00000000..0fa083ec --- /dev/null +++ b/frontend/cypress/support/component.ts @@ -0,0 +1,39 @@ +// *********************************************************** +// This example support/component.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +import { mount } from 'cypress/react18' + +// Augment the Cypress namespace to include type definitions for +// your custom command. +// Alternatively, can be defined in cypress/support/component.d.ts +// with a at the top of your spec. +declare global { + namespace Cypress { + interface Chainable { + mount: typeof mount + } + } +} + +Cypress.Commands.add('mount', mount) + +// Example use: +// cy.mount() \ No newline at end of file diff --git a/frontend/cypress/support/e2e.ts b/frontend/cypress/support/e2e.ts new file mode 100644 index 00000000..f80f74f8 --- /dev/null +++ b/frontend/cypress/support/e2e.ts @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 72c262f1..d0505398 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,7 +7,7 @@ diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts new file mode 100644 index 00000000..918f4fef --- /dev/null +++ b/frontend/jest.config.ts @@ -0,0 +1,31 @@ +import type { Config } from '@jest/types'; + +const config: Config.InitialOptions = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + moduleNameMapper: { + '\\.(jpg|jpeg|png|gif|webp|svg)$': '/__mocks__/fileMock.js', + '\\.(css|less|scss|sass)$': 'identity-obj-proxy', + 'react-markdown': 'jest-transform-stub', + 'react-syntax-highlighter': 'jest-transform-stub', + '@azure/msal-react': 'jest-transform-stub', + '@fontsource/jetbrains-mono': 'jest-transform-stub', + }, + globals: { + 'ts-jest': { + tsconfig: 'tsconfig.json', + }, + }, + transform: { + // '^.+\\.(ts|tsx)$': 'ts-jest', + '^.+\\.(js|jsx)$': 'babel-jest', + }, + transformIgnorePatterns: [ + 'node_modules/(?!(jest-)?@azure/msal-react|react-markdown|deck.gl|ng-dynamic)', + ], + testMatch: [ + '**/?(*.)+(spec|test).ts?(x)', + ], +}; + +export default config; \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4f6b5ac8..bda8ea16 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@fontsource/jetbrains-mono": "^5.0.19", "@fontsource/roboto-mono": "^5.0.17", + "@mdx-js/react": "^3.0.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", @@ -23,10 +24,14 @@ "@vitejs/plugin-react": "^4.2.1", "antd": "^5.14.2", "axios": "^1.6.7", + "file-saver": "^2.0.5", "framer-motion": "^11.0.24", "highlight.js": "^11.9.0", + "http-proxy-middleware": "^3.0.0", "i18next-localstorage-cache": "^1.1.1", + "jszip": "^3.10.1", "lowlight": "^3.1.0", + "papaparse": "^5.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.0.5", @@ -34,29 +39,38 @@ "react-router-dom": "^6.22.1", "react-syntax-highlighter": "^15.5.0", "typescript": "^4.9.5", + "usehooks-ts": "^3.1.0", "vite": "^5.1.7", "vite-tsconfig-paths": "^4.3.2", "web-vitals": "^2.1.4" }, "devDependencies": { + "@babel/preset-env": "^7.24.5", "@testing-library/react": "^14.2.2", + "@types/file-saver": "^2.0.7", + "@types/papaparse": "^5.3.14", "@types/react-syntax-highlighter": "^15.5.11", + "cypress": "^13.9.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "ts-jest": "^29.1.2" + "jest-transform-stub": "^2.0.0", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2" } }, "node_modules/@adobe/css-tools": { "version": "4.3.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", + "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==" }, "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "license": "Apache-2.0", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -64,14 +78,16 @@ }, "node_modules/@ant-design/colors": { "version": "7.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.0.2.tgz", + "integrity": "sha512-7KJkhTiPiLHSu+LmMJnehfJ6242OCxSlR3xHVBecYxnMW8MS/878NXct1GqYARyL59fyeFdKRxXTfvR9SnDgJg==", "dependencies": { "@ctrl/tinycolor": "^3.6.1" } }, "node_modules/@ant-design/cssinjs": { "version": "1.18.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.18.4.tgz", + "integrity": "sha512-IrUAOj5TYuMG556C9gdbFuOrigyhzhU5ZYpWb3gYTxAwymVqRbvLzFCZg6OsjLBR6GhzcxYF3AhxKmjB+rA2xA==", "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", @@ -87,8 +103,9 @@ } }, "node_modules/@ant-design/icons": { - "version": "5.3.1", - "license": "MIT", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.3.6.tgz", + "integrity": "sha512-JeWsgNjvkTTC73YDPgWOgdScRku/iHN9JU0qk39OSEmJSCiRghQMLlxGTCY5ovbRRoXjxHXnUKgQEgBDnQfKmA==", "dependencies": { "@ant-design/colors": "^7.0.0", "@ant-design/icons-svg": "^4.4.0", @@ -106,11 +123,13 @@ }, "node_modules/@ant-design/icons-svg": { "version": "4.4.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==" }, "node_modules/@ant-design/react-slick": { "version": "1.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.0.2.tgz", + "integrity": "sha512-Wj8onxL/T8KQLFFiCA4t8eIRGpRR+UPgOdac2sYzonv+i0n3kXHmvHLLiOYL655DQx2Umii9Y9nNgL7ssu5haQ==", "dependencies": { "@babel/runtime": "^7.10.4", "classnames": "^2.2.5", @@ -124,7 +143,7 @@ }, "node_modules/@azure/msal-browser": { "version": "3.10.0", - "license": "MIT", + "integrity": "sha512-mnmi8dCXVNZI+AGRq0jKQ3YiodlIC4W9npr6FCB9WN6NQT+6rq+cIlxgUb//BjLyzKsnYo+i4LROGeMyU+6v1A==", "dependencies": { "@azure/msal-common": "14.7.1" }, @@ -134,14 +153,15 @@ }, "node_modules/@azure/msal-common": { "version": "14.7.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.7.1.tgz", + "integrity": "sha512-v96btzjM7KrAu4NSEdOkhQSTGOuNUIIsUdB8wlyB9cdgl5KqEKnTonHUZ8+khvZ6Ap542FCErbnTyDWl8lZ2rA==", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-react": { "version": "2.0.12", - "license": "MIT", + "integrity": "sha512-23HKdajBWQ5SSzcwwFKHAWaOCpq4UCthoOBgKpab3UoHx0OuFMQiq6CrNBzBKtBFdyxCjadBGzWshRgl0Nvk1g==", "engines": { "node": ">=10" }, @@ -152,7 +172,8 @@ }, "node_modules/@babel/code-frame": { "version": "7.23.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dependencies": { "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" @@ -162,15 +183,17 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.23.5", - "license": "MIT", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { "version": "7.23.9", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -198,18 +221,20 @@ }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", - "license": "ISC", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.23.6", - "license": "MIT", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -218,7 +243,8 @@ }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.22.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", "dependencies": { "@babel/types": "^7.22.5" }, @@ -226,9 +252,22 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.23.6", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dependencies": { "@babel/compat-data": "^7.23.5", "@babel/helper-validator-option": "^7.23.5", @@ -242,23 +281,25 @@ }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", - "license": "ISC", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.23.10", - "license": "MIT", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", + "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.24.5", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-replace-supers": "^7.24.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-split-export-declaration": "^7.24.5", "semver": "^6.3.1" }, "engines": { @@ -268,23 +309,51 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/helper-environment-visitor": { "version": "7.22.20", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { "version": "7.23.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { "@babel/template": "^7.22.15", "@babel/types": "^7.23.0" @@ -295,7 +364,8 @@ }, "node_modules/@babel/helper-hoist-variables": { "version": "7.22.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { "@babel/types": "^7.22.5" }, @@ -304,34 +374,37 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "license": "MIT", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", + "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", "dependencies": { - "@babel/types": "^7.23.0" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "license": "MIT", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", "dependencies": { - "@babel/types": "^7.22.15" + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "license": "MIT", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-simple-access": "^7.24.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -342,7 +415,8 @@ }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.22.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", "dependencies": { "@babel/types": "^7.22.5" }, @@ -351,18 +425,37 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.0", - "license": "MIT", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-replace-supers": { + "node_modules/@babel/helper-remap-async-to-generator": { "version": "7.22.20", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", + "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { @@ -373,10 +466,11 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "license": "MIT", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -384,7 +478,8 @@ }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.22.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", "dependencies": { "@babel/types": "^7.22.5" }, @@ -393,63 +488,85 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "license": "MIT", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "license": "MIT", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "license": "MIT", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { "version": "7.23.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.5.tgz", + "integrity": "sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.23.0", + "@babel/template": "^7.24.0", + "@babel/types": "^7.24.5" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.23.9", - "license": "MIT", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", "dependencies": { - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.23.4", - "license": "MIT", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.5", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "license": "MIT", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -457,9 +574,74 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.5.tgz", + "integrity": "sha512-LdXRi1wEMTrHVR4Zc9F8OewC3vdm5h4QB6L71zy6StmYeqGi1b3ttIO8UC+BfZKcH9jdr4aI249rBkm+3+YvHw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz", + "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", + "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz", + "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.11", - "license": "MIT", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-create-class-features-plugin": "^7.21.0", @@ -509,6 +691,75 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", + "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", + "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", @@ -622,7 +873,8 @@ }, "node_modules/@babel/plugin-syntax-private-property-in-object": { "version": "7.14.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -663,22 +915,27 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.1", - "license": "MIT", + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-react-jsx-source": { + "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.24.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", + "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.0" }, @@ -689,435 +946,1110 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.23.9", - "license": "MIT", + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.3.tgz", + "integrity": "sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg==", + "dev": true, "dependencies": { - "regenerator-runtime": "^0.14.0" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/template": { - "version": "7.23.9", - "license": "MIT", + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz", + "integrity": "sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==", + "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/helper-module-imports": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/traverse": { - "version": "7.23.9", - "license": "MIT", + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", + "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", + "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/types": { - "version": "7.23.9", - "license": "MIT", + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.5.tgz", + "integrity": "sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==", + "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-plugin-utils": "^7.24.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", + "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/@ctrl/tinycolor": { - "version": "3.6.1", - "license": "MIT", + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", + "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.4", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, "engines": { - "node": ">=10" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" } }, - "node_modules/@emotion/hash": { - "version": "0.8.0", - "license": "MIT" + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.5.tgz", + "integrity": "sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-split-export-declaration": "^7.24.5", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", + "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/template": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.5.tgz", + "integrity": "sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", + "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", + "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", + "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", + "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", + "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", + "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", + "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", + "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", + "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", + "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", + "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", + "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", + "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", + "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", + "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", + "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", + "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", + "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.5.tgz", + "integrity": "sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", + "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", + "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.5.tgz", + "integrity": "sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.5.tgz", + "integrity": "sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", + "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.5.tgz", + "integrity": "sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.5", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", + "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", + "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", + "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", + "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", + "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/@emotion/unitless": { - "version": "0.7.5", - "license": "MIT" + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", + "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - - "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", + "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", + "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", + "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.5.tgz", + "integrity": "sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", + "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", + "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", + "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", + "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/preset-env": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.5.tgz", + "integrity": "sha512-UGK2ifKtcC8i5AI4cH+sbLLuLc2ktYSFJgBAXorKAsHUZmrQ1q6aQ6i3BvU24wWs2AAKqQB6kq3N9V9Gw1HiMQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.4", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.1", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.1", + "@babel/plugin-syntax-import-attributes": "^7.24.1", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.1", + "@babel/plugin-transform-async-generator-functions": "^7.24.3", + "@babel/plugin-transform-async-to-generator": "^7.24.1", + "@babel/plugin-transform-block-scoped-functions": "^7.24.1", + "@babel/plugin-transform-block-scoping": "^7.24.5", + "@babel/plugin-transform-class-properties": "^7.24.1", + "@babel/plugin-transform-class-static-block": "^7.24.4", + "@babel/plugin-transform-classes": "^7.24.5", + "@babel/plugin-transform-computed-properties": "^7.24.1", + "@babel/plugin-transform-destructuring": "^7.24.5", + "@babel/plugin-transform-dotall-regex": "^7.24.1", + "@babel/plugin-transform-duplicate-keys": "^7.24.1", + "@babel/plugin-transform-dynamic-import": "^7.24.1", + "@babel/plugin-transform-exponentiation-operator": "^7.24.1", + "@babel/plugin-transform-export-namespace-from": "^7.24.1", + "@babel/plugin-transform-for-of": "^7.24.1", + "@babel/plugin-transform-function-name": "^7.24.1", + "@babel/plugin-transform-json-strings": "^7.24.1", + "@babel/plugin-transform-literals": "^7.24.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.1", + "@babel/plugin-transform-member-expression-literals": "^7.24.1", + "@babel/plugin-transform-modules-amd": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.24.1", + "@babel/plugin-transform-modules-systemjs": "^7.24.1", + "@babel/plugin-transform-modules-umd": "^7.24.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.24.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1", + "@babel/plugin-transform-numeric-separator": "^7.24.1", + "@babel/plugin-transform-object-rest-spread": "^7.24.5", + "@babel/plugin-transform-object-super": "^7.24.1", + "@babel/plugin-transform-optional-catch-binding": "^7.24.1", + "@babel/plugin-transform-optional-chaining": "^7.24.5", + "@babel/plugin-transform-parameters": "^7.24.5", + "@babel/plugin-transform-private-methods": "^7.24.1", + "@babel/plugin-transform-private-property-in-object": "^7.24.5", + "@babel/plugin-transform-property-literals": "^7.24.1", + "@babel/plugin-transform-regenerator": "^7.24.1", + "@babel/plugin-transform-reserved-words": "^7.24.1", + "@babel/plugin-transform-shorthand-properties": "^7.24.1", + "@babel/plugin-transform-spread": "^7.24.1", + "@babel/plugin-transform-sticky-regex": "^7.24.1", + "@babel/plugin-transform-template-literals": "^7.24.1", + "@babel/plugin-transform-typeof-symbol": "^7.24.5", + "@babel/plugin-transform-unicode-escapes": "^7.24.1", + "@babel/plugin-transform-unicode-property-regex": "^7.24.1", + "@babel/plugin-transform-unicode-regex": "^7.24.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/traverse": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", + "dependencies": { + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/traverse/node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/types": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", + "to-fast-properties": "^2.0.0" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=0.1.90" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, "engines": { "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", "engines": { - "node": ">=12" + "node": ">=10" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], + "node_modules/@cypress/request": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "http-signature": "~1.3.6", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.10.4", + "safe-buffer": "^5.1.2", + "tough-cookie": "^4.1.3", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, "engines": { - "node": ">=12" + "node": ">= 6" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], + "node_modules/@cypress/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, "engines": { - "node": ">=12" + "node": ">= 0.12" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" } }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "node_modules/@fontsource/jetbrains-mono": { "version": "5.0.19", - "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.0.19.tgz", "integrity": "sha512-SdwUuvdfuAvGWRRc4LOFRSmDrpkE+vFUpCtOIOUl1PpXdLfeU//93BZiGf7j/oFGSZJbHAurfux2uLT38/NIjw==" }, "node_modules/@fontsource/roboto-mono": { "version": "5.0.17", - "resolved": "https://registry.npmjs.org/@fontsource/roboto-mono/-/roboto-mono-5.0.17.tgz", "integrity": "sha512-MU6FrAyG7DWMCL8mu0JDPvB2tnFcn/lYvVKixzqHb2uefRsLaD6OBFfF1q5RMFsKcFHyPySHM7ZcGw/Q6A1/FA==" }, "node_modules/@istanbuljs/load-nyc-config": { @@ -1364,9 +2296,9 @@ } }, "node_modules/@jest/core/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, "node_modules/@jest/core/node_modules/supports-color": { @@ -1815,12 +2747,13 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "license": "MIT", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -1828,43 +2761,54 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "license": "MIT", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "license": "MIT", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mdx-js/react": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.1.tgz", + "integrity": "sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, "node_modules/@rc-component/color-picker": { "version": "1.5.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-1.5.2.tgz", + "integrity": "sha512-YJXujYzYFAEtlXJXy0yJUhwzUWPTcniBZto+wZ/vnACmFnUTNR7dH+NOeqSwMMsssh74e9H5Jfpr5LAH2PYqUw==", "dependencies": { "@babel/runtime": "^7.23.6", "@ctrl/tinycolor": "^3.6.1", @@ -1878,7 +2822,8 @@ }, "node_modules/@rc-component/context": { "version": "1.4.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", "dependencies": { "@babel/runtime": "^7.10.1", "rc-util": "^5.27.0" @@ -1890,7 +2835,8 @@ }, "node_modules/@rc-component/mini-decimal": { "version": "1.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", "dependencies": { "@babel/runtime": "^7.18.0" }, @@ -1900,254 +2846,89 @@ }, "node_modules/@rc-component/mutate-observer": { "version": "1.1.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.0", - "classnames": "^2.3.2", - "rc-util": "^5.24.4" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/portal": { - "version": "1.1.2", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.0", - "classnames": "^2.3.2", - "rc-util": "^5.24.4" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/tour": { - "version": "1.12.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.0", - "@rc-component/portal": "^1.0.0-9", - "@rc-component/trigger": "^1.3.6", - "classnames": "^2.3.2", - "rc-util": "^5.24.4" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/trigger": { - "version": "1.18.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", "dependencies": { - "@babel/runtime": "^7.23.2", - "@rc-component/portal": "^1.1.0", - "classnames": "^2.3.2", - "rc-motion": "^2.0.0", - "rc-resize-observer": "^1.3.1", - "rc-util": "^5.38.0" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@remix-run/router": { - "version": "1.15.1", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.2.tgz", - "integrity": "sha512-ahxSgCkAEk+P/AVO0vYr7DxOD3CwAQrT0Go9BJyGQ9Ef0QxVOfjDZMiF4Y2s3mLyPrjonchIMH/tbWHucJMykQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.2.tgz", - "integrity": "sha512-lAarIdxZWbFSHFSDao9+I/F5jDaKyCqAPMq5HqnfpBw8dKDiCaaqM0lq5h1pQTLeIqueeay4PieGR5jGZMWprw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.2.tgz", - "integrity": "sha512-SWsr8zEUk82KSqquIMgZEg2GE5mCSfr9sE/thDROkX6pb3QQWPp8Vw8zOq2GyxZ2t0XoSIUlvHDkrf5Gmf7x3Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.2.tgz", - "integrity": "sha512-o/HAIrQq0jIxJAhgtIvV5FWviYK4WB0WwV91SLUnsliw1lSAoLsmgEEgRWzDguAFeUEUUoIWXiJrPqU7vGiVkA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.2.tgz", - "integrity": "sha512-nwlJ65UY9eGq91cBi6VyDfArUJSKOYt5dJQBq8xyLhvS23qO+4Nr/RreibFHjP6t+5ap2ohZrUJcHv5zk5ju/g==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.2.tgz", - "integrity": "sha512-Pg5TxxO2IVlMj79+c/9G0LREC9SY3HM+pfAwX7zj5/cAuwrbfj2Wv9JbMHIdPCfQpYsI4g9mE+2Bw/3aeSs2rQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.2.tgz", - "integrity": "sha512-cAOTjGNm84gc6tS02D1EXtG7tDRsVSDTBVXOLbj31DkwfZwgTPYZ6aafSU7rD/4R2a34JOwlF9fQayuTSkoclA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.2.tgz", - "integrity": "sha512-4RyT6v1kXb7C0fn6zV33rvaX05P0zHoNzaXI/5oFHklfKm602j+N4mn2YvoezQViRLPnxP8M1NaY4s/5kXO5cw==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.2.tgz", - "integrity": "sha512-KNUH6jC/vRGAKSorySTyc/yRYlCwN/5pnMjXylfBniwtJx5O7X17KG/0efj8XM3TZU7raYRXJFFReOzNmL1n1w==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.2.tgz", - "integrity": "sha512-xPV4y73IBEXToNPa3h5lbgXOi/v0NcvKxU0xejiFw6DtIYQqOTMhZ2DN18/HrrP0PmiL3rGtRG9gz1QE8vFKXQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ] + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.2.tgz", - "integrity": "sha512-QBhtr07iFGmF9egrPOWyO5wciwgtzKkYPNLVCFZTmr4TWmY0oY2Dm/bmhHjKRwZoGiaKdNcKhFtUMBKvlchH+Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.2.tgz", - "integrity": "sha512-8zfsQRQGH23O6qazZSFY5jP5gt4cFvRuKTpuBsC1ZnSWxV8ZKQpPqOZIUtdfMOugCcBvFGRa1pDC/tkf19EgBw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] + "node_modules/@rc-component/tour": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.12.3.tgz", + "integrity": "sha512-U4mf1FiUxGCwrX4ed8op77Y8VKur+8Y/61ylxtqGbcSoh1EBC7bWd/DkLu0ClTUrKZInqEi1FL7YgFtnT90vHA==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^1.3.6", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.2.tgz", - "integrity": "sha512-H4s8UjgkPnlChl6JF5empNvFHp77Jx+Wfy2EtmYPe9G22XV+PMuCinZVHurNe8ggtwoaohxARJZbaH/3xjB/FA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ] + "node_modules/@rc-component/trigger": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-1.18.3.tgz", + "integrity": "sha512-Ksr25pXreYe1gX6ayZ1jLrOrl9OAUHUqnuhEx6MeHnNa1zVM5Y2Aj3Q35UrER0ns8D2cJYtmJtVli+i+4eKrvA==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.38.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.2.tgz", - "integrity": "sha512-djqpAjm/i8erWYF0K6UY4kRO3X5+T4TypIqw60Q8MTqSBaQNpNXDhxdjpZ3ikgb+wn99svA7jxcXpiyg9MUsdw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ] + "node_modules/@remix-run/router": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.1.tgz", + "integrity": "sha512-zcU0gM3z+3iqj8UX45AmWY810l3oUmXM7uH4dt5xtzvMhRtYVhKGOmgOd1877dOPPepfCjUv57w+syamWIYe7w==", + "engines": { + "node": ">=14.0.0" + } }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.2.tgz", - "integrity": "sha512-teAqzLT0yTYZa8ZP7zhFKEx4cotS8Tkk5XiqNMJhD4CpaWB1BHARE4Qy+RzwnXvSAYv+Q3jAqCVBS+PS+Yee8Q==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", "cpu": [ "x64" ], @@ -2182,7 +2963,8 @@ }, "node_modules/@testing-library/dom": { "version": "9.3.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2199,7 +2981,8 @@ }, "node_modules/@testing-library/dom/node_modules/ansi-styles": { "version": "4.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { "color-convert": "^2.0.1" }, @@ -2212,14 +2995,16 @@ }, "node_modules/@testing-library/dom/node_modules/aria-query": { "version": "5.1.3", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", "dependencies": { "deep-equal": "^2.0.5" } }, "node_modules/@testing-library/dom/node_modules/chalk": { "version": "4.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2233,7 +3018,8 @@ }, "node_modules/@testing-library/dom/node_modules/color-convert": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { "color-name": "~1.1.4" }, @@ -2243,18 +3029,21 @@ }, "node_modules/@testing-library/dom/node_modules/color-name": { "version": "1.1.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/@testing-library/dom/node_modules/has-flag": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { "node": ">=8" } }, "node_modules/@testing-library/dom/node_modules/supports-color": { "version": "7.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { "has-flag": "^4.0.0" }, @@ -2264,7 +3053,7 @@ }, "node_modules/@testing-library/jest-dom": { "version": "5.17.0", - "license": "MIT", + "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", "dependencies": { "@adobe/css-tools": "^4.0.1", "@babel/runtime": "^7.9.2", @@ -2284,7 +3073,8 @@ }, "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { "version": "4.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { "color-convert": "^2.0.1" }, @@ -2297,7 +3087,8 @@ }, "node_modules/@testing-library/jest-dom/node_modules/chalk": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2308,7 +3099,8 @@ }, "node_modules/@testing-library/jest-dom/node_modules/color-convert": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { "color-name": "~1.1.4" }, @@ -2318,18 +3110,21 @@ }, "node_modules/@testing-library/jest-dom/node_modules/color-name": { "version": "1.1.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/@testing-library/jest-dom/node_modules/has-flag": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { "node": ">=8" } }, "node_modules/@testing-library/jest-dom/node_modules/supports-color": { "version": "7.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { "has-flag": "^4.0.0" }, @@ -2338,9 +3133,8 @@ } }, "node_modules/@testing-library/react": { - "version": "14.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", - "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", + "version": "14.2.2", + "integrity": "sha512-SOUuM2ysCvjUWBXTNfQ/ztmnKDmqaiPV3SvoIuyxMUca45rbSWWAT/qB8CUs/JQ/ux/8JFs9DNdFQ3f6jH3crA==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -2357,7 +3151,7 @@ }, "node_modules/@testing-library/user-event": { "version": "13.5.0", - "license": "MIT", + "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", "dependencies": { "@babel/runtime": "^7.12.5" }, @@ -2378,13 +3172,39 @@ "node": ">= 10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/aria-query": { "version": "5.0.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==" }, "node_modules/@types/babel__core": { "version": "7.20.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -2395,14 +3215,16 @@ }, "node_modules/@types/babel__generator": { "version": "7.6.8", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { "version": "7.4.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -2410,29 +3232,39 @@ }, "node_modules/@types/babel__traverse": { "version": "7.20.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", "dependencies": { "@babel/types": "^7.20.7" } }, "node_modules/@types/debug": { "version": "4.1.12", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "dependencies": { "@types/ms": "*" } }, "node_modules/@types/estree": { "version": "1.0.5", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", "dependencies": { "@types/estree": "*" } }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2444,11 +3276,20 @@ }, "node_modules/@types/hast": { "version": "3.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dependencies": { "@types/unist": "*" } }, + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2475,7 +3316,8 @@ }, "node_modules/@types/jest": { "version": "27.5.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", + "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", "dependencies": { "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" @@ -2494,53 +3336,80 @@ }, "node_modules/@types/mdast": { "version": "4.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz", + "integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==", "dependencies": { "@types/unist": "*" } }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==" + }, "node_modules/@types/ms": { "version": "0.7.34", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { - "version": "20.11.30", - "license": "MIT", + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", "dependencies": { "undici-types": "~5.26.4" } }, + "node_modules/@types/papaparse": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", + "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prop-types": { - "version": "15.7.11", - "license": "MIT" + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/react": { - "version": "18.2.56", - "license": "MIT", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", + "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.2.19", - "license": "MIT", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "dependencies": { "@types/react": "*" } }, "node_modules/@types/react-syntax-highlighter": { "version": "15.5.11", + "integrity": "sha512-ZqIJl+Pg8kD+47kxUjvrlElrraSUrYa4h0dauY/U/FTUuprSCqvUj+9PNQNQzVc6AJgIWUUxn87/gqsMHNbRjw==", "dev": true, - "license": "MIT", "dependencies": { "@types/react": "*" } }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "license": "MIT" + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true }, "node_modules/@types/stack-utils": { "version": "2.0.3", @@ -2550,7 +3419,8 @@ }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.9", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", + "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==", "dependencies": { "@types/jest": "*" } @@ -2563,7 +3433,8 @@ }, "node_modules/@types/unist": { "version": "3.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, "node_modules/@types/yargs": { "version": "17.0.32", @@ -2580,13 +3451,24 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@vitejs/plugin-react": { "version": "4.2.1", - "license": "MIT", + "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", "dependencies": { "@babel/core": "^7.23.5", "@babel/plugin-transform-react-jsx-self": "^7.23.3", @@ -2601,13 +3483,6 @@ "vite": "^4.2.0 || ^5.0.0" } }, - "node_modules/@vitejs/plugin-react/node_modules/react-refresh": { - "version": "0.14.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -2617,8 +3492,9 @@ }, "node_modules/acorn": { "version": "8.11.3", - "devOptional": true, - "license": "MIT", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2657,6 +3533,28 @@ "node": ">= 6.0.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2674,14 +3572,16 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "3.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dependencies": { "color-convert": "^1.9.0" }, @@ -2691,7 +3591,7 @@ }, "node_modules/antd": { "version": "5.14.2", - "license": "MIT", + "integrity": "sha512-ur0oBI9U7hAeON4ZRs1cAF1suIpTR+uj3YliTZacWkiVxNTZYPaaTdnLuAZDRMT9P2IZ007dCQTqxn5t1Z+Dxw==", "dependencies": { "@ant-design/colors": "^7.0.2", "@ant-design/cssinjs": "^1.18.4", @@ -2763,6 +3663,32 @@ "node": ">= 8" } }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2774,14 +3700,16 @@ }, "node_modules/aria-query": { "version": "5.3.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dependencies": { "dequal": "^2.0.3" } }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dependencies": { "call-bind": "^1.0.5", "is-array-buffer": "^3.0.4" @@ -2795,19 +3723,68 @@ }, "node_modules/array-tree-filter": { "version": "2.1.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz", + "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true }, "node_modules/async-validator": { "version": "4.2.5", - "license": "MIT" + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" }, "node_modules/asynckit": { "version": "0.4.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } }, "node_modules/available-typed-arrays": { - "version": "1.0.6", - "license": "MIT", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2815,9 +3792,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", + "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==", + "dev": true + }, "node_modules/axios": { "version": "1.6.7", - "license": "MIT", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "dependencies": { "follow-redirects": "^1.15.4", "form-data": "^4.0.0", @@ -2947,15 +3939,6 @@ "node": ">=8" } }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/babel-plugin-jest-hoist": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", @@ -2971,6 +3954,45 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", @@ -3012,7 +4034,8 @@ }, "node_modules/bail": { "version": "2.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3024,6 +4047,47 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3038,7 +4102,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -3048,6 +4111,8 @@ }, "node_modules/browserslist": { "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "funding": [ { "type": "opencollective", @@ -3062,7 +4127,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "caniuse-lite": "^1.0.30001587", "electron-to-chromium": "^1.4.668", @@ -3097,14 +4161,58 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "engines": { + "node": ">=6" + } }, "node_modules/call-bind": { "version": "1.0.7", - "license": "MIT", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3138,7 +4246,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001588", + "version": "1.0.30001614", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001614.tgz", + "integrity": "sha512-jmZQ1VpmlRwHgdP1/uiKzgiAuGOfLEJsYFP4+GBou/QQ4U6IOJCB4NP1c+1p9RGLpwObcT94jA5/uO+F1vBbog==", "funding": [ { "type": "opencollective", @@ -3152,12 +4262,18 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true }, "node_modules/ccount": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3165,7 +4281,8 @@ }, "node_modules/chalk": { "version": "2.4.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -3186,7 +4303,8 @@ }, "node_modules/character-entities": { "version": "2.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3194,7 +4312,8 @@ }, "node_modules/character-entities-html4": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3202,7 +4321,8 @@ }, "node_modules/character-entities-legacy": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3210,12 +4330,22 @@ }, "node_modules/character-reference-invalid": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -3232,14 +4362,67 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", - "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", "dev": true }, "node_modules/classnames": { "version": "2.5.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/cliui": { "version": "8.0.1", @@ -3273,18 +4456,27 @@ }, "node_modules/color-convert": { "version": "1.9.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dependencies": { "color-name": "1.1.3" } }, "node_modules/color-name": { "version": "1.1.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true }, "node_modules/combined-stream": { "version": "1.0.8", - "license": "MIT", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3294,15 +4486,35 @@ }, "node_modules/comma-separated-tokens": { "version": "2.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/compute-scroll-into-view": { "version": "3.1.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" }, "node_modules/concat-map": { "version": "0.0.1", @@ -3312,15 +4524,35 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/copy-to-clipboard": { "version": "3.3.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", "dependencies": { "toggle-selection": "^1.0.6" } }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -3379,19 +4611,250 @@ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/create-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/create-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/create-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==" + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/cypress": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.9.0.tgz", + "integrity": "sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^3.0.0", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.1", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/cypress/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cypress/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cypress/node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/cypress/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/create-jest/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/create-jest/node_modules/has-flag": { + "node_modules/cypress/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", @@ -3400,64 +4863,60 @@ "node": ">=8" } }, - "node_modules/create-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/cypress/node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/cypress/node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true + }, + "node_modules/cypress/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "has-flag": "^4.0.0" }, "engines": { - "node": ">= 8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/css.escape": { - "version": "1.5.1", - "license": "MIT" - }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dev": true, "dependencies": { - "cssom": "~0.3.6" + "assert-plus": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10" } }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - }, - "node_modules/csstype": { - "version": "3.1.3", - "license": "MIT" - }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -3474,11 +4933,13 @@ }, "node_modules/dayjs": { "version": "1.11.10", - "license": "MIT" + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "node_modules/debug": { "version": "4.3.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { "ms": "2.1.2" }, @@ -3499,7 +4960,8 @@ }, "node_modules/decode-named-character-reference": { "version": "1.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", "dependencies": { "character-entities": "^2.0.0" }, @@ -3524,7 +4986,8 @@ }, "node_modules/deep-equal": { "version": "2.2.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.5", @@ -3563,7 +5026,8 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3578,7 +5042,8 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -3593,14 +5058,16 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "engines": { "node": ">=0.4.0" } }, "node_modules/dequal": { "version": "2.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "engines": { "node": ">=6" } @@ -3616,7 +5083,8 @@ }, "node_modules/devlop": { "version": "1.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "dependencies": { "dequal": "^2.0.0" }, @@ -3625,16 +5093,27 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "27.5.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, "node_modules/dom-accessibility-api": { "version": "0.5.16", - "license": "MIT" + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" }, "node_modules/domexception": { "version": "4.0.0", @@ -3649,9 +5128,20 @@ "node": ">=12" } }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/electron-to-chromium": { - "version": "1.4.673", - "license": "ISC" + "version": "1.4.751", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.751.tgz", + "integrity": "sha512-2DEPi++qa89SMGRhufWTiLmzqyuGmNF3SK4+PQetW1JKiZdEpF4XQonJXJCzyuYSA6mauiMhbyVhqYAP45Hvfw==" }, "node_modules/emittery": { "version": "0.13.1", @@ -3671,6 +5161,28 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -3694,7 +5206,8 @@ }, "node_modules/es-define-property": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -3704,14 +5217,16 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "engines": { "node": ">= 0.4" } }, "node_modules/es-get-iterator": { "version": "1.1.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -3728,9 +5243,10 @@ } }, "node_modules/esbuild": { - "version": "0.20.2", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -3738,41 +5254,58 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, "node_modules/escalade": { "version": "3.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "engines": { "node": ">=6" } }, "node_modules/escape-string-regexp": { "version": "1.0.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "engines": { "node": ">=0.8.0" } @@ -3822,7 +5355,8 @@ }, "node_modules/estree-util-is-identifier-name": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -3837,6 +5371,17 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3860,6 +5405,18 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -4018,9 +5575,9 @@ } }, "node_modules/expect/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, "node_modules/expect/node_modules/supports-color": { @@ -4037,25 +5594,59 @@ }, "node_modules/extend": { "version": "3.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } }, - "node_modules/fault": { - "version": "1.0.4", - "license": "MIT", + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, "dependencies": { - "format": "^0.2.0" + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -4065,11 +5656,39 @@ "bser": "2.1.1" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4092,13 +5711,14 @@ }, "node_modules/follow-redirects": { "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -4110,11 +5730,21 @@ }, "node_modules/for-each": { "version": "0.3.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "dependencies": { "is-callable": "^1.1.3" } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -4130,14 +5760,15 @@ }, "node_modules/format": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", "engines": { "node": ">=0.4.x" } }, "node_modules/framer-motion": { - "version": "11.0.28", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.28.tgz", - "integrity": "sha512-j/vNYTCH5MX5sY/3dwMs00z1+qAqKX3iIHF762bwqlU814ooD5dDbuj3pA0LmIT5YqyryCkXEb/q+zRblin0lw==", + "version": "11.0.24", + "integrity": "sha512-l2iM8NR53qtcujgAqYvGPJJGModPNWEVUaATRDLfnaLvUoFpImovBm0AHalSSsY8tW6knP8mfJTW4WYGbnAe4w==", "dependencies": { "tslib": "^2.4.0" }, @@ -4158,42 +5789,56 @@ } } }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/functions-have-names": { "version": "1.2.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "engines": { "node": ">=6.9.0" } @@ -4209,7 +5854,8 @@ }, "node_modules/get-intrinsic": { "version": "1.2.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -4245,6 +5891,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4265,20 +5929,38 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globals": { "version": "11.12.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "engines": { "node": ">=4" } }, "node_modules/globrex": { "version": "0.1.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" }, "node_modules/gopd": { "version": "1.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -4300,21 +5982,24 @@ }, "node_modules/has-bigints": { "version": "1.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-flag": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "engines": { "node": ">=4" } }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { "es-define-property": "^1.0.0" }, @@ -4323,8 +6008,9 @@ } }, "node_modules/has-proto": { - "version": "1.0.1", - "license": "MIT", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -4334,7 +6020,8 @@ }, "node_modules/has-symbols": { "version": "1.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "engines": { "node": ">= 0.4" }, @@ -4344,7 +6031,8 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dependencies": { "has-symbols": "^1.0.3" }, @@ -4356,8 +6044,9 @@ } }, "node_modules/hasown": { - "version": "2.0.1", - "license": "MIT", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -4367,7 +6056,8 @@ }, "node_modules/hast-util-parse-selector": { "version": "2.2.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -4375,7 +6065,8 @@ }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", + "integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", @@ -4400,7 +6091,8 @@ }, "node_modules/hast-util-whitespace": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "dependencies": { "@types/hast": "^3.0.0" }, @@ -4411,7 +6103,8 @@ }, "node_modules/hastscript": { "version": "6.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", @@ -4426,18 +6119,21 @@ }, "node_modules/hastscript/node_modules/@types/hast": { "version": "2.3.10", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", "dependencies": { "@types/unist": "^2" } }, "node_modules/hastscript/node_modules/@types/unist": { "version": "2.0.10", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" }, "node_modules/hastscript/node_modules/comma-separated-tokens": { "version": "1.0.8", - "license": "MIT", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -4445,7 +6141,8 @@ }, "node_modules/hastscript/node_modules/property-information": { "version": "5.6.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", "dependencies": { "xtend": "^4.0.0" }, @@ -4456,7 +6153,8 @@ }, "node_modules/hastscript/node_modules/space-separated-tokens": { "version": "1.1.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -4464,7 +6162,8 @@ }, "node_modules/highlight.js": { "version": "11.9.0", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", + "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", "engines": { "node": ">=12.0.0" } @@ -4489,31 +6188,87 @@ }, "node_modules/html-parse-stringify": { "version": "3.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", "dependencies": { "void-elements": "3.1.0" } }, "node_modules/html-url-attributes": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.0.tgz", + "integrity": "sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow==", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0.tgz", + "integrity": "sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==", + "dependencies": { + "@types/http-proxy": "^1.17.10", + "debug": "^4.3.4", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.5" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", "dev": true, "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" }, "engines": { - "node": ">= 6" + "node": ">=0.10" } }, "node_modules/https-proxy-agent": { @@ -4539,7 +6294,9 @@ } }, "node_modules/i18next": { - "version": "23.10.0", + "version": "23.11.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.3.tgz", + "integrity": "sha512-Pq/aSKowir7JM0rj+Wa23Kb6KKDUGno/HjG+wRQu0PxoTbpQ4N89MAT0rFGvXmLkRLNMb1BbBOKGozl01dabzg==", "funding": [ { "type": "individual", @@ -4554,7 +6311,6 @@ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" } ], - "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.23.2" @@ -4562,7 +6318,7 @@ }, "node_modules/i18next-localstorage-cache": { "version": "1.1.1", - "license": "MIT" + "integrity": "sha512-6kgVSg48bhmYCHppMCMExKBiiGOrb85jQBQMPnAwxVumn/fkXJYEfxV0rdfL0EYcZT+ciOqw+7VIMWkTVuk6Ng==" }, "node_modules/iconv-lite": { "version": "0.6.3", @@ -4578,7 +6334,6 @@ }, "node_modules/identity-obj-proxy": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", "dev": true, "dependencies": { @@ -4588,6 +6343,31 @@ "node": ">=4" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -4618,7 +6398,8 @@ }, "node_modules/indent-string": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "engines": { "node": ">=8" } @@ -4636,16 +6417,26 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } }, "node_modules/inline-style-parser": { - "version": "0.2.2", - "license": "MIT" + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz", + "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==" }, "node_modules/internal-slot": { "version": "1.0.7", - "license": "MIT", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.0", @@ -4657,7 +6448,8 @@ }, "node_modules/is-alphabetical": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -4665,7 +6457,8 @@ }, "node_modules/is-alphanumerical": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" @@ -4677,7 +6470,8 @@ }, "node_modules/is-arguments": { "version": "1.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -4691,7 +6485,8 @@ }, "node_modules/is-array-buffer": { "version": "3.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.1" @@ -4711,7 +6506,8 @@ }, "node_modules/is-bigint": { "version": "1.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "dependencies": { "has-bigints": "^1.0.1" }, @@ -4721,7 +6517,8 @@ }, "node_modules/is-boolean-object": { "version": "1.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -4735,7 +6532,8 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "engines": { "node": ">= 0.4" }, @@ -4743,6 +6541,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -4757,7 +6567,8 @@ }, "node_modules/is-date-object": { "version": "1.0.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -4770,12 +6581,21 @@ }, "node_modules/is-decimal": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -4794,17 +6614,49 @@ "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-hexadecimal": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-map": { - "version": "2.0.2", - "license": "MIT", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4813,14 +6665,14 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } }, "node_modules/is-number-object": { "version": "1.0.7", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -4831,6 +6683,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -4839,7 +6711,8 @@ }, "node_modules/is-regex": { "version": "1.1.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -4852,17 +6725,25 @@ } }, "node_modules/is-set": { - "version": "2.0.2", - "license": "MIT", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "license": "MIT", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4882,7 +6763,8 @@ }, "node_modules/is-string": { "version": "1.0.7", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -4895,7 +6777,8 @@ }, "node_modules/is-symbol": { "version": "1.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", "dependencies": { "has-symbols": "^1.0.2" }, @@ -4906,19 +6789,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { - "version": "2.0.1", - "license": "MIT", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-weakset": { - "version": "2.0.2", - "license": "MIT", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4926,7 +6835,8 @@ }, "node_modules/isarray": { "version": "2.0.5", - "license": "MIT" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "node_modules/isexe": { "version": "2.0.0", @@ -4934,6 +6844,12 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -4959,6 +6875,39 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-instrument/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -5023,7 +6972,6 @@ }, "node_modules/jest": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "dependencies": { @@ -5061,6 +7009,21 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-changed-files/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-circus": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", @@ -5166,174 +7129,86 @@ "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/pretty-format/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, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "node_modules/jest-circus/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } } }, - "node_modules/jest-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-circus/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/jest-circus/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "yocto-queue": "^0.1.0" }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-cli/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": ">=7.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-cli/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-cli/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/jest-circus/node_modules/pretty-format/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, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-cli/node_modules/supports-color": { + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-circus/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", @@ -5484,9 +7359,9 @@ } }, "node_modules/jest-config/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, "node_modules/jest-config/node_modules/supports-color": { @@ -5503,7 +7378,8 @@ }, "node_modules/jest-diff": { "version": "27.5.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^27.5.1", @@ -5516,7 +7392,8 @@ }, "node_modules/jest-diff/node_modules/ansi-styles": { "version": "4.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { "color-convert": "^2.0.1" }, @@ -5529,7 +7406,8 @@ }, "node_modules/jest-diff/node_modules/chalk": { "version": "4.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5543,7 +7421,8 @@ }, "node_modules/jest-diff/node_modules/color-convert": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { "color-name": "~1.1.4" }, @@ -5553,18 +7432,21 @@ }, "node_modules/jest-diff/node_modules/color-name": { "version": "1.1.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/jest-diff/node_modules/has-flag": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { "node": ">=8" } }, "node_modules/jest-diff/node_modules/supports-color": { "version": "7.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { "has-flag": "^4.0.0" }, @@ -5694,9 +7576,9 @@ } }, "node_modules/jest-each/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, "node_modules/jest-each/node_modules/supports-color": { @@ -5713,7 +7595,6 @@ }, "node_modules/jest-environment-jsdom": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", "dev": true, "dependencies": { @@ -5757,7 +7638,8 @@ }, "node_modules/jest-get-type": { "version": "27.5.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } @@ -5836,14 +7718,15 @@ } }, "node_modules/jest-leak-detector/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, "node_modules/jest-matcher-utils": { "version": "27.5.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", "dependencies": { "chalk": "^4.0.0", "jest-diff": "^27.5.1", @@ -5856,7 +7739,8 @@ }, "node_modules/jest-matcher-utils/node_modules/ansi-styles": { "version": "4.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { "color-convert": "^2.0.1" }, @@ -5869,7 +7753,8 @@ }, "node_modules/jest-matcher-utils/node_modules/chalk": { "version": "4.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5883,7 +7768,8 @@ }, "node_modules/jest-matcher-utils/node_modules/color-convert": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { "color-name": "~1.1.4" }, @@ -5893,18 +7779,21 @@ }, "node_modules/jest-matcher-utils/node_modules/color-name": { "version": "1.1.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/jest-matcher-utils/node_modules/has-flag": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { "node": ">=8" } }, "node_modules/jest-matcher-utils/node_modules/supports-color": { "version": "7.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { "has-flag": "^4.0.0" }, @@ -6017,9 +7906,9 @@ } }, "node_modules/jest-message-util/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, "node_modules/jest-message-util/node_modules/supports-color": { @@ -6267,6 +8156,21 @@ "node": ">=8" } }, + "node_modules/jest-runner/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-runner/node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -6529,6 +8433,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-snapshot/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-snapshot/node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -6556,11 +8472,26 @@ } }, "node_modules/jest-snapshot/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-snapshot/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6573,6 +8504,18 @@ "node": ">=8" } }, + "node_modules/jest-snapshot/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/jest-transform-stub": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz", + "integrity": "sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg==", + "dev": true + }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -6783,9 +8726,9 @@ } }, "node_modules/jest-validate/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, "node_modules/jest-validate/node_modules/supports-color": { @@ -6904,33 +8847,137 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest/node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { "has-flag": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=8" } }, "node_modules/js-tokens": { "version": "4.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "3.14.1", @@ -6945,6 +8992,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, "node_modules/jsdom": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", @@ -6992,7 +9045,8 @@ }, "node_modules/jsesc": { "version": "2.5.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "bin": { "jsesc": "bin/jsesc" }, @@ -7006,16 +9060,30 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "node_modules/json2mq": { "version": "0.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", "dependencies": { "string-convert": "^0.2.0" } }, "node_modules/json5": { "version": "2.2.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "bin": { "json5": "lib/cli.js" }, @@ -7023,6 +9091,53 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -7032,6 +9147,15 @@ "node": ">=6" } }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -7041,12 +9165,47 @@ "node": ">=6" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -7061,7 +9220,13 @@ }, "node_modules/lodash": { "version": "4.17.21", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -7069,9 +9234,184 @@ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-update/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/longest-streak": { "version": "3.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -7079,7 +9419,8 @@ }, "node_modules/loose-envify": { "version": "1.4.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -7089,7 +9430,7 @@ }, "node_modules/lowlight": { "version": "3.1.0", - "license": "MIT", + "integrity": "sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ==", "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", @@ -7102,14 +9443,16 @@ }, "node_modules/lru-cache": { "version": "5.1.1", - "license": "ISC", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dependencies": { "yallist": "^3.0.2" } }, "node_modules/lz-string": { "version": "1.5.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "bin": { "lz-string": "bin/bin.js" } @@ -7129,6 +9472,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -7146,7 +9522,8 @@ }, "node_modules/mdast-util-from-markdown": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", + "integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", @@ -7168,7 +9545,8 @@ }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", + "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==", "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", @@ -7184,7 +9562,8 @@ }, "node_modules/mdast-util-mdx-jsx": { "version": "3.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz", + "integrity": "sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA==", "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", @@ -7207,7 +9586,8 @@ }, "node_modules/mdast-util-mdxjs-esm": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", @@ -7223,7 +9603,8 @@ }, "node_modules/mdast-util-phrasing": { "version": "4.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" @@ -7235,7 +9616,8 @@ }, "node_modules/mdast-util-to-hast": { "version": "13.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz", + "integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==", "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", @@ -7254,7 +9636,8 @@ }, "node_modules/mdast-util-to-markdown": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", @@ -7272,7 +9655,8 @@ }, "node_modules/mdast-util-to-string": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "dependencies": { "@types/mdast": "^4.0.0" }, @@ -7289,6 +9673,8 @@ }, "node_modules/micromark": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", "funding": [ { "type": "GitHub Sponsors", @@ -7299,7 +9685,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", @@ -7321,7 +9706,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "2.0.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", + "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", "funding": [ { "type": "GitHub Sponsors", @@ -7332,7 +9719,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", @@ -7354,6 +9740,8 @@ }, "node_modules/micromark-factory-destination": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", "funding": [ { "type": "GitHub Sponsors", @@ -7364,7 +9752,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", @@ -7373,6 +9760,8 @@ }, "node_modules/micromark-factory-label": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", "funding": [ { "type": "GitHub Sponsors", @@ -7383,7 +9772,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", @@ -7393,6 +9781,8 @@ }, "node_modules/micromark-factory-space": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", "funding": [ { "type": "GitHub Sponsors", @@ -7403,7 +9793,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -7411,6 +9800,8 @@ }, "node_modules/micromark-factory-title": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", "funding": [ { "type": "GitHub Sponsors", @@ -7421,7 +9812,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", @@ -7431,6 +9821,8 @@ }, "node_modules/micromark-factory-whitespace": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", "funding": [ { "type": "GitHub Sponsors", @@ -7441,7 +9833,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", @@ -7451,6 +9842,8 @@ }, "node_modules/micromark-util-character": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", "funding": [ { "type": "GitHub Sponsors", @@ -7461,7 +9854,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -7469,6 +9861,8 @@ }, "node_modules/micromark-util-chunked": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", "funding": [ { "type": "GitHub Sponsors", @@ -7479,13 +9873,14 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-classify-character": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", "funding": [ { "type": "GitHub Sponsors", @@ -7496,7 +9891,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", @@ -7505,6 +9899,8 @@ }, "node_modules/micromark-util-combine-extensions": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", "funding": [ { "type": "GitHub Sponsors", @@ -7515,7 +9911,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -7523,6 +9918,8 @@ }, "node_modules/micromark-util-decode-numeric-character-reference": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", "funding": [ { "type": "GitHub Sponsors", @@ -7533,13 +9930,14 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-decode-string": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", "funding": [ { "type": "GitHub Sponsors", @@ -7550,7 +9948,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", @@ -7560,6 +9957,8 @@ }, "node_modules/micromark-util-encode": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", "funding": [ { "type": "GitHub Sponsors", @@ -7569,11 +9968,12 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromark-util-html-tag-name": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", "funding": [ { "type": "GitHub Sponsors", @@ -7583,11 +9983,12 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromark-util-normalize-identifier": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", "funding": [ { "type": "GitHub Sponsors", @@ -7598,13 +9999,14 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-resolve-all": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", "funding": [ { "type": "GitHub Sponsors", @@ -7615,13 +10017,14 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-sanitize-uri": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", "funding": [ { "type": "GitHub Sponsors", @@ -7632,7 +10035,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", @@ -7640,7 +10042,9 @@ } }, "node_modules/micromark-util-subtokenize": { - "version": "2.0.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", + "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", "funding": [ { "type": "GitHub Sponsors", @@ -7651,7 +10055,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", @@ -7661,6 +10064,8 @@ }, "node_modules/micromark-util-symbol": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", "funding": [ { "type": "GitHub Sponsors", @@ -7670,11 +10075,12 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromark-util-types": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", "funding": [ { "type": "GitHub Sponsors", @@ -7684,14 +10090,12 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -7702,14 +10106,16 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { "version": "2.1.35", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dependencies": { "mime-db": "1.52.0" }, @@ -7728,7 +10134,8 @@ }, "node_modules/min-indent": { "version": "1.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "engines": { "node": ">=4" } @@ -7745,19 +10152,30 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -7779,7 +10197,8 @@ }, "node_modules/node-releases": { "version": "2.0.14", - "license": "MIT" + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -7803,24 +10222,26 @@ } }, "node_modules/nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.9.tgz", + "integrity": "sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==", "dev": true }, "node_modules/object-inspect": { "version": "1.13.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object-is": { - "version": "1.1.5", - "license": "MIT", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -7831,14 +10252,16 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "engines": { "node": ">= 0.4" } }, "node_modules/object.assign": { "version": "4.1.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dependencies": { "call-bind": "^1.0.5", "define-properties": "^1.2.1", @@ -7876,16 +10299,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true + }, "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "dependencies": { - "yocto-queue": "^0.1.0" + "p-try": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7903,16 +10332,16 @@ "node": ">=8" } }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, "dependencies": { - "p-try": "^2.0.0" + "aggregate-error": "^3.0.0" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7927,9 +10356,20 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, "node_modules/parse-entities": { "version": "4.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", "dependencies": { "@types/unist": "^2.0.0", "character-entities": "^2.0.0", @@ -7947,7 +10387,8 @@ }, "node_modules/parse-entities/node_modules/@types/unist": { "version": "2.0.10", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" }, "node_modules/parse-json": { "version": "5.2.0", @@ -8012,15 +10453,27 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, "node_modules/picocolors": { "version": "1.0.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -8028,6 +10481,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -8049,8 +10511,18 @@ "node": ">=8" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { - "version": "8.4.38", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "funding": [ { "type": "opencollective", @@ -8065,19 +10537,31 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "source-map-js": "^1.0.2" }, "engines": { "node": "^10 || ^12 || >=14" } }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pretty-format": { "version": "27.5.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -8089,7 +10573,8 @@ }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "engines": { "node": ">=10" }, @@ -8099,11 +10584,26 @@ }, "node_modules/prismjs": { "version": "1.29.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", "engines": { "node": ">=6" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -8118,8 +10618,9 @@ } }, "node_modules/property-information": { - "version": "6.4.1", - "license": "MIT", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8127,7 +10628,8 @@ }, "node_modules/proxy-from-env": { "version": "1.1.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/psl": { "version": "1.9.0", @@ -8135,6 +10637,16 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8162,11 +10674,27 @@ }, "node_modules/qrcode.react": { "version": "3.1.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.1.0.tgz", + "integrity": "sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/qs": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -8175,7 +10703,8 @@ }, "node_modules/rc-cascader": { "version": "3.21.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.21.2.tgz", + "integrity": "sha512-J7GozpgsLaOtzfIHFJFuh4oFY0ePb1w10twqK6is3pAkqHkca/PsokbDr822KIRZ8/CK8CqevxohuPDVZ1RO/A==", "dependencies": { "@babel/runtime": "^7.12.5", "array-tree-filter": "^2.1.0", @@ -8191,7 +10720,8 @@ }, "node_modules/rc-checkbox": { "version": "3.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.1.0.tgz", + "integrity": "sha512-PAwpJFnBa3Ei+5pyqMMXdcKYKNBMS+TvSDiLdDnARnMJHC8ESxwPfm4Ao1gJiKtWLdmGfigascnCpwrHFgoOBQ==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", @@ -8204,7 +10734,8 @@ }, "node_modules/rc-collapse": { "version": "3.7.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.7.2.tgz", + "integrity": "sha512-ZRw6ipDyOnfLFySxAiCMdbHtb5ePAsB9mT17PA6y1mRD/W6KHRaZeb5qK/X9xDV1CqgyxMpzw0VdS74PCcUk4A==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -8218,7 +10749,8 @@ }, "node_modules/rc-dialog": { "version": "9.3.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.3.4.tgz", + "integrity": "sha512-975X3018GhR+EjZFbxA2Z57SX5rnu0G0/OxFgMMvZK4/hQWEm3MHaNvP4wXpxYDoJsp+xUvVW+GB9CMMCm81jA==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", @@ -8233,7 +10765,8 @@ }, "node_modules/rc-drawer": { "version": "7.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.0.0.tgz", + "integrity": "sha512-ePcS4KtQnn57bCbVXazHN2iC8nTPCXlWEIA/Pft87Pd9U7ZeDkdRzG47jWG2/TAFXFlFltRAMcslqmUM8NPCGA==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.1.1", @@ -8248,7 +10781,8 @@ }, "node_modules/rc-dropdown": { "version": "4.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.1.0.tgz", + "integrity": "sha512-VZjMunpBdlVzYpEdJSaV7WM7O0jf8uyDjirxXLZRNZ+tAC+NzD3PXPEtliFwGzVwBBdCmGuSqiS9DWcOLxQ9tw==", "dependencies": { "@babel/runtime": "^7.18.3", "@rc-component/trigger": "^1.7.0", @@ -8262,7 +10796,8 @@ }, "node_modules/rc-field-form": { "version": "1.41.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.41.0.tgz", + "integrity": "sha512-k9AS0wmxfJfusWDP/YXWTpteDNaQ4isJx9UKxx4/e8Dub4spFeZ54/EuN2sYrMRID/+hUznPgVZeg+Gf7XSYCw==", "dependencies": { "@babel/runtime": "^7.18.0", "async-validator": "^4.1.0", @@ -8278,7 +10813,8 @@ }, "node_modules/rc-image": { "version": "7.5.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.5.1.tgz", + "integrity": "sha512-Z9loECh92SQp0nSipc0MBuf5+yVC05H/pzC+Nf8xw1BKDFUJzUeehYBjaWlxly8VGBZJcTHYri61Fz9ng1G3Ag==", "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/portal": "^1.0.2", @@ -8294,7 +10830,8 @@ }, "node_modules/rc-input": { "version": "1.4.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.4.3.tgz", + "integrity": "sha512-aHyQUAIRmTlOnvk5EcNqEpJ+XMtfMpYRAJayIlJfsvvH9cAKUWboh4egm23vgMA7E+c/qm4BZcnrDcA960GC1w==", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -8307,7 +10844,8 @@ }, "node_modules/rc-input-number": { "version": "9.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.0.0.tgz", + "integrity": "sha512-RfcDBDdWFFetouWFXBA+WPEC8LzBXyngr9b+yTLVIygfFu7HiLRGn/s/v9wwno94X7KFvnb28FNynMGj9XJlDQ==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/mini-decimal": "^1.0.1", @@ -8322,7 +10860,8 @@ }, "node_modules/rc-mentions": { "version": "2.10.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.10.1.tgz", + "integrity": "sha512-72qsEcr/7su+a07ndJ1j8rI9n0Ka/ngWOLYnWMMv0p2mi/5zPwPrEDTt6Uqpe8FWjWhueDJx/vzunL6IdKDYMg==", "dependencies": { "@babel/runtime": "^7.22.5", "@rc-component/trigger": "^1.5.0", @@ -8339,7 +10878,8 @@ }, "node_modules/rc-menu": { "version": "9.12.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.12.4.tgz", + "integrity": "sha512-t2NcvPLV1mFJzw4F21ojOoRVofK2rWhpKPx69q2raUsiHPDP6DDevsBILEYdsIegqBeSXoWs2bf6CueBKg3BFg==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^1.17.0", @@ -8355,7 +10895,8 @@ }, "node_modules/rc-motion": { "version": "2.9.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.0.tgz", + "integrity": "sha512-XIU2+xLkdIr1/h6ohPZXyPBMvOmuyFZQ/T0xnawz+Rh+gh4FINcnZmMT5UTIj6hgI0VLDjTaPeRd+smJeSPqiQ==", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -8368,7 +10909,8 @@ }, "node_modules/rc-notification": { "version": "5.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.3.0.tgz", + "integrity": "sha512-WCf0uCOkZ3HGfF0p1H4Sgt7aWfipxORWTPp7o6prA3vxwtWhtug3GfpYls1pnBp4WA+j8vGIi5c2/hQRpGzPcQ==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -8385,7 +10927,8 @@ }, "node_modules/rc-overflow": { "version": "1.3.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.3.2.tgz", + "integrity": "sha512-nsUm78jkYAoPygDAcGZeC2VwIg/IBGSodtOY3pMof4W3M9qRJgqaDYm03ZayHlde3I6ipliAxbN0RUcGf5KOzw==", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -8399,7 +10942,8 @@ }, "node_modules/rc-pagination": { "version": "4.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-4.0.4.tgz", + "integrity": "sha512-GGrLT4NgG6wgJpT/hHIpL9nELv27A1XbSZzECIuQBQTVSf4xGKxWr6I/jhpRPauYEWEbWVw22ObG6tJQqwJqWQ==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", @@ -8412,7 +10956,8 @@ }, "node_modules/rc-picker": { "version": "4.1.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.1.5.tgz", + "integrity": "sha512-dh2E9j7HomZW10RWIBsdyXs3geHkSslVqKTx3VZfmi9UEabiQrBBNKVIhqn2m0goia0dqyWJ4qRghAsBVLGzbw==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^1.5.0", @@ -8449,7 +10994,8 @@ }, "node_modules/rc-progress": { "version": "3.5.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-3.5.1.tgz", + "integrity": "sha512-V6Amx6SbLRwPin/oD+k1vbPrO8+9Qf8zW1T8A7o83HdNafEVvAxPV5YsgtKFP+Ud5HghLj33zKOcEHrcrUGkfw==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.6", @@ -8462,7 +11008,8 @@ }, "node_modules/rc-rate": { "version": "2.12.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.12.0.tgz", + "integrity": "sha512-g092v5iZCdVzbjdn28FzvWebK2IutoVoiTeqoLTj9WM7SjA/gOJIw5/JFZMRyJYYVe1jLAU2UhAfstIpCNRozg==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", @@ -8478,7 +11025,8 @@ }, "node_modules/rc-resize-observer": { "version": "1.4.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.0.tgz", + "integrity": "sha512-PnMVyRid9JLxFavTjeDXEXo65HCRqbmLBw9xX9gfC4BZiSzbLXKzW3jPz+J0P71pLbD5tBMTT+mkstV5gD0c9Q==", "dependencies": { "@babel/runtime": "^7.20.7", "classnames": "^2.2.1", @@ -8492,7 +11040,8 @@ }, "node_modules/rc-segmented": { "version": "2.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.3.0.tgz", + "integrity": "sha512-I3FtM5Smua/ESXutFfb8gJ8ZPcvFR+qUgeeGFQHBOvRiRKyAk4aBE5nfqrxXx+h8/vn60DQjOt6i4RNtrbOobg==", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -8506,7 +11055,8 @@ }, "node_modules/rc-select": { "version": "14.11.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.11.0.tgz", + "integrity": "sha512-8J8G/7duaGjFiTXCBLWfh5P+KDWyA3KTlZDfV3xj/asMPqB2cmxfM+lH50wRiPIRsCQ6EbkCFBccPuaje3DHIg==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^1.5.0", @@ -8526,7 +11076,8 @@ }, "node_modules/rc-slider": { "version": "10.5.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.5.0.tgz", + "integrity": "sha512-xiYght50cvoODZYI43v3Ylsqiw14+D7ELsgzR40boDZaya1HFa1Etnv9MDkQE8X/UrXAffwv2AcNAhslgYuDTw==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", @@ -8542,7 +11093,8 @@ }, "node_modules/rc-steps": { "version": "6.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", "dependencies": { "@babel/runtime": "^7.16.7", "classnames": "^2.2.3", @@ -8558,7 +11110,8 @@ }, "node_modules/rc-switch": { "version": "4.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", "dependencies": { "@babel/runtime": "^7.21.0", "classnames": "^2.2.1", @@ -8571,7 +11124,8 @@ }, "node_modules/rc-table": { "version": "7.39.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.39.0.tgz", + "integrity": "sha512-7fHLMNsm/2DlGwyIMkdH2xIeRzb5I69bLsFaEVtX+gqmGhByy0wtOAgHkiOew3PtXozSJyh+iXifjLgQzWdczw==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/context": "^1.4.0", @@ -8590,7 +11144,8 @@ }, "node_modules/rc-tabs": { "version": "14.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-14.0.0.tgz", + "integrity": "sha512-lp1YWkaPnjlyhOZCPrAWxK6/P6nMGX/BAZcAC3nuVwKz0Byfp+vNnQKK8BRCP2g/fzu+SeB5dm9aUigRu3tRkQ==", "dependencies": { "@babel/runtime": "^7.11.2", "classnames": "2.x", @@ -8610,7 +11165,8 @@ }, "node_modules/rc-textarea": { "version": "1.6.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.6.3.tgz", + "integrity": "sha512-8k7+8Y2GJ/cQLiClFMg8kUXOOdvcFQrnGeSchOvI2ZMIVvX5a3zQpLxoODL0HTrvU63fPkRmMuqaEcOF9dQemA==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.1", @@ -8625,7 +11181,8 @@ }, "node_modules/rc-tooltip": { "version": "6.1.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.1.3.tgz", + "integrity": "sha512-HMSbSs5oieZ7XddtINUddBLSVgsnlaSb3bZrzzGWjXa7/B7nNedmsuz72s7EWFEro9mNa7RyF3gOXKYqvJiTcQ==", "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/trigger": "^1.18.0", @@ -8638,7 +11195,8 @@ }, "node_modules/rc-tree": { "version": "5.8.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.8.5.tgz", + "integrity": "sha512-PRfcZtVDNkR7oh26RuNe1hpw11c1wfgzwmPFL0lnxGnYefe9lDAO6cg5wJKIAwyXFVt5zHgpjYmaz0CPy1ZtKg==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -8656,7 +11214,8 @@ }, "node_modules/rc-tree-select": { "version": "5.17.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.17.0.tgz", + "integrity": "sha512-7sRGafswBhf7n6IuHyCEFCildwQIgyKiV8zfYyUoWfZEFdhuk7lCH+DN0aHt+oJrdiY9+6Io/LDXloGe01O8XQ==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -8671,7 +11230,8 @@ }, "node_modules/rc-upload": { "version": "4.5.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.5.2.tgz", + "integrity": "sha512-QO3ne77DwnAPKFn0bA5qJM81QBjQi0e0NHdkvpFyY73Bea2NfITiotqJqVjHgeYPOJu5lLVR32TNGP084aSoXA==", "dependencies": { "@babel/runtime": "^7.18.3", "classnames": "^2.2.5", @@ -8683,8 +11243,9 @@ } }, "node_modules/rc-util": { - "version": "5.38.2", - "license": "MIT", + "version": "5.39.1", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.39.1.tgz", + "integrity": "sha512-OW/ERynNDgNr4y0oiFmtes3rbEamXw7GHGbkbNd9iRr7kgT03T6fT0b9WpJ3mbxKhyOcAHnGcIoh5u/cjrC2OQ==", "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" @@ -8695,12 +11256,14 @@ } }, "node_modules/rc-util/node_modules/react-is": { - "version": "18.2.0", - "license": "MIT" + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/rc-virtual-list": { - "version": "3.11.4", - "license": "MIT", + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.11.5.tgz", + "integrity": "sha512-iZRW99m5jAxtwKNPLwUrPryurcnKpXBdTyhuBp6ythf7kg/otKO5cCiIvL55GQwU0QGSlouQS0tnkciRMJUwRQ==", "dependencies": { "@babel/runtime": "^7.20.0", "classnames": "^2.2.6", @@ -8717,7 +11280,7 @@ }, "node_modules/react": { "version": "18.2.0", - "license": "MIT", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -8727,7 +11290,7 @@ }, "node_modules/react-dom": { "version": "18.2.0", - "license": "MIT", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -8738,7 +11301,7 @@ }, "node_modules/react-i18next": { "version": "14.0.5", - "license": "MIT", + "integrity": "sha512-5+bQSeEtgJrMBABBL5lO7jPdSNAbeAZ+MlFWDw//7FnVacuVu3l9EeWFzBQvZsKy+cihkbThWOAThEdH8YjGEw==", "dependencies": { "@babel/runtime": "^7.23.9", "html-parse-stringify": "^3.0.1" @@ -8758,11 +11321,12 @@ }, "node_modules/react-is": { "version": "17.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, "node_modules/react-markdown": { "version": "9.0.1", - "license": "MIT", + "integrity": "sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==", "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", @@ -8784,9 +11348,18 @@ "react": ">=18" } }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-router": { "version": "6.22.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.1.tgz", + "integrity": "sha512-0pdoRGwLtemnJqn1K0XHUbnKiX0S4X8CgvVVmHGOWmofESj31msHo/1YiqcJWK7Wxfq2a4uvvtS01KAQyWK/CQ==", "dependencies": { "@remix-run/router": "1.15.1" }, @@ -8799,7 +11372,7 @@ }, "node_modules/react-router-dom": { "version": "6.22.1", - "license": "MIT", + "integrity": "sha512-iwMyyyrbL7zkKY7MRjOVRy+TMnS/OPusaFVxM2P11x9dzSzGmLsebkCvYirGq0DWB9K9hOspHYYtDz33gE5Duw==", "dependencies": { "@remix-run/router": "1.15.1", "react-router": "6.22.1" @@ -8814,7 +11387,7 @@ }, "node_modules/react-syntax-highlighter": { "version": "15.5.0", - "license": "MIT", + "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", @@ -8828,14 +11401,16 @@ }, "node_modules/react-syntax-highlighter/node_modules/highlight.js": { "version": "10.7.3", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", "engines": { "node": "*" } }, "node_modules/react-syntax-highlighter/node_modules/lowlight": { "version": "1.20.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" @@ -8845,9 +11420,41 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/react-syntax-highlighter/node_modules/lowlight/node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/redent": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" @@ -8858,7 +11465,8 @@ }, "node_modules/refractor": { "version": "3.6.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", "dependencies": { "hastscript": "^6.0.0", "parse-entities": "^2.0.0", @@ -8871,7 +11479,8 @@ }, "node_modules/refractor/node_modules/character-entities": { "version": "1.2.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8879,7 +11488,8 @@ }, "node_modules/refractor/node_modules/character-entities-legacy": { "version": "1.1.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8887,7 +11497,8 @@ }, "node_modules/refractor/node_modules/character-reference-invalid": { "version": "1.1.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8895,7 +11506,8 @@ }, "node_modules/refractor/node_modules/is-alphabetical": { "version": "1.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8903,7 +11515,8 @@ }, "node_modules/refractor/node_modules/is-alphanumerical": { "version": "1.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" @@ -8915,7 +11528,8 @@ }, "node_modules/refractor/node_modules/is-decimal": { "version": "1.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8923,7 +11537,8 @@ }, "node_modules/refractor/node_modules/is-hexadecimal": { "version": "1.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8931,7 +11546,8 @@ }, "node_modules/refractor/node_modules/parse-entities": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", @@ -8947,18 +11563,48 @@ }, "node_modules/refractor/node_modules/prismjs": { "version": "1.27.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", "engines": { "node": ">=6" } }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dependencies": { "call-bind": "^1.0.6", "define-properties": "^1.2.1", @@ -8972,9 +11618,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, "node_modules/remark-parse": { "version": "11.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", @@ -8988,7 +11673,8 @@ }, "node_modules/remark-rehype": { "version": "11.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", + "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", @@ -9001,6 +11687,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9013,12 +11708,12 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" }, "node_modules/resolve": { "version": "1.22.8", @@ -9067,10 +11762,29 @@ "node": ">=10" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true + }, "node_modules/rollup": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.2.tgz", - "integrity": "sha512-WkeoTWvuBoFjFAhsEOHKRoZ3r9GfTyhh7Vff1zwebEFLEFjT1lG3784xEgKiTa7E+e70vsC81roVL2MP4tgEEQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", + "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", "dependencies": { "@types/estree": "1.0.5" }, @@ -9082,24 +11796,36 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.14.2", - "@rollup/rollup-android-arm64": "4.14.2", - "@rollup/rollup-darwin-arm64": "4.14.2", - "@rollup/rollup-darwin-x64": "4.14.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.14.2", - "@rollup/rollup-linux-arm64-gnu": "4.14.2", - "@rollup/rollup-linux-arm64-musl": "4.14.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.14.2", - "@rollup/rollup-linux-riscv64-gnu": "4.14.2", - "@rollup/rollup-linux-s390x-gnu": "4.14.2", - "@rollup/rollup-linux-x64-gnu": "4.14.2", - "@rollup/rollup-linux-x64-musl": "4.14.2", - "@rollup/rollup-win32-arm64-msvc": "4.14.2", - "@rollup/rollup-win32-ia32-msvc": "4.14.2", - "@rollup/rollup-win32-x64-msvc": "4.14.2", + "@rollup/rollup-android-arm-eabi": "4.13.0", + "@rollup/rollup-android-arm64": "4.13.0", + "@rollup/rollup-darwin-arm64": "4.13.0", + "@rollup/rollup-darwin-x64": "4.13.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", + "@rollup/rollup-linux-arm64-gnu": "4.13.0", + "@rollup/rollup-linux-arm64-musl": "4.13.0", + "@rollup/rollup-linux-riscv64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-musl": "4.13.0", + "@rollup/rollup-win32-arm64-msvc": "4.13.0", + "@rollup/rollup-win32-ia32-msvc": "4.13.0", + "@rollup/rollup-win32-x64-msvc": "4.13.0", "fsevents": "~2.3.2" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -9120,78 +11846,63 @@ }, "node_modules/scheduler": { "version": "0.23.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "dependencies": { "loose-envify": "^1.1.0" } }, "node_modules/scroll-into-view-if-needed": { "version": "3.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" } }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/set-function-length": { - "version": "1.2.1", - "license": "MIT", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.2", + "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/set-function-name": { - "version": "2.0.1", - "license": "MIT", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9214,10 +11925,11 @@ } }, "node_modules/side-channel": { - "version": "1.0.5", - "license": "MIT", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" @@ -9250,35 +11962,74 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-js": { "version": "1.2.0", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/space-separated-tokens": { "version": "2.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -9290,6 +12041,31 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -9313,7 +12089,8 @@ }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", "dependencies": { "internal-slot": "^1.0.4" }, @@ -9321,9 +12098,18 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string-convert": { "version": "0.2.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==" }, "node_modules/string-length": { "version": "4.0.2", @@ -9353,8 +12139,9 @@ } }, "node_modules/stringify-entities": { - "version": "4.0.3", - "license": "MIT", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" @@ -9396,7 +12183,8 @@ }, "node_modules/strip-indent": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dependencies": { "min-indent": "^1.0.0" }, @@ -9417,19 +12205,22 @@ } }, "node_modules/style-to-object": { - "version": "1.0.5", - "license": "MIT", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.6.tgz", + "integrity": "sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==", "dependencies": { - "inline-style-parser": "0.2.2" + "inline-style-parser": "0.2.3" } }, "node_modules/stylis": { - "version": "4.3.1", - "license": "MIT" + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" }, "node_modules/supports-color": { "version": "5.5.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dependencies": { "has-flag": "^3.0.0" }, @@ -9455,30 +12246,6 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, - "node_modules/terser": { - "version": "5.27.1", - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -9495,11 +12262,36 @@ }, "node_modules/throttle-debounce": { "version": "5.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", + "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==", "engines": { "node": ">=12.22" } }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -9508,7 +12300,8 @@ }, "node_modules/to-fast-properties": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "engines": { "node": ">=4" } @@ -9517,7 +12310,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -9527,12 +12319,13 @@ }, "node_modules/toggle-selection": { "version": "1.0.6", - "license": "MIT" + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" }, "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dev": true, "dependencies": { "psl": "^1.1.33", @@ -9558,7 +12351,8 @@ }, "node_modules/trim-lines": { "version": "3.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -9566,7 +12360,8 @@ }, "node_modules/trough": { "version": "2.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -9574,7 +12369,6 @@ }, "node_modules/ts-jest": { "version": "29.1.2", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", "dev": true, "dependencies": { @@ -9615,11 +12409,124 @@ } } }, + "node_modules/ts-jest/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfck": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.3.tgz", + "integrity": "sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -9643,7 +12550,7 @@ }, "node_modules/typescript": { "version": "4.9.5", - "license": "Apache-2.0", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9654,11 +12561,53 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "license": "MIT" + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } }, "node_modules/unified": { "version": "11.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -9673,19 +12622,10 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unified/node_modules/is-plain-obj": { - "version": "4.1.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/unist-util-is": { "version": "6.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", "dependencies": { "@types/unist": "^3.0.0" }, @@ -9696,7 +12636,8 @@ }, "node_modules/unist-util-position": { "version": "5.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "dependencies": { "@types/unist": "^3.0.0" }, @@ -9707,7 +12648,8 @@ }, "node_modules/unist-util-remove-position": { "version": "5.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" @@ -9719,7 +12661,8 @@ }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "dependencies": { "@types/unist": "^3.0.0" }, @@ -9730,7 +12673,8 @@ }, "node_modules/unist-util-visit": { "version": "5.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", @@ -9743,7 +12687,8 @@ }, "node_modules/unist-util-visit-parents": { "version": "6.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" @@ -9762,8 +12707,19 @@ "node": ">= 4.0.0" } }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "funding": [ { "type": "opencollective", @@ -9778,7 +12734,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" @@ -9800,6 +12755,40 @@ "requires-port": "^1.0.0" } }, + "node_modules/usehooks-ts": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.0.tgz", + "integrity": "sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==", + "dependencies": { + "lodash.debounce": "^4.0.8" + }, + "engines": { + "node": ">=16.15.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -9814,9 +12803,30 @@ "node": ">=10.12.0" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, "node_modules/vfile": { "version": "6.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0", @@ -9829,7 +12839,8 @@ }, "node_modules/vfile-message": { "version": "4.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" @@ -9840,12 +12851,12 @@ } }, "node_modules/vite": { - "version": "5.2.6", - "license": "MIT", + "version": "5.1.7", + "integrity": "sha512-sgnEEFTZYMui/sTlH1/XEnVNHMujOahPLGMxn1+5sIT45Xjng1Ec1K78jRP15dSmVgg5WBin9yO81j3o9OxofA==", "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.36", - "rollup": "^4.13.0" + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" }, "bin": { "vite": "bin/vite.js" @@ -9894,7 +12905,7 @@ }, "node_modules/vite-tsconfig-paths": { "version": "4.3.2", - "license": "MIT", + "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", @@ -9909,71 +12920,10 @@ } } }, - "node_modules/vite-tsconfig-paths/node_modules/tsconfck": { - "version": "3.0.3", - "license": "MIT", - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^18 || >=20" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vite-tsconfig-paths/node_modules/typescript": { - "version": "5.4.3", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - - "node_modules/vite/node_modules/rollup": { - "version": "4.13.0", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.0", - "@rollup/rollup-android-arm64": "4.13.0", - "@rollup/rollup-darwin-arm64": "4.13.0", - "@rollup/rollup-darwin-x64": "4.13.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", - "@rollup/rollup-linux-arm64-gnu": "4.13.0", - "@rollup/rollup-linux-arm64-musl": "4.13.0", - "@rollup/rollup-linux-riscv64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-musl": "4.13.0", - "@rollup/rollup-win32-arm64-msvc": "4.13.0", - "@rollup/rollup-win32-ia32-msvc": "4.13.0", - "@rollup/rollup-win32-x64-msvc": "4.13.0", - "fsevents": "~2.3.2" - } - }, "node_modules/void-elements": { "version": "3.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", "engines": { "node": ">=0.10.0" } @@ -10001,7 +12951,7 @@ }, "node_modules/web-vitals": { "version": "2.1.4", - "license": "Apache-2.0" + "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==" }, "node_modules/webidl-conversions": { "version": "7.0.0", @@ -10063,7 +13013,8 @@ }, "node_modules/which-boxed-primitive": { "version": "1.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -10076,27 +13027,32 @@ } }, "node_modules/which-collection": { - "version": "1.0.1", - "license": "MIT", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-typed-array": { - "version": "1.1.14", - "license": "MIT", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dependencies": { - "available-typed-arrays": "^1.0.6", - "call-bind": "^1.0.5", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.1" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -10175,9 +13131,9 @@ } }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", "dev": true, "engines": { "node": ">=10.0.0" @@ -10212,7 +13168,8 @@ }, "node_modules/xtend": { "version": "4.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "engines": { "node": ">=0.4" } @@ -10228,7 +13185,8 @@ }, "node_modules/yallist": { "version": "3.1.1", - "license": "ISC" + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yargs": { "version": "17.7.2", @@ -10257,6 +13215,25 @@ "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -10271,7 +13248,8 @@ }, "node_modules/zwitch": { "version": "2.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" diff --git a/frontend/package.json b/frontend/package.json index c90435ff..698fb3c4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@fontsource/jetbrains-mono": "^5.0.19", "@fontsource/roboto-mono": "^5.0.17", + "@mdx-js/react": "^3.0.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", @@ -18,10 +19,14 @@ "@vitejs/plugin-react": "^4.2.1", "antd": "^5.14.2", "axios": "^1.6.7", + "file-saver": "^2.0.5", "framer-motion": "^11.0.24", "highlight.js": "^11.9.0", + "http-proxy-middleware": "^3.0.0", "i18next-localstorage-cache": "^1.1.1", + "jszip": "^3.10.1", "lowlight": "^3.1.0", + "papaparse": "^5.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.0.5", @@ -29,6 +34,7 @@ "react-router-dom": "^6.22.1", "react-syntax-highlighter": "^15.5.0", "typescript": "^4.9.5", + "usehooks-ts": "^3.1.0", "vite": "^5.1.7", "vite-tsconfig-paths": "^4.3.2", "web-vitals": "^2.1.4" @@ -37,7 +43,8 @@ "start": "vite", "build": "tsc && vite build", "preview": "vite preview", - "test": "jest" + "test": "jest", + "docs": "docusaurus" }, "jest": { "preset": "ts-jest", @@ -72,11 +79,17 @@ ] }, "devDependencies": { + "@babel/preset-env": "^7.24.5", "@testing-library/react": "^14.2.2", + "@types/file-saver": "^2.0.7", + "@types/papaparse": "^5.3.14", "@types/react-syntax-highlighter": "^15.5.11", + "cypress": "^13.9.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "ts-jest": "^29.1.2" + "jest-transform-stub": "^2.0.0", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2" } } diff --git a/frontend/public/docker_langauges/bash.svg b/frontend/public/docker_langauges/bash.svg new file mode 100644 index 00000000..890b5d92 --- /dev/null +++ b/frontend/public/docker_langauges/bash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/docker_langauges/custom.svg b/frontend/public/docker_langauges/custom.svg new file mode 100644 index 00000000..c3416414 --- /dev/null +++ b/frontend/public/docker_langauges/custom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/docker_langauges/haskell.svg b/frontend/public/docker_langauges/haskell.svg new file mode 100644 index 00000000..0c627ebd --- /dev/null +++ b/frontend/public/docker_langauges/haskell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/docker_langauges/node-js.svg b/frontend/public/docker_langauges/node-js.svg new file mode 100644 index 00000000..3e77c253 --- /dev/null +++ b/frontend/public/docker_langauges/node-js.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/docker_langauges/python.svg b/frontend/public/docker_langauges/python.svg new file mode 100644 index 00000000..bf2a1601 --- /dev/null +++ b/frontend/public/docker_langauges/python.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index a11777cc..7b8b3186 100644 Binary files a/frontend/public/favicon.ico and b/frontend/public/favicon.ico differ diff --git a/frontend/public/favicon1.ico b/frontend/public/favicon1.ico new file mode 100644 index 00000000..44694fb5 Binary files /dev/null and b/frontend/public/favicon1.ico differ diff --git a/frontend/public/logo192.png b/frontend/public/logo192.png index fc44b0a3..3034ab15 100644 Binary files a/frontend/public/logo192.png and b/frontend/public/logo192.png differ diff --git a/frontend/public/logo512.png b/frontend/public/logo512.png index a4e47a65..5682ceb6 100644 Binary files a/frontend/public/logo512.png and b/frontend/public/logo512.png differ diff --git a/frontend/src/@types/appTypes.ts b/frontend/src/@types/appTypes.ts index fd803f10..91617758 100644 --- a/frontend/src/@types/appTypes.ts +++ b/frontend/src/@types/appTypes.ts @@ -1,11 +1,15 @@ - +export enum LoginStatus { + LOGIN_IN_PROGRESS = "login_busy", + LOGOUT_IN_PROGRESS = "logout_busy", + LOGGED_IN = "login_done", + LOGGED_OUT = "logout_done" +} export enum Themes { LIGHT = "light", DARK = "dark", - DODONA = "dodona" } export enum Language { diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 21b5e0bc..f0718c14 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -1,44 +1,64 @@ import type {ProjectFormData} from "../pages/projectCreate/components/ProjectCreateService"; +import {Account} from "../providers/AuthProvider"; /** * Routes used to make API calls */ export enum ApiRoutes { - USER_COURSES = "api/courses", - COURSES = "api/courses", - - COURSE = "api/courses/:courseId", - COURSE_MEMBERS = "api/courses/:courseId/members", - COURSE_PROJECTS = "api/courses/:id/projects", - COURSE_CLUSTERS = "api/courses/:id/clusters", - COURSE_GRADES = '/api/courses/:id/grades', - - PROJECTS = "api/projects", - PROJECT = "api/projects/:id", - PROJECT_CREATE = "api/courses/:courseId/projects", - PROJECT_TESTS = "api/projects/:id/tests", - PROJECT_SUBMISSIONS = "api/projects/:id/submissions", - PROJECT_SCORE = "api/projects/:id/groups/:groupId/score", - PROJECT_GROUP = "api/projects/:id/groups/:groupId", - PROJECT_GROUPS = "api/projects/:id/groups", - PROJECT_GROUP_SUBMISSIONS = "api/projects/:projectId/submissions/:groupId", - - SUBMISSION = "api/submissions/:id", - SUBMISSION_FILE = "api/submissions/:id/file", - SUBMISSION_STRUCTURE_FEEDBACK= "/api/submissions/:id/structurefeedback", - SUBMISSION_DOCKER_FEEDBACK= "/api/submissions/:id/dockerfeedback", - - CLUSTER = "api/clusters/:id", - - GROUP = "api/groups/:id", - GROUP_MEMBERS = "api/groups/:id/members", - GROUP_MEMBER = "api/groups/:id/members/:userId", - GROUP_SUBMISSIONS = "api/projects/:id/groups/:id/submissions", - - TEST = "api/test", - USER = "api/users/:id", - USERS = "api/users", - USER_AUTH = "api/auth", + + AUTH_INFO = "/web/users/isAuthenticated", + + USER_COURSES = "/web/api/courses", + COURSES = "/web/api/courses", + + + COURSE = "/web/api/courses/:courseId", + COURSE_MEMBERS = "/web/api/courses/:courseId/members", + COURSE_MEMBER = "/web/api/courses/:courseId/members/:userId", + COURSE_PROJECTS = "/web/api/courses/:id/projects", + COURSE_CLUSTERS = "/web/api/courses/:id/clusters", + COURSE_GRADES = "/web/api/courses/:id/grades", + COURSE_LEAVE = "/web/api/courses/:courseId/leave", + COURSE_COPY = "/web/api/courses/:courseId/copy", + COURSE_JOIN = "/web/api/courses/:courseId/join/:courseKey", + COURSE_JOIN_WITHOUT_KEY = "/web/api/courses/:courseId/join", + COURSE_JOIN_LINK = "/web/api/courses/:courseId/joinKey", + + PROJECTS = "/web/api/projects", + PROJECT = "/web/api/projects/:id", + PROJECT_CREATE = "/web/api/courses/:courseId/projects", + PROJECT_TESTS = "/web/api/projects/:id/tests", + PROJECT_SUBMISSIONS = "/web/api/projects/:id/submissions", + PROJECT_SCORE = "/web/api/projects/:id/groups/:groupId/score", + PROJECT_GROUP = "/web/api/projects/:id/groups/:groupId", + PROJECT_GROUPS = "/web/api/projects/:id/groups", + PROJECT_GROUP_SUBMISSIONS = "/web/api/projects/:projectId/submissions/:groupId", + PROJECT_TEST_SUBMISSIONS = "/web/api/projects/:projectId/adminsubmissions", + PROJECT_TESTS_UPLOAD = "/web/api/projects/:id/tests/extrafiles", + PROJECT_SUBMIT = "/web/api/projects/:id/submit", + PROJECT_DOWNLOAD_ALL_SUBMISSIONS = "/web/api/projects/:id/submissions/files", + + + SUBMISSION = "/web/api/submissions/:id", + SUBMISSION_FILE = "/web/api/submissions/:id/file", + SUBMISSION_STRUCTURE_FEEDBACK= "/web/api/submissions/:id/structurefeedback", + SUBMISSION_DOCKER_FEEDBACK= "/web/api/submissions/:id/dockerfeedback", + SUBMISSION_ARTIFACT="/web/api/submissions/:id/artifacts", + + + + CLUSTER = "/web/api/clusters/:id", + CLUSTER_FILL = "/web/api/clusters/:id/fill", + CLUSTER_GROUPS = "/web/api/clusters/:id/groups", + + GROUP = "/web/api/groups/:id", + GROUP_MEMBERS = "/web/api/groups/:id/members", + GROUP_MEMBER = "/web/api/groups/:id/members/:userId", + GROUP_SUBMISSIONS = "/web/api/projects/:id/groups/:id/submissions", + + USER = "/web/api/users/:id", + USERS = "/web/api/users", + USER_AUTH = "/web/api/user", } export type Timestamp = string @@ -51,8 +71,35 @@ export type POST_Requests = { name: string description:string } - [ApiRoutes.PROJECT_CREATE]: - ProjectFormData + [ApiRoutes.PROJECT_CREATE]: { + name: string; + description: string; + groupClusterId: number; + testId: number | null; + visible: boolean; + maxScore: number; + deadline: Date | null; + visibleAfter: Date | null; +} + + [ApiRoutes.GROUP_MEMBERS]: { + id: number + } + [ApiRoutes.PROJECT_SUBMIT]: { + file: FormData + } + + [ApiRoutes.COURSE_CLUSTERS]: { + name: string + capacity: number + groupCount: number + }, + [ApiRoutes.PROJECT_TESTS]: Omit + [ApiRoutes.COURSE_COPY]: undefined + [ApiRoutes.COURSE_JOIN]: undefined + [ApiRoutes.COURSE_JOIN_WITHOUT_KEY]: undefined + [ApiRoutes.PROJECT_SCORE]: Omit + [ApiRoutes.CLUSTER_GROUPS]: {name: string} } /** @@ -60,9 +107,16 @@ export type POST_Requests = { */ export type POST_Responses = { + [ApiRoutes.PROJECT_SUBMIT]: GET_Responses[ApiRoutes.SUBMISSION] [ApiRoutes.COURSES]: GET_Responses[ApiRoutes.COURSE], [ApiRoutes.PROJECT_CREATE]: GET_Responses[ApiRoutes.PROJECT] - + [ApiRoutes.GROUP_MEMBERS]: GET_Responses[ApiRoutes.GROUP_MEMBERS] + [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER], + [ApiRoutes.PROJECT_TESTS]: GET_Responses[ApiRoutes.PROJECT_TESTS] + [ApiRoutes.COURSE_COPY]: GET_Responses[ApiRoutes.COURSE] + [ApiRoutes.COURSE_JOIN]: {name:string, description: string} + [ApiRoutes.COURSE_JOIN_WITHOUT_KEY]: POST_Responses[ApiRoutes.COURSE_JOIN] + [ApiRoutes.PROJECT_SCORE]: GET_Responses[ApiRoutes.PROJECT_SCORE] } /** @@ -71,70 +125,123 @@ export type POST_Responses = { export type DELETE_Requests = { [ApiRoutes.COURSE]: undefined [ApiRoutes.PROJECT]: undefined + [ApiRoutes.GROUP_MEMBER]: undefined + [ApiRoutes.COURSE_LEAVE]: undefined + [ApiRoutes.COURSE_MEMBER]: undefined + [ApiRoutes.PROJECT_TESTS]: undefined + [ApiRoutes.COURSE_JOIN_LINK]: undefined + [ApiRoutes.PROJECT_TESTS_UPLOAD]: undefined + [ApiRoutes.CLUSTER]: undefined } /** - * the body of the PUT requests + * the body of the PUT & PATCH requests */ export type PUT_Requests = { [ApiRoutes.COURSE]: POST_Requests[ApiRoutes.COURSE] + [ApiRoutes.PROJECT]: ProjectFormData + [ApiRoutes.COURSE_MEMBER]: { relation: CourseRelation } + [ApiRoutes.PROJECT_SCORE]: { score: number | null , feedback: string}, + [ApiRoutes.PROJECT_TESTS]: POST_Requests[ApiRoutes.PROJECT_TESTS] + [ApiRoutes.USER]: { + name: string + surname: string + email: string + role: UserRole + } -} + [ApiRoutes.CLUSTER_FILL]: { + [groupName:string]: number[] /* userId[] */ + } + [ApiRoutes.COURSE_JOIN_LINK]: undefined + [ApiRoutes.PROJECT_TESTS_UPLOAD]: FormData +} export type PUT_Responses = { [ApiRoutes.COURSE]: GET_Responses[ApiRoutes.COURSE] + [ApiRoutes.PROJECT]: GET_Responses[ApiRoutes.PROJECT] + [ApiRoutes.USER]: GET_Responses[ApiRoutes.USER] + [ApiRoutes.COURSE_MEMBER]: GET_Responses[ApiRoutes.COURSE_MEMBERS] + [ApiRoutes.PROJECT_SCORE]: GET_Responses[ApiRoutes.PROJECT_SCORE] + [ApiRoutes.PROJECT_TESTS]: GET_Responses[ApiRoutes.PROJECT_TESTS] + [ApiRoutes.CLUSTER_FILL]: PUT_Requests[ApiRoutes.CLUSTER_FILL] + [ApiRoutes.COURSE_JOIN_LINK]: ApiRoutes.COURSE_JOIN + [ApiRoutes.PROJECT_TESTS_UPLOAD]: undefined } type CourseTeacher = { - name: string - surname: string - url: string, + name: string + surname: string + url: string, } type Course = { - courseUrl: string - name: string + courseUrl: string + name: string } -export type ProjectStatus = "correct" | "incorrect" | "not started" +export type DockerStatus = "no_test" | "running" | "finished" | "aborted" +export type ProjectStatus = "correct" | "incorrect" | "not started" | "no group" export type CourseRelation = "enrolled" | "course_admin" | "creator" export type UserRole = "student" | "teacher" | "admin" +type SubTest = { + testName: string, // naam van de test + testDescription: string, // beschrijving van de test + correct: string, // verwachte output + output: string, // gegenereerde output + required: boolean, // of de test verplicht is + //FIXME: typo, moet success zijn ipv succes + succes: boolean, // of de test geslaagd is +} + +type DockerFeedback = { + type: "SIMPLE", + feedback: string, // de logs van de dockerrun + allowed: boolean // vat samen of de test geslaagd is of niet +} | { + type: "TEMPLATE", + feedback: { + subtests: SubTest[] + } + allowed: boolean +} | { + type: "NONE", + feedback: "", + allowed: true +} + + + /** * The response you get from the GET request */ export type GET_Responses = { - - [ApiRoutes.TEST]: { - name: string - firstName: string - lastName: string - email: string - oid: string - } [ApiRoutes.PROJECT_SUBMISSIONS]: { - feedback: GET_Responses[ApiRoutes.PROJECT_SCORE] | null, - group: GET_Responses[ApiRoutes.GROUP], + feedback: GET_Responses[ApiRoutes.PROJECT_SCORE] | null, + group: GET_Responses[ApiRoutes.GROUP], submission: GET_Responses[ApiRoutes.SUBMISSION] | null // null if no submission yet }[], [ApiRoutes.PROJECT_GROUP_SUBMISSIONS]: GET_Responses[ApiRoutes.SUBMISSION][] + [ApiRoutes.PROJECT_TEST_SUBMISSIONS]: GET_Responses[ApiRoutes.PROJECT_GROUP_SUBMISSIONS] [ApiRoutes.GROUP_SUBMISSIONS]: GET_Responses[ApiRoutes.SUBMISSION] [ApiRoutes.SUBMISSION]: { submissionId: number projectId: number - groupId: number - structureAccepted: boolean - dockerAccepted: boolean + groupId: number | null + structureAccepted: boolean, + dockerStatus: DockerStatus, submissionTime: Timestamp projectUrl: ApiRoutes.PROJECT - groupUrl: ApiRoutes.GROUP + groupUrl: ApiRoutes.GROUP | null fileUrl: ApiRoutes.SUBMISSION_FILE - structureFeedbackUrl: ApiRoutes.SUBMISSION_STRUCTURE_FEEDBACK - dockerFeedbackUrl: ApiRoutes.SUBMISSION_DOCKER_FEEDBACK + structureFeedback: string + dockerFeedback: DockerFeedback, + artifactUrl: ApiRoutes.SUBMISSION_ARTIFACT | null } [ApiRoutes.SUBMISSION_FILE]: BlobPart [ApiRoutes.COURSE_PROJECTS]: GET_Responses[ApiRoutes.PROJECT][] @@ -146,12 +253,14 @@ export type GET_Responses = { } deadline: Timestamp description: string + clusterId: number | null; projectId: number name: string submissionUrl: ApiRoutes.PROJECT_GROUP_SUBMISSIONS testsUrl: string - maxScore:number + maxScore: number | null visible: boolean + visibleAfter?: Timestamp status?: ProjectStatus progress: { completed: number @@ -159,7 +268,16 @@ export type GET_Responses = { }, groupId: number | null // null if not in a group } - [ApiRoutes.PROJECT_TESTS]: {} // ?? + [ApiRoutes.PROJECT_TESTS]: { + projectUrl: ApiRoutes.PROJECT, + dockerImage: string | null, + dockerScript: string | null, + dockerTemplate: string | null, + structureTest: string | null, + extraFilesUrl: ApiRoutes.PROJECT_TESTS_UPLOAD + extraFilesName: string + } + [ApiRoutes.GROUP]: { groupId: number, capacity: number, @@ -176,15 +294,17 @@ export type GET_Responses = { [ApiRoutes.GROUP_MEMBER]: { email: string name: string - id: number + userId: number + studentNumber: string | null // Null in case of enrolled/student } [ApiRoutes.USERS]: { name: string - userId: number + surname: string + id: number url: string email: string role: UserRole - } + }[] [ApiRoutes.GROUP_MEMBERS]: GET_Responses[ApiRoutes.GROUP_MEMBER][] [ApiRoutes.COURSE_CLUSTERS]: GET_Responses[ApiRoutes.CLUSTER][] @@ -194,9 +314,10 @@ export type GET_Responses = { name: string; capacity: number; groupCount: number; - created_at: Timestamp; + createdAt: Timestamp; groups: GET_Responses[ApiRoutes.GROUP][] - courseUrl: ApiRoutes.COURSE + courseUrl: ApiRoutes.COURSE, + lockGroupsAfter: Timestamp | null // means students can't join or leave the group } [ApiRoutes.COURSE]: { description: string @@ -205,7 +326,11 @@ export type GET_Responses = { name: string teacher: CourseTeacher assistents: CourseTeacher[] - joinUrl: string + joinUrl: ApiRoutes.COURSE_JOIN + joinKey: string | null + archivedAt: Timestamp | null // null if not archived + year: number + createdAt: Timestamp } [ApiRoutes.COURSE_MEMBERS]: { relation: CourseRelation, @@ -226,24 +351,85 @@ export type GET_Responses = { courseId:number, name:string, relation: CourseRelation, + memberCount: number, + archivedAt: Timestamp | null, // null if not archived + year: number // Year of the course url:string }[], //[ApiRoutes.PROJECT_GROUP]: GET_Responses[ApiRoutes.CLUSTER_GROUPS][number] [ApiRoutes.PROJECT_GROUPS]: GET_Responses[ApiRoutes.GROUP][] //GET_Responses[ApiRoutes.PROJECT_GROUP][] - [ApiRoutes.PROJECTS]: { - enrolledProjects: {project: GET_Responses[ApiRoutes.PROJECT], status: ProjectStatus}[], - adminProjects: Omit[] - }, + [ApiRoutes.CLUSTER]: { + clusterId: number; + name: string; + capacity: number; + groupCount: number; + createdAt: Timestamp; + groups: GET_Responses[ApiRoutes.GROUP][] + courseUrl: ApiRoutes.COURSE + } + [ApiRoutes.COURSE]: { + description: string + courseId: number + memberUrl: ApiRoutes.COURSE_MEMBERS + name: string + teacher: CourseTeacher + assistents: CourseTeacher[] + joinUrl: string + archivedAt: Timestamp | null // null if not archived + year: number + createdAt: Timestamp + } + [ApiRoutes.COURSE_MEMBERS]: { + relation: CourseRelation, + user: GET_Responses[ApiRoutes.GROUP_MEMBER] + }[], + [ApiRoutes.USER]: { + courseUrl: string + projects_url: string + url: string + role: UserRole + email: string + id: number + name: string + surname: string + studentNumber: string | null // Null in case of enrolled/student + }, + [ApiRoutes.USER_AUTH]: GET_Responses[ApiRoutes.USER], + [ApiRoutes.USER_COURSES]: { + courseId: number, + name: string, + relation: CourseRelation, + memberCount: number, + archivedAt: Timestamp | null, // null if not archived + year: number // Year of the course + url: string + }[], + //[ApiRoutes.PROJECT_GROUP]: GET_Responses[ApiRoutes.CLUSTER_GROUPS][number] + [ApiRoutes.PROJECT_GROUPS]: GET_Responses[ApiRoutes.GROUP][] //GET_Responses[ApiRoutes.PROJECT_GROUP][] + + [ApiRoutes.PROJECTS]: { + enrolledProjects: { project: GET_Responses[ApiRoutes.PROJECT], status: ProjectStatus }[], + adminProjects: Omit[] + }, [ApiRoutes.COURSE_GRADES]: { projectName: string, projectUrl: string, projectId: number, - maxScore: number, - groupFeedback: GET_Responses[ApiRoutes.PROJECT_SCORE] + maxScore: number | null, + groupFeedback: GET_Responses[ApiRoutes.PROJECT_SCORE] | null }[] [ApiRoutes.SUBMISSION_STRUCTURE_FEEDBACK]: string | null // Null if no feedback is given [ApiRoutes.SUBMISSION_DOCKER_FEEDBACK]: string | null // Null if no feedback is given + + [ApiRoutes.SUBMISSION_ARTIFACT]: Blob // returned het artifact als zip + + [ApiRoutes.COURSE_JOIN]: GET_Responses[ApiRoutes.COURSE] + [ApiRoutes.COURSE_JOIN_WITHOUT_KEY]: GET_Responses[ApiRoutes.COURSE] + [ApiRoutes.PROJECT_TESTS_UPLOAD]: Blob + [ApiRoutes.PROJECT_DOWNLOAD_ALL_SUBMISSIONS]: Blob + [ApiRoutes.AUTH_INFO]: {isAuthenticated:boolean, account: Account} } + diff --git a/frontend/src/@types/routes.ts b/frontend/src/@types/routes.ts index 2e73f9dc..2808d121 100644 --- a/frontend/src/@types/routes.ts +++ b/frontend/src/@types/routes.ts @@ -5,10 +5,10 @@ */ export enum AppRoutes { HOME = "/", - COURSES = "/courses", PROJECT = "/courses/:courseId/projects/:projectId", PROJECT_CREATE = "/courses/:courseId/create", PROJECT_TESTS = "/courses/:courseId/projects/:projectId/tests", + DOWNLOAD_PROJECT_TESTS = "/courses/:courseId/projects/:projectId/tests/download", COURSE = "/courses/:courseId", NEW_SUBMISSION = "/courses/:courseId/projects/:projectId/submit", EDIT_PROJECT = "/courses/:courseId/projects/:projectId/edit", @@ -17,5 +17,6 @@ export enum AppRoutes { ERROR = "/error", NOT_FOUND = "/not-found", EDIT_ROLE = "/edit-role", - COURSE_INVITE = "/invite/:inviteId" + COURSE_INVITE = "/invite/:courseId", + COURSES = "/courses", } diff --git a/frontend/src/@types/types.d.ts b/frontend/src/@types/types.d.ts index daec4a4b..719ad8de 100644 --- a/frontend/src/@types/types.d.ts +++ b/frontend/src/@types/types.d.ts @@ -1,4 +1,11 @@ +declare module "react" { + interface InputHTMLAttributes extends HTMLAttributes { + webkitdirectory?: string; + directory?:string + mozdirectory?: string + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3eac0b40..2499f4a1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,36 +1,32 @@ import AppRouter from "./router/AppRouter" -import { IPublicClientApplication } from "@azure/msal-browser" -import { MsalProvider } from "@azure/msal-react" -import { useNavigate } from "react-router-dom" -import CustomNavigation from "./auth/CustomNavigation" + import Layout from "./components/layout/nav/Layout" import "./i18n/config" import ThemeProvider from "./theme/ThemeProvider" import { AppProvider } from "./providers/AppProvider" +import {AuthProvider} from "./providers/AuthProvider" import { UserProvider } from "./providers/UserProvider" import AppApiProvider from "./providers/AppApiProvider" +import ErrorProvider from "./providers/ErrorProvider" -type AppProps = { - pca: IPublicClientApplication -} -function App({ pca }: AppProps) { - const navigate = useNavigate() - const navigationClient = new CustomNavigation(navigate) - pca.setNavigationClient(navigationClient) +function App() { + return (
- + - + + + - + diff --git a/frontend/src/auth/MsGraphApiCall.ts b/frontend/src/auth/MsGraphApiCall.ts deleted file mode 100644 index 3a2cc7d5..00000000 --- a/frontend/src/auth/MsGraphApiCall.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { loginRequest, graphConfig } from "./AuthConfig"; -import { msalInstance } from "../index"; - -export async function callMsGraph() { - const account = msalInstance.getActiveAccount(); - if (!account) { - throw Error("No active account! Verify a user has been signed in and setActiveAccount has been called."); - } - - - const response = await msalInstance.acquireTokenSilent({ - ...loginRequest, - account: account - }); - - const headers = new Headers(); - const bearer = `Bearer ${response.accessToken}`; - - headers.append("Authorization", bearer); - - const options = { - method: "GET", - headers: headers - }; - - return fetch(graphConfig.graphMeEndpoint, options) - .then(response => response.json()) - .catch(error => console.log(error)); -} \ No newline at end of file diff --git a/frontend/src/components/LanguageDropdown.tsx b/frontend/src/components/LanguageDropdown.tsx index e550bbf3..ca9a3b8a 100644 --- a/frontend/src/components/LanguageDropdown.tsx +++ b/frontend/src/components/LanguageDropdown.tsx @@ -26,7 +26,7 @@ const LanguageDropdown = () => { return - + {app.language} diff --git a/frontend/src/components/common/AcademicYearSelect.tsx b/frontend/src/components/common/AcademicYearSelect.tsx new file mode 100644 index 00000000..86e3ba79 --- /dev/null +++ b/frontend/src/components/common/AcademicYearSelect.tsx @@ -0,0 +1,25 @@ +import { Select } from "antd"; +import { SelectProps } from "antd/lib"; +import { FC } from "react"; + + +const AcademicYearSelect: FC> = (props) => { + const currentYear = new Date().getFullYear(); + const years = Array.from({ length: 5 }, (_, i) => currentYear + i); // Generate next 10 years + + + const items: SelectProps["options"] = years.map((year) => ({ + + label: `${year-1} - ${year}`, + value: year-1, + })) + + + return ( + + ); +}; + +export default AcademicYearSelect; \ No newline at end of file diff --git a/frontend/src/components/common/MarkdownTooltip.tsx b/frontend/src/components/common/MarkdownTooltip.tsx new file mode 100644 index 00000000..89ca83ed --- /dev/null +++ b/frontend/src/components/common/MarkdownTooltip.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Space, Tooltip } from 'antd'; +import { QuestionCircleOutlined } from '@ant-design/icons'; +import MarkdownTextfield from '../input/MarkdownTextfield'; + +interface CustomTooltipProps { + label: string; + tooltipContent: string; + placement?: 'top' | 'left' | 'right' | 'bottom' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' | 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom'; +} + +const CustomTooltip: React.FC = ({ label, tooltipContent, placement = 'bottom' }) => { + + const contentLength = tooltipContent.length; + const calculatedWidth = contentLength > 100 ? "500px" : "auto"; + + const overlayInnerStyle = { width: calculatedWidth, maxWidth: "75vw", paddingLeft:"12px"}; + + return ( + + {label} + + } overlayInnerStyle={overlayInnerStyle} className='tooltip-markdown'> + + + + ); +}; + +export default CustomTooltip; diff --git a/frontend/src/components/common/PeriodTag.tsx b/frontend/src/components/common/PeriodTag.tsx new file mode 100644 index 00000000..14068ee7 --- /dev/null +++ b/frontend/src/components/common/PeriodTag.tsx @@ -0,0 +1,11 @@ +import { Tag } from "antd"; +import { FC } from "react"; + + +// +const PeriodTag:FC<{year: number }> = ({year}) => { + + return {year} - {year+1} +} + +export default PeriodTag; \ No newline at end of file diff --git a/frontend/src/components/common/saveDockerForm.tsx b/frontend/src/components/common/saveDockerForm.tsx new file mode 100644 index 00000000..e31c0392 --- /dev/null +++ b/frontend/src/components/common/saveDockerForm.tsx @@ -0,0 +1,70 @@ +import { FormInstance} from "antd" +import { ApiRoutes, POST_Requests } from "../../@types/requests.d" +import { UseApiType } from "../../hooks/useApi" +import { RcFile } from "antd/es/upload" + +export type DockerFormData = POST_Requests[ApiRoutes.PROJECT_TESTS] +type FileType = RcFile //Parameters>[0] + +const saveDockerForm = async (form: FormInstance, initialDockerValues: (DockerFormData & {dockerMode:boolean}) | null, API: UseApiType, projectId: string) => { + const dockerImage = form.getFieldValue("dockerImage") + const dockerScript = form.getFieldValue("dockerScript") + const dockerTemplate = form.getFieldValue("dockerTemplate") + const structureTest = form.getFieldValue("structureTest") + const dockerMode = form.getFieldValue("dockerMode") + + let success = true + + if (form.isFieldsTouched(["dockerImage", "dockerScript", "dockerTemplate", "structureTest", "dockerMode"]) && (!initialDockerValues || initialDockerValues.dockerImage !== dockerImage || initialDockerValues.dockerScript !== dockerScript || initialDockerValues.dockerTemplate !== dockerTemplate || initialDockerValues.structureTest !== structureTest || initialDockerValues.dockerMode !== dockerMode)) { + let data: DockerFormData = form.getFieldsValue(["dockerImage", "dockerScript", "dockerTemplate", "structureTest"]) + if (!data.dockerImage?.length) { + data.dockerScript = null + data.dockerTemplate = null + } + if (!dockerMode) data.dockerTemplate = null + + if (!initialDockerValues) { + // We do a POST request + const r = await API.POST(ApiRoutes.PROJECT_TESTS, { body: data, pathValues: { id: projectId } }, "message") + success &&= r.success + } else if ((data.dockerImage == null || data.dockerImage.length === 0) && !data.structureTest?.length) { + // We do a delete + const r = await API.DELETE(ApiRoutes.PROJECT_TESTS, { pathValues: { id: projectId } }, "message") + success &&= r.success + } else { + // We do a PUT + const r = await API.PUT(ApiRoutes.PROJECT_TESTS, { body: data, pathValues: { id: projectId }, headers: {} }, "message") + success &&= r.success + } + } + + if (form.isFieldTouched("dockerTestDir")) { + const val: FileType | undefined = form.getFieldValue("dockerTestDir")?.[0]?.originFileObj + + if (val === undefined) { + // DELETE + const r = await API.DELETE(ApiRoutes.PROJECT_TESTS_UPLOAD, { pathValues: { id: projectId } }, "message") + success &&= r.success + } else { + const formData = new FormData() + formData.append("file", val, val.name) + const r = await API.PUT(ApiRoutes.PROJECT_TESTS_UPLOAD, { body: formData, pathValues: { id: projectId } }, "message") + success &&= r.success + + // await API.PUT( + // ApiRoutes.PROJECT_TESTS_UPLOAD, + // { + // body: formData, + // pathValues: { id: projectId }, + // headers: { + // "Content-Type": "multipart/form-data", + // }, + // }, + // "message" + // ) + } + } + return success +} + +export default saveDockerForm diff --git a/frontend/src/components/forms/ClusterForm.tsx b/frontend/src/components/forms/ClusterForm.tsx new file mode 100644 index 00000000..ffd77fc7 --- /dev/null +++ b/frontend/src/components/forms/ClusterForm.tsx @@ -0,0 +1,57 @@ +import { DatePicker, Form, Input, InputNumber } from "antd" +import { useTranslation } from "react-i18next" + +const ClusterForm = () => { + const { t } = useTranslation() + + return ( + <> + + + + + + + + + + + + + + + + + ) +} + +export default ClusterForm diff --git a/frontend/src/components/forms/CourseForm.tsx b/frontend/src/components/forms/CourseForm.tsx index a9ed831e..5228bb3c 100644 --- a/frontend/src/components/forms/CourseForm.tsx +++ b/frontend/src/components/forms/CourseForm.tsx @@ -1,17 +1,25 @@ -import { Form, FormInstance, Input } from "antd" +import { Form, FormInstance, Input, Typography } from "antd" import { FC, PropsWithChildren } from "react" import { useTranslation } from "react-i18next" import MarkdownEditor from "../input/MarkdownEditor" +import AcademicYearSelect from "../common/AcademicYearSelect" - - -const CourseForm:FC<{form:FormInstance} & PropsWithChildren> = ({form,children}) => { +const CourseForm: FC<{ form: FormInstance } & PropsWithChildren> = ({ form, children }) => { const { t } = useTranslation() + const description = Form.useWatch("description", form) return ( -
+ @@ -21,16 +29,21 @@ const CourseForm:FC<{form:FormInstance} & PropsWithChildren> = ({form,children}) /> + {t("project.change.description")} + + - + + {children} ) diff --git a/frontend/src/components/forms/ProjectForm.tsx b/frontend/src/components/forms/ProjectForm.tsx index 76d0ccc3..d7728ff2 100644 --- a/frontend/src/components/forms/ProjectForm.tsx +++ b/frontend/src/components/forms/ProjectForm.tsx @@ -1,62 +1,79 @@ -import { Checkbox, DatePicker, Form, FormInstance, Input, Switch } from "antd" -import { FC } from "react" +import { Card, CardProps, FormInstance } from "antd" +import { FC, PropsWithChildren, useState } from "react" import { useTranslation } from "react-i18next" -import GroupClusterDropdown from "../../pages/projectCreate/components/GroupClusterDropdown" -import { useParams } from "react-router-dom" +import { TabsProps } from "antd/lib" +import GeneralFormTab from "./projectFormTabs/GeneralFormTab" +import GroupsFormTab from "./projectFormTabs/GroupsFormTab" +import StructureFormTab from "./projectFormTabs/StructureFormTab" +import DockerFormTab from "./projectFormTabs/DockerFormTab" +import { useLocation, useNavigate } from "react-router-dom" -const ProjectForm: FC<{}> = () => { +const VisibleTab: FC> = ({ visible, children }) => { + return
{children}
+} + +const ProjectForm: FC> = ({ children, cardProps, form,error }) => { const { t } = useTranslation() - const { courseId } = useParams<{ courseId: string }>() + const location = useLocation() + const navigate = useNavigate() + + + const tabs: TabsProps["items"] = [ + { + key: "general", + label: t("project.change.general"), + }, + { + key: "groups", + label: t("project.change.groups"), + }, + { + key: "structure", + label: t("project.change.structure"), + }, + { + key: "tests", + label: t("project.change.tests"), + }, + ] + + const onTabChange = (key: string) => { + navigate(`#${key}`) + } + + const activeTab = location.hash.slice(1) || "general" + + // Note: we need to render all tabs, even if they are not visible. Otherwise the form cannot get its values return ( - <> - - - - - - - - - - - - - - - - - - - + + {error} + + + + + + + + + + + + + + {children} + ) } diff --git a/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx b/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx new file mode 100644 index 00000000..f09d02a6 --- /dev/null +++ b/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx @@ -0,0 +1,269 @@ +import { CodepenCircleFilled, InboxOutlined, UploadOutlined } from "@ant-design/icons" +import { Button, Dropdown, Form, Input, Menu, Select, SelectProps, Switch, Upload } from "antd" +import { TextAreaProps } from "antd/es/input" +import { FormInstance } from "antd/lib" +import React, { FC, useEffect, useLayoutEffect, useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import useAppApi from "../../../hooks/useAppApi" +import MarkdownTooltip from "../../common/MarkdownTooltip" +import MarkdownTextfield from "../../input/MarkdownTextfield" +import TextArea from "antd/es/input/TextArea" + +import BashIcon from "../../../../public/docker_langauges/bash.svg" +import PythonIcon from "../../../../public/docker_langauges/python.svg" +import NodeIcon from "../../../../public/docker_langauges/node-js.svg" +import HaskellIcon from "../../../../public/docker_langauges/haskell.svg" +import Custom from "../../../../public/docker_langauges/custom.svg" + + +type DockerLanguage = "bash" | "python" | "node" | "haskell" | "custom" +const languageOptions: Record = { + bash: "fedora", + python: "python", + node: "node", + haskell: "haskell", + custom: "" +} + +const imageToLanguage: Record = { + fedora: "bash", + python: "python", + node: "node", + haskell: "haskell", +} + + +const languagesSelectorItems:SelectProps["options"] = [ + { + label: <>Bash, + value: "bash", + },{ + label: <>Python, + value: "python", + }, { + label: <>NodeJS, + value: "node", + }, { + label: <>Haskell, + value: "haskell", + }, { + label: <>Custom, + value: "custom", + } +] + + + +const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { + const { t } = useTranslation() + const { message } = useAppApi() + const dockerImage = Form.useWatch("dockerImage", form) + const dockerTemplate = Form.useWatch("dockerTemplate", form) + const dockerMode = Form.useWatch("dockerMode", form) + + const dockerDisabled = !dockerImage?.length + + + const withTemplate = (dockerMode === null && !!dockerTemplate?.length) || !!dockerMode + + + useEffect(() => { + + form.validateFields(["dockerScript", "dockerTemplate"]) + }, [dockerDisabled]) + + + const dockerImageSelect= useMemo(()=> imageToLanguage[dockerImage] || "custom",[dockerImage]) + + function isValidTemplate(template: string): string { + if (template.length === 0) { + return t("project.tests.dockerTemplateValidation.emptyTemplate") + } + let atLeastOne = false // Template should not be empty + const lines = template.split("\n") + if (lines[0].charAt(0) !== "@") { + return t("project.tests.dockerTemplateValidation.inValidFirstLine") + } + let isConfigurationLine = false + let lineNumber = 0 + for (const line of lines) { + lineNumber++ + if (line.length === 0) { + // skip line if empty + continue + } + if (line.charAt(0) === "@") { + atLeastOne = true + isConfigurationLine = true + continue + } + if (isConfigurationLine) { + if (line.charAt(0) === ">") { + const isDescription = line.length >= 13 && line.substring(0, 13).toLowerCase() === ">description=" + // option lines + if (line.toLowerCase() !== ">required" && line.toLowerCase() !== ">optional" && !isDescription) { + return t("project.tests.dockerTemplateValidation.inValidOptions", { line: lineNumber.toString() }) + } + } else { + isConfigurationLine = false + } + } + } + if (!atLeastOne) { + return t("project.tests.dockerTemplateValidation.emptyTemplate") + } + return "" + } + + const normFile = (e: any) => { + if (Array.isArray(e)) { + return e + } + return e?.fileList + } + + let switchClassName = "template-switch" + let scriptPlaceholder + + if (withTemplate) { + switchClassName += " template-switch-active" + scriptPlaceholder = 'bash /shared/input/helloworld.sh > "/shared/output/helloWorldTest"\n' + 'bash /shared/input/helloug.sh > "/shared/output/helloUGent"\n' + } else { + switchClassName += " template-switch-inactive" + scriptPlaceholder = "output=$(bash /shared/input/helloworld.sh)\n" + 'if [[ "$output" == "Hello World" ]]; then \n' + " echo 'Test one is successful\n" + " echo 'PUSH ALLOWED' > /shared/output/testOutput\n" + "else\n" + " echo 'Test one failed: script failed to print \"Hello World\"'\n" + "fi" + } + + + return ( + <> + + } + name="dockerImage" + > + form.setFieldValue("dockerImage", languageOptions[val])} + options={languagesSelectorItems} + />} + placeholder={t("project.tests.dockerImagePlaceholder")} + /> + + <> + + } + name="dockerScript" + > +