diff --git a/.ci/E2E-tests/execute.sh b/.ci/E2E-tests/execute.sh index 971d05dea8a6..a75ebfb382e2 100755 --- a/.ci/E2E-tests/execute.sh +++ b/.ci/E2E-tests/execute.sh @@ -23,12 +23,3 @@ cd docker docker compose -f $COMPOSE_FILE pull artemis-cypress $DB nginx docker compose -f $COMPOSE_FILE build --build-arg WAR_FILE_STAGE=external_builder --no-cache --pull artemis-app docker compose -f $COMPOSE_FILE up --exit-code-from artemis-cypress -exitCode=$? -cd .. -echo "Cypress container exit code: $exitCode" -if [ $exitCode -eq 0 ] -then - touch .successful -else - echo "Not creating success file because the tests failed" -fi diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8d07af38982d..5e0eb0a988fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -120,14 +120,14 @@ jobs: uses: docker/setup-buildx-action@v3 # Build and Push to GitHub Container Registry - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 if: ${{ steps.compute-tag.outputs.result != 'FALSE' }} with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and Push to GitHub Container Registry - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 if: ${{ steps.compute-tag.outputs.result != 'FALSE' }} with: # beware that the linux/arm64 build from the registry is using an amd64 compiled .war file as diff --git a/README.md b/README.md index 73b97ae88f5e..69f118e11e07 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,11 @@ 15. **[Learning analytics](https://ls1intum.github.io/Artemis/user/learning-analytics/)**: Artemis integrated different statistics for students to compare themselves to the course average. It allows instructors to evaluate the average student performance based on exercises and competencies. 16. **[Adaptive Learning](https://ls1intum.github.io/Artemis/user/adaptive-learning/)**: Artemis allows instructors and students to define and track competencies. Students can monitor their progress towards these goals, while instructors can provide tailored feedback. This approach integrates lectures and exercises under overarching learning objectives. 17. **[Tutorial Groups](https://ls1intum.github.io/Artemis/user/tutorialgroups/)**: Artemis support the management of tutorial groups of a course. This includes planning the sessions, assigning responsible tutors, registering students and tracking the attendance. -18. **[Scalable](https://ls1intum.github.io/Artemis/user/scaling/)**: Artemis scales to multiple courses with thousands of students. In fact, the largest course had 2,400 students. Administrators can easily scale Artemis with additional build agents in the continuous integration environment. -19. **[High user satisfaction](https://ls1intum.github.io/Artemis/user/user-experience/)**: Artemis is easy to use, provides guided tutorials. Developers focus on usability, user experience, and performance. -20. **Customizable**: It supports multiple instructors, editors, and tutors per course and allows instructors to customize many course settings -21. **[Open-source](https://ls1intum.github.io/Artemis/dev/open-source/)**: Free to use with a large community and many active maintainers. +18. **[Iris](https://artemis.cit.tum.de/about-iris)**: Artemis integrates Iris, a chatbot that supports students and instructors with common questions and tasks. +19. **[Scalable](https://ls1intum.github.io/Artemis/user/scaling/)**: Artemis scales to multiple courses with thousands of students. In fact, the largest course had 2,400 students. Administrators can easily scale Artemis with additional build agents in the continuous integration environment. +20. **[High user satisfaction](https://ls1intum.github.io/Artemis/user/user-experience/)**: Artemis is easy to use, provides guided tutorials. Developers focus on usability, user experience, and performance. +21. **Customizable**: It supports multiple instructors, editors, and tutors per course and allows instructors to customize many course settings +22. **[Open-source](https://ls1intum.github.io/Artemis/dev/open-source/)**: Free to use with a large community and many active maintainers. ## Roadmap diff --git a/docker/atlassian.yml b/docker/atlassian.yml index cf5b1047797b..6b8e33418747 100644 --- a/docker/atlassian.yml +++ b/docker/atlassian.yml @@ -11,7 +11,7 @@ services: image: ghcr.io/ls1intum/artemis-jira:9.4.3 pull_policy: always volumes: - - artemis-jira-data:/var/atlassian/application-data/jira + - artemis-jira-data:/var/atlassian/application-data/jira ports: - "8081:8080" # expose the port to make it reachable docker internally even if the external port mapping changes @@ -57,6 +57,12 @@ services: - "8085" networks: - artemis + healthcheck: + test: curl -f http://localhost:8085/rest/api/latest/server | grep "RUNNING" + interval: 10s + timeout: 5s + start_period: 40s + retries: 120 # = 20 minutes startup time during setup bamboo-build-agent: container_name: artemis-bamboo-build-agent @@ -74,6 +80,9 @@ services: BAMBOO_SERVER: "http://bamboo:8085" networks: - artemis + depends_on: + bamboo: + condition: service_healthy networks: artemis: diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml index 1a83fba70835..9522486c382b 100644 --- a/docs/.readthedocs.yaml +++ b/docs/.readthedocs.yaml @@ -5,6 +5,8 @@ build: os: ubuntu-22.04 tools: python: "3.10" +sphinx: + fail_on_warning: true python: install: - requirements: docs/requirements.txt diff --git a/docs/Makefile b/docs/Makefile index 3473b9c8d84f..e7c2e9cec73a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,8 @@ # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= +# -W: treat warnings as errors +SPHINXOPTS ?= -W SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build diff --git a/docs/admin/pyris-setup.rst b/docs/admin/pyris-setup.rst index 1ebe29de6586..372ddeea6116 100644 --- a/docs/admin/pyris-setup.rst +++ b/docs/admin/pyris-setup.rst @@ -36,7 +36,7 @@ E.g.: ``cp Pyris/application.example.yml application.yml`` Now you need to configure the ``application.yml`` file. Here is an example configuration: -.. code-block:: yml +.. code-block:: yaml pyris: api_keys: diff --git a/docs/dev/cypress.rst b/docs/dev/cypress.rst index c38dd0767f8c..d8131628da3d 100644 --- a/docs/dev/cypress.rst +++ b/docs/dev/cypress.rst @@ -30,7 +30,7 @@ Follow these steps to create your local cypress instance: 2. Customize Cypress settings - To connect cypress to our local Artemis instance, we need to adjust some configurations. + To connect cypress to our local Artemis instance, we need to adjust some configurations. First we need to set the URL or IP of the Artemis instance in the ``cypress.config.ts`` file. Adjust the ``baseUrl`` setting to fit your setup (e.g. ``baseUrl: 'http://localhost:9000',``) @@ -39,7 +39,7 @@ Follow these steps to create your local cypress instance: We also need to adjust the user setting, which will determine the usernames and passwords, that cypress will use. These settings are located within the ``cypress.env.json`` file. If you use the Atlassian setup, the file should typically look like this: - + .. code-block:: json { @@ -54,8 +54,9 @@ Follow these steps to create your local cypress instance: "instructorGroupName": "instructors" } - The ``USERID`` part will be automatically replaced by different user ids. These are set within the ``support/users.ts`` file. + The ``USERID`` part will be automatically replaced by different user ids. These are set within the ``support/users.ts`` file. For a typical local installation the IDs are: + - studentOne: 1 - studentTwo: 2 - studentThree: 3 @@ -87,7 +88,7 @@ Follow these steps to create your local cypress instance: :align: center :alt: Cypress cypress-open-screenshot - You can now click on any test suite and it should run. + You can now click on any test suite and it should run. .. warning:: **IMPORTANT**: If you run the E2E tests for the first time, always run the ``ImportUsers.ts`` tests first, @@ -97,18 +98,18 @@ Follow these steps to create your local cypress instance: Debug using Sorry Cypress ------------------------- -Since the E2E tests are sometimes hard to debug, we provide a dashboard, that allows to inspect the -CI run and even watch a video of the UI interaction with Artemis in that run. +Since the E2E tests are sometimes hard to debug, we provide a dashboard, that allows to inspect the +CI run and even watch a video of the UI interaction with Artemis in that run. It's based on Sorry Cypress a open source and selfhostable alternative to the paid cypress cloud. The dashboard itself can be access here: https://sorry-cypress.ase.cit.tum.de/ -To access it, you need these basic auth credentials (sorry cypress itself does not provide an auth +To access it, you need these basic auth credentials (sorry cypress itself does not provide an auth system, so we are forced to use nginx basic auth here). You can find these credentials on our confluence page: https://confluence.ase.in.tum.de/display/ArTEMiS/Sorry+Cypress+Dashboard -After that you will see the initial dashboard. +After that you will see the initial dashboard. You first have to select a project in the left sidebar (mysql or postgresql): @@ -122,31 +123,31 @@ Now you get a list of the last runs. In the top right you can enter your branch :align: center :alt: Sorry Cypress last runs -The name of the run consists of the branch name followed by the run number. The last part is MySQL or -PostgreSQL depending on the run environment. If you are in the MySQL project, you will of course only see the MySQL runs. +The name of the run consists of the branch name followed by the run number. The last part is MySQL or +PostgreSQL depending on the run environment. If you are in the MySQL project, you will of course only see the MySQL runs. -If you now click on the run, you can see detailed information about the test suites (corresponding +If you now click on the run, you can see detailed information about the test suites (corresponding to components within Artemis). For each suite there is information about the run time, the successful/failed/flaky/skipped/ignored tests: .. figure:: cypress/sorry-cypress-run.png :align: center :alt: Sorry Cypress single run -If you want to further debug one test suite, just click on it. +If you want to further debug one test suite, just click on it. .. figure:: cypress/sorry-cypress-test.png :align: center :alt: Sorry Cypress single test -Here you can see the single tests on the left and a video on the right. This is a screen capture of -the actual run and can tremendously help debug failing E2E tests. +Here you can see the single tests on the left and a video on the right. This is a screen capture of +the actual run and can tremendously help debug failing E2E tests. -Sometimes the video can be a little bit to fast to debug easily. Just download the video on your -computer and play it with a video player, that allows you to slow the video down. +Sometimes the video can be a little bit to fast to debug easily. Just download the video on your +computer and play it with a video player, that allows you to slow the video down. .. note:: - For maintenance reasons videos are deleted after 14 days. So if you have a failing test, debug - it before this period to get access to the video. + For maintenance reasons videos are deleted after 14 days. So if you have a failing test, debug + it before this period to get access to the video. Best practice when writing new E2E tests @@ -154,12 +155,12 @@ Best practice when writing new E2E tests **Understanding the System and Requirements** -Before writing tests, a deep understanding of the system and its requirements is crucial. -This understanding guides determining what needs testing and what defines a successful test. +Before writing tests, a deep understanding of the system and its requirements is crucial. +This understanding guides determining what needs testing and what defines a successful test. The best way to understand is to consolidate the original system`s developer or a person actively working on this component. -**Identify Main Test Scenarios** +**Identify Main Test Scenarios** Identify what are the main ways the component is supposed to be used. Try the action with all involved user roles and test as many different inputs as @@ -209,12 +210,12 @@ and fx the issue, or update the test if the requirements have changed. **Regularly Review and Refactor Your Tests** -Tests, like code, can accumulate technical debt. Regular reviews for duplication, +Tests, like code, can accumulate technical debt. Regular reviews for duplication, unnecessary complexity, and other issues help maintain tests and enhance reliability. **Use HTML IDs instead of classes or other attributes** -When searching for a single element within the DOM of an HTML page, try to use ID selectors as much as possible. +When searching for a single element within the DOM of an HTML page, try to use ID selectors as much as possible. They are more reliable since there can only be one element with this ID on one single page according to the HTML diff --git a/docs/dev/development-process.rst b/docs/dev/development-process.rst index 4206185d1849..b5e073fb9da4 100644 --- a/docs/dev/development-process.rst +++ b/docs/dev/development-process.rst @@ -12,7 +12,8 @@ Naming Conventions for GitHub Pull Requests 1. The first term is a main feature of Artemis and is using code highlighting, e.g. “``Programming exercises``:”. 1. Possible feature tags are: ``Programming exercises``, ``Quiz exercises``, ``Modeling exercises``, ``Text exercises``, ``File upload exercises``, ``Exam mode``, - ``Grading``, ``Assessment``, ``Communication``, ``Notifications``, ``Team exercises``, ``Lectures``, ``Plagiarism checks``, ``Learning analytics``, ``Adaptive learning``, ``Tutorial groups``. + ``Grading``, ``Assessment``, ``Communication``, ``Notifications``, ``Team exercises``, ``Lectures``, ``Plagiarism checks``, ``Learning analytics``, + ``Adaptive learning``, ``Tutorial groups``, ``Iris``. 2. If the change is not visible to end users, or it is a pure development or test improvement, we use the term “``Development``:”. 3. Everything else belongs to the ``General`` category. diff --git a/docs/dev/setup.rst b/docs/dev/setup.rst index 61a797b9f302..568762fe24df 100644 --- a/docs/dev/setup.rst +++ b/docs/dev/setup.rst @@ -709,7 +709,7 @@ HTTP. We need to extend the configuration in the file ------------------------------------------------------------------------------------------------------------------------ Iris/Pyris Service --------------- +------------------ Iris is an intelligent virtual tutor integrated into the Artemis platform. It is designed to provide one-on-one programming assistance without human tutors. @@ -726,7 +726,7 @@ Prerequisites - Set up a running instance of Pyris_. Refer to the :doc:`../admin/pyris-setup` for more information. Enable the ``iris`` Spring profile: -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :: diff --git a/docs/dev/setup/bamboo-bitbucket-jira.rst b/docs/dev/setup/bamboo-bitbucket-jira.rst index d198da3c60c7..11e7bbb7605d 100644 --- a/docs/dev/setup/bamboo-bitbucket-jira.rst +++ b/docs/dev/setup/bamboo-bitbucket-jira.rst @@ -369,8 +369,8 @@ Configure Artemis server: port: 8080 # The port of artemis url: http://172.20.0.1:8080 # needs to be an ip - // url: http://docker.for.mac.host.internal:8080 # If the above one does not work for mac try this one - // url: http://host.docker.internal:8080 # If the above one does not work for windows try this one + # url: http://docker.for.mac.host.internal:8080 # If the above one does not work for mac try this one + # url: http://host.docker.internal:8080 # If the above one does not work for windows try this one In addition, you have to start Artemis with the profiles ``bamboo``, ``bitbucket`` and ``jira`` so that the correct adapters will be used, diff --git a/docs/dev/setup/jenkins-gitlab.rst b/docs/dev/setup/jenkins-gitlab.rst index 9c9e7f395c85..1390aec98bbb 100644 --- a/docs/dev/setup/jenkins-gitlab.rst +++ b/docs/dev/setup/jenkins-gitlab.rst @@ -125,11 +125,16 @@ both are set up correctly and follow these steps: INSERT INTO `artemis`.`jhi_user_authority` (`user_id`, `authority_name`) VALUES (1,"ROLE_ADMIN"); INSERT INTO `artemis`.`jhi_user_authority` (`user_id`, `authority_name`) VALUES (1,"ROLE_USER"); -4. Create a user in Gitlab (``http://your-gitlab-domain/admin/users/new``) and make sure that the username, -email, and password are the same as the user from the database: +4. Create a user in Gitlab (``http://your-gitlab-domain/admin/users/new``) and make sure that the username and +email are the same as the user from the database: .. figure:: setup/jenkins-gitlab/gitlab_admin_user.png +5. Edit the new admin user (``http://your-gitlab-domain/admin/users/artemis_admin/edit``) to set the password to the +same value as in the database: + +.. figure:: setup/jenkins-gitlab/gitlab_admin_user_password.png + Starting the Artemis server should now succeed. GitLab @@ -188,9 +193,9 @@ tokens instead of the predefined ones. :: - docker compose -f docker/.yml exec gitlab gitlab-rails runner "token = User.find_by_username('root').personal_access_tokens.create(scopes: [:api, :read_user, :read_api, :read_repository, :write_repository, :sudo], name: 'Artemis Admin Token'); token.set_token('artemis-gitlab-token'); token.save!" + docker compose -f docker/.yml exec gitlab gitlab-rails runner "token = User.find_by_username('root').personal_access_tokens.create(scopes: ['api', 'read_api', 'read_user', 'read_repository', 'write_repository', 'sudo'], name: 'Artemis Admin Token', expires_at: 365.days.from_now); token.set_token('artemis-gitlab-token'); token.save!" - | You can also manually create in by navigating to ``http://localhost:8081/-/profile/personal_access_tokens`` and + | You can also manually create in by navigating to ``http://localhost:8081/-/profile/personal_access_tokens?name=Artemis+Admin+token&scopes=api,read_api,read_user,read_repository,write_repository,sudo`` and generate a token with all scopes. | Copy this token into the ``ADMIN_PERSONAL_ACCESS_TOKEN`` field in the ``docker/gitlab/gitlab-local-setup.sh`` file. @@ -325,7 +330,7 @@ GitLab Access Token .. figure:: setup/jenkins-gitlab/gitlab_access_tokens_button.png :align: center -10. Create a new token named “Artemis” and give it **all** rights. +10. Create a new token named “Artemis” and give it rights ``api``, ``read_api``, ``read_user``, ``read_repository``, ``write_repository``, and ``sudo``. .. figure:: setup/jenkins-gitlab/artemis_gitlab_access_token.png :align: center @@ -469,7 +474,7 @@ do either do it manually or using the following command: :: - docker compose -f docker/.yml exec gitlab gitlab-rails runner "token = User.find_by_username('root').personal_access_tokens.create(scopes: [:api, :read_repository], name: 'Jenkins'); token.set_token('jenkins-gitlab-token'); token.save!" + docker compose -f docker/.yml exec gitlab gitlab-rails runner "token = User.find_by_username('root').personal_access_tokens.create(scopes: ['api', 'read_repository'], name: 'Jenkins', expires_at: 365.days.from_now); token.set_token('jenkins-gitlab-token'); token.save!" @@ -746,7 +751,7 @@ Choose “Download now and install after restart” and checking the Timestamper Configuration """"""""""""""""""""""""" -Go to *Manage Jenkins → Configure System*. There you will find the +Go to *Manage Jenkins → System Configuration → Configure*. There you will find the Timestamper configuration, use the following value for both formats: :: @@ -767,8 +772,8 @@ JUnit formatted results to any URL. You can download the current release of the plugin `here `__ (Download the **.hpi** file). Go to the Jenkins plugin page (*Manage -Jenkins → Manage Plugins*) and install the downloaded file under the -*Advanced* tab under *Upload Plugin* +Jenkins → System Configuration → Plugins*) and install the downloaded file under the +*Advanced settings* tab under *Deploy Plugin* .. figure:: setup/jenkins-gitlab/jenkins_custom_plugin.png :align: center @@ -776,7 +781,7 @@ Jenkins → Manage Plugins*) and install the downloaded file under the Jenkins Credentials """"""""""""""""""" -Go to *Manage Jenkins -> Security -> Manage Credentials → Jenkins → Global credentials* and create the +Go to *Manage Jenkins → Security → Credentials → Jenkins → Global credentials* and create the following credentials GitLab API Token @@ -798,7 +803,7 @@ GitLab API Token 4. Leave the ID field blank 5. The description is up to you -3. Go to the Jenkins settings *Manage Jenkins → Configure System*. There +3. Go to the Jenkins settings *Manage Jenkins → System*. There you will find the GitLab settings. Fill in the URL of your GitLab instance and select the just created API token in the credentials dropdown. After you click on “Test Connection”, everything should @@ -1013,7 +1018,8 @@ You can either run the builds locally (that means on the machine that hosts Jenk Configuring local build agents """""""""""""""""""""""""""""" -Go to `Manage Jenkins` > `Manage Nodes and Clouds` > `master` +Go to `Manage Jenkins` → `Nodes` → `Built-In Node` → `Configure` + Configure your master node like this (adjust the number of executors, if needed). Make sure to add the docker label. .. figure:: setup/jenkins-gitlab/jenkins_local_node.png @@ -1071,26 +1077,28 @@ Add agent in Jenkins: 1. Open Jenkins in your browser (e.g. localhost:8082) -2. Go to Manage Jenkins -> Manage Credentials -> (global) -> Add Credentials +2. Go to Manage Jenkins → Credentials → System → Global credentials (unrestricted) → Add Credentials - Kind: SSH Username with private key + - Scope: Global (Jenkins, nodes, items, all child items, etc) + - ID: leave blank - Description: Up to you - Username: jenkins - - Private Key: (e.g /root/.ssh/id_rsa) + - Private Key: (e.g /root/.ssh/id_rsa) - - Passphrase: (you can leave it blank if none has been specified) + - Passphrase: (you can leave it blank if none has been specified) .. figure:: setup/jenkins-gitlab/alternative_jenkins_node_credentials.png :align: center -3. Go to Manage Jenkins -> Manage Nodes and Clouds -> New Node +3. Go to Manage Jenkins → Nodes → New Node - - Node name: Up to you (e.g. Docker) + - Node name: Up to you (e.g. Docker agent node) - Check 'Permanent Agent' @@ -1223,10 +1231,10 @@ access control in Jenkins. This enables specific Artemis users to access build plans and execute actions such as triggering a build. This section explains the changes required in Jenkins in order to set up build plan access control: -1. Navigate to Manage Jenkins -> Manage Plugins -> Installed and make sure that you have the +1. Navigate to Manage Jenkins → Plugins → Installed plugins and make sure that you have the `Matrix Authorization Strategy `__ plugin installed -2. Navigate to Manage Jenkins -> Configure Global Security and navigate to the "Authorization" section +2. Navigate to Manage Jenkins → Security and navigate to the "Authorization" section 3. Select the "Project-based Matrix Authorization Strategy" option diff --git a/docs/dev/setup/jenkins-gitlab/alternative_jenkins_node_credentials.png b/docs/dev/setup/jenkins-gitlab/alternative_jenkins_node_credentials.png index 4901e0cd2cce..133b81583b92 100644 Binary files a/docs/dev/setup/jenkins-gitlab/alternative_jenkins_node_credentials.png and b/docs/dev/setup/jenkins-gitlab/alternative_jenkins_node_credentials.png differ diff --git a/docs/dev/setup/jenkins-gitlab/alternative_jenkins_node_setup.png b/docs/dev/setup/jenkins-gitlab/alternative_jenkins_node_setup.png index 29f5738eafb5..7eca324029fe 100644 Binary files a/docs/dev/setup/jenkins-gitlab/alternative_jenkins_node_setup.png and b/docs/dev/setup/jenkins-gitlab/alternative_jenkins_node_setup.png differ diff --git a/docs/dev/setup/jenkins-gitlab/artemis_gitlab_access_token.png b/docs/dev/setup/jenkins-gitlab/artemis_gitlab_access_token.png index 87c382c449e9..f99c0a490422 100644 Binary files a/docs/dev/setup/jenkins-gitlab/artemis_gitlab_access_token.png and b/docs/dev/setup/jenkins-gitlab/artemis_gitlab_access_token.png differ diff --git a/docs/dev/setup/jenkins-gitlab/gitlab_access_tokens_button.png b/docs/dev/setup/jenkins-gitlab/gitlab_access_tokens_button.png index e0ccc8e9ffaa..bdd5741b632d 100644 Binary files a/docs/dev/setup/jenkins-gitlab/gitlab_access_tokens_button.png and b/docs/dev/setup/jenkins-gitlab/gitlab_access_tokens_button.png differ diff --git a/docs/dev/setup/jenkins-gitlab/gitlab_admin_user.png b/docs/dev/setup/jenkins-gitlab/gitlab_admin_user.png index 7ea3865f73e5..f884f92a04d7 100644 Binary files a/docs/dev/setup/jenkins-gitlab/gitlab_admin_user.png and b/docs/dev/setup/jenkins-gitlab/gitlab_admin_user.png differ diff --git a/docs/dev/setup/jenkins-gitlab/gitlab_admin_user_password.png b/docs/dev/setup/jenkins-gitlab/gitlab_admin_user_password.png new file mode 100644 index 000000000000..7a9003dd9584 Binary files /dev/null and b/docs/dev/setup/jenkins-gitlab/gitlab_admin_user_password.png differ diff --git a/docs/dev/setup/jenkins-gitlab/gitlab_jenkins_token_rights.png b/docs/dev/setup/jenkins-gitlab/gitlab_jenkins_token_rights.png index 737b827b5880..2e3707db0503 100644 Binary files a/docs/dev/setup/jenkins-gitlab/gitlab_jenkins_token_rights.png and b/docs/dev/setup/jenkins-gitlab/gitlab_jenkins_token_rights.png differ diff --git a/docs/dev/setup/jenkins-gitlab/gitlab_preferences_button.png b/docs/dev/setup/jenkins-gitlab/gitlab_preferences_button.png index ba0bae2cf859..f106698f5557 100644 Binary files a/docs/dev/setup/jenkins-gitlab/gitlab_preferences_button.png and b/docs/dev/setup/jenkins-gitlab/gitlab_preferences_button.png differ diff --git a/docs/dev/setup/jenkins-gitlab/jenkins_authorization_permissions.png b/docs/dev/setup/jenkins-gitlab/jenkins_authorization_permissions.png index 1dfcd3dfa26d..47b8e19a5be4 100644 Binary files a/docs/dev/setup/jenkins-gitlab/jenkins_authorization_permissions.png and b/docs/dev/setup/jenkins-gitlab/jenkins_authorization_permissions.png differ diff --git a/docs/dev/setup/jenkins-gitlab/jenkins_custom_plugin.png b/docs/dev/setup/jenkins-gitlab/jenkins_custom_plugin.png index 6223b74fdc40..aa8cb370489a 100644 Binary files a/docs/dev/setup/jenkins-gitlab/jenkins_custom_plugin.png and b/docs/dev/setup/jenkins-gitlab/jenkins_custom_plugin.png differ diff --git a/docs/dev/setup/jenkins-gitlab/jenkins_gitlab_configuration.png b/docs/dev/setup/jenkins-gitlab/jenkins_gitlab_configuration.png index 669cf01eb3f0..ac349efee7b7 100644 Binary files a/docs/dev/setup/jenkins-gitlab/jenkins_gitlab_configuration.png and b/docs/dev/setup/jenkins-gitlab/jenkins_gitlab_configuration.png differ diff --git a/docs/dev/setup/jenkins-gitlab/jenkins_local_node.png b/docs/dev/setup/jenkins-gitlab/jenkins_local_node.png index 4c6ceba8831b..8c1a9c20f55a 100644 Binary files a/docs/dev/setup/jenkins-gitlab/jenkins_local_node.png and b/docs/dev/setup/jenkins-gitlab/jenkins_local_node.png differ diff --git a/docs/dev/setup/jenkins-gitlab/jenkins_master_node.png b/docs/dev/setup/jenkins-gitlab/jenkins_master_node.png index e39e292e1784..48fe8c638a29 100644 Binary files a/docs/dev/setup/jenkins-gitlab/jenkins_master_node.png and b/docs/dev/setup/jenkins-gitlab/jenkins_master_node.png differ diff --git a/docs/dev/setup/jenkins-gitlab/jenkins_node.png b/docs/dev/setup/jenkins-gitlab/jenkins_node.png index 28fabdb3d9c9..77a89ef0b9ce 100644 Binary files a/docs/dev/setup/jenkins-gitlab/jenkins_node.png and b/docs/dev/setup/jenkins-gitlab/jenkins_node.png differ diff --git a/docs/dev/setup/jenkins-gitlab/jenkins_ssh_credentials.png b/docs/dev/setup/jenkins-gitlab/jenkins_ssh_credentials.png index f4b27958d527..9e127e1e193e 100644 Binary files a/docs/dev/setup/jenkins-gitlab/jenkins_ssh_credentials.png and b/docs/dev/setup/jenkins-gitlab/jenkins_ssh_credentials.png differ diff --git a/docs/dev/setup/jenkins-gitlab/jenkins_test_project.png b/docs/dev/setup/jenkins-gitlab/jenkins_test_project.png index 724e79cd909a..f37c0dd6ff6f 100644 Binary files a/docs/dev/setup/jenkins-gitlab/jenkins_test_project.png and b/docs/dev/setup/jenkins-gitlab/jenkins_test_project.png differ diff --git a/docs/dev/setup/jenkins-gitlab/timestamper_config.png b/docs/dev/setup/jenkins-gitlab/timestamper_config.png index 69a3a39e9da4..7fc88479c419 100644 Binary files a/docs/dev/setup/jenkins-gitlab/timestamper_config.png and b/docs/dev/setup/jenkins-gitlab/timestamper_config.png differ diff --git a/docs/user/exams/instructor/buttons/exam_timeline.png b/docs/user/exams/instructor/buttons/exam_timeline.png new file mode 100644 index 000000000000..9d1e127b3ce9 Binary files /dev/null and b/docs/user/exams/instructor/buttons/exam_timeline.png differ diff --git a/docs/user/exams/instructor/exam_timeline_example.png b/docs/user/exams/instructor/exam_timeline_example.png new file mode 100644 index 000000000000..c356e7c6a1ca Binary files /dev/null and b/docs/user/exams/instructor/exam_timeline_example.png differ diff --git a/docs/user/exams/instructors_guide.rst b/docs/user/exams/instructors_guide.rst index 49666841cee5..f82856586a94 100644 --- a/docs/user/exams/instructors_guide.rst +++ b/docs/user/exams/instructors_guide.rst @@ -451,10 +451,23 @@ If you want you can also enable the :ref:`second correction `_. +Please report any issues on the `GitHub repository `__. iOS Application --------------- @@ -107,7 +107,7 @@ The iOS application supports the following features: - + #. View your courses: #. Register in new courses #. View courses you have already registered for @@ -198,7 +198,7 @@ In this screen, users can choose which notification types they want to receive. Problems ^^^^^^^^ -Please report any issues on the `GitHub repository `_. +Please report any issues on the `GitHub repository `__. .. |server-selection-overview-android| image:: native-applications/android/server_selection_overview.png :width: 300 diff --git a/src/main/java/de/tum/in/www1/artemis/config/CacheConfiguration.java b/src/main/java/de/tum/in/www1/artemis/config/CacheConfiguration.java index 1629799b1de5..219808bb495b 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/CacheConfiguration.java +++ b/src/main/java/de/tum/in/www1/artemis/config/CacheConfiguration.java @@ -1,5 +1,6 @@ package de.tum.in.www1.artemis.config; +import java.nio.file.Path; import java.util.Collections; import javax.annotation.PreDestroy; @@ -26,6 +27,7 @@ import com.hazelcast.core.HazelcastInstance; import com.hazelcast.spring.context.SpringManagedContext; +import de.tum.in.www1.artemis.service.HazelcastPathSerializer; import de.tum.in.www1.artemis.service.scheduled.cache.quiz.QuizScheduleService; import tech.jhipster.config.JHipsterProperties; import tech.jhipster.config.cache.PrefixedKeyGenerator; @@ -110,6 +112,9 @@ public HazelcastInstance hazelcastInstance(JHipsterProperties jHipsterProperties // Allows using @SpringAware and therefore Spring Services in distributed tasks config.setManagedContext(new SpringManagedContext(applicationContext)); config.setClassLoader(applicationContext.getClassLoader()); + + config.getSerializationConfig().addSerializerConfig(createPathSerializerConfig()); + if (registration == null) { log.warn("No discovery service is set up, Hazelcast cannot create a cluster."); hazelcastBindOnlyOnInterface("127.0.0.1", config); @@ -174,6 +179,13 @@ private void hazelcastBindOnlyOnInterface(String hazelcastInterface, Config conf config.setProperty("hazelcast.socket.client.bind.any", "false"); } + private SerializerConfig createPathSerializerConfig() { + SerializerConfig serializerConfig = new SerializerConfig(); + serializerConfig.setTypeClass(Path.class); + serializerConfig.setImplementation(new HazelcastPathSerializer()); + return serializerConfig; + } + @Autowired(required = false) // ok public void setGitProperties(GitProperties gitProperties) { this.gitProperties = gitProperties; diff --git a/src/main/java/de/tum/in/www1/artemis/config/Constants.java b/src/main/java/de/tum/in/www1/artemis/config/Constants.java index cc4cc689e911..38af0f2f21f7 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/Constants.java +++ b/src/main/java/de/tum/in/www1/artemis/config/Constants.java @@ -263,6 +263,8 @@ public final class Constants { public static final int HAZELCAST_QUIZ_EXERCISE_CACHE_SERIALIZER_ID = 1; + public static final int HAZELCAST_PATH_SERIALIZER_ID = 2; + public static final String HAZELCAST_PLAGIARISM_PREFIX = "plagiarism-"; public static final String HAZELCAST_ACTIVE_PLAGIARISM_CHECKS_PER_COURSE_CACHE = HAZELCAST_PLAGIARISM_PREFIX + "active-plagiarism-checks-per-course-cache"; diff --git a/src/main/java/de/tum/in/www1/artemis/config/MetricsBean.java b/src/main/java/de/tum/in/www1/artemis/config/MetricsBean.java index 878d043d2d93..47e02b7a6233 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/MetricsBean.java +++ b/src/main/java/de/tum/in/www1/artemis/config/MetricsBean.java @@ -437,36 +437,32 @@ public void updatePublicArtemisMetrics() { if (!scheduledMetricsEnabled) { return; } - var startDate = System.currentTimeMillis(); + + final long startDate = System.currentTimeMillis(); // The authorization object has to be set because this method is not called by a user but by the scheduler SecurityUtils.setAuthorizationObject(); - ZonedDateTime now = ZonedDateTime.now(); + final ZonedDateTime now = ZonedDateTime.now(); - var courses = courseRepository.findAll(); + final List courses = courseRepository.findAllActiveWithoutTestCourses(now); // We set the number of students once to prevent multiple queries for the same date courses.forEach(course -> course.setNumberOfStudents(userRepository.countByGroupsIsContaining(course.getStudentGroupName()))); - ensureCourseInformationIsSet(courses); - var activeCourses = courses.stream() - .filter(course -> (course.getStartDate() == null || course.getStartDate().isBefore(now)) && (course.getEndDate() == null || course.getEndDate().isAfter(now))) - .toList(); - - List examsInActiveCourses = new ArrayList<>(); - activeCourses.forEach(course -> examsInActiveCourses.addAll(examRepository.findByCourseId(course.getId()))); + final List courseIds = courses.stream().mapToLong(Course::getId).boxed().toList(); + final List examsInActiveCourses = examRepository.findExamsInCourses(courseIds); // Update multi gauges - updateStudentsCourseMultiGauge(activeCourses); + updateStudentsCourseMultiGauge(courses); updateStudentsExamMultiGauge(examsInActiveCourses, courses); updateActiveUserMultiGauge(now); updateActiveExerciseMultiGauge(); updateExerciseMultiGauge(); // Update normal Gauges - activeCoursesGauge.set(activeCourses.size()); - coursesGauge.set(courses.size()); + activeCoursesGauge.set(courses.size()); + coursesGauge.set((int) courseRepository.count()); activeExamsGauge.set(examRepository.countAllActiveExams(now)); examsGauge.set((int) examRepository.count()); @@ -491,14 +487,18 @@ private void updateStudentsCourseMultiGauge(List activeCourses) { } private void updateStudentsExamMultiGauge(List examsInActiveCourses, List courses) { - studentsExamGauge.register(examsInActiveCourses.stream().map(exam -> MultiGauge.Row.of(Tags.of("examName", exam.getTitle(), - // The course semester.getCourse() is not populated (the semester property is not set) -> Use course from the courses list, which contains the semester - "semester", courses.stream().filter(course -> Objects.equals(course.getId(), exam.getCourse().getId())).findAny().map(Course::getSemester).orElse("No semester")), - studentExamRepository.findByExamId(exam.getId()).size())) + studentsExamGauge.register(examsInActiveCourses.stream() + .map(exam -> MultiGauge.Row.of(Tags.of("examName", exam.getTitle(), "semester", getExamSemester(courses, exam)), + studentExamRepository.findByExamId(exam.getId()).size())) // A mutable list is required here because otherwise the values can not be updated correctly .collect(Collectors.toCollection(ArrayList::new)), true); } + private String getExamSemester(final List courses, final Exam exam) { + // The exam.getCourse() is not populated (the semester property is not set) -> Use course from the courses list, which contains the semester + return courses.stream().filter(course -> Objects.equals(course.getId(), exam.getCourse().getId())).findAny().map(Course::getSemester).orElse("No semester"); + } + private void updateActiveExerciseMultiGauge() { var results = new ArrayList>(); var result = exerciseRepository.countActiveExercisesGroupByExerciseType(ZonedDateTime.now()); diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Attachment.java b/src/main/java/de/tum/in/www1/artemis/domain/Attachment.java index 705b61ccd009..153b9d7b2fd7 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Attachment.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Attachment.java @@ -15,6 +15,7 @@ import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.enumeration.AttachmentType; import de.tum.in.www1.artemis.domain.lecture.AttachmentUnit; +import de.tum.in.www1.artemis.service.EntityFileService; import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.FileService; @@ -27,9 +28,15 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public class Attachment extends DomainObject implements Serializable { + @Transient + private final transient FilePathService filePathService = new FilePathService(); + @Transient private final transient FileService fileService = new FileService(); + @Transient + private final transient EntityFileService entityFileService = new EntityFileService(fileService, filePathService); + @Transient private String prevLink; @@ -97,7 +104,17 @@ else if (attachmentType == AttachmentType.FILE && getAttachmentUnit() != null && */ @PrePersist public void beforeCreate() { - handleFileChange(); + if (link == null) { + return; + } + if (attachmentType == AttachmentType.FILE && getLecture() != null) { + Path targetFolder = FilePathService.getLectureAttachmentFilePath().resolve(getLecture().getId().toString()); + link = entityFileService.moveFileBeforeEntityPersistenceWithIdIfIsTemp(link, targetFolder, true, getLecture().getId()); + } + else if (attachmentType == AttachmentType.FILE && getAttachmentUnit() != null) { + Path targetFolder = FilePathService.getAttachmentUnitFilePath().resolve(getAttachmentUnit().getId().toString()); + link = entityFileService.moveFileBeforeEntityPersistenceWithIdIfIsTemp(link, targetFolder, true, getAttachmentUnit().getId()); + } } /** @@ -121,19 +138,13 @@ else if (attachmentType == AttachmentType.FILE && link != null && link.contains( */ @PreUpdate public void onUpdate() { - handleFileChange(); - } - - private void handleFileChange() { if (attachmentType == AttachmentType.FILE && getLecture() != null) { - // move file and delete old file if necessary - var targetFolder = Path.of(FilePathService.getLectureAttachmentFilePath(), getLecture().getId().toString()).toString(); - link = fileService.manageFilesForUpdatedFilePath(prevLink, link, targetFolder, getLecture().getId(), true); + Path targetFolder = FilePathService.getLectureAttachmentFilePath().resolve(getLecture().getId().toString()); + link = entityFileService.handlePotentialFileUpdateBeforeEntityPersistence(getLecture().getId(), prevLink, link, targetFolder, true); } else if (attachmentType == AttachmentType.FILE && getAttachmentUnit() != null) { - // move file and delete old file if necessary - var targetFolder = Path.of(FilePathService.getAttachmentUnitFilePath(), getAttachmentUnit().getId().toString()).toString(); - link = fileService.manageFilesForUpdatedFilePath(prevLink, link, targetFolder, getAttachmentUnit().getId(), true); + Path targetFolder = FilePathService.getAttachmentUnitFilePath().resolve(getAttachmentUnit().getId().toString()); + link = entityFileService.handlePotentialFileUpdateBeforeEntityPersistence(getAttachmentUnit().getId(), prevLink, link, targetFolder, true); } } @@ -143,15 +154,8 @@ else if (attachmentType == AttachmentType.FILE && getAttachmentUnit() != null) { */ @PostRemove public void onDelete() { - if (attachmentType == AttachmentType.FILE && getLecture() != null) { - // delete old file if necessary - var targetFolder = Path.of(FilePathService.getLectureAttachmentFilePath(), getLecture().getId().toString()).toString(); - fileService.manageFilesForUpdatedFilePath(prevLink, null, targetFolder, getLecture().getId(), true); - } - else if (attachmentType == AttachmentType.FILE && getAttachmentUnit() != null) { - // delete old file if necessary - var targetFolder = Path.of(FilePathService.getAttachmentUnitFilePath(), getAttachmentUnit().getId().toString()).toString(); - fileService.manageFilesForUpdatedFilePath(prevLink, null, targetFolder, getAttachmentUnit().getId(), true); + if (prevLink != null && attachmentType == AttachmentType.FILE) { + fileService.schedulePathForDeletion(Path.of(prevLink), 0); } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Course.java b/src/main/java/de/tum/in/www1/artemis/domain/Course.java index ed728a567fa1..be230de89c1b 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Course.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Course.java @@ -2,6 +2,7 @@ import static de.tum.in.www1.artemis.config.Constants.*; +import java.nio.file.Path; import java.time.ZonedDateTime; import java.util.HashSet; import java.util.Set; @@ -30,6 +31,7 @@ import de.tum.in.www1.artemis.domain.tutorialgroups.TutorialGroup; import de.tum.in.www1.artemis.domain.tutorialgroups.TutorialGroupsConfiguration; import de.tum.in.www1.artemis.domain.view.QuizView; +import de.tum.in.www1.artemis.service.EntityFileService; import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @@ -48,7 +50,13 @@ public class Course extends DomainObject { private static final int DEFAULT_COMPLAINT_TEXT_LIMIT = 2000; @Transient - private transient FileService fileService = new FileService(); + private final transient FilePathService filePathService = new FilePathService(); + + @Transient + private final transient FileService fileService = new FileService(); + + @Transient + private final transient EntityFileService entityFileService = new EntityFileService(fileService, filePathService); @Transient private String prevCourseIcon; @@ -679,8 +687,9 @@ public void onLoad() { @PrePersist public void beforeCreate() { - // move file if necessary (id at this point will be null, so placeholder will be inserted) - courseIcon = fileService.manageFilesForUpdatedFilePath(prevCourseIcon, courseIcon, FilePathService.getCourseIconFilePath(), getId()); + if (courseIcon != null) { + courseIcon = entityFileService.moveTempFileBeforeEntityPersistence(courseIcon, FilePathService.getCourseIconFilePath(), false); + } } @PostPersist @@ -694,13 +703,14 @@ public void afterCreate() { @PreUpdate public void onUpdate() { // move file and delete old file if necessary - courseIcon = fileService.manageFilesForUpdatedFilePath(prevCourseIcon, courseIcon, FilePathService.getCourseIconFilePath(), getId()); + courseIcon = entityFileService.handlePotentialFileUpdateBeforeEntityPersistence(getId(), prevCourseIcon, courseIcon, FilePathService.getCourseIconFilePath(), false); } @PostRemove public void onDelete() { - // delete old file if necessary - fileService.manageFilesForUpdatedFilePath(prevCourseIcon, null, FilePathService.getCourseIconFilePath(), getId()); + if (prevCourseIcon != null) { + fileService.schedulePathForDeletion(Path.of(prevCourseIcon), 0); + } } @Override diff --git a/src/main/java/de/tum/in/www1/artemis/domain/FileUploadSubmission.java b/src/main/java/de/tum/in/www1/artemis/domain/FileUploadSubmission.java index 2fee2ecf8998..29bafff4bcb4 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/FileUploadSubmission.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/FileUploadSubmission.java @@ -4,11 +4,8 @@ import javax.persistence.*; -import org.apache.commons.lang3.math.NumberUtils; - import com.fasterxml.jackson.annotation.JsonInclude; -import de.tum.in.www1.artemis.exception.FilePathParsingException; import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.FileService; @@ -26,7 +23,7 @@ public String getSubmissionExerciseType() { } @Transient - private transient FileService fileService = new FileService(); + private final transient FileService fileService = new FileService(); @Column(name = "file_path") private String filePath; @@ -37,14 +34,7 @@ public String getSubmissionExerciseType() { @PostRemove public void onDelete() { if (filePath != null) { - // delete old file if necessary - final var splittedPath = filePath.split("/"); - final var shouldBeExerciseId = splittedPath.length >= 5 ? splittedPath[4] : null; - if (!NumberUtils.isCreatable(shouldBeExerciseId)) { - throw new FilePathParsingException("Unexpected String in upload file path. Should contain the exercise ID: " + shouldBeExerciseId); - } - final var exerciseId = Long.parseLong(shouldBeExerciseId); - fileService.manageFilesForUpdatedFilePath(filePath, null, FileUploadSubmission.buildFilePath(exerciseId, getId()), getId(), true); + fileService.schedulePathForDeletion(Path.of(filePath), 0); } } @@ -59,8 +49,8 @@ public String getFilePath() { * @param submissionId the id of the submission * @return path where submission for file upload exercise is stored */ - public static String buildFilePath(Long exerciseId, Long submissionId) { - return Path.of(FilePathService.getFileUploadExercisesFilePath(), String.valueOf(exerciseId), String.valueOf(submissionId)).toString(); + public static Path buildFilePath(Long exerciseId, Long submissionId) { + return FilePathService.getFileUploadExercisesFilePath().resolve(exerciseId.toString()).resolve(submissionId.toString()); } public void setFilePath(String filePath) { diff --git a/src/main/java/de/tum/in/www1/artemis/domain/exam/ExamUser.java b/src/main/java/de/tum/in/www1/artemis/domain/exam/ExamUser.java index ee80e01e05c6..e0a18932ed91 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/exam/ExamUser.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/exam/ExamUser.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.domain.exam; +import java.nio.file.Path; + import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.JoinColumn; @@ -18,6 +20,7 @@ import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.AbstractAuditingEntity; import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.service.EntityFileService; import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.FileService; @@ -26,9 +29,15 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public class ExamUser extends AbstractAuditingEntity { + @Transient + private final transient FilePathService filePathService = new FilePathService(); + @Transient private final transient FileService fileService = new FileService(); + @Transient + private final transient EntityFileService entityFileService = new EntityFileService(fileService, filePathService); + @Transient private String prevSigningImagePath; @@ -189,11 +198,18 @@ public void onLoad() { prevStudentImagePath = studentImagePath; // save current path as old path (needed to know old path in onUpdate() and onDelete()) } + /** + * Will be called before the entity is persisted (saved). + * Manages files by taking care of file system changes for this entity. + */ @PrePersist public void beforeCreate() { - // move file if necessary (id at this point will be null, so placeholder will be inserted) - signingImagePath = fileService.manageFilesForUpdatedFilePath(prevSigningImagePath, signingImagePath, FilePathService.getExamUserSignatureFilePath(), getId()); - studentImagePath = fileService.manageFilesForUpdatedFilePath(prevStudentImagePath, studentImagePath, FilePathService.getStudentImageFilePath(), getId()); + if (signingImagePath != null) { + signingImagePath = entityFileService.moveTempFileBeforeEntityPersistence(signingImagePath, FilePathService.getExamUserSignatureFilePath(), false); + } + if (studentImagePath != null) { + studentImagePath = entityFileService.moveTempFileBeforeEntityPersistence(studentImagePath, FilePathService.getStudentImageFilePath(), false); + } } /** @@ -212,17 +228,29 @@ public void afterCreate() { } } + /** + * Will be called before the entity is flushed. + * Manages files by taking care of file system changes for this entity. + */ @PreUpdate public void onUpdate() { - // move file and delete old file if necessary - signingImagePath = fileService.manageFilesForUpdatedFilePath(prevSigningImagePath, signingImagePath, FilePathService.getExamUserSignatureFilePath(), getId()); - studentImagePath = fileService.manageFilesForUpdatedFilePath(prevStudentImagePath, studentImagePath, FilePathService.getStudentImageFilePath(), getId()); + signingImagePath = entityFileService.handlePotentialFileUpdateBeforeEntityPersistence(getId(), prevSigningImagePath, signingImagePath, + FilePathService.getExamUserSignatureFilePath(), false); + studentImagePath = entityFileService.handlePotentialFileUpdateBeforeEntityPersistence(getId(), prevStudentImagePath, studentImagePath, + FilePathService.getStudentImageFilePath(), false); } + /** + * Will be called after the entity is removed (deleted). + * Manages files by taking care of file system changes for this entity. + */ @PostRemove public void onDelete() { - // delete old file if necessary - fileService.manageFilesForUpdatedFilePath(prevSigningImagePath, null, FilePathService.getExamUserSignatureFilePath(), getId()); - fileService.manageFilesForUpdatedFilePath(prevStudentImagePath, null, FilePathService.getStudentImageFilePath(), getId()); + if (signingImagePath != null) { + fileService.schedulePathForDeletion(Path.of(prevSigningImagePath), 0); + } + if (studentImagePath != null) { + fileService.schedulePathForDeletion(Path.of(prevStudentImagePath), 0); + } } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lecture/Slide.java b/src/main/java/de/tum/in/www1/artemis/domain/lecture/Slide.java index 5b3f91c68de5..02835ba8797d 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lecture/Slide.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lecture/Slide.java @@ -7,7 +7,6 @@ import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.PostLoad; -import javax.persistence.PostPersist; import javax.persistence.PostRemove; import javax.persistence.PrePersist; import javax.persistence.PreUpdate; @@ -19,6 +18,7 @@ import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.DomainObject; +import de.tum.in.www1.artemis.service.EntityFileService; import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.FileService; @@ -27,9 +27,15 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public class Slide extends DomainObject { + @Transient + private final transient FilePathService filePathService = new FilePathService(); + @Transient private final transient FileService fileService = new FileService(); + @Transient + private final transient EntityFileService entityFileService = new EntityFileService(fileService, filePathService); + @Transient private String prevSlideImagePath; @@ -80,35 +86,30 @@ public void onLoad() { prevSlideImagePath = slideImagePath; // save current path as old path (needed to know old path in onUpdate() and onDelete()) } - @PrePersist - public void beforeCreate() { - var targetFolder = Path.of(FilePathService.getAttachmentUnitFilePath(), getAttachmentUnit().getId().toString(), "slide", String.valueOf(getSlideNumber())).toString(); - slideImagePath = fileService.manageFilesForUpdatedFilePath(prevSlideImagePath, slideImagePath, targetFolder, (long) getSlideNumber(), false); - } - /** - * Will be called after the entity is persisted (saved). - * Manages files by taking care of file system changes for this entity. + * Before persisting the slide, we need to move the file from the temp folder to the actual folder */ - @PostPersist - public void afterCreate() { - // replace placeholder with actual id if necessary (id is no longer null at this point) - if (slideImagePath != null && slideImagePath.contains(Constants.FILEPATH_ID_PLACEHOLDER)) { - slideImagePath = slideImagePath.replace(Constants.FILEPATH_ID_PLACEHOLDER, getAttachmentUnit().getId().toString()); + @PrePersist + public void beforeCreate() { + if (slideImagePath == null) { + return; } + slideImagePath = entityFileService.moveFileBeforeEntityPersistenceWithIdIfIsTemp(slideImagePath, getTargetFolder(), false, (long) getSlideNumber()); } @PreUpdate public void onUpdate() { - // move file and delete old file if necessary - var targetFolder = Path.of(FilePathService.getAttachmentUnitFilePath(), getAttachmentUnit().getId().toString(), "slide", String.valueOf(getSlideNumber())).toString(); - slideImagePath = fileService.manageFilesForUpdatedFilePath(prevSlideImagePath, slideImagePath, targetFolder, (long) getSlideNumber(), false); + slideImagePath = entityFileService.handlePotentialFileUpdateBeforeEntityPersistence((long) getSlideNumber(), prevSlideImagePath, slideImagePath, getTargetFolder(), false); } @PostRemove public void onDelete() { - // delete old file if necessary - var targetFolder = Path.of(FilePathService.getAttachmentUnitFilePath(), getAttachmentUnit().getId().toString(), "slide", String.valueOf(getSlideNumber())).toString(); - fileService.manageFilesForUpdatedFilePath(prevSlideImagePath, null, targetFolder, (long) getSlideNumber(), false); + if (prevSlideImagePath != null) { + fileService.schedulePathForDeletion(Path.of(prevSlideImagePath), 0); + } + } + + private Path getTargetFolder() { + return FilePathService.getAttachmentUnitFilePath().resolve(Path.of(getAttachmentUnit().getId().toString(), "slide", String.valueOf(getSlideNumber()))); } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragAndDropQuestion.java b/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragAndDropQuestion.java index cafba8b0bf5e..acd0e7ce53fc 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragAndDropQuestion.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragAndDropQuestion.java @@ -1,5 +1,6 @@ package de.tum.in.www1.artemis.domain.quiz; +import java.nio.file.Path; import java.util.*; import javax.persistence.*; @@ -14,6 +15,7 @@ import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.quiz.scoring.*; import de.tum.in.www1.artemis.domain.view.QuizView; +import de.tum.in.www1.artemis.service.EntityFileService; import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.FileService; @@ -26,7 +28,13 @@ public class DragAndDropQuestion extends QuizQuestion { @Transient - private transient FileService fileService = new FileService(); + private final transient FilePathService filePathService = new FilePathService(); + + @Transient + private final transient FileService fileService = new FileService(); + + @Transient + private final transient EntityFileService entityFileService = new EntityFileService(fileService, filePathService); @Transient private String prevBackgroundFilePath; @@ -162,8 +170,9 @@ public void onLoad() { @PrePersist public void beforeCreate() { - // move file if necessary (id at this point will be null, so placeholder will be inserted) - backgroundFilePath = fileService.manageFilesForUpdatedFilePath(prevBackgroundFilePath, backgroundFilePath, FilePathService.getDragAndDropBackgroundFilePath(), getId()); + if (backgroundFilePath != null) { + backgroundFilePath = entityFileService.moveTempFileBeforeEntityPersistence(backgroundFilePath, FilePathService.getDragAndDropBackgroundFilePath(), false); + } } @PostPersist @@ -176,14 +185,15 @@ public void afterCreate() { @PreUpdate public void onUpdate() { - // move file and delete old file if necessary - backgroundFilePath = fileService.manageFilesForUpdatedFilePath(prevBackgroundFilePath, backgroundFilePath, FilePathService.getDragAndDropBackgroundFilePath(), getId()); + backgroundFilePath = entityFileService.handlePotentialFileUpdateBeforeEntityPersistence(getId(), prevBackgroundFilePath, backgroundFilePath, + FilePathService.getDragAndDropBackgroundFilePath(), false); } @PostRemove public void onDelete() { - // delete old file if necessary - fileService.manageFilesForUpdatedFilePath(prevBackgroundFilePath, null, FilePathService.getDragAndDropBackgroundFilePath(), getId()); + if (prevBackgroundFilePath != null) { + fileService.schedulePathForDeletion(Path.of(prevBackgroundFilePath), 0); + } } /** diff --git a/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragItem.java b/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragItem.java index cd0559973441..a328ee1704f5 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragItem.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/quiz/DragItem.java @@ -1,5 +1,6 @@ package de.tum.in.www1.artemis.domain.quiz; +import java.nio.file.Path; import java.util.HashSet; import java.util.Set; @@ -15,6 +16,7 @@ import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.TempIdObject; import de.tum.in.www1.artemis.domain.view.QuizView; +import de.tum.in.www1.artemis.service.EntityFileService; import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.FileService; @@ -28,7 +30,13 @@ public class DragItem extends TempIdObject implements QuizQuestionComponent { @Transient - private transient FileService fileService = new FileService(); + private final transient FilePathService filePathService = new FilePathService(); + + @Transient + private final transient FileService fileService = new FileService(); + + @Transient + private final transient EntityFileService entityFileService = new EntityFileService(fileService, filePathService); @Transient private String prevPictureFilePath; @@ -138,8 +146,9 @@ public void onLoad() { @PrePersist public void beforeCreate() { - // move file if necessary (id at this point will be null, so placeholder will be inserted) - pictureFilePath = fileService.manageFilesForUpdatedFilePath(prevPictureFilePath, pictureFilePath, FilePathService.getDragItemFilePath(), getId()); + if (pictureFilePath != null) { + pictureFilePath = entityFileService.moveTempFileBeforeEntityPersistence(pictureFilePath, FilePathService.getDragItemFilePath(), false); + } } @PostPersist @@ -152,14 +161,15 @@ public void afterCreate() { @PreUpdate public void onUpdate() { - // move file and delete old file if necessary - pictureFilePath = fileService.manageFilesForUpdatedFilePath(prevPictureFilePath, pictureFilePath, FilePathService.getDragItemFilePath(), getId()); + pictureFilePath = entityFileService.handlePotentialFileUpdateBeforeEntityPersistence(getId(), prevPictureFilePath, pictureFilePath, FilePathService.getDragItemFilePath(), + false); } @PostRemove public void onDelete() { - // delete old file if necessary - fileService.manageFilesForUpdatedFilePath(prevPictureFilePath, null, FilePathService.getDragItemFilePath(), getId()); + if (prevPictureFilePath != null) { + fileService.schedulePathForDeletion(Path.of(prevPictureFilePath), 0); + } } @Override diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java index 301f88ef300d..c7b97619da85 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java @@ -81,12 +81,22 @@ SELECT CASE WHEN (count(c) > 0) THEN true ELSE false END boolean informationSharingConfigurationIsOneOf(@Param("courseId") long courseId, @Param("values") Set values); @Query(""" - SELECT DISTINCT c FROM Course c + SELECT DISTINCT c + FROM Course c WHERE (c.startDate <= :now OR c.startDate IS NULL) AND (c.endDate >= :now OR c.endDate IS NULL) """) List findAllActive(@Param("now") ZonedDateTime now); + @Query(""" + SELECT DISTINCT c + FROM Course c + WHERE (c.startDate <= :now OR c.startDate IS NULL) + AND (c.endDate >= :now OR c.endDate IS NULL) + AND c.testCourse = false + """) + List findAllActiveWithoutTestCourses(@Param("now") ZonedDateTime now); + /** * Note: you should not add exercises or exercises+categories here, because this would make the query too complex and would take significantly longer * diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java index 5d6ff8d80255..bb02b590b690 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java @@ -31,6 +31,13 @@ public interface ExamRepository extends JpaRepository { List findByCourseId(long courseId); + @Query(""" + SELECT DISTINCT exam + FROM Exam exam + WHERE exam.course.id IN :courses + """) + List findExamsInCourses(@Param("courses") Iterable courseId); + @Query(""" SELECT DISTINCT ex FROM Exam ex diff --git a/src/main/java/de/tum/in/www1/artemis/service/AttachmentUnitService.java b/src/main/java/de/tum/in/www1/artemis/service/AttachmentUnitService.java index d4a8023c2f55..103045c8daef 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/AttachmentUnitService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/AttachmentUnitService.java @@ -1,5 +1,6 @@ package de.tum.in.www1.artemis.service; +import java.net.URI; import java.time.ZonedDateTime; import java.util.Objects; @@ -25,6 +26,8 @@ public class AttachmentUnitService { private final FileService fileService; + private final FilePathService filePathService; + private final CacheManager cacheManager; private final SlideSplitterService slideSplitterService; @@ -32,10 +35,11 @@ public class AttachmentUnitService { private final SlideRepository slideRepository; public AttachmentUnitService(SlideRepository slideRepository, SlideSplitterService slideSplitterService, AttachmentUnitRepository attachmentUnitRepository, - AttachmentRepository attachmentRepository, FileService fileService, CacheManager cacheManager) { + AttachmentRepository attachmentRepository, FileService fileService, FilePathService filePathService, CacheManager cacheManager) { this.attachmentUnitRepository = attachmentUnitRepository; this.attachmentRepository = attachmentRepository; this.fileService = fileService; + this.filePathService = filePathService; this.cacheManager = cacheManager; this.slideSplitterService = slideSplitterService; this.slideRepository = slideRepository; @@ -140,7 +144,7 @@ private void updateAttachment(Attachment existingAttachment, Attachment updateAt */ private void handleFile(MultipartFile file, Attachment attachment, boolean keepFilename) { if (file != null && !file.isEmpty()) { - String filePath = fileService.handleSaveFile(file, keepFilename, false); + String filePath = fileService.handleSaveFile(file, keepFilename, false).toString(); attachment.setLink(filePath); attachment.setUploadDate(ZonedDateTime.now()); } @@ -154,7 +158,7 @@ private void handleFile(MultipartFile file, Attachment attachment, boolean keepF */ private void evictCache(MultipartFile file, AttachmentUnit attachmentUnit) { if (file != null && !file.isEmpty()) { - this.cacheManager.getCache("files").evict(fileService.actualPathForPublicPath(attachmentUnit.getAttachment().getLink())); + this.cacheManager.getCache("files").evict(filePathService.actualPathForPublicPath(URI.create(attachmentUnit.getAttachment().getLink())).toString()); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/AuthorizationCheckService.java b/src/main/java/de/tum/in/www1/artemis/service/AuthorizationCheckService.java index 9e30e5b261c8..e786797c2827 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/AuthorizationCheckService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/AuthorizationCheckService.java @@ -5,6 +5,7 @@ import java.util.function.Consumer; import java.util.regex.Pattern; +import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.validation.constraints.NotNull; @@ -62,6 +63,7 @@ public AuthorizationCheckService(UserRepository userRepository, CourseRepository * @param exercise belongs to a course that will be checked for permission rights * @return true if the currently logged-in user is at least an editor (also if the user is instructor or admin), false otherwise */ + @CheckReturnValue public boolean isAtLeastEditorForExercise(@NotNull Exercise exercise) { return isAtLeastEditorInCourse(exercise.getCourseViaExerciseGroupOrCourseMember(), null); } @@ -74,6 +76,7 @@ public boolean isAtLeastEditorForExercise(@NotNull Exercise exercise) { * @param user the user whose permissions should be checked * @return true if the currently logged-in user is at least an editor, false otherwise */ + @CheckReturnValue public boolean isAtLeastEditorForExercise(@NotNull Exercise exercise, @Nullable User user) { return isAtLeastEditorInCourse(exercise.getCourseViaExerciseGroupOrCourseMember(), user); } @@ -98,6 +101,7 @@ private void checkIsAtLeastEditorInCourseElseThrow(@NotNull Course course, @Null * @param user the user whose permissions should be checked * @return true if the passed user is at least an editor in the course, false otherwise */ + @CheckReturnValue public boolean isAtLeastEditorInCourse(@NotNull Course course, @Nullable User user) { user = loadUserIfNeeded(user); return isEditorInCourse(course, user) || isInstructorInCourse(course, user) || isAdmin(user); @@ -107,12 +111,13 @@ public boolean isAtLeastEditorInCourse(@NotNull Course course, @Nullable User us * Given any type of exercise, the method returns if the current user is at least TA for the course the exercise belongs to. If exercise is not present, it will return false, * because the optional will be empty, and therefore `isPresent()` will return false This is due how `filter` works: If a value is present, apply the provided mapping function * to it, and if the result is non-null, return an Optional describing the result. Otherwise, return an empty Optional. - * https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html#filter-java.util.function.Predicate + * https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Optional.html#filter(java.util.function.Predicate) * * @param exercise the exercise that needs to be checked * @param The type of the concrete exercise, because Exercise is an abstract class * @return true if the user is at least a teaching assistant (also if the user is instructor or admin) in the course of the given exercise */ + @CheckReturnValue public boolean isAtLeastTeachingAssistantForExercise(Optional exercise) { return exercise.filter(this::isAtLeastTeachingAssistantForExercise).isPresent(); } @@ -124,6 +129,7 @@ public boolean isAtLeastTeachingAssistantForExercise(Option * @param exercise belongs to a course that will be checked for permission rights * @return true if the currently logged-in user is at least a teaching assistant (also if the user is instructor or admin), false otherwise */ + @CheckReturnValue public boolean isAtLeastTeachingAssistantForExercise(@NotNull Exercise exercise) { return isAtLeastTeachingAssistantInCourse(exercise.getCourseViaExerciseGroupOrCourseMember(), null); } @@ -136,6 +142,7 @@ public boolean isAtLeastTeachingAssistantForExercise(@NotNull Exercise exercise) * @param user the user whose permissions should be checked * @return true if the passed user is at least a teaching assistant (also if the user is instructor or admin), false otherwise */ + @CheckReturnValue public boolean isAtLeastTeachingAssistantForExercise(@NotNull Exercise exercise, @Nullable User user) { user = loadUserIfNeeded(user); return isAtLeastTeachingAssistantInCourse(exercise.getCourseViaExerciseGroupOrCourseMember(), user); @@ -147,6 +154,7 @@ public boolean isAtLeastTeachingAssistantForExercise(@NotNull Exercise exercise, * @param exercise belongs to a course that will be checked for permission rights * @return true if the currently logged-in user is at least a student (also if the user is teaching assistant, instructor or admin), false otherwise */ + @CheckReturnValue public boolean isAtLeastStudentForExercise(@NotNull Exercise exercise) { return isAtLeastStudentForExercise(exercise, null); } @@ -158,6 +166,7 @@ public boolean isAtLeastStudentForExercise(@NotNull Exercise exercise) { * @param user the user whose permissions should be checked * @return true if the currently logged-in user is at least a student (also if the user is teaching assistant, instructor or admin), false otherwise */ + @CheckReturnValue public boolean isAtLeastStudentForExercise(@NotNull Exercise exercise, @Nullable User user) { user = loadUserIfNeeded(user); return isStudentInCourse(exercise.getCourseViaExerciseGroupOrCourseMember(), user) || isAtLeastTeachingAssistantForExercise(exercise, user); @@ -183,6 +192,7 @@ private void checkIsAtLeastTeachingAssistantInCourseElseThrow(@NotNull Course co * @param user the user whose permissions should be checked * @return true if the passed user is at least a teaching assistant in the course (also if the user is instructor or admin), false otherwise */ + @CheckReturnValue public boolean isAtLeastTeachingAssistantInCourse(@NotNull Course course, @Nullable User user) { user = loadUserIfNeeded(user); return isTeachingAssistantInCourse(course, user) || isEditorInCourse(course, user) || isInstructorInCourse(course, user) || isAdmin(user); @@ -220,6 +230,7 @@ private enum EnrollmentAuthorization { * @return `EnrollmentAuthorization.ALLOWED` if the user is allowed to self enroll in the course, * or the reason why the user is not allowed to self enroll in the course otherwise */ + @CheckReturnValue public EnrollmentAuthorization getUserEnrollmentAuthorizationForCourse(User user, Course course) { if (allowedCourseEnrollmentUsernamePattern != null && !allowedCourseEnrollmentUsernamePattern.matcher(user.getLogin()).matches()) { return EnrollmentAuthorization.USERNAME_PATTERN; @@ -248,6 +259,7 @@ public EnrollmentAuthorization getUserEnrollmentAuthorizationForCourse(User user * @param course The course to which the user wants to self enroll * @return boolean, true if the user is allowed to self enroll in the course, false otherwise */ + @CheckReturnValue public boolean isUserAllowedToSelfEnrollInCourse(User user, Course course) { return EnrollmentAuthorization.ALLOWED.equals(getUserEnrollmentAuthorizationForCourse(user, course)); } @@ -290,6 +302,7 @@ private enum UnenrollmentAuthorization { * @return `UnenrollmentAuthorization.ALLOWED` if the user is allowed to self unenroll from the course, * or the reason why the user is not allowed to self unenroll from the course otherwise */ + @CheckReturnValue public UnenrollmentAuthorization getUserUnenrollmentAuthorizationForCourse(User user, Course course) { if (!course.isUnenrollmentEnabled()) { return UnenrollmentAuthorization.UNENROLLMENT_STATUS; @@ -326,6 +339,7 @@ public void checkUserAllowedToUnenrollFromCourseElseThrow(User user, Course cour * @param user the user whose permissions should be checked * @return true if the passed user is at least a teaching assistant in the course (also if the user is instructor or admin), false otherwise */ + @CheckReturnValue public boolean isAtLeastStudentInCourse(@NotNull Course course, @Nullable User user) { user = loadUserIfNeeded(user); return isStudentInCourse(course, user) || isTeachingAssistantInCourse(course, user) || isEditorInCourse(course, user) || isInstructorInCourse(course, user) @@ -340,6 +354,7 @@ public boolean isAtLeastStudentInCourse(@NotNull Course course, @Nullable User u * @param user the user whose permissions should be checked * @return true if the currently logged-in user is at least an instructor (or admin), false otherwise */ + @CheckReturnValue public boolean isAtLeastInstructorForExercise(@NotNull Exercise exercise, @Nullable User user) { return isAtLeastInstructorInCourse(exercise.getCourseViaExerciseGroupOrCourseMember(), user); } @@ -350,6 +365,7 @@ public boolean isAtLeastInstructorForExercise(@NotNull Exercise exercise, @Nulla * @param exercise belongs to a course that will be checked for permission rights * @return true if the currently logged-in user is at least an instructor (or admin), false otherwise */ + @CheckReturnValue public boolean isAtLeastInstructorForExercise(@NotNull Exercise exercise) { return isAtLeastInstructorForExercise(exercise, null); } @@ -420,6 +436,7 @@ private void checkIsAtLeastInstructorInCourseElseThrow(@NotNull Course course, @ * @param user the user whose permissions should be checked * @return true if the passed user is at least instructor in the course (also if the user is admin), false otherwise */ + @CheckReturnValue public boolean isAtLeastInstructorInCourse(@NotNull Course course, @Nullable User user) { user = loadUserIfNeeded(user); return user.getGroups().contains(course.getInstructorGroupName()) || isAdmin(user); @@ -432,6 +449,7 @@ public boolean isAtLeastInstructorInCourse(@NotNull Course course, @Nullable Use * @param user the user whose permissions should be checked * @return true, if user is instructor of this course, otherwise false */ + @CheckReturnValue public boolean isInstructorInCourse(@NotNull Course course, @Nullable User user) { user = loadUserIfNeeded(user); return user.getGroups().contains(course.getInstructorGroupName()); @@ -444,6 +462,7 @@ public boolean isInstructorInCourse(@NotNull Course course, @Nullable User user) * @param user the user whose permissions should be checked * @return true, if user is an editor of this course, otherwise false */ + @CheckReturnValue public boolean isEditorInCourse(@NotNull Course course, @Nullable User user) { user = loadUserIfNeeded(user); return user.getGroups().contains(course.getEditorGroupName()); @@ -456,6 +475,7 @@ public boolean isEditorInCourse(@NotNull Course course, @Nullable User user) { * @param user the user whose permissions should be checked * @return true, if user is teaching assistant of this course, otherwise false */ + @CheckReturnValue public boolean isTeachingAssistantInCourse(@NotNull Course course, @Nullable User user) { user = loadUserIfNeeded(user); return user.getGroups().contains(course.getTeachingAssistantGroupName()); @@ -468,6 +488,7 @@ public boolean isTeachingAssistantInCourse(@NotNull Course course, @Nullable Use * @param user the user whose permissions should be checked * @return true, if user is only student of this course, otherwise false */ + @CheckReturnValue public boolean isOnlyStudentInCourse(@NotNull Course course, @Nullable User user) { user = loadUserIfNeeded(user); return user.getGroups().contains(course.getStudentGroupName()) && !isAtLeastTeachingAssistantInCourse(course, user); @@ -480,6 +501,7 @@ public boolean isOnlyStudentInCourse(@NotNull Course course, @Nullable User user * @param user the user whose permissions should be checked * @return true, if user is student of this course, otherwise false */ + @CheckReturnValue public boolean isStudentInCourse(@NotNull Course course, @Nullable User user) { user = loadUserIfNeeded(user); return user.getGroups().contains(course.getStudentGroupName()); @@ -491,6 +513,7 @@ public boolean isStudentInCourse(@NotNull Course course, @Nullable User user) { * @param participation the participation that needs to be checked * @return true, if user is student is owner of this participation, otherwise false */ + @CheckReturnValue public boolean isOwnerOfParticipation(@NotNull StudentParticipation participation) { if (participation.getParticipant() == null) { return false; @@ -519,6 +542,7 @@ public void isOwnerOfParticipationElseThrow(@NotNull StudentParticipation partic * @param user the user whose permissions should be checked * @return true, if user is student is owner of this participation, otherwise false */ + @CheckReturnValue public boolean isOwnerOfParticipation(@NotNull StudentParticipation participation, @Nullable User user) { user = loadUserIfNeeded(user); if (participation.getParticipant() == null) { @@ -536,6 +560,7 @@ public boolean isOwnerOfParticipation(@NotNull StudentParticipation participatio * @param user the user whose permissions should be checked * @return true if user is owner of this team, otherwise false */ + @CheckReturnValue public boolean isOwnerOfTeam(@NotNull Team team, @NotNull User user) { return user.equals(team.getOwner()); } @@ -548,6 +573,7 @@ public boolean isOwnerOfTeam(@NotNull Team team, @NotNull User user) { * @param user the user whose permissions should be checked * @return true, if user is student is owner of this team, otherwise false */ + @CheckReturnValue public boolean isStudentInTeam(@NotNull Course course, String teamShortName, @NotNull User user) { return userRepository.findAllInTeam(course.getId(), teamShortName).contains(user); } @@ -559,6 +585,7 @@ public boolean isStudentInTeam(@NotNull Course course, String teamShortName, @No * @param user the user whose permissions should be checked * @return true, if user is allowed to see this exercise, otherwise false */ + @CheckReturnValue public boolean isAllowedToSeeExercise(@NotNull Exercise exercise, @Nullable User user) { user = loadUserIfNeeded(user); if (isAdmin(user)) { @@ -594,6 +621,7 @@ public void checkIsAllowedToSeeLectureElseThrow(@NotNull Lecture lecture, @Nulla * @param user the user for which to check permission * @return true if the user is allowed, false otherwise */ + @CheckReturnValue public boolean isAllowedToSeeLectureUnit(@NotNull LectureUnit lectureUnit, @Nullable User user) { user = loadUserIfNeeded(user); if (isAdmin(user)) { @@ -611,6 +639,7 @@ public boolean isAllowedToSeeLectureUnit(@NotNull LectureUnit lectureUnit, @Null * * @return true, if user is admin, otherwise false */ + @CheckReturnValue public boolean isAdmin() { return SecurityUtils.isCurrentUserInRole(Role.ADMIN.getAuthority()); } @@ -621,6 +650,7 @@ public boolean isAdmin() { * @param user the user with authorities. If the user is null, the currently logged-in user will be used. * @return true, if user is admin, otherwise false */ + @CheckReturnValue public boolean isAdmin(@Nullable User user) { if (user == null) { return isAdmin(); @@ -649,6 +679,7 @@ public void checkIsAdminElseThrow(@Nullable User user) { * @param result the result that should be sent to the client * @return true if the user is allowed to retrieve the given result, false otherwise */ + @CheckReturnValue public boolean isUserAllowedToGetResult(Exercise exercise, StudentParticipation participation, Result result) { return isAtLeastStudentForExercise(exercise) && (isOwnerOfParticipation(participation) || isAtLeastInstructorForExercise(exercise)) && ExerciseDateService.isAfterAssessmentDueDate(exercise) && result.getAssessor() != null && result.getCompletionDate() != null; @@ -667,6 +698,7 @@ public boolean isUserAllowedToGetResult(Exercise exercise, StudentParticipation * @param user - User that requests the result * @return true if user is allowed to see the result, false otherwise */ + @CheckReturnValue public boolean isAllowedToGetExamResult(Exercise exercise, StudentParticipation studentParticipation, User user) { if (this.isAtLeastTeachingAssistantInCourse(exercise.getCourseViaExerciseGroupOrCourseMember(), user) || exercise.isCourseExercise()) { return true; @@ -691,6 +723,7 @@ public boolean isAllowedToGetExamResult(Exercise exercise, StudentParticipation * @param resultId of the result the teaching assistant wants to assess * @return true if caller is allowed to assess submissions */ + @CheckReturnValue public boolean isAllowedToAssessExercise(Exercise exercise, User user, Long resultId) { return this.isAtLeastTeachingAssistantForExercise(exercise, user) && (resultId == null || isAtLeastInstructorForExercise(exercise, user)); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/DragAndDropQuizAnswerConversionService.java b/src/main/java/de/tum/in/www1/artemis/service/DragAndDropQuizAnswerConversionService.java index dddd4a12dfca..b78e6f457feb 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/DragAndDropQuizAnswerConversionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/DragAndDropQuizAnswerConversionService.java @@ -3,8 +3,8 @@ import java.awt.*; import java.awt.geom.Line2D; import java.awt.image.BufferedImage; -import java.io.File; import java.io.IOException; +import java.net.URI; import java.nio.file.Path; import java.util.List; import java.util.Set; @@ -30,14 +30,14 @@ @Service public class DragAndDropQuizAnswerConversionService { - private final FileService fileService; + private final FilePathService filePathService; // Drop locations in quiz exercises are relatively positioned and sized using integers in the interval [0, 200] // this value needs to be consistent with MAX_SIZE_UNIT in quiz-exercise-generator.ts private static final int MAX_SIZE_UNIT = 200; - public DragAndDropQuizAnswerConversionService(FileService fileService) { - this.fileService = fileService; + public DragAndDropQuizAnswerConversionService(FilePathService filePathService) { + this.filePathService = filePathService; } /** @@ -50,7 +50,7 @@ public DragAndDropQuizAnswerConversionService(FileService fileService) { public void convertDragAndDropQuizAnswerAndStoreAsPdf(DragAndDropSubmittedAnswer dragAndDropSubmittedAnswer, Path outputDir, boolean showResult) throws IOException { DragAndDropQuestion question = (DragAndDropQuestion) dragAndDropSubmittedAnswer.getQuizQuestion(); String backgroundFilePath = question.getBackgroundFilePath(); - BufferedImage backgroundImage = ImageIO.read(new File(fileService.actualPathForPublicPath(backgroundFilePath))); + BufferedImage backgroundImage = ImageIO.read(filePathService.actualPathForPublicPath(URI.create(backgroundFilePath)).toFile()); generateDragAndDropSubmittedAnswerImage(backgroundImage, dragAndDropSubmittedAnswer, showResult); Path dndSubmissionPathPdf = outputDir.resolve( @@ -121,7 +121,7 @@ private void drawTextDragItem(Graphics2D graphics, DropLocationCoordinates dropL } private void drawPictureDragItem(Graphics2D graphics, DropLocationCoordinates dropLocationCoordinates, DragAndDropMapping mapping) throws IOException { - BufferedImage dragItem = ImageIO.read(new File(fileService.actualPathForPublicPath(mapping.getDragItem().getPictureFilePath()))); + BufferedImage dragItem = ImageIO.read(filePathService.actualPathForPublicPath(URI.create(mapping.getDragItem().getPictureFilePath())).toFile()); Dimension scaledDimForDragItem = getScaledDimension(new Dimension(dragItem.getWidth(), dragItem.getHeight()), new Dimension(dropLocationCoordinates.width, dropLocationCoordinates.height)); graphics.drawImage(dragItem, dropLocationCoordinates.x, dropLocationCoordinates.y, (int) scaledDimForDragItem.getWidth(), (int) scaledDimForDragItem.getHeight(), null); diff --git a/src/main/java/de/tum/in/www1/artemis/service/EntityFileService.java b/src/main/java/de/tum/in/www1/artemis/service/EntityFileService.java new file mode 100644 index 000000000000..49c6a4ff6452 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/EntityFileService.java @@ -0,0 +1,113 @@ +package de.tum.in.www1.artemis.service; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.jvnet.hk2.annotations.Service; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Service for handling file operations for entities. + */ +@Service +public class EntityFileService { + + private final Logger log = LoggerFactory.getLogger(EntityFileService.class); + + private final FileService fileService; + + private final FilePathService filePathService; + + public EntityFileService(FileService fileService, FilePathService filePathService) { + this.fileService = fileService; + this.filePathService = filePathService; + } + + /** + * Moves a temporary file to the target folder and returns the new path. A placeholder is used as id. + * Use {@link #moveFileBeforeEntityPersistenceWithIdIfIsTemp(String, Path, boolean, Long)} to provide an existing id. + * + * @param entityFilePath the path of the temporary file + * @param targetFolder the target folder to move the file to + * @param keepFilename whether to keep the filename or generate a new one + * @return the new file path as string + */ + @Nonnull + public String moveTempFileBeforeEntityPersistence(@Nonnull String entityFilePath, @Nonnull Path targetFolder, boolean keepFilename) { + return moveFileBeforeEntityPersistenceWithIdIfIsTemp(entityFilePath, targetFolder, keepFilename, null); + } + + /** + * Moves a temporary file to the target folder and returns the new path. If the file is not a temporary file, the original path is returned without any changes. + * + * @param entityFilePath the path of the temporary file + * @param targetFolder the target folder to move the file to + * @param keepFilename whether to keep the filename or generate a new one + * @param entityId the id of the entity that is being persisted, if null, a placeholder gets used + * @return the new file path as string + */ + @Nonnull + public String moveFileBeforeEntityPersistenceWithIdIfIsTemp(@Nonnull String entityFilePath, @Nonnull Path targetFolder, boolean keepFilename, @Nullable Long entityId) { + URI filePath = URI.create(entityFilePath); + String filename = Path.of(entityFilePath).getFileName().toString(); + String extension = FilenameUtils.getExtension(filename); + try { + Path source = filePathService.actualPathForPublicPathOrThrow(filePath); + if (!source.startsWith(FilePathService.getTempFilePath())) { + return entityFilePath; + } + Path target; + if (keepFilename) { + target = targetFolder.resolve(filename); + } + else { + target = fileService.generateFilePath(fileService.generateTargetFilenameBase(targetFolder), extension, targetFolder); + } + FileUtils.moveFile(source.toFile(), target.toFile(), REPLACE_EXISTING); + URI newPath = filePathService.publicPathForActualPathOrThrow(target, entityId); + log.debug("Moved File from {} to {}", source, target); + return newPath.toString(); + } + catch (IOException e) { + log.error("Error moving file: {}", filePath, e); + // fallback return original path + return filePath.toString(); + } + } + + /** + * Handles a potential file update before entity persistence. It thus does nothing if the optional file doesn't change and otherwise moves a temporary file to the target and/or + * deletes the old file. + * + * @param entityId the id of the entity that is being persisted + * @param oldEntityFilePath the old file path of the file that is being updated + * @param newEntityFilePath the new file path of the file that is being updated + * @param targetFolder the target folder to move the file to + * @param keepFilename whether to keep the filename or generate a new one + * @return the new file path as string, null if no file exists + */ + @Nullable + public String handlePotentialFileUpdateBeforeEntityPersistence(@Nonnull Long entityId, @Nullable String oldEntityFilePath, @Nullable String newEntityFilePath, + @Nonnull Path targetFolder, boolean keepFilename) { + String resultingPath = newEntityFilePath; + if (newEntityFilePath != null) { + resultingPath = moveFileBeforeEntityPersistenceWithIdIfIsTemp(newEntityFilePath, targetFolder, keepFilename, entityId); + } + if (oldEntityFilePath != null && !oldEntityFilePath.equals(newEntityFilePath)) { + Path oldFilePath = filePathService.actualPathForPublicPathOrThrow(URI.create(oldEntityFilePath)); + if (oldFilePath.toFile().exists()) { + fileService.schedulePathForDeletion(oldFilePath, 0); + } + } + return resultingPath; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/FilePathService.java b/src/main/java/de/tum/in/www1/artemis/service/FilePathService.java index 83b64342d9c4..8281338bb1ee 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/FilePathService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/FilePathService.java @@ -1,10 +1,17 @@ package de.tum.in.www1.artemis.service; +import java.net.URI; import java.nio.file.Path; +import javax.annotation.Nullable; + import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import de.tum.in.www1.artemis.config.Constants; +import de.tum.in.www1.artemis.domain.FileUploadSubmission; +import de.tum.in.www1.artemis.exception.FilePathParsingException; + @Service public class FilePathService { @@ -20,43 +27,229 @@ public void setFileUploadPathStatic(String fileUploadPath) { FilePathService.fileUploadPath = fileUploadPath; } - public static String getTempFilePath() { - return Path.of(fileUploadPath, "images", "temp").toString(); + public static Path getTempFilePath() { + return Path.of(fileUploadPath, "images", "temp"); + } + + public static Path getDragAndDropBackgroundFilePath() { + return Path.of(fileUploadPath, "images", "drag-and-drop", "backgrounds"); + } + + public static Path getDragItemFilePath() { + return Path.of(fileUploadPath, "images", "drag-and-drop", "drag-items"); + } + + public static Path getCourseIconFilePath() { + return Path.of(fileUploadPath, "images", "course", "icons"); + } + + public static Path getExamUserSignatureFilePath() { + return Path.of(fileUploadPath, "images", "exam-user", "signatures"); + } + + public static Path getStudentImageFilePath() { + return Path.of(fileUploadPath, "images", "exam-user"); } - public static String getDragAndDropBackgroundFilePath() { - return Path.of(fileUploadPath, "images", "drag-and-drop", "backgrounds").toString(); + public static Path getLectureAttachmentFilePath() { + return Path.of(fileUploadPath, "attachments", "lecture"); } - public static String getDragItemFilePath() { - return Path.of(fileUploadPath, "images", "drag-and-drop", "drag-items").toString(); + public static Path getAttachmentUnitFilePath() { + return Path.of(fileUploadPath, "attachments", "attachment-unit"); } - public static String getCourseIconFilePath() { - return Path.of(fileUploadPath, "images", "course", "icons").toString(); + public static Path getFileUploadExercisesFilePath() { + return Path.of(fileUploadPath, "file-upload-exercises"); } - public static String getExamUserSignatureFilePath() { - return Path.of(fileUploadPath, "images", "exam-user", "signatures").toString(); + public static Path getMarkdownFilePath() { + return Path.of(fileUploadPath, "markdown"); } - public static String getStudentImageFilePath() { - return Path.of(fileUploadPath, "images", "exam-user").toString(); + /** + * Convert the given public file url to its corresponding local path + * + * @param publicPath the public file url to convert + * @throws FilePathParsingException if the path is unknown + * @return the actual path to that file in the local filesystem + */ + public Path actualPathForPublicPathOrThrow(URI publicPath) { + Path actualPath = actualPathForPublicPath(publicPath); + if (actualPath == null) { + // path is unknown => cannot convert + throw new FilePathParsingException("Unknown Filepath: " + publicPath); + } + + return actualPath; + } + + /** + * Convert the given public file url to its corresponding local path + * + * @param publicPath the public file url to convert + * @return the actual path to that file in the local filesystem + */ + public Path actualPathForPublicPath(URI publicPath) { + // first extract the filename from the url + String uriPath = publicPath.getPath(); + Path path = Path.of(uriPath); + String filename = path.getFileName().toString(); + + // check for known path to convert + if (uriPath.startsWith("/api/files/temp")) { + return FilePathService.getTempFilePath().resolve(filename); + } + if (uriPath.startsWith("/api/files/drag-and-drop/backgrounds")) { + return FilePathService.getDragAndDropBackgroundFilePath().resolve(filename); + } + if (uriPath.startsWith("/api/files/drag-and-drop/drag-items")) { + return FilePathService.getDragItemFilePath().resolve(filename); + } + if (uriPath.startsWith("/api/files/course/icons")) { + return FilePathService.getCourseIconFilePath().resolve(filename); + } + if (uriPath.startsWith("/api/files/exam-user/signatures")) { + return FilePathService.getExamUserSignatureFilePath().resolve(filename); + } + if (uriPath.startsWith("/api/files/exam-user")) { + return FilePathService.getStudentImageFilePath().resolve(filename); + } + if (uriPath.startsWith("/api/files/attachments/lecture")) { + String lectureId = path.getName(4).toString(); + return FilePathService.getLectureAttachmentFilePath().resolve(Path.of(lectureId, filename)); + } + if (uriPath.startsWith("/api/files/attachments/attachment-unit")) { + return actualPathForPublicAttachmentUnitFilePath(publicPath, filename); + } + if (uriPath.startsWith("/api/files/file-upload-exercises")) { + return actualPathForPublicFileUploadExercisesFilePath(publicPath, filename); + } + + return null; + } + + private Path actualPathForPublicAttachmentUnitFilePath(URI publicPath, String filename) { + Path path = Path.of(publicPath.getPath()); + if (!publicPath.toString().contains("/slide")) { + String attachmentUnitId = path.getName(4).toString(); + return FilePathService.getAttachmentUnitFilePath().resolve(Path.of(attachmentUnitId, filename)); + } + try { + String attachmentUnitId = path.getName(4).toString(); + String slideId = path.getName(6).toString(); + // check if the ids are valid long values + Long.parseLong(attachmentUnitId); + Long.parseLong(slideId); + return FilePathService.getAttachmentUnitFilePath().resolve(Path.of(attachmentUnitId, "slide", slideId, filename)); + } + catch (IllegalArgumentException e) { + throw new FilePathParsingException("Public path does not contain correct attachmentUnitId or slideId: " + publicPath, e); + } + } + + private Path actualPathForPublicFileUploadExercisesFilePath(URI publicPath, String filename) { + Path path = Path.of(publicPath.getPath()); + try { + String expectedExerciseId = path.getName(3).toString(); + String expectedSubmissionId = path.getName(5).toString(); + Long exerciseId = Long.parseLong(expectedExerciseId); + Long submissionId = Long.parseLong(expectedSubmissionId); + return FileUploadSubmission.buildFilePath(exerciseId, submissionId).resolve(filename); + } + catch (IllegalArgumentException e) { + throw new FilePathParsingException("Public path does not contain correct exerciseId or submissionId: " + publicPath, e); + } } - public static String getLectureAttachmentFilePath() { - return Path.of(fileUploadPath, "attachments", "lecture").toString(); + /** + * Generate the public path for the file at the given path + * + * @param actualPathString the path to the file in the local filesystem + * @param entityId the id of the entity associated with the file + * @throws FilePathParsingException if the path is unknown + * @return the public file url that can be used by users to access the file from outside + */ + public URI publicPathForActualPathOrThrow(Path actualPathString, @Nullable Long entityId) { + URI publicPath = publicPathForActualPath(actualPathString, entityId); + if (publicPath == null) { + // path is unknown => cannot convert + throw new FilePathParsingException("Unknown Filepath: " + actualPathString); + } + + return publicPath; } - public static String getAttachmentUnitFilePath() { - return Path.of(fileUploadPath, "attachments", "attachment-unit").toString(); + /** + * Generate the public path for the file at the given path + * + * @param path the path to the file in the local filesystem + * @param entityId the id of the entity associated with the file + * @return the public file url that can be used by users to access the file from outside + */ + public URI publicPathForActualPath(Path path, @Nullable Long entityId) { + // first extract filename + String filename = path.getFileName().toString(); + + // generate part for id + String id = entityId == null ? Constants.FILEPATH_ID_PLACEHOLDER : entityId.toString(); + // check for known path to convert + if (path.startsWith(FilePathService.getTempFilePath())) { + return URI.create(FileService.DEFAULT_FILE_SUBPATH + filename); + } + if (path.startsWith(FilePathService.getDragAndDropBackgroundFilePath())) { + return URI.create("/api/files/drag-and-drop/backgrounds/" + id + "/" + filename); + } + if (path.startsWith(FilePathService.getDragItemFilePath())) { + return URI.create("/api/files/drag-and-drop/drag-items/" + id + "/" + filename); + } + if (path.startsWith(FilePathService.getCourseIconFilePath())) { + return URI.create("/api/files/course/icons/" + id + "/" + filename); + } + if (path.startsWith(FilePathService.getExamUserSignatureFilePath())) { + return URI.create("/api/files/exam-user/signatures/" + id + "/" + filename); + } + if (path.startsWith(FilePathService.getStudentImageFilePath())) { + return URI.create("/api/files/exam-user/" + id + "/" + filename); + } + if (path.startsWith(FilePathService.getLectureAttachmentFilePath())) { + return URI.create("/api/files/attachments/lecture/" + id + "/" + filename); + } + if (path.startsWith(FilePathService.getAttachmentUnitFilePath())) { + return publicPathForActualAttachmentUnitFilePath(path, filename, id); + } + if (path.startsWith(FilePathService.getFileUploadExercisesFilePath())) { + return publicPathForActualFileUploadExercisesFilePath(path, filename, id); + } + + return null; } - public static String getFileUploadExercisesFilePath() { - return Path.of(fileUploadPath, "file-upload-exercises").toString(); + private URI publicPathForActualAttachmentUnitFilePath(Path path, String filename, String id) { + if (!path.toString().contains("/slide")) { + return URI.create("/api/files/attachments/attachment-unit/" + id + "/" + filename); + } + try { + // The last name is the file name, the one before that is the slide number and the one before that is the attachmentUnitId, in which we are interested + // (e.g. uploads/attachments/attachment-unit/941/slide/1/State_pattern_941_Slide_1.png) + final String expectedAttachmentUnitId = path.getName(path.getNameCount() - 4).toString(); + final long attachmentUnitId = Long.parseLong(expectedAttachmentUnitId); + return URI.create("/api/files/attachments/attachment-unit/" + attachmentUnitId + "/slide/" + id + "/" + filename); + } + catch (IllegalArgumentException e) { + throw new FilePathParsingException("Unexpected String in upload file path. AttachmentUnit ID should be present here: " + path, e); + } } - public static String getMarkdownFilePath() { - return Path.of(fileUploadPath, "markdown").toString(); + private URI publicPathForActualFileUploadExercisesFilePath(Path path, String filename, String id) { + try { + // The last name is the file name, the one before that is the submissionId and the one before that is the exerciseId, in which we are interested + final var expectedExerciseId = path.getName(path.getNameCount() - 3).toString(); + final long exerciseId = Long.parseLong(expectedExerciseId); + return URI.create("/api/files/file-upload-exercises/" + exerciseId + "/submissions/" + id + "/" + filename); + } + catch (IllegalArgumentException e) { + throw new FilePathParsingException("Unexpected String in upload file path. Exercise ID should be present here: " + path, e); + } } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/FileService.java b/src/main/java/de/tum/in/www1/artemis/service/FileService.java index 629708ecbfbc..ac239e2b645d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/FileService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/FileService.java @@ -1,14 +1,13 @@ package de.tum.in.www1.artemis.service; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import java.io.*; +import java.net.URI; import java.net.URLDecoder; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.time.ZonedDateTime; import java.util.*; import java.util.concurrent.*; @@ -25,7 +24,6 @@ import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.filefilter.FileFilterUtils; import org.apache.commons.io.filefilter.IOFileFilter; -import org.apache.commons.lang3.math.NumberUtils; import org.apache.pdfbox.multipdf.PDFMergerUtility; import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.apache.tomcat.util.http.fileupload.IOUtils; @@ -36,15 +34,12 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; -import org.springframework.util.FileSystemUtils; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.commons.CommonsMultipartFile; import com.fasterxml.jackson.databind.ObjectMapper; import com.ibm.icu.text.CharsetDetector; -import de.tum.in.www1.artemis.config.Constants; -import de.tum.in.www1.artemis.domain.FileUploadSubmission; import de.tum.in.www1.artemis.exception.FilePathParsingException; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; import de.tum.in.www1.artemis.web.rest.errors.InternalServerErrorException; @@ -62,8 +57,8 @@ public class FileService implements DisposableBean { * A list of common binary file extensions. * Extensions must be lower-case without leading dots. */ - private static final Set binaryFileExtensions = Set.of("png", "jpg", "jpeg", "heic", "gif", "tiff", "psd", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "pages", - "numbers", "key", "odt", "zip", "rar", "7z", "tar", "iso", "mdb", "sqlite", "exe", "jar", "bin", "so", "dll"); + private static final Set BINARY_FILE_EXTENSIONS = Set.of("png", "jpg", "jpeg", "heic", "gif", "tiff", "psd", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", + "pages", "numbers", "key", "odt", "zip", "rar", "7z", "tar", "iso", "mdb", "sqlite", "exe", "jar", "bin", "so", "dll"); /** * The list of file extensions that are allowed to be uploaded in a Markdown editor. @@ -118,18 +113,20 @@ public void destroy() { * @throws IOException if the file can't be accessed. */ @Cacheable(value = "files", unless = "#result == null") - public byte[] getFileForPath(String path) throws IOException { - File file = new File(path); - if (file.exists()) { - return Files.readAllBytes(file.toPath()); - } - else { - return null; + public byte[] getFileForPath(Path path) throws IOException { + if (Files.exists(path)) { + return Files.readAllBytes(path); } + return null; } + /** + * Evict the cache for the given path + * + * @param path the path for the file to evict from cache + */ @CacheEvict(value = "files", key = "#path") - public void evictCacheForPath(String path) { + public void evictCacheForPath(Path path) { log.info("Invalidate files cache for {}", path); // Intentionally blank } @@ -152,19 +149,14 @@ public static String sanitizeFilename(String filename) { * Helper method which handles the file creation for both normal file uploads and for markdown * * @param file The file to be uploaded with a maximum file size set in resources/config/application.yml - * @param keepFileName specifies if original file name should be kept + * @param keepFilename specifies if original file name should be kept * @param markdown boolean which is set to true, when we are uploading a file within the markdown editor - * @return The path of the file + * @return The API path of the file */ @NotNull - public String handleSaveFile(MultipartFile file, boolean keepFileName, boolean markdown) { + public URI handleSaveFile(MultipartFile file, boolean keepFilename, boolean markdown) { // check for file type - String filename = file.getOriginalFilename(); - if (filename == null) { - throw new IllegalArgumentException("Filename cannot be null"); - } - - filename = sanitizeFilename(filename); + String filename = checkAndSanitizeFilename(file.getOriginalFilename()); // Check the allowed file extensions final String fileExtension = FilenameUtils.getExtension(filename); @@ -174,35 +166,20 @@ public String handleSaveFile(MultipartFile file, boolean keepFileName, boolean m throw new BadRequestAlertException("Unsupported file type! Allowed file types: " + String.join(", ", allowedExtensions), "file", null, true); } - final String filePath = markdown ? FilePathService.getMarkdownFilePath() : FilePathService.getTempFilePath(); - final String fileNameAddition = markdown ? "Markdown_" : "Temp_"; - final StringBuilder responsePath = new StringBuilder(markdown ? MARKDOWN_FILE_SUBPATH : DEFAULT_FILE_SUBPATH); - - String savedFileName = saveFile(filePath, filename, fileNameAddition, fileExtension, keepFileName, file); - responsePath.append(savedFileName); - - return responsePath.toString(); - } + final String filenamePrefix = markdown ? "Markdown_" : "Temp_"; + final Path path = markdown ? FilePathService.getMarkdownFilePath() : FilePathService.getTempFilePath(); - /** - * Saves a file to the given path - * - * @param filePath the path to save the file to excluding the filename - * @param filename the filename of the file to save including the extension - * @param fileNameAddition the addition to the filename to make sure it is unique - * @param fileExtension the extension of the file to save - * @param keepFileName specifies if original file name should be kept - * @param file the file to save - * @return the name of the saved file - */ - public String saveFile(String filePath, String filename, String fileNameAddition, String fileExtension, boolean keepFileName, MultipartFile file) { + Path filePath; + if (keepFilename) { + filePath = path.resolve(filename); + } + else { + filePath = generateFilePath(filenamePrefix, fileExtension, path); + } try { - File newFile = createNewFile(filePath, filename, fileNameAddition, fileExtension, keepFileName); - - // copy contents of uploaded file into newly created file - Files.copy(file.getInputStream(), newFile.toPath(), REPLACE_EXISTING); + FileUtils.copyToFile(file.getInputStream(), filePath.toFile()); - return newFile.toPath().getFileName().toString(); + return generateResponsePath(filePath, markdown); } catch (IOException e) { log.error("Could not save file {}", filename, e); @@ -210,381 +187,103 @@ public String saveFile(String filePath, String filename, String fileNameAddition } } - /** - * Creates a new file from given contents - * - * @param filePath the path to save the file to excluding the filename - * @param filename the filename of the file to save - * @param fileNameAddition the addition to the filename to make sure it is unique - * @param fileExtension the extension of the file to save - * @param keepFileName specifies if original file name should be kept - * @return the created file - */ - private File createNewFile(String filePath, String filename, String fileNameAddition, String fileExtension, boolean keepFileName) throws IOException { - try { - Files.createDirectories(Paths.get(filePath)); - } - catch (IOException e) { - log.error("Could not create directory: {}", filePath); - throw e; - } - boolean fileCreated; - File newFile; - String newFilename = filename; - do { - if (!keepFileName) { - // append a timestamp and some randomness to the filename to avoid conflicts - newFilename = fileNameAddition + ZonedDateTime.now().toString().substring(0, 23).replaceAll("[:.]", "-") + "_" + UUID.randomUUID().toString().substring(0, 8) + "." - + fileExtension; - } - - newFile = Path.of(filePath, newFilename).toFile(); - if (keepFileName && newFile.exists()) { - Files.delete(newFile.toPath()); - } - fileCreated = newFile.createNewFile(); + private String checkAndSanitizeFilename(String filename) { + if (filename == null) { + throw new IllegalArgumentException("Filename cannot be null"); } - while (!fileCreated); - return newFile; + return sanitizeFilename(filename); } /** - * Copies an existing file (if not a temporary file) to a target location. Returns the public path for the resulting file. + * Generates the API path getting returned to the client * - * @param oldFilePath the old file path - * @param targetFolder the folder that a file should be copied to - * @param entityId id of the entity this file belongs to (needed to generate public path). If this is null, a placeholder will be inserted where the id would be - * @return the resulting public path + * @param filePath the file system path of the file + * @param markdown boolean which is set to true, when we are uploading a file in the Markdown format + * @return the API path of the file */ - public String copyExistingFileToTarget(String oldFilePath, String targetFolder, Long entityId) { - if (oldFilePath != null && !oldFilePath.contains("files/temp")) { - try { - Path source = Path.of(actualPathForPublicPathOrThrow(oldFilePath)); - File targetFile = generateTargetFile(oldFilePath, targetFolder, false); - Path target = targetFile.toPath(); - Files.copy(source, target, REPLACE_EXISTING); - String newFilePath = publicPathForActualPathOrThrow(target.toString(), entityId); - log.debug("Moved File from {} to {}", source, target); - return newFilePath; - } - catch (IOException e) { - log.error("Error moving file: {}", oldFilePath); - } + private URI generateResponsePath(Path filePath, boolean markdown) { + String filename = filePath.getFileName().toString(); + if (markdown) { + return URI.create(MARKDOWN_FILE_SUBPATH).resolve(filename); } - return oldFilePath; + return URI.create(DEFAULT_FILE_SUBPATH).resolve(filename); } /** - * Takes care of any changes that have to be made to the filesystem (deleting old files, moving temporary files into their proper location) and returns the public path for the - * resulting file (as it might have been moved from newFilePath to another path) + * Generates the path for the file to be saved to with a random file name based on the parameters. * - * @param oldFilePath the old file path (this file will be deleted if not null and different from newFilePath) - * @param newFilePath the new file path (this file will be moved into its proper location, if it was a temporary file) - * @param targetFolder the folder that a temporary file should be moved to - * @param entityId id of the entity this file belongs to (needed to generate - * public path). If this is null, a placeholder will be inserted where the id would be - * @return the resulting public path (is identical to newFilePath, if file didn't need to be moved) + * @param filenamePrefix the prefix of the filename + * @param fileExtension the extension of the file + * @param folder the folder to save the file to + * @return the path to save the file to */ - public String manageFilesForUpdatedFilePath(String oldFilePath, String newFilePath, String targetFolder, Long entityId) { - return manageFilesForUpdatedFilePath(oldFilePath, newFilePath, targetFolder, entityId, false); + public Path generateFilePath(String filenamePrefix, String fileExtension, Path folder) { + // append a timestamp and some randomness to the filename to avoid conflicts + String generatedFilename = filenamePrefix + ZonedDateTime.now().toString().substring(0, 23).replaceAll("[:.]", "-") + "_" + UUID.randomUUID().toString().substring(0, 8) + + "." + fileExtension; + return folder.resolve(generatedFilename); } /** - * Takes care of any changes that have to be made to the filesystem (deleting old files, moving temporary files into their proper location) and returns the public path for the - * resulting file (as it might have been moved from newFilePath to another path) + * Copies an existing non-temporary file to a target location. * - * @param oldFilePath the old file path (this file will be deleted if not null and different from newFilePath) - * @param newFilePath the new file path (this file will be moved into its proper location, if it was a temporary file) - * @param targetFolder the folder that a temporary file should be moved to - * @param entityId id of the entity this file belongs to (needed to generate public path). If this is null, a placeholder will be inserted where the id would be - * @param keepFileName flag for determining if the current filename should be kept. - * @return the resulting public path (is identical to newFilePath, if file didn't need to be moved) + * @param oldFilePath the old file path + * @param targetFolder the folder that a file should be copied to + * @return the resulting file path or null on error */ - public String manageFilesForUpdatedFilePath(String oldFilePath, String newFilePath, String targetFolder, Long entityId, Boolean keepFileName) { - if (oldFilePath != null) { - if (oldFilePath.equals(newFilePath)) { - // Do nothing - return newFilePath; - } - else { - // delete old file - log.debug("Delete old file {}", oldFilePath); - try { - File oldFile = new File(actualPathForPublicPathOrThrow(oldFilePath)); - - if (!FileSystemUtils.deleteRecursively(oldFile)) { - log.warn("FileService.manageFilesForUpdatedFilePath: Could not delete old file: {}", oldFile); - } - else { - log.debug("Deleted Orphaned File: {}", oldFile); - } - } - catch (Exception ex) { - log.warn("FileService.manageFilesForUpdatedFilePath: Could not delete old file '{}' due to exception {}", oldFilePath, ex.getMessage()); - } - } - } - - return moveFileIfTemporaryAndReturnPath(newFilePath, targetFolder, entityId, keepFileName); - } - - private String moveFileIfTemporaryAndReturnPath(String path, String targetFolder, Long entityId, Boolean keepFileName) { - if (path != null && path.contains("files/temp")) { - // rename and move file + public Path copyExistingFileToTarget(Path oldFilePath, Path targetFolder) { + if (oldFilePath != null && !pathContains(oldFilePath, Path.of(("files/temp")))) { + String filename = oldFilePath.getFileName().toString(); try { - Path source = Path.of(actualPathForPublicPathOrThrow(path)); - File targetFile = generateTargetFile(path, targetFolder, keepFileName); - Path target = targetFile.toPath(); - Files.move(source, target, REPLACE_EXISTING); - log.debug("Moved File from {} to {}", source, target); - return publicPathForActualPathOrThrow(target.toString(), entityId); + Path target = generateFilePath(generateTargetFilenameBase(targetFolder), FilenameUtils.getExtension(filename), targetFolder); + FileUtils.copyFile(oldFilePath.toFile(), target.toFile()); + log.debug("Moved File from {} to {}", oldFilePath, target); + return target; } catch (IOException e) { - log.error("Error moving file: {}", path); + log.error("Error moving file: {}", oldFilePath, e); } } - return path; - } - - /** - * Convert the given public file url to its corresponding local path - * - * @param publicPath the public file url to convert - * @return the actual path to that file in the local filesystem - */ - public String actualPathForPublicPathOrThrow(String publicPath) { - String actualPath = actualPathForPublicPath(publicPath); - if (actualPath == null) { - // path is unknown => cannot convert - throw new FilePathParsingException("Unknown Filepath: " + publicPath); - } - - return actualPath; - } - - /** - * Convert the given public file url to its corresponding local path - * - * @param publicPath the public file url to convert - * @return the actual path to that file in the local filesystem - */ - public String actualPathForPublicPath(String publicPath) { - // first extract the filename from the url - String filename = publicPath.substring(publicPath.lastIndexOf("/") + 1); - - // check for known path to convert - if (publicPath.contains("files/temp")) { - return Path.of(FilePathService.getTempFilePath(), filename).toString(); - } - if (publicPath.contains("files/drag-and-drop/backgrounds")) { - return Path.of(FilePathService.getDragAndDropBackgroundFilePath(), filename).toString(); - } - if (publicPath.contains("files/drag-and-drop/drag-items")) { - return Path.of(FilePathService.getDragItemFilePath(), filename).toString(); - } - if (publicPath.contains("files/course/icons")) { - return Path.of(FilePathService.getCourseIconFilePath(), filename).toString(); - } - if (publicPath.contains("files/exam-user")) { - return Path.of(FilePathService.getStudentImageFilePath(), filename).toString(); - } - if (publicPath.contains("files/exam-user/signatures")) { - return Path.of(FilePathService.getExamUserSignatureFilePath(), filename).toString(); - } - if (publicPath.contains("files/attachments/lecture")) { - String lectureId = publicPath.replace(filename, "").replace("/api/files/attachments/lecture/", ""); - return Path.of(FilePathService.getLectureAttachmentFilePath(), lectureId, filename).toString(); - } - if (publicPath.contains("files/attachments/attachment-unit")) { - if (!publicPath.contains("/slide")) { - String attachmentUnitId = publicPath.replace(filename, "").replace("/api/files/attachments/attachment-unit/", ""); - return Path.of(FilePathService.getAttachmentUnitFilePath(), attachmentUnitId, filename).toString(); - } - final var slideSubPath = publicPath.replace(filename, "").replace("/api/files/attachments/attachment-unit/", "").split("/"); - final var shouldBeAttachmentUnitId = slideSubPath[0]; - final var shouldBeSlideId = slideSubPath.length >= 3 ? slideSubPath[2] : null; - if (!NumberUtils.isCreatable(shouldBeAttachmentUnitId) || !NumberUtils.isCreatable(shouldBeSlideId)) { - throw new FilePathParsingException("Public path does not contain correct shouldBeAttachmentUnitId or shouldBeSlideId: " + publicPath); - } - final var attachmentUnitId = Long.parseLong(shouldBeAttachmentUnitId); - final var slideId = Long.parseLong(shouldBeSlideId); - return Path.of(FilePathService.getAttachmentUnitFilePath(), String.valueOf(attachmentUnitId), "slide", String.valueOf(slideId), filename).toString(); - } - if (publicPath.contains("files/file-upload-exercises")) { - final var uploadSubPath = publicPath.replace(filename, "").replace("/api/files/file-upload-exercises/", "").split("/"); - final var shouldBeExerciseId = uploadSubPath[0]; - final var shouldBeSubmissionId = uploadSubPath.length >= 3 ? uploadSubPath[2] : null; - if (!NumberUtils.isCreatable(shouldBeExerciseId) || !NumberUtils.isCreatable(shouldBeSubmissionId)) { - throw new FilePathParsingException("Public path does not contain correct exerciseId or submissionId: " + publicPath); - } - final var exerciseId = Long.parseLong(shouldBeExerciseId); - final var submissionId = Long.parseLong(shouldBeSubmissionId); - return Path.of(FileUploadSubmission.buildFilePath(exerciseId, submissionId), filename).toString(); - } - - return null; - } - - /** - * Generate the public path for the file at the given path - * - * @param actualPathString the path to the file in the local filesystem - * @param entityId the id of the entity associated with the file - * @return the public file url that can be used by users to access the file from outside - * @throws FilePathParsingException if the path is unknown - */ - public String publicPathForActualPathOrThrow(String actualPathString, @Nullable Long entityId) { - String publicPath = publicPathForActualPath(actualPathString, entityId); - if (publicPath == null) { - // path is unknown => cannot convert - throw new FilePathParsingException("Unknown Filepath: " + actualPathString); - } - - return publicPath; - } - - /** - * Generate the public path for the file at the given path - * - * @param actualPathString the path to the file in the local filesystem - * @param entityId the id of the entity associated with the file - * @return the public file url that can be used by users to access the file from outside - */ - public String publicPathForActualPath(String actualPathString, @Nullable Long entityId) { - // first extract filename - Path actualPath = Path.of(actualPathString); - String filename = actualPath.getFileName().toString(); - - // generate part for id - String id = entityId == null ? Constants.FILEPATH_ID_PLACEHOLDER : entityId.toString(); - // check for known path to convert - if (actualPathString.contains(FilePathService.getTempFilePath())) { - return DEFAULT_FILE_SUBPATH + filename; - } - if (actualPathString.contains(FilePathService.getDragAndDropBackgroundFilePath())) { - return "/api/files/drag-and-drop/backgrounds/" + id + "/" + filename; - } - if (actualPathString.contains(FilePathService.getDragItemFilePath())) { - return "/api/files/drag-and-drop/drag-items/" + id + "/" + filename; - } - if (actualPathString.contains(FilePathService.getCourseIconFilePath())) { - return "/api/files/course/icons/" + id + "/" + filename; - } - if (actualPathString.contains(FilePathService.getExamUserSignatureFilePath())) { - return "/api/files/exam-user/signatures/" + id + "/" + filename; - } - if (actualPathString.contains(FilePathService.getStudentImageFilePath())) { - return "/api/files/exam-user/" + id + "/" + filename; - } - if (actualPathString.contains(FilePathService.getLectureAttachmentFilePath())) { - return "/api/files/attachments/lecture/" + id + "/" + filename; - } - if (actualPathString.contains(FilePathService.getAttachmentUnitFilePath())) { - if (!actualPathString.contains("/slide")) { - return "/api/files/attachments/attachment-unit/" + id + "/" + filename; - } - try { - // The last name is the file name, the one before that is the slide number and the one before that is the attachmentUnitId, in which we are interested - // (e.g. uploads/attachments/attachment-unit/941/slide/1/State_pattern_941_Slide_1.png) - final var shouldBeAttachmentUnitId = actualPath.getName(actualPath.getNameCount() - 4).toString(); - final long attachmentUnitId = Long.parseLong(shouldBeAttachmentUnitId); - return "/api/files/attachments/attachment-unit/" + attachmentUnitId + "/slide/" + id + "/" + filename; - } - catch (IllegalArgumentException e) { - throw new FilePathParsingException("Unexpected String in upload file path. AttachmentUnit ID should be present here: " + actualPathString); - } - } - if (actualPathString.contains(FilePathService.getFileUploadExercisesFilePath())) { - final long exerciseId; - try { - // The last name is the file name, the one before that is the submissionId and the one before that is the exerciseId, in which we are interested - final var shouldBeExerciseId = actualPath.getName(actualPath.getNameCount() - 3).toString(); - exerciseId = Long.parseLong(shouldBeExerciseId); - } - catch (IllegalArgumentException e) { - throw new FilePathParsingException("Unexpected String in upload file path. Exercise ID should be present here: " + actualPathString); - } - return "/api/files/file-upload-exercises/" + exerciseId + "/submissions/" + id + "/" + filename; - } - return null; } /** - * Creates a new file at the given location with a proper filename consisting of type, timestamp and a random part + * Generates a prefix for the filename based on the target folder * - * @param originalFilename the original filename of the file (needed to determine the file type) - * @param targetFolder the folder where the new file should be created - * @param keepFileName if true, the original filename will be kept, otherwise a new filename will be generated - * @return the newly created file - * @throws IOException if the file can't be generated. + * @param targetFolder the target folder + * @return the prefix ending with an underscore character as a separator */ - public File generateTargetFile(String originalFilename, String targetFolder, Boolean keepFileName) throws IOException { - // determine the base for the filename - String filenameBase = "Unspecified_"; + public String generateTargetFilenameBase(Path targetFolder) { if (targetFolder.equals(FilePathService.getDragAndDropBackgroundFilePath())) { - filenameBase = "DragAndDropBackground_"; + return "DragAndDropBackground_"; } if (targetFolder.equals(FilePathService.getDragItemFilePath())) { - filenameBase = "DragItem_"; + return "DragItem_"; } if (targetFolder.equals(FilePathService.getCourseIconFilePath())) { - filenameBase = "CourseIcon_"; + return "CourseIcon_"; } if (targetFolder.equals(FilePathService.getExamUserSignatureFilePath())) { - filenameBase = "ExamUserSignature_"; + return "ExamUserSignature_"; } if (targetFolder.equals(FilePathService.getStudentImageFilePath())) { - filenameBase = "ExamUserImage_"; + return "ExamUserImage_"; } - if (targetFolder.contains(FilePathService.getLectureAttachmentFilePath())) { - filenameBase = "LectureAttachment_"; + if (pathContains(targetFolder, FilePathService.getLectureAttachmentFilePath())) { + return "LectureAttachment_"; } - if (targetFolder.contains(FilePathService.getAttachmentUnitFilePath())) { - filenameBase = "AttachmentUnit_"; + if (pathContains(targetFolder, FilePathService.getAttachmentUnitFilePath())) { + return "AttachmentUnit_"; } - if (targetFolder.contains(FilePathService.getAttachmentUnitFilePath()) && targetFolder.contains("/slide")) { - filenameBase = "AttachmentUnitSlide_"; + if (pathContains(targetFolder, FilePathService.getAttachmentUnitFilePath()) && pathContains(targetFolder, Path.of("/slide"))) { + return "AttachmentUnitSlide_"; } + return "Unspecified_"; + } - // extract the file extension - String fileExtension = FilenameUtils.getExtension(originalFilename); - - // create folder if necessary - File folder = new File(targetFolder); - if (!folder.exists()) { - if (!folder.mkdirs()) { - log.error("Could not create directory: {}", targetFolder); - throw new IOException("Could not create directory: " + targetFolder); - } - } - - // create the file (retry if filename already exists) - boolean fileCreated; - File newFile; - String filename = originalFilename; - do { - if (keepFileName) { - if (filename.contains(DEFAULT_FILE_SUBPATH)) { - filename = filename.replace(DEFAULT_FILE_SUBPATH, ""); - } - } - else { - filename = filenameBase + ZonedDateTime.now().toString().substring(0, 23).replaceAll("[:.]", "-") + "_" + UUID.randomUUID().toString().substring(0, 8) + "." - + fileExtension; - } - var path = Path.of(targetFolder, filename).toString(); - - newFile = new File(path); - if (keepFileName && newFile.exists()) { - Files.delete(newFile.toPath()); - } - fileCreated = newFile.createNewFile(); - } - while (!fileCreated); - - return newFile; + private boolean pathContains(Path path, Path subPath) { + return path.normalize().toString().contains(subPath.normalize().toString()); } /** @@ -616,21 +315,20 @@ public void copyResources(final Resource[] resources, final Path prefix, final P * @throws IOException If the copying operation fails. */ public void copyResource(final Resource resource, final Path prefix, final Path targetDirectory, final boolean keepParentDirectories) throws IOException { - final Path targetPath = getTargetPath(resource, prefix, targetDirectory, keepParentDirectories); + final Path targetPath = generateTargetPath(resource, prefix, targetDirectory, keepParentDirectories); if (isIgnoredDirectory(targetPath)) { return; } - Files.createDirectories(targetPath.getParent()); - Files.copy(resource.getInputStream(), targetPath, REPLACE_EXISTING); + FileUtils.copyToFile(resource.getInputStream(), targetPath.toFile()); if (targetPath.endsWith("gradlew")) { targetPath.toFile().setExecutable(true); } } - private Path getTargetPath(final Resource resource, final Path prefix, final Path targetDirectory, final boolean keepParentDirectory) throws IOException { + private Path generateTargetPath(final Resource resource, final Path prefix, final Path targetDirectory, final boolean keepParentDirectory) throws IOException { final Path filePath; if (resource.isFile()) { filePath = resource.getFile().toPath(); @@ -640,15 +338,15 @@ private Path getTargetPath(final Resource resource, final Path prefix, final Pat filePath = Path.of(url); } - final Path targetPath = getTargetPath(filePath, prefix, targetDirectory, keepParentDirectory); + final Path targetPath = generateTargetPath(filePath, prefix, targetDirectory, keepParentDirectory); return applyFilenameReplacements(targetPath); } /** - * Determines the target file path which a resource should be copied to. + * Generates the target file path which a resource should be copied to. *

* Searches for {@code prefix} in the {@code source} and removes all path elements including and up to the prefix. - * The target file path is then determined by resolving this trimmed path against the target directory. + * The target file path is then determined by resolving the remaining path against the target directory. * * @param source The path where the resource is copied from. * @param prefix The prefix that should be trimmed from the source path. @@ -656,7 +354,7 @@ private Path getTargetPath(final Resource resource, final Path prefix, final Pat * @param keepParentDirectory Keep directories in the path between prefix and filename. * @return The target path where the resource should be copied to. */ - private Path getTargetPath(final Path source, final Path prefix, final Path targetDirectory, final boolean keepParentDirectory) { + private Path generateTargetPath(final Path source, final Path prefix, final Path targetDirectory, final boolean keepParentDirectory) { if (!keepParentDirectory) { return targetDirectory.resolve(source.getFileName()); } @@ -666,14 +364,14 @@ private Path getTargetPath(final Path source, final Path prefix, final Path targ final int prefixStartIdx = Collections.indexOfSubList(sourcePathElements, prefixPathElements); - if (prefixStartIdx >= 0) { - final int startIdx = prefixStartIdx + prefixPathElements.size(); - final Path relativeSource = source.subpath(startIdx, sourcePathElements.size()); - return targetDirectory.resolve(relativeSource); - } - else { + if (prefixStartIdx < 0) { return targetDirectory.resolve(source); } + + final int startIdx = prefixStartIdx + prefixPathElements.size(); + final Path relativeSource = source.subpath(startIdx, sourcePathElements.size()); + + return targetDirectory.resolve(relativeSource); } private List getPathElements(final Path path) { @@ -717,14 +415,14 @@ private boolean isIgnoredDirectory(final Path filePath) { * @param targetDirectoryPath the path of the folder where the renamed folder should be located * @throws IOException if the directory could not be renamed. */ - public void renameDirectory(String oldDirectoryPath, String targetDirectoryPath) throws IOException { - File oldDirectory = new File(oldDirectoryPath); + public void renameDirectory(Path oldDirectoryPath, Path targetDirectoryPath) throws IOException { + File oldDirectory = oldDirectoryPath.toFile(); if (!oldDirectory.exists()) { log.error("Directory {} should be renamed but does not exist.", oldDirectoryPath); throw new RuntimeException("Directory " + oldDirectoryPath + " should be renamed but does not exist."); } - File targetDirectory = new File(targetDirectoryPath); + File targetDirectory = targetDirectoryPath.toFile(); FileUtils.moveDirectory(oldDirectory, targetDirectory); } @@ -735,9 +433,9 @@ public void renameDirectory(String oldDirectoryPath, String targetDirectoryPath) * @param filePath of file to look for replaceable sections in. * @param sections of structure String (section name) / Boolean (keep content in section or remove it). */ - public void replacePlaceholderSections(String filePath, Map sections) { + public void replacePlaceholderSections(Path filePath, Map sections) { Map patternBooleanMap = sections.entrySet().stream().collect(Collectors.toMap(e -> Pattern.compile(".*%" + e.getKey() + ".*%.*"), Map.Entry::getValue)); - File file = new File(filePath); + File file = filePath.toFile(); File tempFile = new File(filePath + "_temp"); if (!file.exists()) { throw new FilePathParsingException("File " + filePath + " should be updated but does not exist."); @@ -791,7 +489,7 @@ public void replacePlaceholderSections(String filePath, Map sec // Accessing already opened files will cause an exception on Windows machines, therefore close the streams try { Files.delete(file.toPath()); - FileUtils.moveFile(tempFile, new File(filePath)); + FileUtils.moveFile(tempFile, filePath.toFile()); } catch (IOException ex) { throw new RuntimeException("Error encountered when reading File " + filePath + ".", ex); @@ -806,17 +504,17 @@ public void replacePlaceholderSections(String filePath, Map sec * @param replacementString the string that should be used to replace the target * @throws IOException if an issue occurs on file access for the replacement of the variables. */ - public void replaceVariablesInDirectoryName(String startPath, String targetString, String replacementString) throws IOException { + public void replaceVariablesInDirectoryName(Path startPath, String targetString, String replacementString) throws IOException { log.debug("Replacing {} with {} in directory {}", targetString, replacementString, startPath); - File directory = new File(startPath); + File directory = startPath.toFile(); if (!directory.exists() || !directory.isDirectory()) { throw new RuntimeException("Directory " + startPath + " should be replaced but does not exist."); } - - if (startPath.contains(targetString)) { + String pathString = startPath.toString(); + if (pathString.contains(targetString)) { log.debug("Target String found, replacing.."); - String targetPath = startPath.replace(targetString, replacementString); - renameDirectory(startPath, targetPath); + String targetPath = pathString.replace(targetString, replacementString); + renameDirectory(startPath, Path.of(targetPath)); directory = new File(targetPath); } @@ -825,7 +523,7 @@ public void replaceVariablesInDirectoryName(String startPath, String targetStrin if (subDirectories != null) { for (String subDirectory : subDirectories) { - replaceVariablesInDirectoryName(Path.of(directory.getAbsolutePath(), subDirectory).toString(), targetString, replacementString); + replaceVariablesInDirectoryName(directory.toPath().toAbsolutePath().resolve(subDirectory), targetString, replacementString); } } } @@ -838,18 +536,20 @@ public void replaceVariablesInDirectoryName(String startPath, String targetStrin * @param replacementString the string that should be used to replace the target * @throws IOException if an issue occurs on file access for the replacement of the variables. */ - public void replaceVariablesInFileName(String startPath, String targetString, String replacementString) throws IOException { + public void replaceVariablesInFilename(Path startPath, String targetString, String replacementString) throws IOException { log.debug("Replacing {} with {} in directory {}", targetString, replacementString, startPath); - File directory = new File(startPath); + File directory = startPath.toFile(); if (!directory.exists() || !directory.isDirectory()) { throw new FileNotFoundException("Files in the directory " + startPath + " should be replaced but it does not exist."); } // rename all files in the file tree - try (var files = Files.find(Path.of(startPath), Integer.MAX_VALUE, (filePath, fileAttr) -> fileAttr.isRegularFile() && filePath.toString().contains(targetString))) { + try (var files = Files.find(startPath, Integer.MAX_VALUE, (filePath, fileAttr) -> fileAttr.isRegularFile() && filePath.toString().contains(targetString))) { files.forEach(filePath -> { try { - Files.move(filePath, Path.of(filePath.toString().replace(targetString, replacementString))); + // We expect the strings to be clean already, so the filename shouldn't change. If it does, we are on the safe side with the sanitation. + String cleanFileName = sanitizeFilename(filePath.toString().replace(targetString, replacementString)); + FileUtils.moveFile(filePath.toFile(), new File(cleanFileName)); } catch (IOException e) { throw new RuntimeException("File " + filePath + " should be replaced but does not exist."); @@ -892,7 +592,7 @@ public void replaceVariablesInFileRecursive(Path startPath, Map // filter out files that should be ignored files = Arrays.stream(files).filter(Predicate.not(filesToIgnore::contains)).toArray(String[]::new); for (String file : files) { - replaceVariablesInFile(Path.of(directory.getAbsolutePath(), file), replacements); + replaceVariablesInFile(directory.toPath().toAbsolutePath().resolve(file), replacements); } } @@ -904,7 +604,7 @@ public void replaceVariablesInFileRecursive(Path startPath, Map // ignore files in the '.git' folder continue; } - replaceVariablesInFileRecursive(Path.of(directory.getAbsolutePath(), subDirectory), replacements, filesToIgnore); + replaceVariablesInFileRecursive(directory.toPath().toAbsolutePath().resolve(subDirectory), replacements, filesToIgnore); } } } @@ -918,7 +618,6 @@ public void replaceVariablesInFileRecursive(Path startPath, Map */ public void replaceVariablesInFile(Path filePath, Map replacements) { log.debug("Replacing {} in file {}", replacements, filePath); - if (isBinaryFile(filePath)) { // do not try to read binary files with 'readString' return; @@ -931,7 +630,7 @@ public void replaceVariablesInFile(Path filePath, Map replacemen for (Map.Entry replacement : replacements.entrySet()) { fileContent = fileContent.replace(replacement.getKey(), replacement.getValue()); } - Files.writeString(filePath, fileContent, UTF_8); + FileUtils.writeStringToFile(filePath.toFile(), fileContent, UTF_8); } catch (IOException ex) { log.warn("Exception {} occurred when trying to replace {} in (binary) file {}", ex.getMessage(), replacements, filePath); @@ -948,7 +647,7 @@ public void replaceVariablesInFile(Path filePath, Map replacemen */ private static boolean isBinaryFile(Path filePath) { final String fileExtension = FilenameUtils.getExtension(filePath.getFileName().toString()); - return binaryFileExtensions.stream().anyMatch(fileExtension::equalsIgnoreCase); + return BINARY_FILE_EXTENSIONS.stream().anyMatch(fileExtension::equalsIgnoreCase); } /** @@ -972,7 +671,7 @@ public void normalizeLineEndingsDirectory(Path startPath) throws IOException { Collection files = FileUtils.listFiles(directory, FileFilterUtils.trueFileFilter(), directoryFileFilter); for (File file : files) { - normalizeLineEndings(file.toPath()); + normalizeLineEndings(file.toPath().toAbsolutePath()); } } @@ -993,7 +692,7 @@ public void normalizeLineEndings(Path filePath) throws IOException { // https://stackoverflow.com/questions/3776923/how-can-i-normalize-the-eol-character-in-java String fileContent = Files.readString(filePath, UTF_8); fileContent = fileContent.replaceAll("\\r\\n?", "\n"); - Files.writeString(filePath, fileContent, UTF_8); + FileUtils.writeStringToFile(filePath.toFile(), fileContent, UTF_8); } /** @@ -1004,7 +703,7 @@ public void normalizeLineEndings(Path filePath) throws IOException { * @param startPath the path where the start directory is located * @throws IOException if an issue occurs on file access when converting to UTF-8. */ - public void convertToUTF8Directory(Path startPath) throws IOException { + public void convertFilesInDirectoryToUtf8(Path startPath) throws IOException { log.debug("Converting files in directory {} to UTF-8", startPath); File directory = startPath.toFile(); if (!directory.exists() || !directory.isDirectory()) { @@ -1037,7 +736,7 @@ public void convertToUTF8(Path filePath) throws IOException { String fileContent = new String(contentArray, charset); - Files.writeString(filePath, fileContent, UTF_8); + FileUtils.writeStringToFile(filePath.toFile(), fileContent, UTF_8); } /** @@ -1054,12 +753,15 @@ public Charset detectCharset(byte[] contentArray) { } /** - * Schedule the deletion of the given path with a given delay + * Schedule the deletion of the given nullsafe path with a given delay * * @param path The path that should be deleted * @param delayInMinutes The delay in minutes after which the path should be deleted */ - public void scheduleForDeletion(Path path, long delayInMinutes) { + public void schedulePathForDeletion(@Nullable Path path, long delayInMinutes) { + if (path == null) { + return; + } ScheduledFuture future = executor.schedule(() -> { try { if (Files.exists(path)) { @@ -1077,12 +779,15 @@ public void scheduleForDeletion(Path path, long delayInMinutes) { } /** - * Schedule the recursive deletion of the given directory with a given delay. + * Schedule the recursive deletion of the given nullsafe directory with a given delay. * * @param path The path to the directory that should be deleted * @param delayInMinutes The delay in minutes after which the path should be deleted */ - public void scheduleForDirectoryDeletion(Path path, long delayInMinutes) { + public void scheduleDirectoryPathForRecursiveDeletion(@Nullable Path path, long delayInMinutes) { + if (path == null) { + return; + } ScheduledFuture future = executor.schedule(() -> { try { if (Files.exists(path) && Files.isDirectory(path)) { @@ -1106,7 +811,7 @@ public void scheduleForDirectoryDeletion(Path path, long delayInMinutes) { * @param path the original path, e.g. /opt/artemis/repos-download * @return the unique path, e.g. /opt/artemis/repos-download/1609579674868 */ - private Path getUniquePath(Path path) { + public Path getUniqueSubfolderPath(Path path) { var uniquePath = path.resolve(String.valueOf(System.currentTimeMillis())); if (!Files.exists(uniquePath) && Files.isDirectory(path)) { try { @@ -1120,15 +825,16 @@ private Path getUniquePath(Path path) { } /** - * create a unique path by appending a folder named with the current milliseconds (e.g. 1609579674868) of the system and schedules it for deletion + * create a unique path by appending a folder named with the current milliseconds (e.g. 1609579674868) of the system and schedules it for deletion. + * See {@link #getUniqueSubfolderPath(Path)} for more information. * * @param path the original path, e.g. /opt/artemis/repos-download * @param deleteDelayInMinutes the delay in minutes after which the path should be deleted * @return the unique path, e.g. /opt/artemis/repos-download/1609579674868 */ - public Path getTemporaryUniquePath(Path path, long deleteDelayInMinutes) { - var temporaryPath = getUniquePath(path); - scheduleForDirectoryDeletion(temporaryPath, deleteDelayInMinutes); + public Path getTemporaryUniqueSubfolderPath(Path path, long deleteDelayInMinutes) { + var temporaryPath = getUniqueSubfolderPath(path); + scheduleDirectoryPathForRecursiveDeletion(temporaryPath, deleteDelayInMinutes); return temporaryPath; } @@ -1143,7 +849,7 @@ public Path getTemporaryUniquePath(Path path, long deleteDelayInMinutes) { */ public Path getTemporaryUniquePathWithoutPathCreation(Path path, long deleteDelayInMinutes) { var temporaryPath = path.resolve(String.valueOf(System.currentTimeMillis())); - scheduleForDirectoryDeletion(temporaryPath, deleteDelayInMinutes); + scheduleDirectoryPathForRecursiveDeletion(temporaryPath, deleteDelayInMinutes); return temporaryPath; } @@ -1179,10 +885,10 @@ public Path writeObjectToJsonFile(Object object, ObjectMapper objectMapper, Path * Merge the PDF files located in the given paths. * * @param paths list of paths to merge - * @param mergedPdfFileName title of merged pdf file + * @param mergedPdfFilename title of merged pdf file * @return byte array of the merged file */ - public Optional mergePdfFiles(List paths, String mergedPdfFileName) { + public Optional mergePdfFiles(List paths, String mergedPdfFilename) { if (paths == null || paths.isEmpty()) { return Optional.empty(); } @@ -1198,7 +904,7 @@ public Optional mergePdfFiles(List paths, String mergedPdfFileNa } PDDocumentInformation pdDocumentInformation = new PDDocumentInformation(); - pdDocumentInformation.setTitle(mergedPdfFileName); + pdDocumentInformation.setTitle(mergedPdfFilename); pdfMerger.setDestinationDocumentInformation(pdDocumentInformation); pdfMerger.setDestinationStream(outputStream); @@ -1232,18 +938,18 @@ public void deleteFiles(List filePaths) { /** * Convert byte[] to MultipartFile by using CommonsMultipartFile * - * @param fileName file name to set file name + * @param filename file name to set file name * @param extension extension of the file (e.g .pdf or .png) * @param streamByteArray byte array to save to the temp file * @return multipartFile wrapper for the file stored on disk with a sanitized name */ - public MultipartFile convertByteArrayToMultipart(String fileName, String extension, byte[] streamByteArray) { + public MultipartFile convertByteArrayToMultipart(String filename, String extension, byte[] streamByteArray) { try { - String cleanFileName = sanitizeFilename(fileName); - Path tempPath = Path.of(FilePathService.getTempFilePath(), cleanFileName + extension); - Files.write(tempPath, streamByteArray); + String cleanFilename = sanitizeFilename(filename); + Path tempPath = FilePathService.getTempFilePath().resolve(cleanFilename + extension); + FileUtils.writeByteArrayToFile(tempPath.toFile(), streamByteArray); File outputFile = tempPath.toFile(); - FileItem fileItem = new DiskFileItem(cleanFileName, Files.probeContentType(tempPath), false, outputFile.getName(), (int) outputFile.length(), + FileItem fileItem = new DiskFileItem(cleanFilename, Files.probeContentType(tempPath), false, outputFile.getName(), (int) outputFile.length(), outputFile.getParentFile()); try (InputStream input = new FileInputStream(outputFile); OutputStream fileItemOutputStream = fileItem.getOutputStream()) { @@ -1252,7 +958,7 @@ public MultipartFile convertByteArrayToMultipart(String fileName, String extensi return new CommonsMultipartFile(fileItem); } catch (IOException e) { - log.error("Could not convert file {}.", fileName, e); + log.error("Could not convert file {}.", filename, e); throw new InternalServerErrorException("Error while converting byte[] to MultipartFile by using CommonsMultipartFile"); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/FileUploadSubmissionService.java b/src/main/java/de/tum/in/www1/artemis/service/FileUploadSubmissionService.java index 538676be7d47..df4454fddcc9 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/FileUploadSubmissionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/FileUploadSubmissionService.java @@ -2,14 +2,15 @@ import java.io.File; import java.io.IOException; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Optional; import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -35,17 +36,21 @@ public class FileUploadSubmissionService extends SubmissionService { private final FileService fileService; + private final FilePathService filePathService; + private final ExerciseDateService exerciseDateService; public FileUploadSubmissionService(FileUploadSubmissionRepository fileUploadSubmissionRepository, SubmissionRepository submissionRepository, ResultRepository resultRepository, ParticipationService participationService, UserRepository userRepository, StudentParticipationRepository studentParticipationRepository, FileService fileService, AuthorizationCheckService authCheckService, FeedbackRepository feedbackRepository, ExamDateService examDateService, ExerciseDateService exerciseDateService, - CourseRepository courseRepository, ParticipationRepository participationRepository, ComplaintRepository complaintRepository, FeedbackService feedbackService) { + CourseRepository courseRepository, ParticipationRepository participationRepository, ComplaintRepository complaintRepository, FeedbackService feedbackService, + FilePathService filePathService) { super(submissionRepository, userRepository, authCheckService, resultRepository, studentParticipationRepository, participationService, feedbackRepository, examDateService, exerciseDateService, courseRepository, participationRepository, complaintRepository, feedbackService); this.fileUploadSubmissionRepository = fileUploadSubmissionRepository; this.fileService = fileService; this.exerciseDateService = exerciseDateService; + this.filePathService = filePathService; } /** @@ -115,7 +120,7 @@ public Optional getRandomFileUploadSubmissionEligibleForNe public FileUploadSubmission save(FileUploadSubmission fileUploadSubmission, MultipartFile file, StudentParticipation participation, FileUploadExercise exercise) throws IOException, EmptyFileException { - String newFilePath = storeFile(fileUploadSubmission, participation, file, exercise); + URI newFilePath = storeFile(fileUploadSubmission, participation, file, exercise); // update submission properties fileUploadSubmission.setSubmissionDate(ZonedDateTime.now()); @@ -132,14 +137,14 @@ public FileUploadSubmission save(FileUploadSubmission fileUploadSubmission, Mult // Note: we save before the new file path is set to potentially remove the old file on the file system fileUploadSubmission = fileUploadSubmissionRepository.save(fileUploadSubmission); - fileUploadSubmission.setFilePath(newFilePath); + fileUploadSubmission.setFilePath(newFilePath.toString()); // Note: we save again so that the new file is stored on the file system fileUploadSubmission = fileUploadSubmissionRepository.save(fileUploadSubmission); return fileUploadSubmission; } - private String storeFile(FileUploadSubmission fileUploadSubmission, StudentParticipation participation, MultipartFile file, FileUploadExercise exercise) + private URI storeFile(FileUploadSubmission fileUploadSubmission, StudentParticipation participation, MultipartFile file, FileUploadExercise exercise) throws EmptyFileException, IOException { if (file.isEmpty()) { throw new EmptyFileException(file.getOriginalFilename()); @@ -150,11 +155,11 @@ private String storeFile(FileUploadSubmission fileUploadSubmission, StudentParti if (fileUploadSubmission.getId() == null) { fileUploadSubmission = fileUploadSubmissionRepository.save(fileUploadSubmission); } - final String savePath = saveFileForSubmission(file, fileUploadSubmission, exercise); - final String newFilePath = fileService.publicPathForActualPath(savePath, fileUploadSubmission.getId()); + final Path savePath = saveFileForSubmission(file, fileUploadSubmission, exercise); + final URI newFilePath = filePathService.publicPathForActualPath(savePath, fileUploadSubmission.getId()); // We need to ensure that we can access the store file and the stored file is the same as was passed to us in the request - final var storedFileHash = DigestUtils.md5Hex(Files.newInputStream(Path.of(savePath))); + final var storedFileHash = DigestUtils.md5Hex(Files.newInputStream(savePath)); if (!multipartFileHash.equals(storedFileHash)) { throw new IOException("The file " + file.getName() + "could not be stored"); } @@ -163,7 +168,7 @@ private String storeFile(FileUploadSubmission fileUploadSubmission, StudentParti Optional previousFileUploadSubmission = participation.findLatestSubmission(); previousFileUploadSubmission.filter(previousSubmission -> previousSubmission.getFilePath() != null).ifPresent(previousSubmission -> { - final String oldFilePath = previousSubmission.getFilePath(); + final URI oldFilePath = URI.create(previousSubmission.getFilePath()); // check if we already had a file associated with this submission if (!oldFilePath.equals(newFilePath)) { // different name // IMPORTANT: only delete the file when it has changed the name @@ -177,7 +182,7 @@ private String storeFile(FileUploadSubmission fileUploadSubmission, StudentParti return newFilePath; } - private String saveFileForSubmission(final MultipartFile file, final Submission submission, FileUploadExercise exercise) throws IOException { + private Path saveFileForSubmission(final MultipartFile file, final Submission submission, FileUploadExercise exercise) throws IOException { final var exerciseId = exercise.getId(); final var submissionId = submission.getId(); var filename = file.getOriginalFilename(); @@ -186,22 +191,17 @@ private String saveFileForSubmission(final MultipartFile file, final Submission var components = filename.split("\\\\"); filename = components[components.length - 1]; } - // replace all illegal characters with ascii characters \w means A-Za-z0-9 to avoid problems during download later on - filename = filename.replaceAll("[^\\w.-]", ""); + filename = FileService.sanitizeFilename(filename); // if the filename is now too short, we prepend "file" // this prevents potential problems when users call their file e.g. ßßß.pdf if (filename.length() < 5) { filename = "file" + filename; } - final var dirPath = FileUploadSubmission.buildFilePath(exerciseId, submissionId); - final var filePath = Path.of(dirPath, filename).toString(); - final var savedFile = new File(filePath); - final var dir = new File(dirPath); + final Path dirPath = FileUploadSubmission.buildFilePath(exerciseId, submissionId); + final Path filePath = dirPath.resolve(filename); + final File savedFile = filePath.toFile(); - if (!dir.exists()) { - dir.mkdirs(); - } - Files.copy(file.getInputStream(), savedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + FileUtils.copyToFile(file.getInputStream(), savedFile); return filePath; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/HazelcastPathSerializer.java b/src/main/java/de/tum/in/www1/artemis/service/HazelcastPathSerializer.java new file mode 100644 index 000000000000..23a55c67d48d --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/HazelcastPathSerializer.java @@ -0,0 +1,28 @@ +package de.tum.in.www1.artemis.service; + +import static de.tum.in.www1.artemis.config.Constants.HAZELCAST_PATH_SERIALIZER_ID; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +import com.hazelcast.nio.serialization.ByteArraySerializer; + +public class HazelcastPathSerializer implements ByteArraySerializer { + + @Override + public byte[] write(Path path) throws IOException { + return path.toString().getBytes(StandardCharsets.UTF_8); + } + + @Override + public Path read(byte[] buffer) throws IOException { + String pathString = new String(buffer, StandardCharsets.UTF_8); + return Path.of(pathString); + } + + @Override + public int getTypeId() { + return HAZELCAST_PATH_SERIALIZER_ID; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/LectureImportService.java b/src/main/java/de/tum/in/www1/artemis/service/LectureImportService.java index 0c8bc8131492..203f8ea870c9 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LectureImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LectureImportService.java @@ -1,11 +1,13 @@ package de.tum.in.www1.artemis.service; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + import java.io.IOException; -import java.nio.file.Files; +import java.net.URI; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.*; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -30,14 +32,14 @@ public class LectureImportService { private final AttachmentRepository attachmentRepository; - private final FileService fileService; + private final FilePathService filePathService; public LectureImportService(LectureRepository lectureRepository, LectureUnitRepository lectureUnitRepository, AttachmentRepository attachmentRepository, - FileService fileService) { + FilePathService filePathService) { this.lectureRepository = lectureRepository; this.lectureUnitRepository = lectureUnitRepository; this.attachmentRepository = attachmentRepository; - this.fileService = fileService; + this.filePathService = filePathService; } /** @@ -151,15 +153,15 @@ private Attachment cloneAttachment(final Attachment importedAttachment) { attachment.setVersion(importedAttachment.getVersion()); attachment.setAttachmentType(importedAttachment.getAttachmentType()); - Path oldPath = Path.of(fileService.actualPathForPublicPath(importedAttachment.getLink())); - Path tempPath = Path.of(FilePathService.getTempFilePath(), oldPath.getFileName().toString()); + Path oldPath = filePathService.actualPathForPublicPath(URI.create(importedAttachment.getLink())); + Path tempPath = FilePathService.getTempFilePath().resolve(oldPath.getFileName()); try { log.debug("Copying attachment file from {} to {}", oldPath, tempPath); - Files.copy(oldPath, tempPath, StandardCopyOption.REPLACE_EXISTING); + FileUtils.copyFile(oldPath.toFile(), tempPath.toFile(), REPLACE_EXISTING); // File was copied to a temp directory and will be moved once we persist the attachment - attachment.setLink(fileService.publicPathForActualPath(tempPath.toString(), null)); + attachment.setLink(filePathService.publicPathForActualPath(tempPath, null).toString()); } catch (IOException e) { log.error("Error while copying file", e); diff --git a/src/main/java/de/tum/in/www1/artemis/service/LegalDocumentService.java b/src/main/java/de/tum/in/www1/artemis/service/LegalDocumentService.java index d3c5c857462b..22924c761d6c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LegalDocumentService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LegalDocumentService.java @@ -1,11 +1,12 @@ package de.tum.in.www1.artemis.service; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.util.Optional; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -155,8 +156,7 @@ protected LegalDocument updateLegalDocument(LegalDocument legalDocument) { if (!Files.exists(legalDocumentsBasePath)) { Files.createDirectories(legalDocumentsBasePath); } - Files.writeString(getLegalDocumentPath(legalDocument.getLanguage(), legalDocument.getType()), legalDocument.getText(), StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING); + FileUtils.writeStringToFile(getLegalDocumentPath(legalDocument.getLanguage(), legalDocument.getType()).toFile(), legalDocument.getText(), StandardCharsets.UTF_8); return legalDocument; } catch (IOException e) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/PlantUmlService.java b/src/main/java/de/tum/in/www1/artemis/service/PlantUmlService.java index 8006f3a40275..e1a43e3f8c39 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/PlantUmlService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/PlantUmlService.java @@ -8,15 +8,14 @@ import java.nio.file.Paths; import java.util.stream.Stream; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; -import net.sourceforge.plantuml.FileFormat; -import net.sourceforge.plantuml.FileFormatOption; -import net.sourceforge.plantuml.SourceStringReader; +import net.sourceforge.plantuml.*; @Service public class PlantUmlService { @@ -50,8 +49,7 @@ private void ensureThemes() { log.info("Storing UML theme to temporary directory"); final var themeResource = resourceLoaderService.getResource(Path.of("puml", fileName)); try (var inputStream = themeResource.getInputStream()) { - Files.createDirectories(PATH_TMP_THEME); - Files.write(path, inputStream.readAllBytes()); + FileUtils.copyToFile(inputStream, path.toFile()); log.info("UML theme stored successfully to {}", path); } catch (IOException e) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/QuizExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/QuizExerciseImportService.java index 9d24d2383fb4..29638f7d66b5 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/QuizExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/QuizExerciseImportService.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.service; +import java.net.URI; +import java.nio.file.Path; import java.util.*; import javax.validation.constraints.NotNull; @@ -23,14 +25,18 @@ public class QuizExerciseImportService extends ExerciseImportService { private final FileService fileService; + private final FilePathService filePathService; + private final ChannelService channelService; public QuizExerciseImportService(QuizExerciseService quizExerciseService, FileService fileService, ExampleSubmissionRepository exampleSubmissionRepository, - SubmissionRepository submissionRepository, ResultRepository resultRepository, ChannelService channelService, FeedbackService feedbackService) { + SubmissionRepository submissionRepository, ResultRepository resultRepository, ChannelService channelService, FeedbackService feedbackService, + FilePathService filePathService) { super(exampleSubmissionRepository, submissionRepository, resultRepository, feedbackService); this.quizExerciseService = quizExerciseService; this.fileService = fileService; this.channelService = channelService; + this.filePathService = filePathService; } /** @@ -97,9 +103,15 @@ private void copyQuizQuestions(QuizExercise importedExercise, QuizExercise newEx } } else if (quizQuestion instanceof DragAndDropQuestion dndQuestion) { - // Need to copy the file and get a new path, otherwise two different questions would share the same image and would cause problems in case one was deleted - dndQuestion - .setBackgroundFilePath(fileService.copyExistingFileToTarget(dndQuestion.getBackgroundFilePath(), FilePathService.getDragAndDropBackgroundFilePath(), null)); + if (dndQuestion.getBackgroundFilePath() != null) { + // Need to copy the file and get a new path, otherwise two different questions would share the same image and would cause problems in case one was deleted + Path oldPath = filePathService.actualPathForPublicPath(URI.create(dndQuestion.getBackgroundFilePath())); + Path newPath = fileService.copyExistingFileToTarget(oldPath, FilePathService.getDragAndDropBackgroundFilePath()); + dndQuestion.setBackgroundFilePath(filePathService.publicPathForActualPath(newPath, null).toString()); + } + else { + log.warn("BackgroundFilePath of DragAndDropQuestion {} is null", dndQuestion.getId()); + } for (DropLocation dropLocation : dndQuestion.getDropLocations()) { dropLocation.setId(null); @@ -110,7 +122,9 @@ else if (quizQuestion instanceof DragAndDropQuestion dndQuestion) { dragItem.setQuestion(dndQuestion); if (dragItem.getPictureFilePath() != null) { // Need to copy the file and get a new path, same as above - dragItem.setPictureFilePath(fileService.copyExistingFileToTarget(dragItem.getPictureFilePath(), FilePathService.getDragItemFilePath(), null)); + Path oldDragItemPath = filePathService.actualPathForPublicPath(URI.create(dragItem.getPictureFilePath())); + Path newDragItemPath = fileService.copyExistingFileToTarget(oldDragItemPath, FilePathService.getDragItemFilePath()); + dragItem.setPictureFilePath(filePathService.publicPathForActualPath(newDragItemPath, null).toString()); } } for (DragAndDropMapping dragAndDropMapping : dndQuestion.getCorrectMappings()) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/RepositoryService.java b/src/main/java/de/tum/in/www1/artemis/service/RepositoryService.java index 324ecd8fad0d..8b294f0bdddb 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/RepositoryService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/RepositoryService.java @@ -186,7 +186,7 @@ public Map getFilesWithInformationAboutChange(Repository reposi public void createFile(Repository repository, String filePath, InputStream inputStream) throws IOException { Path safePath = checkIfPathIsValidAndExistanceAndReturnSafePath(repository, filePath, false); File file = checkIfPathAndFileAreValidAndReturnSafeFile(repository, safePath); - Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING); + FileUtils.copyToFile(inputStream, file); repository.setContent(null); // invalidate cache inputStream.close(); } @@ -205,7 +205,7 @@ public void createFolder(Repository repository, String folderPath, InputStream i Files.createDirectory(repository.getLocalPath().resolve(safePath)); // We need to add an empty keep file so that the folder can be added to the git repository File keep = new File(repository.getLocalPath().resolve(safePath).resolve(".keep"), repository); - Files.copy(inputStream, keep.toPath(), StandardCopyOption.REPLACE_EXISTING); + FileUtils.copyToFile(inputStream, keep); repository.setContent(null); // invalidate cache inputStream.close(); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/ResourceLoaderService.java b/src/main/java/de/tum/in/www1/artemis/service/ResourceLoaderService.java index 42f7936fa5e9..62abfa99e07b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ResourceLoaderService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ResourceLoaderService.java @@ -2,13 +2,11 @@ import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -16,6 +14,7 @@ import javax.annotation.Nonnull; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -200,13 +199,10 @@ public Path getResourceFilePath(Path path) throws IOException, URISyntaxExceptio } else if ("jar".equals(resourceUrl.getProtocol())) { // Resource is in a jar file. - InputStream resourceInputStream = resource.getInputStream(); - Path resourcePath = Files.createTempFile(UUID.randomUUID().toString(), ""); - Files.copy(resourceInputStream, resourcePath, StandardCopyOption.REPLACE_EXISTING); - resourceInputStream.close(); - // Delete the temporary file when the JVM exits. - resourcePath.toFile().deleteOnExit(); + File file = resourcePath.toFile(); + file.deleteOnExit(); + FileUtils.copyInputStreamToFile(resource.getInputStream(), file); return resourcePath; } throw new IllegalArgumentException("Unsupported protocol: " + resourceUrl.getProtocol()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/SlideSplitterService.java b/src/main/java/de/tum/in/www1/artemis/service/SlideSplitterService.java index 623f49fe8933..7c22ef6cf264 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/SlideSplitterService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/SlideSplitterService.java @@ -4,6 +4,8 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; import javax.imageio.ImageIO; @@ -33,10 +35,13 @@ public class SlideSplitterService { private final FileService fileService; + private final FilePathService filePathService; + private final SlideRepository slideRepository; - public SlideSplitterService(FileService fileService, SlideRepository slideRepository) { + public SlideSplitterService(FileService fileService, FilePathService filePathService, SlideRepository slideRepository) { this.fileService = fileService; + this.filePathService = filePathService; this.slideRepository = slideRepository; } @@ -47,8 +52,8 @@ public SlideSplitterService(FileService fileService, SlideRepository slideReposi */ @Async public void splitAttachmentUnitIntoSingleSlides(AttachmentUnit attachmentUnit) { - String attachmentPath = fileService.actualPathForPublicPath(attachmentUnit.getAttachment().getLink()); - File file = new File(attachmentPath); + Path attachmentPath = filePathService.actualPathForPublicPath(URI.create(attachmentUnit.getAttachment().getLink())); + File file = attachmentPath.toFile(); try (PDDocument document = Loader.loadPDF(file)) { String pdfFilename = file.getName(); splitAttachmentUnitIntoSingleSlides(document, attachmentUnit, pdfFilename); @@ -77,7 +82,7 @@ public void splitAttachmentUnitIntoSingleSlides(PDDocument document, AttachmentU BufferedImage bufferedImage = pdfRenderer.renderImageWithDPI(page, 72, ImageType.RGB); byte[] imageInByte = bufferedImageToByteArray(bufferedImage, "png"); MultipartFile slideFile = fileService.convertByteArrayToMultipart(fileNameWithOutExt + "_" + attachmentUnit.getId() + "_Slide_" + (page + 1), ".png", imageInByte); - String filePath = fileService.handleSaveFile(slideFile, true, false); + String filePath = fileService.handleSaveFile(slideFile, true, false).toString(); Slide slideEntity = new Slide(); slideEntity.setSlideImagePath(filePath); slideEntity.setSlideNumber(page + 1); diff --git a/src/main/java/de/tum/in/www1/artemis/service/ZipFileService.java b/src/main/java/de/tum/in/www1/artemis/service/ZipFileService.java index c686196aebdd..45cb8e38262a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ZipFileService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ZipFileService.java @@ -12,6 +12,7 @@ import javax.annotation.Nullable; import org.apache.commons.compress.utils.FileNameUtils; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -62,7 +63,7 @@ else if (Files.isReadable(path) && Files.isDirectory(path)) { */ public void createTemporaryZipFile(Path zipFilePath, List paths, long deleteDelayInMinutes) throws IOException { createZipFile(zipFilePath, paths); - fileService.scheduleForDeletion(zipFilePath, deleteDelayInMinutes); + fileService.schedulePathForDeletion(zipFilePath, deleteDelayInMinutes); } /** @@ -119,7 +120,7 @@ private void copyToZipFile(ZipOutputStream zipOutputStream, Path path, ZipEntry try { if (Files.exists(path)) { zipOutputStream.putNextEntry(zipEntry); - Files.copy(path, zipOutputStream); + FileUtils.copyFile(path.toFile(), zipOutputStream); zipOutputStream.closeEntry(); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIContainerService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIContainerService.java index 920471153db6..a5499626c650 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIContainerService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIContainerService.java @@ -9,6 +9,7 @@ import java.util.concurrent.CountDownLatch; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -272,7 +273,7 @@ public Path createBuildScript(ProgrammingExercise programmingExercise, List optionalVcsUserManagementService; + + private final Optional optionalCIUserManagementService; + public LtiService(UserCreationService userCreationService, UserRepository userRepository, ArtemisAuthenticationProvider artemisAuthenticationProvider, - JWTCookieService jwtCookieService) { + JWTCookieService jwtCookieService, Optional optionalVcsUserManagementService, + Optional optionalCIUserManagementService) { this.userCreationService = userCreationService; this.userRepository = userRepository; this.artemisAuthenticationProvider = artemisAuthenticationProvider; this.jwtCookieService = jwtCookieService; + this.optionalVcsUserManagementService = optionalVcsUserManagementService; + this.optionalCIUserManagementService = optionalCIUserManagementService; } /** @@ -112,11 +123,18 @@ private Authentication createNewUserFromLaunchRequest(String email, String usern final User newUser; final var groups = new HashSet(); groups.add(LTI_GROUP_NAME); - newUser = userCreationService.createUser(username, null, groups, firstName, lastName, email, null, null, Constants.DEFAULT_LANGUAGE, true); + + var password = RandomUtil.generatePassword(); + newUser = userCreationService.createUser(username, password, groups, firstName, lastName, email, null, null, Constants.DEFAULT_LANGUAGE, true); newUser.setActivationKey(null); userRepository.save(newUser); + + optionalVcsUserManagementService.ifPresent(vcsUserManagementService -> vcsUserManagementService.createVcsUser(newUser, password)); + optionalCIUserManagementService.ifPresent(ciUserManagementService -> ciUserManagementService.createUser(newUser, password)); + log.info("Created new user {}", newUser); return newUser; + }); log.info("createNewUserFromLaunchRequest: {}", user); diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamUserService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamUserService.java index cd2aabaaad9f..89f640e742f6 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamUserService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamUserService.java @@ -110,7 +110,7 @@ public ExamUsersNotFoundDTO saveImages(long examId, MultipartFile file) { ExamUser examUser = examUserOptional.get(); MultipartFile studentImageFile = fileService.convertByteArrayToMultipart(examUserWithImageDTO.studentRegistrationNumber() + "_student_image", ".png", examUserWithImageDTO.image().imageInBytes()); - String responsePath = fileService.handleSaveFile(studentImageFile, false, false); + String responsePath = fileService.handleSaveFile(studentImageFile, false, false).toString(); examUser.setStudentImagePath(responsePath); examUserRepository.save(examUser); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/CourseExamExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/CourseExamExportService.java index e2f39720073c..ddd5c15e5a5a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/CourseExamExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/CourseExamExportService.java @@ -128,7 +128,7 @@ private Optional zipExportedExercises(Path outputDir, List exportE var exportedCourse = createCourseZipFile(courseZip, List.of(tmpDir), exportErrors); // Delete temporary directory used for zipping - fileService.scheduleForDirectoryDeletion(tmpDir, 1); + fileService.scheduleDirectoryPathForRecursiveDeletion(tmpDir, 1); var exportState = exportErrors.isEmpty() ? CourseExamExportState.COMPLETED : CourseExamExportState.COMPLETED_WITH_WARNINGS; notifyUserAboutExerciseExportState(notificationTopic, exportState, exportErrors); diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportCreationService.java b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportCreationService.java index ff1d639e0b3a..b059fdefba47 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportCreationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportCreationService.java @@ -11,6 +11,7 @@ import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -113,7 +114,7 @@ private DataExport createDataExportWithContent(DataExport dataExport) throws IOE private void addReadmeFile(Path workingDirectory) throws IOException, URISyntaxException { var readmeInDataExportPath = workingDirectory.resolve("README.md"); var readmeTemplatePath = Path.of("templates", "dataexport", "README.md"); - Files.copy(resourceLoaderService.getResourceFilePath(readmeTemplatePath), readmeInDataExportPath); + FileUtils.copyFile(resourceLoaderService.getResourceFilePath(readmeTemplatePath).toFile(), readmeInDataExportPath.toFile()); } /** @@ -195,7 +196,7 @@ private Path prepareDataExport(DataExport dataExport) throws IOException { } dataExport = dataExportRepository.save(dataExport); Path workingDirectory = Files.createTempDirectory(dataExportsPath, "data-export-working-dir"); - fileService.scheduleForDirectoryDeletion(workingDirectory, 30); + fileService.scheduleDirectoryPathForRecursiveDeletion(workingDirectory, 30); dataExport.setDataExportState(DataExportState.IN_CREATION); dataExportRepository.save(dataExport); return workingDirectory; diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportExerciseCreationService.java b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportExerciseCreationService.java index ea1d17ab992c..f55ccbb21e3e 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportExerciseCreationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportExerciseCreationService.java @@ -5,8 +5,8 @@ import static de.tum.in.www1.artemis.service.export.DataExportUtil.retrieveCourseDirPath; import static de.tum.in.www1.artemis.service.util.RoundingUtil.roundToNDecimalPlaces; -import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -149,7 +149,7 @@ public void createProgrammingExerciseExport(ProgrammingExercise programmingExerc // we use this directory only to clone the repository and don't do this in our current directory because the current directory is part of the final data export // --> we can delete it after use - var tempRepoWorkingDir = fileService.getTemporaryUniquePath(repoClonePath, 10); + var tempRepoWorkingDir = fileService.getTemporaryUniqueSubfolderPath(repoClonePath, 10); programmingExerciseExportService.exportStudentRepositories(programmingExercise, listOfProgrammingExerciseParticipations, repositoryExportOptions, tempRepoWorkingDir, exerciseDir, Collections.synchronizedList(new ArrayList<>())); @@ -235,7 +235,7 @@ private void storeModelingSubmissionContent(ModelingSubmission modelingSubmissio } try (var modelAsPdf = apollonConversionService.get().convertModel(modelingSubmission.getModel())) { - Files.write(outputDir.resolve(fileName + PDF_FILE_EXTENSION), modelAsPdf.readAllBytes()); + FileUtils.writeByteArrayToFile(outputDir.resolve(fileName + PDF_FILE_EXTENSION).toFile(), modelAsPdf.readAllBytes()); } catch (Exception e) { log.warn("Failed to include the model as pdf, going to include it as plain JSON file."); @@ -254,11 +254,11 @@ private void storeModelingSubmissionContent(ModelingSubmission modelingSubmissio * @throws IOException if the file cannot be written */ private void addModelJsonWithExplanationHowToView(String model, Path outputDir, String fileName) throws IOException { - Files.writeString(outputDir.resolve(fileName + ".json"), model); + FileUtils.writeStringToFile(outputDir.resolve(fileName + ".md").toFile(), model, StandardCharsets.UTF_8); String explanation = """ You can view your model if you go to [Apollon Modeling Editor](https://www.apollon.ase.in.tum.de) and click on File --> Import and select the .json file. """; - Files.writeString(outputDir.resolve("view_model.md"), explanation); + FileUtils.writeStringToFile(outputDir.resolve("view_model.md").toFile(), explanation, StandardCharsets.UTF_8); } /** @@ -271,7 +271,8 @@ private void addModelJsonWithExplanationHowToView(String model, Path outputDir, private void storeTextSubmissionContent(TextSubmission textSubmission, Path outputDir) throws IOException { // text can be null which leads to an exception if (textSubmission.getText() != null) { - Files.writeString(outputDir.resolve("text_exercise_submission_" + textSubmission.getId() + "_text.txt"), textSubmission.getText()); + FileUtils.writeStringToFile(outputDir.resolve("text_exercise_submission_" + textSubmission.getId() + "_text.txt").toFile(), textSubmission.getText(), + StandardCharsets.UTF_8); } else { log.warn("Cannot include text submission content in data export because content is null for submission with id: {}", textSubmission.getId()); @@ -329,7 +330,8 @@ private void createResultsAndComplaintFiles(Submission submission, Path outputDi resultScoreAndFeedbacks.append("\n"); } } - Files.writeString(outputDir.resolve("submission_" + submission.getId() + "_result_" + result.getId() + TXT_FILE_EXTENSION), resultScoreAndFeedbacks); + FileUtils.writeStringToFile(outputDir.resolve("submission_" + submission.getId() + "_result_" + result.getId() + TXT_FILE_EXTENSION).toFile(), + resultScoreAndFeedbacks.toString(), StandardCharsets.UTF_8); } resultScoreAndFeedbacks = new StringBuilder(); } @@ -430,9 +432,9 @@ else if (plagiarismCase.getVerdict() == PlagiarismVerdict.WARNING) { * @param fileUploadSubmission the file upload submission for which the file should be copied * @throws IOException if the file cannot be copied */ - private void copyFileUploadSubmissionFile(String submissionFilePath, Path outputDir, FileUploadSubmission fileUploadSubmission) throws IOException { + private void copyFileUploadSubmissionFile(Path submissionFilePath, Path outputDir, FileUploadSubmission fileUploadSubmission) throws IOException { try { - FileUtils.copyDirectory(new File(submissionFilePath), outputDir.toFile()); + FileUtils.copyDirectory(submissionFilePath.toFile(), outputDir.toFile()); } catch (IOException exception) { log.info("Cannot include submission for file upload exercise stored at {}", submissionFilePath); @@ -449,8 +451,8 @@ private void copyFileUploadSubmissionFile(String submissionFilePath, Path output */ private void addInfoThatFileForFileUploadSubmissionNoLongerExists(Path outputDir, FileUploadSubmission fileUploadSubmission) throws IOException { var exercise = fileUploadSubmission.getParticipation().getExercise(); - Files.writeString(outputDir.resolve("submission_file_no_longer_exists.md"), - String.format("Your submitted file for the exercise %s no longer exists on the file system.", exercise)); + FileUtils.writeStringToFile(outputDir.resolve("submission_file_no_longer_exists.md").toFile(), + String.format("Your submitted file for the exercise %s no longer exists on the file system.", exercise), StandardCharsets.UTF_8); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportQuizExerciseCreationService.java b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportQuizExerciseCreationService.java index a89e41411f0a..dcb16d013298 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportQuizExerciseCreationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportQuizExerciseCreationService.java @@ -1,12 +1,13 @@ package de.tum.in.www1.artemis.service.export; import java.io.IOException; -import java.nio.file.Files; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.io.FileUtils; import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; @@ -73,11 +74,12 @@ else if (submittedAnswer instanceof MultipleChoiceSubmittedAnswer multipleChoice } } if (!multipleChoiceQuestionsSubmissions.isEmpty()) { - Files.write(outputDir.resolve("quiz_submission_" + submission.getId() + "_multiple_choice_questions_answers" + TXT_FILE_EXTENSION), - multipleChoiceQuestionsSubmissions); + FileUtils.writeLines(outputDir.resolve("quiz_submission_" + submission.getId() + "_multiple_choice_questions_answers" + TXT_FILE_EXTENSION).toFile(), + StandardCharsets.UTF_8.name(), multipleChoiceQuestionsSubmissions); } if (!shortAnswerQuestionsSubmissions.isEmpty()) { - Files.write(outputDir.resolve("quiz_submission_" + submission.getId() + "_short_answer_questions_answers" + TXT_FILE_EXTENSION), shortAnswerQuestionsSubmissions); + FileUtils.writeLines(outputDir.resolve("quiz_submission_" + submission.getId() + "_short_answer_questions_answers" + TXT_FILE_EXTENSION).toFile(), + StandardCharsets.UTF_8.name(), shortAnswerQuestionsSubmissions); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportService.java index 6972474acadd..19890057c762 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/DataExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/DataExportService.java @@ -150,7 +150,7 @@ public void deleteDataExportAndSetDataExportState(DataExport dataExport) { if (dataExport.getFilePath() == null) { return; } - fileService.scheduleForDeletion(Path.of(dataExport.getFilePath()), 2); + fileService.schedulePathForDeletion(Path.of(dataExport.getFilePath()), 2); if (dataExport.getDataExportState().hasBeenDownloaded()) { dataExport.setDataExportState(DataExportState.DOWNLOADED_DELETED); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/ExerciseWithSubmissionsExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/ExerciseWithSubmissionsExportService.java index 691c4c875843..b57dcda0b5b9 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/ExerciseWithSubmissionsExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/ExerciseWithSubmissionsExportService.java @@ -1,12 +1,14 @@ package de.tum.in.www1.artemis.service.export; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; @@ -71,7 +73,7 @@ private void exportProblemStatementWithEmbeddedFiles(Exercise exercise, List exp */ private void constructFilenameAndCopyFile(Exercise exercise, List exportErrors, Path embeddedFilesDir, String filePath) { String fileName = filePath.replace(API_MARKDOWN_FILE_PATH, ""); - Path imageFilePath = Path.of(FilePathService.getMarkdownFilePath(), fileName); + Path imageFilePath = FilePathService.getMarkdownFilePath().resolve(fileName); Path imageExportPath = embeddedFilesDir.resolve(fileName); // we need this check as it might be that the matched string is different and not filtered out above but the file is already copied if (!Files.exists(imageExportPath)) { try { - Files.copy(imageFilePath, imageExportPath); + FileUtils.copyFile(imageFilePath.toFile(), imageExportPath.toFile()); } catch (IOException e) { exportErrors.add("Failed to copy embedded files: " + e.getMessage()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/FileUploadSubmissionExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/FileUploadSubmissionExportService.java index a8302aff65c5..928c98d4b9c5 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/FileUploadSubmissionExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/FileUploadSubmissionExportService.java @@ -6,6 +6,7 @@ import java.nio.file.Path; import java.util.regex.Pattern; +import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -34,17 +35,16 @@ protected void saveSubmissionToFile(Exercise exercise, Submission submission, Fi } // we need to get the 'real' file path here, the submission only has the api url path - String filePath = FileUploadSubmission.buildFilePath(exercise.getId(), submission.getId()); - Path filePathPath = Path.of(filePath); + Path filePath = FileUploadSubmission.buildFilePath(exercise.getId(), submission.getId()); - if (!Files.exists(filePathPath)) { // throw if submission file does not exist - throw new IOException("Cannot export submission " + submission.getId() + " because the uploaded file " + filePathPath + " doesn't exist."); + if (!Files.exists(filePath)) { // throw if submission file does not exist + throw new IOException("Cannot export submission " + submission.getId() + " because the uploaded file " + filePath + " doesn't exist."); } - try (var files = Files.list(filePathPath)) { + try (var files = Files.list(filePath)) { files.forEach(content -> { try { - Files.copy(content, file.toPath()); + FileUtils.copyFile(content.toFile(), file); } catch (IOException e) { log.error("Failed to copy file {} to zip file", content, e); diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java index 29bafc2e0296..400e771aea10 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java @@ -675,7 +675,7 @@ private Path createZipForRepositoryWithParticipation(final ProgrammingExercise p try { log.debug("Normalizing code style for participation {}", participation); fileService.normalizeLineEndingsDirectory(repository.getLocalPath()); - fileService.convertToUTF8Directory(repository.getLocalPath()); + fileService.convertFilesInDirectoryToUtf8(repository.getLocalPath()); } catch (IOException ex) { log.warn("Cannot normalize code style in the repository {} due to the following exception: {}", repository.getLocalPath(), ex.getMessage()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/SubmissionExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/SubmissionExportService.java index 9bf450a76ccf..545971416039 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/SubmissionExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/SubmissionExportService.java @@ -82,7 +82,7 @@ public File exportStudentSubmissionsElseThrow(Long exerciseId, SubmissionExportO * @return the zipped file with the exported submissions */ public List exportStudentSubmissions(Long exerciseId, SubmissionExportOptionsDTO submissionExportOptions) { - Path outputDir = fileService.getTemporaryUniquePath(submissionExportPath, EXPORTED_SUBMISSIONS_DELETION_DELAY_IN_MINUTES); + Path outputDir = fileService.getTemporaryUniqueSubfolderPath(submissionExportPath, EXPORTED_SUBMISSIONS_DELETION_DELAY_IN_MINUTES); return exportStudentSubmissions(exerciseId, submissionExportOptions, true, outputDir, new ArrayList<>(), new ArrayList<>()); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java index ca47d0ef6f4e..b135284995e9 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java @@ -169,7 +169,7 @@ public File checkPlagiarismWithJPlagReport(long programmingExerciseId, float sim @NotNull private JPlagResult computeJPlagResult(ProgrammingExercise programmingExercise, float similarityThreshold, int minimumScore) { long programmingExerciseId = programmingExercise.getId(); - final var targetPath = fileService.getTemporaryUniquePath(repoDownloadClonePath, 60); + final var targetPath = fileService.getTemporaryUniqueSubfolderPath(repoDownloadClonePath, 60); List participations = filterStudentParticipationsForComparison(programmingExercise, minimumScore); log.info("Download repositories for JPlag for programming exercise {} to compare {} participations", programmingExerciseId, participations.size()); @@ -245,9 +245,9 @@ private void limitAndSavePlagiarismResult(TextPlagiarismResult textPlagiarismRes * @return the zip file */ public File generateJPlagReportZip(JPlagResult jPlagResult, ProgrammingExercise programmingExercise) { - final var targetPath = fileService.getTemporaryUniquePath(repoDownloadClonePath, 5); - final var reportFolder = targetPath.resolve(programmingExercise.getProjectKey() + " JPlag Report").toString(); - final var reportFolderFile = new File(reportFolder); + final var targetPath = fileService.getTemporaryUniqueSubfolderPath(repoDownloadClonePath, 5); + final var reportFolder = targetPath.resolve(programmingExercise.getProjectKey() + " JPlag Report"); + final var reportFolderFile = reportFolder.toFile(); // Create directories. if (!reportFolderFile.mkdirs()) { @@ -259,11 +259,11 @@ public File generateJPlagReportZip(JPlagResult jPlagResult, ProgrammingExercise // Write JPlag report result to the file. log.info("Write JPlag report to file system and zip it"); ReportObjectFactory reportObjectFactory = new ReportObjectFactory(); - reportObjectFactory.createAndSaveReport(jPlagResult, reportFolder); + reportObjectFactory.createAndSaveReport(jPlagResult, reportFolder.toString()); // JPlag automatically zips the report var zipFile = new File(reportFolder + ".zip"); - fileService.scheduleForDeletion(zipFile.getAbsoluteFile().toPath(), 1); + fileService.schedulePathForDeletion(zipFile.getAbsoluteFile().toPath(), 1); return zipFile; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/JavaTemplateUpgradeService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/JavaTemplateUpgradeService.java index 692b99aeb043..e0cc65715604 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/JavaTemplateUpgradeService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/JavaTemplateUpgradeService.java @@ -1,12 +1,11 @@ package de.tum.in.www1.artemis.service.programming; import java.io.*; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.*; import java.util.function.Predicate; +import org.apache.commons.io.FileUtils; import org.apache.maven.model.Dependency; import org.apache.maven.model.Model; import org.apache.maven.model.Plugin; @@ -290,7 +289,7 @@ private void overwriteFilesIfPresent(Repository repository, Resource[] templateR Optional templateResource = getFileByName(templateResources, filename); if (repoFile.isPresent() && templateResource.isPresent()) { try (InputStream inputStream = templateResource.get().getInputStream()) { - Files.copy(inputStream, repoFile.get().toPath(), StandardCopyOption.REPLACE_EXISTING); + FileUtils.copyToFile(inputStream, repoFile.get()); } } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportFromFileService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportFromFileService.java index fcd9fe6c3999..2d085d54a80d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportFromFileService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportFromFileService.java @@ -89,7 +89,7 @@ public ProgrammingExercise importProgrammingExerciseFromFile(ProgrammingExercise } finally { // want to make sure the directories are deleted, even if an exception is thrown - fileService.scheduleForDirectoryDeletion(importExerciseDir, 5); + fileService.scheduleDirectoryPathForRecursiveDeletion(importExerciseDir, 5); } return importedProgrammingExercise; } @@ -106,11 +106,10 @@ private void copyEmbeddedFiles(Path importExerciseDir) throws IOException { return; } try (var embeddedFiles = Files.list(embeddedFilesDir)) { - for (var file : embeddedFiles.toList()) { - var targetPath = Path.of(FilePathService.getMarkdownFilePath(), file.getFileName().toString()); - // we need this check because the detection if a file exists of Files.copy seems not to work properly + for (Path file : embeddedFiles.toList()) { + Path targetPath = FilePathService.getMarkdownFilePath().resolve(file.getFileName()); if (!Files.exists(targetPath)) { - Files.copy(file, targetPath); + FileUtils.copyFile(file.toFile(), targetPath.toFile()); } } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java index b43038b0a0b6..65f093083484 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseRepositoryService.java @@ -10,6 +10,7 @@ import java.time.ZonedDateTime; import java.util.*; +import org.apache.commons.io.FileUtils; import org.eclipse.jgit.api.errors.GitAPIException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -310,7 +311,7 @@ private void setupJVMTestTemplateAndPush(final RepositoryResources resources, fi if (ProjectType.MAVEN_BLACKBOX.equals(projectType)) { Path dejagnuLibFolderPath = repoLocalPath.resolve("testsuite").resolve("lib"); - fileService.replaceVariablesInFileName(dejagnuLibFolderPath.toString(), PACKAGE_NAME_FILE_PLACEHOLDER, programmingExercise.getPackageName()); + fileService.replaceVariablesInFilename(dejagnuLibFolderPath, PACKAGE_NAME_FILE_PLACEHOLDER, programmingExercise.getPackageName()); } final Map sectionsMap = new HashMap<>(); @@ -422,7 +423,7 @@ private void setupBuildToolProjectFile(final Path repoLocalPath, final ProjectTy projectFileFileName = POM_XML; } - fileService.replacePlaceholderSections(repoLocalPath.resolve(projectFileFileName).toAbsolutePath().toString(), activeFeatures); + fileService.replacePlaceholderSections(repoLocalPath.resolve(projectFileFileName).toAbsolutePath(), activeFeatures); } private void setupStaticCodeAnalysisConfigFiles(final RepositoryResources resources, final Path templatePath, final Path repoLocalPath) throws IOException { @@ -483,7 +484,7 @@ private void setupTestTemplateSequentialTestRuns(final RepositoryResources resou final Path repoLocalPath = getRepoAbsoluteLocalPath(resources.repository); - fileService.replacePlaceholderSections(repoLocalPath.resolve(projectFileName).toAbsolutePath().toString(), sectionsMap); + fileService.replacePlaceholderSections(repoLocalPath.resolve(projectFileName).toAbsolutePath(), sectionsMap); final Optional stagePomXml = getStagePomXml(templatePath, projectTemplatePath, isMaven); @@ -545,7 +546,7 @@ private void setupBuildStage(final Path resourcePrefix, final Path templatePath, // staging project files are only required for maven final boolean isMaven = isMavenProject(projectType); if (isMaven && stagePomXml.isPresent()) { - Files.copy(stagePomXml.get().getInputStream(), buildStagePath.resolve(POM_XML)); + FileUtils.copyFile(stagePomXml.get().getFile(), buildStagePath.resolve(POM_XML).toFile()); } final Path buildStageResourcesPath = templatePath.resolve(TEST_FILES_PATH).resolve(buildStageTemplateSubDirectory); @@ -582,8 +583,7 @@ void replacePlaceholders(final ProgrammingExercise programmingExercise, final Re switch (programmingLanguage) { case JAVA, KOTLIN -> { - fileService.replaceVariablesInDirectoryName(getRepoAbsoluteLocalPath(repository).toString(), PACKAGE_NAME_FOLDER_PLACEHOLDER, - programmingExercise.getPackageFolderName()); + fileService.replaceVariablesInDirectoryName(getRepoAbsoluteLocalPath(repository), PACKAGE_NAME_FOLDER_PLACEHOLDER, programmingExercise.getPackageFolderName()); replacements.put(PACKAGE_NAME_PLACEHOLDER, programmingExercise.getPackageName()); } case SWIFT -> replaceSwiftPlaceholders(replacements, programmingExercise, repository); @@ -608,20 +608,23 @@ void replacePlaceholders(final ProgrammingExercise programmingExercise, final Re * @throws IOException Thrown if accessing repository files fails. */ private void replaceSwiftPlaceholders(final Map replacements, final ProgrammingExercise programmingExercise, final Repository repository) throws IOException { - final String repositoryLocalPath = getRepoAbsoluteLocalPath(repository).toString(); + final Path repositoryLocalPath = getRepoAbsoluteLocalPath(repository); final String packageName = programmingExercise.getPackageName(); + // The client already provides a clean package name, but we have to make sure that no one abuses the API for injection. + // So usually, the name should not change. + final String cleanPackageName = packageName.replaceAll("[^a-zA-Z\\d]", ""); if (ProjectType.PLAIN.equals(programmingExercise.getProjectType())) { - fileService.replaceVariablesInDirectoryName(repositoryLocalPath, PACKAGE_NAME_FOLDER_PLACEHOLDER, packageName); - fileService.replaceVariablesInFileName(repositoryLocalPath, PACKAGE_NAME_FILE_PLACEHOLDER, packageName); + fileService.replaceVariablesInDirectoryName(repositoryLocalPath, PACKAGE_NAME_FOLDER_PLACEHOLDER, cleanPackageName); + fileService.replaceVariablesInFilename(repositoryLocalPath, PACKAGE_NAME_FILE_PLACEHOLDER, cleanPackageName); - replacements.put(PACKAGE_NAME_PLACEHOLDER, packageName); + replacements.put(PACKAGE_NAME_PLACEHOLDER, cleanPackageName); } else if (ProjectType.XCODE.equals(programmingExercise.getProjectType())) { - fileService.replaceVariablesInDirectoryName(repositoryLocalPath, APP_NAME_PLACEHOLDER, packageName); - fileService.replaceVariablesInFileName(repositoryLocalPath, APP_NAME_PLACEHOLDER, packageName); + fileService.replaceVariablesInDirectoryName(repositoryLocalPath, APP_NAME_PLACEHOLDER, cleanPackageName); + fileService.replaceVariablesInFilename(repositoryLocalPath, APP_NAME_PLACEHOLDER, cleanPackageName); - replacements.put(APP_NAME_PLACEHOLDER, packageName); + replacements.put(APP_NAME_PLACEHOLDER, cleanPackageName); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java index 459b46953ca8..536f716a23a8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java @@ -4,6 +4,7 @@ import static de.tum.in.www1.artemis.domain.enumeration.BuildPlanType.TEMPLATE; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.ZonedDateTime; @@ -14,6 +15,7 @@ import javax.annotation.Nullable; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.api.errors.GitAPIException; @@ -566,7 +568,7 @@ private boolean saveAndPushStructuralOracle(User user, Repository testRepository // If not, then update the oracle file by rewriting it and push the changes. if (!Files.exists(structureOraclePath)) { try { - Files.write(structureOraclePath, structureOracleJSON.getBytes()); + FileUtils.writeStringToFile(structureOraclePath.toFile(), structureOracleJSON, StandardCharsets.UTF_8); gitService.stageAllChanges(testRepository); gitService.commitAndPush(testRepository, "Generate the structure oracle file.", true, user); return true; @@ -586,7 +588,7 @@ private boolean saveAndPushStructuralOracle(User user, Repository testRepository } else { try { - Files.write(structureOraclePath, structureOracleJSON.getBytes()); + FileUtils.writeStringToFile(structureOraclePath.toFile(), structureOracleJSON, StandardCharsets.UTF_8); gitService.stageAllChanges(testRepository); gitService.commitAndPush(testRepository, "Update the structure oracle file.", true, user); return true; diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/NotificationScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/NotificationScheduleService.java index d9bb867f3bc4..d1f49b6e1997 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/NotificationScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/NotificationScheduleService.java @@ -164,8 +164,9 @@ private void scheduleNotificationForAssessedExercisesSubmissions(Exercise exerci * @return true if the time is valid else false */ private boolean checkIfTimeIsCorrectForScheduledTask(ZonedDateTime relevantTime) { - // only send a notification if relevantTime is defined and not in the future (i.e. in the range [now-2 minutes, now]) (due to possible delays in scheduling) - return relevantTime != null && !relevantTime.isBefore(ZonedDateTime.now().minusMinutes(2)) && !relevantTime.isAfter(ZonedDateTime.now()); + // Only send a notification if relevantTime is defined and close to the current time (i.e. in the range [now-2 minutes, now+2 minutes]) (due to possible delays in + // scheduling) + return relevantTime != null && !relevantTime.isBefore(ZonedDateTime.now().minusMinutes(2)) && !relevantTime.isAfter(ZonedDateTime.now().plusMinutes(2)); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/user/UserCreationService.java b/src/main/java/de/tum/in/www1/artemis/service/user/UserCreationService.java index 0f1ff37b08e5..c1f4b98fafb6 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/user/UserCreationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/user/UserCreationService.java @@ -7,7 +7,6 @@ import java.util.Optional; import java.util.Set; import java.util.regex.PatternSyntaxException; -import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.validation.constraints.NotNull; @@ -162,11 +161,9 @@ public User createUser(ManagedUserVM userDTO) { else { user.setLangKey(userDTO.getLangKey()); } - if (userDTO.getAuthorities() != null) { - Set authorities = userDTO.getAuthorities().stream().map(authorityRepository::findById).filter(Optional::isPresent).map(Optional::get) - .collect(Collectors.toSet()); - user.setAuthorities(authorities); - } + + setUserAuthorities(userDTO, user); + String password = userDTO.getPassword() == null ? RandomUtil.generatePassword() : userDTO.getPassword(); String passwordHash = passwordService.hashPassword(password); user.setPassword(passwordHash); @@ -197,6 +194,24 @@ public User createUser(ManagedUserVM userDTO) { return user; } + /** + * Updates the authorities for the user according to the ones set in the DTO. + * + * @param userDTO The source for the authorities that should be set. + * @param user The target user where the authorities are set. + */ + private void setUserAuthorities(final ManagedUserVM userDTO, final User user) { + // A user needs to have at least some role, otherwise an authentication token can never be constructed + if (userDTO.getAuthorities() == null || userDTO.getAuthorities().isEmpty()) { + userDTO.setAuthorities(Set.of(STUDENT.getAuthority())); + } + + // clear and add instead of new Set for Hibernate change tracking + final Set authorities = user.getAuthorities(); + authorities.clear(); + userDTO.getAuthorities().stream().map(authorityRepository::findById).flatMap(Optional::stream).forEach(authorities::add); + } + /** * Update basic information (first name, last name, email, language) for the current user. * This method is typically invoked by the user @@ -244,9 +259,8 @@ public User updateUser(@NotNull User user, ManagedUserVM updatedUserDTO) { user.setPassword(passwordService.hashPassword(updatedUserDTO.getPassword())); } user.setOrganizations(updatedUserDTO.getOrganizations()); - Set managedAuthorities = user.getAuthorities(); - managedAuthorities.clear(); - updatedUserDTO.getAuthorities().stream().map(authorityRepository::findById).filter(Optional::isPresent).map(Optional::get).forEach(managedAuthorities::add); + setUserAuthorities(updatedUserDTO, user); + log.debug("Changed Information for User: {}", user); return saveUser(user); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java index 961db819c051..47e27d39c576 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AttachmentResource.java @@ -24,6 +24,7 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; import de.tum.in.www1.artemis.service.AuthorizationCheckService; +import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; import de.tum.in.www1.artemis.web.rest.util.HeaderUtil; @@ -53,15 +54,18 @@ public class AttachmentResource { private final FileService fileService; + private final FilePathService filePathService; + private final CacheManager cacheManager; public AttachmentResource(AttachmentRepository attachmentRepository, GroupNotificationService groupNotificationService, AuthorizationCheckService authorizationCheckService, - UserRepository userRepository, FileService fileService, CacheManager cacheManager) { + UserRepository userRepository, FileService fileService, FilePathService filePathService, CacheManager cacheManager) { this.attachmentRepository = attachmentRepository; this.groupNotificationService = groupNotificationService; this.authorizationCheckService = authorizationCheckService; this.userRepository = userRepository; this.fileService = fileService; + this.filePathService = filePathService; this.cacheManager = cacheManager; } @@ -79,11 +83,11 @@ public ResponseEntity createAttachment(@RequestPart Attachment attac log.debug("REST request to save Attachment : {}", attachment); attachment.setId(null); - String pathString = fileService.handleSaveFile(file, false, false); + String pathString = fileService.handleSaveFile(file, false, false).toString(); attachment.setLink(pathString); Attachment result = attachmentRepository.save(attachment); - this.cacheManager.getCache("files").evict(fileService.actualPathForPublicPath(result.getLink())); + this.cacheManager.getCache("files").evict(filePathService.actualPathForPublicPath(URI.create(result.getLink())).toString()); return ResponseEntity.created(new URI("/api/attachments/" + result.getId())).body(result); } @@ -109,12 +113,12 @@ public ResponseEntity updateAttachment(@PathVariable Long attachment attachment.setAttachmentUnit(originalAttachment.getAttachmentUnit()); if (file != null) { - String pathString = fileService.handleSaveFile(file, false, false); + String pathString = fileService.handleSaveFile(file, false, false).toString(); attachment.setLink(pathString); } Attachment result = attachmentRepository.save(attachment); - this.cacheManager.getCache("files").evict(fileService.actualPathForPublicPath(result.getLink())); + this.cacheManager.getCache("files").evict(filePathService.actualPathForPublicPath(URI.create(result.getLink())).toString()); if (notificationText != null) { groupNotificationService.notifyStudentGroupAboutAttachmentChange(result, notificationText); } @@ -169,7 +173,7 @@ public ResponseEntity deleteAttachment(@PathVariable Long attachmentId) { course = attachment.getLecture().getCourse(); relatedEntity = "lecture " + attachment.getLecture().getTitle(); try { - this.cacheManager.getCache("files").evict(fileService.actualPathForPublicPath(attachment.getLink())); + this.cacheManager.getCache("files").evict(filePathService.actualPathForPublicPath(URI.create(attachment.getLink())).toString()); } catch (RuntimeException exception) { // this catch is required for deleting wrongly formatted attachment database entries diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java index a5a26daf1153..dd503338932c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java @@ -219,7 +219,7 @@ public ResponseEntity updateCourse(@PathVariable Long courseId, @Request courseUpdate.validateUnenrollmentEndDate(); if (file != null) { - String pathString = fileService.handleSaveFile(file, false, false); + String pathString = fileService.handleSaveFile(file, false, false).toString(); courseUpdate.setCourseIcon(pathString); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ExamUserResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ExamUserResource.java index f4651b7b789f..e4006712c5b1 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ExamUserResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ExamUserResource.java @@ -71,7 +71,7 @@ public ResponseEntity updateExamUser(@RequestPart ExamUserDTO examUser .orElseThrow(() -> new EntityNotFoundException("Exam user with login: \"" + examUserDTO.login() + "\" does not exist")); if (signatureFile != null) { - String responsePath = fileService.handleSaveFile(signatureFile, true, false); + String responsePath = fileService.handleSaveFile(signatureFile, true, false).toString(); examUser.setSigningImagePath(responsePath); } examUser.setDidCheckImage(examUserDTO.didCheckImage()); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java index 7cd98dca5191..2464683b3a36 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java @@ -58,6 +58,8 @@ public class FileResource { private final FileService fileService; + private final FilePathService filePathService; + private final ResourceLoaderService resourceLoaderService; private final LectureRepository lectureRepository; @@ -86,10 +88,12 @@ public class FileResource { private final CourseRepository courseRepository; - public FileResource(SlideRepository slideRepository, AuthorizationCheckService authorizationCheckService, FileService fileService, ResourceLoaderService resourceLoaderService, - LectureRepository lectureRepository, FileUploadSubmissionRepository fileUploadSubmissionRepository, FileUploadExerciseRepository fileUploadExerciseRepository, - AttachmentRepository attachmentRepository, AttachmentUnitRepository attachmentUnitRepository, AuthorizationCheckService authCheckService, UserRepository userRepository, - ExamUserRepository examUserRepository, QuizQuestionRepository quizQuestionRepository, DragItemRepository dragItemRepository, CourseRepository courseRepository) { + public FileResource(FilePathService filePathService, SlideRepository slideRepository, AuthorizationCheckService authorizationCheckService, FileService fileService, + ResourceLoaderService resourceLoaderService, LectureRepository lectureRepository, FileUploadSubmissionRepository fileUploadSubmissionRepository, + FileUploadExerciseRepository fileUploadExerciseRepository, AttachmentRepository attachmentRepository, AttachmentUnitRepository attachmentUnitRepository, + AuthorizationCheckService authCheckService, UserRepository userRepository, ExamUserRepository examUserRepository, QuizQuestionRepository quizQuestionRepository, + DragItemRepository dragItemRepository, CourseRepository courseRepository) { + this.filePathService = filePathService; this.fileService = fileService; this.resourceLoaderService = resourceLoaderService; this.lectureRepository = lectureRepository; @@ -122,7 +126,7 @@ public FileResource(SlideRepository slideRepository, AuthorizationCheckService a @EnforceAtLeastTutor public ResponseEntity saveFile(@RequestParam(value = "file") MultipartFile file, @RequestParam(defaultValue = "false") boolean keepFileName) throws URISyntaxException { log.debug("REST request to upload file : {}", file.getOriginalFilename()); - String responsePath = fileService.handleSaveFile(file, keepFileName, false); + String responsePath = fileService.handleSaveFile(file, keepFileName, false).toString(); // return path for getting the file String responseBody = "{\"path\":\"" + responsePath + "\"}"; @@ -142,7 +146,7 @@ public ResponseEntity saveFile(@RequestParam(value = "file") MultipartFi public ResponseEntity getTempFile(@PathVariable String filename) { log.debug("REST request to get file : {}", filename); sanitizeFilenameElseThrow(filename); - return responseEntityForFilePath(Path.of(FilePathService.getTempFilePath(), filename).toString()); + return responseEntityForFilePath(FilePathService.getTempFilePath().resolve(filename)); } /** @@ -158,7 +162,7 @@ public ResponseEntity getTempFile(@PathVariable String filename) { public ResponseEntity saveMarkdownFile(@RequestParam(value = "file") MultipartFile file, @RequestParam(defaultValue = "false") boolean keepFileName) throws URISyntaxException { log.debug("REST request to upload file for markdown: {}", file.getOriginalFilename()); - String responsePath = fileService.handleSaveFile(file, keepFileName, true); + String responsePath = fileService.handleSaveFile(file, keepFileName, true).toString(); // return path for getting the file String responseBody = "{\"path\":\"" + responsePath + "\"}"; @@ -233,7 +237,7 @@ public ResponseEntity getDragAndDropBackgroundFile(@PathVariable Long qu DragAndDropQuestion question = quizQuestionRepository.findDnDQuestionByIdOrElseThrow(questionId); Course course = question.getExercise().getCourseViaExerciseGroupOrCourseMember(); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); - return responseEntityForFilePath(fileService.actualPathForPublicPath(question.getBackgroundFilePath())); + return responseEntityForFilePath(filePathService.actualPathForPublicPath(URI.create(question.getBackgroundFilePath()))); } /** @@ -252,7 +256,7 @@ public ResponseEntity getDragItemFile(@PathVariable Long dragItemId) { if (dragItem.getPictureFilePath() == null) { throw new EntityNotFoundException("Drag item " + dragItemId + " has no picture file"); } - return responseEntityForFilePath(fileService.actualPathForPublicPath(dragItem.getPictureFilePath())); + return responseEntityForFilePath(filePathService.actualPathForPublicPath(URI.create(dragItem.getPictureFilePath()))); } /** @@ -286,7 +290,7 @@ public ResponseEntity getFileUploadSubmission(@PathVariable Long exercis throw new AccessForbiddenException(); } - return buildFileResponse(fileService.actualPathForPublicPath(submission.getFilePath()), false); + return buildFileResponse(filePathService.actualPathForPublicPath(URI.create(submission.getFilePath())), false); } /** @@ -301,7 +305,7 @@ public ResponseEntity getCourseIcon(@PathVariable Long courseId) { log.debug("REST request to get icon for course : {}", courseId); Course course = courseRepository.findByIdElseThrow(courseId); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); - return responseEntityForFilePath(fileService.actualPathForPublicPath(course.getCourseIcon())); + return responseEntityForFilePath(filePathService.actualPathForPublicPath(URI.create(course.getCourseIcon()))); } /** @@ -317,7 +321,7 @@ public ResponseEntity getUserSignature(@PathVariable Long examUserId) { ExamUser examUser = examUserRepository.findWithExamById(examUserId).orElseThrow(); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, examUser.getExam().getCourse(), null); - return buildFileResponse(fileService.actualPathForPublicPath(examUser.getSigningImagePath()), false); + return buildFileResponse(filePathService.actualPathForPublicPath(URI.create(examUser.getSigningImagePath())), false); } /** @@ -333,7 +337,7 @@ public ResponseEntity getExamUserImage(@PathVariable Long examUserId) { ExamUser examUser = examUserRepository.findWithExamById(examUserId).orElseThrow(); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, examUser.getExam().getCourse(), null); - return buildFileResponse(fileService.actualPathForPublicPath(examUser.getStudentImagePath()), true); + return buildFileResponse(filePathService.actualPathForPublicPath(URI.create(examUser.getStudentImagePath())), true); } /** @@ -360,7 +364,7 @@ public ResponseEntity getLectureAttachment(@PathVariable Long lectureId, // check if the user is authorized to access the requested attachment unit checkAttachmentAuthorizationOrThrow(course, attachment); - return buildFileResponse(fileService.actualPathForPublicPath(attachment.getLink()), false); + return buildFileResponse(filePathService.actualPathForPublicPath(URI.create(attachment.getLink())), false); } /** @@ -385,8 +389,8 @@ public ResponseEntity getLecturePdfAttachmentsMerged(@PathVariable Long List attachmentLinks = lectureAttachments.stream() .filter(unit -> authCheckService.isAllowedToSeeLectureUnit(unit, user) && "pdf".equals(StringUtils.substringAfterLast(unit.getAttachment().getLink(), "."))) - .map(unit -> Path.of(FilePathService.getAttachmentUnitFilePath(), String.valueOf(unit.getId()), StringUtils.substringAfterLast(unit.getAttachment().getLink(), "/")) - .toString()) + .map(unit -> FilePathService.getAttachmentUnitFilePath() + .resolve(Path.of(String.valueOf(unit.getId()), StringUtils.substringAfterLast(unit.getAttachment().getLink(), "/"))).toString()) .toList(); Optional file = fileService.mergePdfFiles(attachmentLinks, lectureRepository.getLectureTitle(lectureId)); @@ -417,7 +421,7 @@ public ResponseEntity getAttachmentUnitAttachment(@PathVariable Long att // check if the user is authorized to access the requested attachment unit checkAttachmentAuthorizationOrThrow(course, attachment); - return buildFileResponse(fileService.actualPathForPublicPath(attachment.getLink()), false); + return buildFileResponse(filePathService.actualPathForPublicPath(URI.create(attachment.getLink())), false); } /** @@ -448,8 +452,8 @@ public ResponseEntity getAttachmentUnitAttachmentSlide(@PathVariable Lon if (matcher.matches()) { String fileName = matcher.group(1); return buildFileResponse( - Path.of(FilePathService.getAttachmentUnitFilePath(), String.valueOf(attachmentUnit.getId()), "slide", String.valueOf(slide.getSlideNumber())).toString(), - fileName, true); + FilePathService.getAttachmentUnitFilePath().resolve(Path.of(attachmentUnit.getId().toString(), "slide", String.valueOf(slide.getSlideNumber()))), fileName, + true); } else { throw new EntityNotFoundException("Slide", slideNumber); @@ -463,20 +467,19 @@ public ResponseEntity getAttachmentUnitAttachmentSlide(@PathVariable Lon * @param filename the name of the file * @return response entity */ - private ResponseEntity buildFileResponse(String path, String filename) { + private ResponseEntity buildFileResponse(Path path, String filename) { return buildFileResponse(path, filename, false); } /** * Builds the response with headers, body and content type for specified path containing the file name * - * @param pathString to the file including the file name - * @param cache true if the response should contain a header that allows caching; false otherwise + * @param path to the file including the file name + * @param cache true if the response should contain a header that allows caching; false otherwise * @return response entity */ - private ResponseEntity buildFileResponse(String pathString, boolean cache) { - Path path = Path.of(pathString); - return buildFileResponse(path.getParent().toString(), path.getFileName().toString(), cache); + private ResponseEntity buildFileResponse(Path path, boolean cache) { + return buildFileResponse(path.getParent(), path.getFileName().toString(), cache); } /** @@ -487,10 +490,10 @@ private ResponseEntity buildFileResponse(String pathString, boolean cach * @param cache true if the response should contain a header that allows caching; false otherwise * @return response entity */ - private ResponseEntity buildFileResponse(String path, String filename, boolean cache) { + private ResponseEntity buildFileResponse(Path path, String filename, boolean cache) { try { - var actualPath = Path.of(path, filename).toString(); - var file = fileService.getFileForPath(actualPath); + Path actualPath = path.resolve(filename); + byte[] file = fileService.getFileForPath(actualPath); if (file == null) { return ResponseEntity.notFound().build(); } @@ -547,7 +550,7 @@ private void checkAttachmentAuthorizationOrThrow(Course course, Attachment attac * @param filePath the path for the file to read * @return ResponseEntity with status 200 and the file as byte stream, status 404 if the file doesn't exist, or status 500 if there is an error while reading the file */ - private ResponseEntity responseEntityForFilePath(String filePath) { + private ResponseEntity responseEntityForFilePath(Path filePath) { try { var file = fileService.getFileForPath(filePath); if (file == null) { @@ -556,7 +559,7 @@ private ResponseEntity responseEntityForFilePath(String filePath) { return ResponseEntity.ok(file); } catch (IOException e) { - e.printStackTrace(); + log.error("Failed to return requested file with path {}", filePath, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminCourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminCourseResource.java index c57e0b30b911..dae6f829e55f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminCourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminCourseResource.java @@ -124,7 +124,7 @@ public ResponseEntity createCourse(@RequestPart Course course, @RequestP courseService.createOrValidateGroups(course); if (file != null) { - String pathString = fileService.handleSaveFile(file, false, false); + String pathString = fileService.handleSaveFile(file, false, false).toString(); course.setCourseIcon(pathString); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryResource.java index d94088c67748..5cce9e0333c0 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryResource.java @@ -8,8 +8,6 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.FileAlreadyExistsException; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -17,6 +15,7 @@ import javax.servlet.http.HttpServletRequest; +import org.apache.commons.io.FileUtils; import org.eclipse.jgit.api.errors.CheckoutConflictException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; @@ -391,7 +390,7 @@ private void fetchAndUpdateFile(FileSubmission submission, Repository repository } InputStream inputStream = new ByteArrayInputStream(submission.getFileContent().getBytes(StandardCharsets.UTF_8)); - Files.copy(inputStream, file.get().toPath(), StandardCopyOption.REPLACE_EXISTING); + FileUtils.copyToFile(inputStream, file.get()); inputStream.close(); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/websocket/QuizSubmissionWebsocketService.java b/src/main/java/de/tum/in/www1/artemis/web/websocket/QuizSubmissionWebsocketService.java index e394a3863d1f..8aac25aa6091 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/websocket/QuizSubmissionWebsocketService.java +++ b/src/main/java/de/tum/in/www1/artemis/web/websocket/QuizSubmissionWebsocketService.java @@ -4,8 +4,6 @@ import javax.validation.Valid; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; @@ -14,13 +12,12 @@ import de.tum.in.www1.artemis.domain.quiz.QuizSubmission; import de.tum.in.www1.artemis.exception.QuizSubmissionException; import de.tum.in.www1.artemis.security.SecurityUtils; -import de.tum.in.www1.artemis.service.*; +import de.tum.in.www1.artemis.service.QuizSubmissionService; +import de.tum.in.www1.artemis.service.WebsocketMessagingService; @Controller public class QuizSubmissionWebsocketService { - private static final Logger log = LoggerFactory.getLogger(QuizSubmissionWebsocketService.class); - private final QuizSubmissionService quizSubmissionService; private final WebsocketMessagingService websocketMessagingService; @@ -45,29 +42,11 @@ public void saveSubmission(@DestinationVariable Long exerciseId, @Valid @Payload // Without this, custom jpa repository methods don't work in websocket channel. SecurityUtils.setAuthorizationObject(); try { - QuizSubmission updatedQuizSubmission = quizSubmissionService.saveSubmissionForLiveMode(exerciseId, quizSubmission, principal.getName(), false); - // send updated submission over websocket (use a thread to prevent that the outbound channel blocks the inbound channel (e.g. due a slow client)) - // to improve the performance, this is currently deactivated: slow clients might lead to bottlenecks so that more important messages can not be distributed any more - // new Thread(() -> sendSubmissionToUser(username, exerciseId, quizSubmission)).start(); - - // log.info("WS.Inbound: Sent quiz submission (async) back to user {} in quiz {} after {} µs ", principal.getName(), exerciseId, (System.nanoTime() - start) / 1000); + quizSubmissionService.saveSubmissionForLiveMode(exerciseId, quizSubmission, principal.getName(), false); } catch (QuizSubmissionException ex) { // send error message over websocket (use Async to prevent that the outbound channel blocks the inbound channel (e.g. due a slow client)) websocketMessagingService.sendMessageToUser(principal.getName(), "/topic/quizExercise/" + exerciseId + "/submission", new WebsocketError(ex.getMessage())); } } - - /** - * Should be invoked using a thread asynchronously - * - * @param username the user who saved / submitted the quiz submission - * @param exerciseId the quiz exercise id - * @param quizSubmission the quiz submission that is returned back to the user - */ - private void sendSubmissionToUser(String username, Long exerciseId, QuizSubmission quizSubmission) { - long start = System.nanoTime(); - websocketMessagingService.sendMessageToUser(username, "/topic/quizExercise/" + exerciseId + "/submission", quizSubmission); - log.info("WS.Outbound: Sent quiz submission to user {} in quiz {} in {} µs ", username, exerciseId, (System.nanoTime() - start) / 1000); - } } diff --git a/src/main/webapp/app/core/about-us/about-us.component.ts b/src/main/webapp/app/core/about-us/about-us.component.ts index 34ba2b455a03..1d6fb1b5fd58 100644 --- a/src/main/webapp/app/core/about-us/about-us.component.ts +++ b/src/main/webapp/app/core/about-us/about-us.component.ts @@ -41,6 +41,7 @@ export class AboutUsComponent implements OnInit { ['learningAnalytics', { learningAnalyticsUrl: 'https://ls1intum.github.io/Artemis/user/learning-analytics/' }], ['adaptiveLearning', { adaptiveLearningUrl: 'https://ls1intum.github.io/Artemis/user/adaptive-learning/' }], ['tutorialGroups', { tutorialGroupsUrl: 'https://ls1intum.github.io/Artemis/user/tutorialgroups/' }], + ['iris', { irisUrl: 'https://artemis.cit.tum.de/about-iris' }], ['scalable', { scalingUrl: 'https://ls1intum.github.io/Artemis/user/scaling/' }], ['highUserSatisfaction', { userExperienceUrl: 'https://ls1intum.github.io/Artemis/user/user-experience/' }], ['customizable', { customizableUrl: 'https://ls1intum.github.io/Artemis/user/courses/customizable' }], diff --git a/src/main/webapp/app/course/manage/course-lti-configuration/course-lti-configuration.component.ts b/src/main/webapp/app/course/manage/course-lti-configuration/course-lti-configuration.component.ts index bcb3d0e29ed7..47f8b19703e4 100644 --- a/src/main/webapp/app/course/manage/course-lti-configuration/course-lti-configuration.component.ts +++ b/src/main/webapp/app/course/manage/course-lti-configuration/course-lti-configuration.component.ts @@ -72,7 +72,7 @@ export class CourseLtiConfigurationComponent implements OnInit { * Gets the deep linking url */ getDeepLinkingUrl(): string { - return `${location.origin}/api/lti13/deep-linking/${this.course.id}`; // Needs to match url in CustomLti13Configurer + return `${location.origin}/api/public/lti13/deep-linking/${this.course.id}`; // Needs to match url in CustomLti13Configurer } /** @@ -93,14 +93,14 @@ export class CourseLtiConfigurationComponent implements OnInit { * Gets the initiate login url */ getInitiateLoginUrl(): string { - return `${location.origin}/api/lti13/initiate-login/${this.onlineCourseConfiguration?.registrationId}`; // Needs to match uri in CustomLti13Configurer + return `${location.origin}/api/public/lti13/initiate-login/${this.onlineCourseConfiguration?.registrationId}`; // Needs to match uri in CustomLti13Configurer } /** * Gets the redirect uri */ getRedirectUri(): string { - return `${location.origin}/api/lti13/auth-callback`; // Needs to match uri in CustomLti13Configurer + return `${location.origin}/api/public/lti13/auth-callback`; // Needs to match uri in CustomLti13Configurer } /** diff --git a/src/main/webapp/app/entities/exercise.model.ts b/src/main/webapp/app/entities/exercise.model.ts index 11474da66052..286e89a58bef 100644 --- a/src/main/webapp/app/entities/exercise.model.ts +++ b/src/main/webapp/app/entities/exercise.model.ts @@ -152,6 +152,10 @@ export abstract class Exercise implements BaseEntity { } } +/** + * Get an icon for the type of the given exercise. + * @param exerciseType {ExerciseType} + */ export function getIcon(exerciseType?: ExerciseType): IconProp { if (!exerciseType) { return faQuestion as IconProp; diff --git a/src/main/webapp/app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component.html b/src/main/webapp/app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component.html index 087c41cd6df0..f4c2bade4233 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component.html @@ -90,7 +90,7 @@

{{ exercise.id }} - + {{ exerciseGroup.title }}

- + - + - + - +
diff --git a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.html b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.html index 31c31af74ec5..b79c4330340f 100644 --- a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.html +++ b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.html @@ -2,6 +2,9 @@ + @@ -20,6 +23,9 @@ +
+ + ID  Title  Release 
+ + {{ fileUploadExercise.id }} @@ -92,4 +98,18 @@
+
+ +
diff --git a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.ts b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.ts index 207b2266048a..3f2df9caec08 100644 --- a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.ts +++ b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.ts @@ -38,7 +38,7 @@ export class FileUploadExerciseComponent extends ExerciseComponent { constructor( public exerciseService: ExerciseService, - private fileUploadExerciseService: FileUploadExerciseService, + public fileUploadExerciseService: FileUploadExerciseService, private courseExerciseService: CourseExerciseService, private alertService: AlertService, private accountService: AccountService, @@ -64,6 +64,7 @@ export class FileUploadExerciseComponent extends ExerciseComponent { this.fileUploadExercises.forEach((exercise) => { exercise.course = this.course; this.accountService.setAccessRightsForExercise(exercise); + this.selectedExercises = []; }); this.emitExerciseCount(this.fileUploadExercises.length); this.applyFilter(); diff --git a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.html b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.html index 28240deb42ed..7b0dfc88772d 100644 --- a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.html +++ b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.html @@ -2,6 +2,9 @@ + @@ -20,6 +23,9 @@ +
+ + ID  Title  Release 
+ +
+
+ +
diff --git a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.ts b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.ts index b440cba6f51a..308ccb453493 100644 --- a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.ts +++ b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.ts @@ -36,7 +36,7 @@ export class ModelingExerciseComponent extends ExerciseComponent { constructor( public exerciseService: ExerciseService, - private modelingExerciseService: ModelingExerciseService, + public modelingExerciseService: ModelingExerciseService, private courseExerciseService: CourseExerciseService, private alertService: AlertService, private accountService: AccountService, @@ -60,6 +60,7 @@ export class ModelingExerciseComponent extends ExerciseComponent { this.modelingExercises.forEach((exercise) => { exercise.course = this.course; this.accountService.setAccessRightsForExercise(exercise); + this.selectedExercises = []; }); this.applyFilter(); this.emitExerciseCount(this.modelingExercises.length); diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-create-buttons.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise-create-buttons.component.html index 91bee476f954..bfd15c7e0002 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-create-buttons.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-create-buttons.component.html @@ -8,6 +8,7 @@ > Create Programming Exercise +
diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-create-buttons.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise-create-buttons.component.ts index c1fa37c0a30c..5ec55e54f894 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-create-buttons.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-create-buttons.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from '@angular/core'; import { Course } from 'app/entities/course.model'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; -import { faFileImport, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { faFileImport, faKeyboard, faPlus } from '@fortawesome/free-solid-svg-icons'; import { ExerciseImportWrapperComponent } from 'app/exercises/shared/import/exercise-import-wrapper/exercise-import-wrapper.component'; import { ExerciseType } from 'app/entities/exercise.model'; import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; @@ -20,6 +20,7 @@ export class ProgrammingExerciseCreateButtonsComponent { faPlus = faPlus; faFileImport = faFileImport; + faKeyboard = faKeyboard; constructor( private router: Router, diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html index 07c74473dcc1..cc6a94b1f1d0 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html @@ -3,7 +3,7 @@ - + ID  Title  @@ -29,7 +29,7 @@ - + @@ -251,20 +251,34 @@ -
+
- - + + +
diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts index 0de8bc36014b..6da5f5ff34f4 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts @@ -1,5 +1,6 @@ import { Component, ContentChild, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { merge } from 'rxjs'; import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; import { ProgrammingExerciseInstructorRepositoryType, ProgrammingExerciseService } from './services/programming-exercise.service'; import { ActivatedRoute, Router } from '@angular/router'; @@ -46,12 +47,10 @@ import { PROFILE_LOCALVC } from 'app/app.constants'; export class ProgrammingExerciseComponent extends ExerciseComponent implements OnInit, OnDestroy { @Input() programmingExercises: ProgrammingExercise[]; filteredProgrammingExercises: ProgrammingExercise[]; - selectedProgrammingExercises: ProgrammingExercise[]; readonly ActionType = ActionType; FeatureToggle = FeatureToggle; solutionParticipationType = ProgrammingExerciseParticipationType.SOLUTION; templateParticipationType = ProgrammingExerciseParticipationType.TEMPLATE; - allChecked = false; // Used to make the repository links download the repositories instead of linking to Bitbucket/GitLab. localVCEnabled = false; @@ -92,7 +91,6 @@ export class ProgrammingExerciseComponent extends ExerciseComponent implements O ) { super(courseService, translateService, route, eventManager); this.programmingExercises = []; - this.selectedProgrammingExercises = []; } ngOnInit(): void { @@ -127,7 +125,7 @@ export class ProgrammingExerciseComponent extends ExerciseComponent implements O ); } } - this.selectedProgrammingExercises = []; + this.selectedExercises = []; }); this.applyFilter(); this.emitExerciseCount(this.programmingExercises.length); @@ -163,6 +161,27 @@ export class ProgrammingExerciseComponent extends ExerciseComponent implements O }); } + /** + * Deletes all the given programming exercises + * @param exercisesToDelete the exercise objects which are to be deleted + * @param event contains additional checks which are performed for all these exercises + */ + deleteMultipleProgrammingExercises(exercisesToDelete: ProgrammingExercise[], event: { [key: string]: boolean }) { + const deletionObservables = exercisesToDelete.map((exercise) => + this.programmingExerciseService.delete(exercise.id!, event.deleteStudentReposBuildPlans, event.deleteBaseReposBuildPlans), + ); + return merge(...deletionObservables).subscribe({ + next: () => { + this.eventManager.broadcast({ + name: 'programmingExerciseListModification', + content: 'Deleted selected programmingExercises', + }); + this.dialogErrorSource.next(''); + }, + error: (error: HttpErrorResponse) => this.dialogErrorSource.next(error.message), + }); + } + protected getChangeEventName(): string { return 'programmingExerciseListModification'; } @@ -172,33 +191,12 @@ export class ProgrammingExerciseComponent extends ExerciseComponent implements O this.applyFilter(); } - toggleProgrammingExercise(programmingExercise: ProgrammingExercise) { - const programmingExerciseIndex = this.selectedProgrammingExercises.indexOf(programmingExercise); - if (programmingExerciseIndex !== -1) { - this.selectedProgrammingExercises.splice(programmingExerciseIndex, 1); - } else { - this.selectedProgrammingExercises.push(programmingExercise); - } - } - - toggleAllProgrammingExercises() { - this.selectedProgrammingExercises = []; - if (!this.allChecked) { - this.selectedProgrammingExercises = this.selectedProgrammingExercises.concat(this.programmingExercises); - } - this.allChecked = !this.allChecked; - } - - isExerciseSelected(programmingExercise: ProgrammingExercise) { - return this.selectedProgrammingExercises.includes(programmingExercise); - } - openEditSelectedModal() { const modalRef = this.modalService.open(ProgrammingExerciseEditSelectedComponent, { size: 'xl', backdrop: 'static', }); - modalRef.componentInstance.selectedProgrammingExercises = this.selectedProgrammingExercises; + modalRef.componentInstance.selectedProgrammingExercises = this.selectedExercises; modalRef.closed.subscribe(() => { location.reload(); }); @@ -209,7 +207,7 @@ export class ProgrammingExerciseComponent extends ExerciseComponent implements O */ checkConsistencies() { const modalRef = this.modalService.open(ConsistencyCheckComponent, { keyboard: true, size: 'lg' }); - modalRef.componentInstance.exercisesToCheck = this.selectedProgrammingExercises; + modalRef.componentInstance.exercisesToCheck = this.selectedExercises; } /** diff --git a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-create-buttons.component.html b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-create-buttons.component.html index b6057241b807..fbc66fcfd615 100644 --- a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-create-buttons.component.html +++ b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-create-buttons.component.html @@ -7,14 +7,17 @@ > Create new Quiz + diff --git a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-create-buttons.component.ts b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-create-buttons.component.ts index 7f35097392ab..80924d7e8254 100644 --- a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-create-buttons.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-create-buttons.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; import { Course } from 'app/entities/course.model'; -import { faFileExport, faFileImport, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { faCheckDouble, faFileExport, faFileImport, faPlus } from '@fortawesome/free-solid-svg-icons'; import { ExerciseImportWrapperComponent } from 'app/exercises/shared/import/exercise-import-wrapper/exercise-import-wrapper.component'; import { ExerciseType } from 'app/entities/exercise.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -19,6 +19,7 @@ export class QuizExerciseCreateButtonsComponent { faPlus = faPlus; faFileImport = faFileImport; faFileExport = faFileExport; + faCheckDouble = faCheckDouble; constructor( private router: Router, diff --git a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.html b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.html index 3af0624eb5da..e409b73e4fb6 100644 --- a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.html +++ b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.html @@ -2,6 +2,9 @@ + @@ -20,6 +23,9 @@ +
+ + ID  Title  Status 
+ + {{ quizExercise.id }} @@ -239,4 +245,18 @@
+
+ +
diff --git a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.ts b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.ts index ed8d7322e6ea..4fb994ab00bd 100644 --- a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise.component.ts @@ -41,7 +41,7 @@ export class QuizExerciseComponent extends ExerciseComponent { faStopCircle = faStopCircle; constructor( - private quizExerciseService: QuizExerciseService, + public quizExerciseService: QuizExerciseService, private accountService: AccountService, private alertService: AlertService, private modalService: NgbModal, @@ -68,6 +68,7 @@ export class QuizExerciseComponent extends ExerciseComponent { exercise.isAtLeastInstructor = this.accountService.isAtLeastInstructorInCourse(exercise.course); exercise.quizBatches = exercise.quizBatches?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)); exercise.isEditable = isQuizEditable(exercise); + this.selectedExercises = []; }); this.setQuizExercisesStatus(); this.emitExerciseCount(this.quizExercises.length); diff --git a/src/main/webapp/app/exercises/shared/exercise/exercise.component.ts b/src/main/webapp/app/exercises/shared/exercise/exercise.component.ts index 9f763d937f05..3d53f2ec7edb 100644 --- a/src/main/webapp/app/exercises/shared/exercise/exercise.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise/exercise.component.ts @@ -1,11 +1,17 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Subject, Subscription } from 'rxjs'; +import { Observable, Subject, Subscription, merge } from 'rxjs'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { TranslateService } from '@ngx-translate/core'; import { Course } from 'app/entities/course.model'; import { EventManager } from 'app/core/util/event-manager.service'; import { ExerciseFilter } from 'app/entities/exercise-filter.model'; +import { Exercise } from 'app/entities/exercise.model'; + +interface DeletionServiceInterface { + delete: (id: number) => Observable>; +} @Component({ template: '' }) export abstract class ExerciseComponent implements OnInit, OnDestroy { @@ -20,6 +26,9 @@ export abstract class ExerciseComponent implements OnInit, OnDestroy { predicate: string; reverse: boolean; + selectedExercises: Exercise[] = []; + allChecked = false; + // These two variables are used to emit errors to the delete dialog protected dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); @@ -104,4 +113,45 @@ export abstract class ExerciseComponent implements OnInit, OnDestroy { private registerChangeInExercises() { this.eventSubscriber = this.eventManager.subscribe(this.getChangeEventName(), () => this.load()); } + + /** + * Deletes all the given exercises (does not work for programming exercises) + * @param exercisesToDelete the exercise objects which are to be deleted + * @param exerciseService service that is used to delete the exercise + * @param event contains additional checks which are performed for all these exercises + */ + deleteMultipleExercises(exercisesToDelete: Exercise[], exerciseService: DeletionServiceInterface) { + const deletionObservables = exercisesToDelete.map((exercise) => exerciseService.delete(exercise.id!)); + return merge(...deletionObservables).subscribe({ + next: () => { + this.eventManager.broadcast({ + name: this.getChangeEventName(), + content: 'Deleted selected Exercises', + }); + this.dialogErrorSource.next(''); + }, + error: (error: HttpErrorResponse) => this.dialogErrorSource.next(error.message), + }); + } + + toggleExercise(exercise: Exercise) { + const exerciseIndex = this.selectedExercises.indexOf(exercise); + if (exerciseIndex !== -1) { + this.selectedExercises.splice(exerciseIndex, 1); + } else { + this.selectedExercises.push(exercise); + } + } + + toggleMultipleExercises(exercises: Exercise[]) { + this.selectedExercises = []; + if (!this.allChecked) { + this.selectedExercises = this.selectedExercises.concat(exercises); + } + this.allChecked = !this.allChecked; + } + + isExerciseSelected(exercise: Exercise) { + return this.selectedExercises.includes(exercise); + } } diff --git a/src/main/webapp/app/exercises/shared/manage/exercise-create-buttons.component.html b/src/main/webapp/app/exercises/shared/manage/exercise-create-buttons.component.html index 4d616bbed07b..5cc06d26350f 100644 --- a/src/main/webapp/app/exercises/shared/manage/exercise-create-buttons.component.html +++ b/src/main/webapp/app/exercises/shared/manage/exercise-create-buttons.component.html @@ -7,9 +7,11 @@ > Create new Exercise + diff --git a/src/main/webapp/app/exercises/shared/manage/exercise-create-buttons.component.ts b/src/main/webapp/app/exercises/shared/manage/exercise-create-buttons.component.ts index 8a738a285ee1..f24d1d507c9d 100644 --- a/src/main/webapp/app/exercises/shared/manage/exercise-create-buttons.component.ts +++ b/src/main/webapp/app/exercises/shared/manage/exercise-create-buttons.component.ts @@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { Course } from 'app/entities/course.model'; import { faFileImport, faPlus } from '@fortawesome/free-solid-svg-icons'; import { ExerciseImportWrapperComponent } from 'app/exercises/shared/import/exercise-import-wrapper/exercise-import-wrapper.component'; +import { getIcon } from 'app/entities/exercise.model'; import { Exercise, ExerciseType } from 'app/entities/exercise.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { Router } from '@angular/router'; @@ -19,6 +20,8 @@ export class ExerciseCreateButtonsComponent implements OnInit { faPlus = faPlus; faFileImport = faFileImport; + getExerciseTypeIcon = getIcon; + constructor( private router: Router, private modalService: NgbModal, diff --git a/src/main/webapp/app/exercises/shared/participation-submission/participation-submission.component.ts b/src/main/webapp/app/exercises/shared/participation-submission/participation-submission.component.ts index 37e84077110a..2fbff8096d40 100644 --- a/src/main/webapp/app/exercises/shared/participation-submission/participation-submission.component.ts +++ b/src/main/webapp/app/exercises/shared/participation-submission/participation-submission.component.ts @@ -134,29 +134,29 @@ export class ParticipationSubmissionComponent implements OnInit { } fetchParticipationAndSubmissionsForStudent() { - this.participationService - .find(this.participationId) + combineLatest([this.participationService.find(this.participationId), this.submissionService.findAllSubmissionsOfParticipation(this.participationId)]) .pipe( - map(({ body }) => body), + map((res) => [res[0].body, res[1].body]), catchError(() => of(null)), ) - .subscribe((participation) => { + .subscribe((response) => { + this.isLoading = false; + if (!response) { + return; + } + + const participation = response[0] as StudentParticipation; + const submissions = response[1] as Submission[]; if (participation) { this.participation = participation; this.updateStatusBadgeColor(); - this.isLoading = false; } - }); - this.submissionService - .findAllSubmissionsOfParticipation(this.participationId) - .pipe( - map(({ body }) => body), - catchError(() => of([])), - ) - .subscribe((submissions) => { + if (submissions) { this.submissions = submissions; - this.isLoading = false; + if (this.participation) { + this.participation.submissions = submissions; + } // set the submission to every result so it can be accessed via the result submissions.forEach((submission: Submission) => { if (submission.results) { diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.html b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.html index 9612c07060e7..78aa5e087a29 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.html +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.html @@ -2,6 +2,9 @@ + @@ -19,6 +22,9 @@ +
+ + ID  Title  Release 
+ + {{ textExercise.id @@ -45,4 +51,18 @@
+
+ +
diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.ts index 5645812359ee..cf1b9a375252 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.ts @@ -14,7 +14,7 @@ import { SortService } from 'app/shared/service/sort.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AlertService } from 'app/core/util/alert.service'; import { EventManager } from 'app/core/util/event-manager.service'; -import { faPlus, faSort } from '@fortawesome/free-solid-svg-icons'; +import { faPlus, faSort, faTimes } from '@fortawesome/free-solid-svg-icons'; import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; import { ExerciseImportWrapperComponent } from 'app/exercises/shared/import/exercise-import-wrapper/exercise-import-wrapper.component'; @@ -29,10 +29,11 @@ export class TextExerciseComponent extends ExerciseComponent { // Icons faSort = faSort; faPlus = faPlus; + faTimes = faTimes; constructor( public exerciseService: ExerciseService, - private textExerciseService: TextExerciseService, + public textExerciseService: TextExerciseService, private courseExerciseService: CourseExerciseService, private modalService: NgbModal, private router: Router, @@ -58,6 +59,7 @@ export class TextExerciseComponent extends ExerciseComponent { this.textExercises.forEach((exercise) => { exercise.course = this.course; this.accountService.setAccessRightsForExercise(exercise); + this.selectedExercises = []; }); this.applyFilter(); this.emitExerciseCount(this.textExercises.length); diff --git a/src/main/webapp/app/lti/lti13-exercise-launch.component.ts b/src/main/webapp/app/lti/lti13-exercise-launch.component.ts index bd2f2f4e2a0c..cd0e3e15a6e1 100644 --- a/src/main/webapp/app/lti/lti13-exercise-launch.component.ts +++ b/src/main/webapp/app/lti/lti13-exercise-launch.component.ts @@ -42,7 +42,7 @@ export class Lti13ExerciseLaunchComponent implements OnInit { const requestBody = new HttpParams().set('state', state).set('id_token', idToken); this.http - .post('api/lti13/auth-login', requestBody.toString(), { + .post('api/public/lti13/auth-login', requestBody.toString(), { headers: new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded'), }) .subscribe({ diff --git a/src/main/webapp/i18n/de/aboutUs.json b/src/main/webapp/i18n/de/aboutUs.json index 26078189fa76..43112e6bb220 100644 --- a/src/main/webapp/i18n/de/aboutUs.json +++ b/src/main/webapp/i18n/de/aboutUs.json @@ -56,6 +56,9 @@ "tutorialGroups": { "text": "
Übungsgruppen: Verwaltung der Übungsgruppen eines Kurses. Planung der Sitzungen, Zuweisung der verantwortlichen Tutor:innen, Registrierung der Studierenden und Verfolgung der Anwesenheit." }, + "iris": { + "text": "Iris: Artemis integriert Iris, einen Chatbot, der Studierende und Lehrende bei häufig gestellten Fragen und Aufgaben unterstützt." + }, "scalable": { "text": "Skalierbar auf mehrere Kurse mit Tausenden von Studierenden. Konfigurieren von zusätzlichen Build-Agenten in der kontinuierlichen Integrationsumgebung." }, diff --git a/src/main/webapp/i18n/de/exercise-actions.json b/src/main/webapp/i18n/de/exercise-actions.json index 74aacd425350..3db070689778 100644 --- a/src/main/webapp/i18n/de/exercise-actions.json +++ b/src/main/webapp/i18n/de/exercise-actions.json @@ -58,7 +58,8 @@ "uploadFile": "Datei hochladen", "viewTeam": "Team", "sshKeyTip": "Um SSH zu nutzen, musst du {link:hier} einen SSH-Schlüssel zu deinem Konto hinzufügen.", - "startExerciseBeforeStartDate": "Du kannst vor dem Startdatum nicht an der Aufgabe teilnehmen." + "startExerciseBeforeStartDate": "Du kannst vor dem Startdatum nicht an der Aufgabe teilnehmen.", + "deleteMultipleExercisesQuestion": "Sollen die ausgewählten Aufgaben wirklich dauerhaft gelöscht werden?" } } } diff --git a/src/main/webapp/i18n/de/health.json b/src/main/webapp/i18n/de/health.json index f059ccd6a1bf..16a7a407c33d 100644 --- a/src/main/webapp/i18n/de/health.json +++ b/src/main/webapp/i18n/de/health.json @@ -28,7 +28,8 @@ "hazelcast": "Hazelcast", "websocketBroker": "Websocket Broker (Server -> Broker)", "athena": "Athena", - "apollon": "Apollon Conversion Server" + "apollon": "Apollon Conversion Server", + "iris": "Pyris Server" }, "table": { "service": "Dienst Name", diff --git a/src/main/webapp/i18n/en/aboutUs.json b/src/main/webapp/i18n/en/aboutUs.json index 67d52eae0dd6..0e582d2793cf 100644 --- a/src/main/webapp/i18n/en/aboutUs.json +++ b/src/main/webapp/i18n/en/aboutUs.json @@ -56,6 +56,9 @@ "tutorialGroups": { "text": "Tutorial Groups: Manage the tutorial groups of a course. Plan the sessions, assign responsible tutors, register students and track the attendance." }, + "iris": { + "text": "Iris: Artemis integrates Iris, a chatbot that supports students and instructors with common questions and tasks." + }, "scalable": { "text": "Scalable to multiple courses with thousands of students. Configure additional build agents in the continuous integration environment." }, diff --git a/src/main/webapp/i18n/en/exercise-actions.json b/src/main/webapp/i18n/en/exercise-actions.json index cd664cbea4f7..1741827eeb25 100644 --- a/src/main/webapp/i18n/en/exercise-actions.json +++ b/src/main/webapp/i18n/en/exercise-actions.json @@ -59,7 +59,8 @@ "uploadFile": "Upload a file", "viewTeam": "Team", "sshKeyTip": "To use ssh, you need to add an ssh-key to your account {link:here}.", - "startExerciseBeforeStartDate": "You cannot participate before the start date of the exercise." + "startExerciseBeforeStartDate": "You cannot participate before the start date of the exercise.", + "deleteMultipleExercisesQuestion": "Are you sure you want to delete the selected exercises?" } } } diff --git a/src/main/webapp/i18n/en/health.json b/src/main/webapp/i18n/en/health.json index 8fe047ae5775..b100bc030af0 100644 --- a/src/main/webapp/i18n/en/health.json +++ b/src/main/webapp/i18n/en/health.json @@ -28,7 +28,8 @@ "hazelcast": "Hazelcast", "websocketBroker": "Websocket Broker (Server -> Broker)", "athena": "Athena", - "apollon": "Apollon Conversion Server" + "apollon": "Apollon Conversion Server", + "iris": "Pyris Server" }, "table": { "service": "Service name", diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationBambooBitbucketJiraTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationBambooBitbucketJiraTest.java index bb2788dbb75c..b5d0a0e03fcf 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationBambooBitbucketJiraTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationBambooBitbucketJiraTest.java @@ -17,6 +17,9 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceLock; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -57,6 +60,8 @@ @SpringBootTest @AutoConfigureMockMvc @ExtendWith(SpringExtension.class) +@Execution(ExecutionMode.CONCURRENT) +@ResourceLock("AbstractSpringIntegrationBambooBitbucketJiraTest") @AutoConfigureEmbeddedDatabase // NOTE: we use a common set of active profiles to reduce the number of application launches during testing. This significantly saves time and memory! @ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "bamboo", "bitbucket", "jira", "ldap", "scheduling", "athena", "apollon", "iris" }) diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationGitlabCIGitlabSamlTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationGitlabCIGitlabSamlTest.java index c04448a40faa..9203c5e0ac25 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationGitlabCIGitlabSamlTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationGitlabCIGitlabSamlTest.java @@ -1,7 +1,5 @@ package de.tum.in.www1.artemis; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import static tech.jhipster.config.JHipsterConstants.SPRING_PROFILE_TEST; @@ -13,6 +11,9 @@ import org.gitlab4j.api.models.PipelineStatus; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceLock; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -44,6 +45,8 @@ @SpringBootTest @AutoConfigureMockMvc @ExtendWith(SpringExtension.class) +@Execution(ExecutionMode.CONCURRENT) +@ResourceLock("AbstractSpringIntegrationGitlabCIGitlabSamlTest") @AutoConfigureEmbeddedDatabase // NOTE: we use a common set of active profiles to reduce the number of application launches during testing. This significantly saves time and memory! @ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "gitlabci", "gitlab", "saml2", "scheduling" }) diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationIndependentTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationIndependentTest.java new file mode 100644 index 000000000000..f1e0b8343697 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationIndependentTest.java @@ -0,0 +1,301 @@ +package de.tum.in.www1.artemis; + +import static tech.jhipster.config.JHipsterConstants.SPRING_PROFILE_TEST; + +import java.util.Set; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.participation.AbstractBaseProgrammingExerciseParticipation; +import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; +import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; +import de.tum.in.www1.artemis.util.AbstractArtemisIntegrationTest; +import io.zonky.test.db.AutoConfigureEmbeddedDatabase; + +/** + * This SpringBootTest is used for tests that only require a minimal set of Active Spring Profiles. + */ +@SpringBootTest +@AutoConfigureMockMvc +@ExtendWith(SpringExtension.class) +@Execution(ExecutionMode.CONCURRENT) +@ResourceLock("AbstractSpringIntegrationIndependentTest") +@AutoConfigureEmbeddedDatabase +// NOTE: we use a common set of active profiles to reduce the number of application launches during testing. This significantly saves time and memory! +@ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "scheduling" }) +@TestPropertySource(properties = { "artemis.user-management.use-external=false" }) +public abstract class AbstractSpringIntegrationIndependentTest extends AbstractArtemisIntegrationTest { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @AfterEach + protected void resetSpyBeans() { + super.resetSpyBeans(); + } + + @Override + public void mockConnectorRequestsForSetup(ProgrammingExercise exercise, boolean failToCreateCiProject) { + log.debug("Called mockConnectorRequestsForSetup with args {}, {}", exercise, failToCreateCiProject); + } + + @Override + public void mockConnectorRequestsForImport(ProgrammingExercise sourceExercise, ProgrammingExercise exerciseToBeImported, boolean recreateBuildPlans, boolean addAuxRepos) { + log.debug("Called mockConnectorRequestsForImport with args {}, {}, {}, {}", sourceExercise, exerciseToBeImported, recreateBuildPlans, addAuxRepos); + } + + @Override + public void mockConnectorRequestForImportFromFile(ProgrammingExercise exerciseForImport) { + log.debug("Called mockConnectorRequestForImportFromFile with args {}", exerciseForImport); + } + + @Override + public void mockImportProgrammingExerciseWithFailingEnablePlan(ProgrammingExercise sourceExercise, ProgrammingExercise exerciseToBeImported, boolean planExistsInCi, + boolean shouldPlanEnableFail) { + log.debug("Called mockImportProgrammingExerciseWithFailingEnablePlan with args {}, {}, {}, {}", sourceExercise, exerciseToBeImported, planExistsInCi, shouldPlanEnableFail); + } + + @Override + public void mockConnectorRequestsForStartParticipation(ProgrammingExercise exercise, String username, Set users, boolean ltiUserExists) { + log.debug("Called mockConnectorRequestsForStartParticipation with args {}, {}, {}, {}", exercise, username, users, ltiUserExists); + } + + @Override + public void mockConnectorRequestsForResumeParticipation(ProgrammingExercise exercise, String username, Set users, boolean ltiUserExists) { + log.debug("Called mockConnectorRequestsForResumeParticipation with args {}, {}, {}, {}", exercise, username, users, ltiUserExists); + } + + @Override + public void mockUpdatePlanRepositoryForParticipation(ProgrammingExercise exercise, String username) { + log.debug("Called mockUpdatePlanRepositoryForParticipation with args {}, {}", exercise, username); + } + + @Override + public void mockUpdatePlanRepository(ProgrammingExercise exercise, String planName, String repoNameInCI, String repoNameInVcs) { + log.debug("Called mockUpdatePlanRepository with args {}, {}, {}, {}", exercise, planName, repoNameInCI, repoNameInVcs); + } + + @Override + public void mockRemoveRepositoryAccess(ProgrammingExercise exercise, Team team, User firstStudent) { + log.debug("Called mockRemoveRepositoryAccess with args {}, {}, {}", exercise, team, firstStudent); + } + + @Override + public void mockCopyRepositoryForParticipation(ProgrammingExercise exercise, String username) { + log.debug("Called mockCopyRepositoryForParticipation with args {}, {}", exercise, username); + } + + @Override + public void mockRepositoryWritePermissionsForTeam(Team team, User newStudent, ProgrammingExercise exercise, HttpStatus status) { + log.debug("Called mockRepositoryWritePermissionsForTeam with args {}, {}, {}, {}", team, newStudent, exercise, status); + } + + @Override + public void mockRepositoryWritePermissionsForStudent(User student, ProgrammingExercise exercise, HttpStatus status) { + log.debug("Called mockRepositoryWritePermissionsForStudent with args {}, {}, {}", student, exercise, status); + } + + @Override + public void mockRetrieveArtifacts(ProgrammingExerciseStudentParticipation participation) { + log.debug("Called mockRetrieveArtifacts with args {}", participation); + } + + @Override + public void mockFetchCommitInfo(String projectKey, String repositorySlug, String hash) { + log.debug("Called mockFetchCommitInfo with args {}, {}, {}", projectKey, repositorySlug, hash); + } + + @Override + public void mockCopyBuildPlan(ProgrammingExerciseStudentParticipation participation) { + log.debug("Called mockCopyBuildPlan with args {}", participation); + } + + @Override + public void mockConfigureBuildPlan(ProgrammingExerciseStudentParticipation participation) { + log.debug("Called mockConfigureBuildPlan with args {}", participation); + } + + @Override + public void mockTriggerFailedBuild(ProgrammingExerciseStudentParticipation participation) { + log.debug("Called mockTriggerFailedBuild with args {}", participation); + } + + @Override + public void mockGrantReadAccess(ProgrammingExerciseStudentParticipation participation) { + log.debug("Called mockGrantReadAccess with args {}", participation); + } + + @Override + public void mockNotifyPush(ProgrammingExerciseStudentParticipation participation) { + log.debug("Called mockNotifyPush with args {}", participation); + } + + @Override + public void mockTriggerParticipationBuild(ProgrammingExerciseStudentParticipation participation) { + log.debug("Called mockTriggerParticipationBuild with args {}", participation); + } + + @Override + public void mockTriggerInstructorBuildAll(ProgrammingExerciseStudentParticipation participation) { + log.debug("Called mockTriggerInstructorBuildAll with args {}", participation); + } + + @Override + public void resetMockProvider() { + log.debug("Called resetMockProvider"); + } + + @Override + public void verifyMocks() { + log.debug("Called verifyMocks"); + } + + @Override + public void mockUpdateUserInUserManagement(String oldLogin, User user, String password, Set oldGroups) { + log.debug("Called mockUpdateUserInUserManagement with args {}, {}, {}, {}", oldLogin, user, password, oldGroups); + } + + @Override + public void mockUpdateCoursePermissions(Course updatedCourse, String oldInstructorGroup, String oldEditorGroup, String oldTeachingAssistantGroup) { + log.debug("Called mockUpdateCoursePermissions with args {}, {}, {}, {}", updatedCourse, oldInstructorGroup, oldEditorGroup, oldTeachingAssistantGroup); + } + + @Override + public void mockFailUpdateCoursePermissionsInCi(Course updatedCourse, String oldInstructorGroup, String oldEditorGroup, String oldTeachingAssistantGroup, + boolean failToAddUsers, boolean failToRemoveUsers) { + log.debug("Called mockFailUpdateCoursePermissionsInCi with args {}, {}, {}, {}, {}, {}", updatedCourse, oldInstructorGroup, oldEditorGroup, oldTeachingAssistantGroup, + failToAddUsers, failToRemoveUsers); + } + + @Override + public void mockCreateUserInUserManagement(User user, boolean userExistsInCi) { + log.debug("Called mockCreateUserInUserManagement with args {}, {}", user, userExistsInCi); + } + + @Override + public void mockFailToCreateUserInExternalUserManagement(User user, boolean failInVcs, boolean failInCi, boolean failToGetCiUser) { + log.debug("Called mockFailToCreateUserInExternalUserManagement with args {}, {}, {}, {}", user, failInVcs, failInCi, failToGetCiUser); + } + + @Override + public void mockDeleteUserInUserManagement(User user, boolean userExistsInUserManagement, boolean failInVcs, boolean failInCi) { + log.debug("Called mockDeleteUserInUserManagement with args {}, {}, {}, {}", user, userExistsInUserManagement, failInVcs, failInCi); + } + + @Override + public void mockCreateGroupInUserManagement(String groupName) { + log.debug("Called mockCreateGroupInUserManagement with args {}", groupName); + } + + @Override + public void mockDeleteGroupInUserManagement(String groupName) { + log.debug("Called mockDeleteGroupInUserManagement with args {}", groupName); + } + + @Override + public void mockAddUserToGroupInUserManagement(User user, String group, boolean failInCi) { + log.debug("Called mockAddUserToGroupInUserManagement with args {}, {}, {}", user, group, failInCi); + } + + @Override + public void mockRemoveUserFromGroup(User user, String group, boolean failInCi) { + log.debug("Called mockRemoveUserFromGroup with args {}, {}, {}", user, group, failInCi); + } + + @Override + public void mockDeleteRepository(String projectKey, String repositoryName, boolean shouldFail) { + log.debug("Called mockDeleteRepository with args {}, {}, {}", projectKey, repositoryName, shouldFail); + } + + @Override + public void mockDeleteProjectInVcs(String projectKey, boolean shouldFail) { + log.debug("Called mockDeleteProjectInVcs with args {}, {}", projectKey, shouldFail); + } + + @Override + public void mockDeleteBuildPlan(String projectKey, String planName, boolean shouldFail) { + log.debug("Called mockDeleteBuildPlan with args {}, {}, {}", projectKey, planName, shouldFail); + } + + @Override + public void mockDeleteBuildPlanProject(String projectKey, boolean shouldFail) { + log.debug("Called mockDeleteBuildPlanProject with args {}, {}", projectKey, shouldFail); + } + + @Override + public void mockGetBuildPlan(String projectKey, String planName, boolean planExistsInCi, boolean planIsActive, boolean planIsBuilding, boolean failToGetBuild) { + log.debug("Called mockGetBuildPlan with args {}, {}, {}, {}, {}, {}", projectKey, planName, planExistsInCi, planIsActive, planIsBuilding, failToGetBuild); + } + + @Override + public void mockHealthInCiService(boolean isRunning, HttpStatus httpStatus) { + log.debug("Called mockHealthInCiService with args {}, {}", isRunning, httpStatus); + } + + @Override + public void mockConfigureBuildPlan(ProgrammingExerciseParticipation participation, String defaultBranch) { + log.debug("Called mockConfigureBuildPlan with args {}, {}", participation, defaultBranch); + } + + @Override + public void mockCheckIfProjectExistsInVcs(ProgrammingExercise exercise, boolean existsInVcs) { + log.debug("Called mockCheckIfProjectExistsInVcs with args {}, {}", exercise, existsInVcs); + } + + @Override + public void mockCheckIfProjectExistsInCi(ProgrammingExercise exercise, boolean existsInCi, boolean shouldFail) { + log.debug("Called mockCheckIfProjectExistsInCi with args {}, {}, {}", exercise, existsInCi, shouldFail); + } + + @Override + public void mockCheckIfBuildPlanExists(String projectKey, String templateBuildPlanId, boolean buildPlanExists, boolean shouldFail) { + log.debug("Called mockCheckIfBuildPlanExists with args {}, {}, {}, {}", projectKey, templateBuildPlanId, buildPlanExists, shouldFail); + } + + @Override + public void mockRepositoryUrlIsValid(VcsRepositoryUrl vcsTemplateRepositoryUrl, String projectKey, boolean b) { + log.debug("Called mockRepositoryUrlIsValid with args {}, {}, {}", vcsTemplateRepositoryUrl, projectKey, b); + } + + @Override + public void mockTriggerBuild(AbstractBaseProgrammingExerciseParticipation solutionParticipation) { + log.debug("Called mockTriggerBuild with args {}", solutionParticipation); + } + + @Override + public void mockTriggerBuildFailed(AbstractBaseProgrammingExerciseParticipation solutionParticipation) { + log.debug("Called mockTriggerBuildFailed with args {}", solutionParticipation); + } + + @Override + public void mockSetRepositoryPermissionsToReadOnly(VcsRepositoryUrl repositoryUrl, String projectKey, Set users) { + log.debug("Called mockSetRepositoryPermissionsToReadOnly with args {}, {}, {}", repositoryUrl, projectKey, users); + } + + @Override + public void mockConfigureRepository(ProgrammingExercise exercise, String participantIdentifier, Set students, boolean userExists) { + log.debug("Called mockConfigureRepository with args {}, {}, {}, {}", exercise, participantIdentifier, students, userExists); + } + + @Override + public void mockDefaultBranch(ProgrammingExercise programmingExercise) { + log.debug("Called mockDefaultBranch with args {}", programmingExercise); + } + + @Override + public void mockUserExists(String username) { + log.debug("Called mockUserExists with args {}", username); + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationJenkinsGitlabTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationJenkinsGitlabTest.java index 6408d1206b73..e1b6e7a24cf5 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationJenkinsGitlabTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationJenkinsGitlabTest.java @@ -13,6 +13,9 @@ import org.gitlab4j.api.GitLabApiException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceLock; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -40,6 +43,8 @@ @SpringBootTest @AutoConfigureMockMvc @ExtendWith(SpringExtension.class) +@Execution(ExecutionMode.CONCURRENT) +@ResourceLock("AbstractSpringIntegrationJenkinsGitlabTest") @AutoConfigureEmbeddedDatabase // NOTE: we use a common set of active profiles to reduce the number of application launches during testing. This significantly saves time and memory! @ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "gitlab", "jenkins", "athena", "scheduling" }) diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java index b95466f8b7fe..b46c528daa67 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java @@ -11,6 +11,9 @@ import org.gitlab4j.api.GitLabApiException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceLock; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -55,6 +58,8 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @AutoConfigureMockMvc @ExtendWith(SpringExtension.class) +@Execution(ExecutionMode.CONCURRENT) +@ResourceLock("AbstractSpringIntegrationLocalCILocalVCTest") @AutoConfigureEmbeddedDatabase // NOTE: we use a common set of active profiles to reduce the number of application launches during testing. This significantly saves time and memory! @ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", "localci", "localvc", "scheduling", "ldap-only" }) diff --git a/src/test/java/de/tum/in/www1/artemis/ArchitectureTest.java b/src/test/java/de/tum/in/www1/artemis/ArchitectureTest.java index eaef679a24ec..3a7aded9442a 100644 --- a/src/test/java/de/tum/in/www1/artemis/ArchitectureTest.java +++ b/src/test/java/de/tum/in/www1/artemis/ArchitectureTest.java @@ -1,11 +1,15 @@ package de.tum.in.www1.artemis; import static com.tngtech.archunit.base.DescribedPredicate.*; +import static com.tngtech.archunit.core.domain.JavaCall.Predicates.target; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*; +import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.nameMatching; +import static com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With.owner; import static com.tngtech.archunit.lang.SimpleConditionEvent.violated; import static com.tngtech.archunit.lang.conditions.ArchPredicates.*; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*; +import java.nio.file.Files; import java.util.*; import java.util.stream.Collectors; @@ -84,6 +88,14 @@ void testValidSimpMessageSendingOperationsUsage() { usage.check(productionClasses); } + @Test + void testFileWriteUsage() { + ArchRule usage = noClasses().should() + .callMethodWhere(target(owner(assignableTo(Files.class))).and(target(nameMatching("copy")).or(target(nameMatching("move"))).or(target(nameMatching("write.*"))))) + .because("Files.copy does not create directories if they do not exist. Use Apache FileUtils instead."); + usage.check(allClasses); + } + // Custom Predicates for JavaAnnotations since ArchUnit only defines them for classes private DescribedPredicate> simpleNameAnnotation(String name) { diff --git a/src/test/java/de/tum/in/www1/artemis/ClientForwardTest.java b/src/test/java/de/tum/in/www1/artemis/ClientForwardTest.java index b3fe8d4e6a05..452ff334c92c 100644 --- a/src/test/java/de/tum/in/www1/artemis/ClientForwardTest.java +++ b/src/test/java/de/tum/in/www1/artemis/ClientForwardTest.java @@ -20,7 +20,7 @@ * * @see ClientForwardResource */ -class ClientForwardTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ClientForwardTest extends AbstractSpringIntegrationIndependentTest { @Autowired private JWTCookieService jwtCookieService; diff --git a/src/test/java/de/tum/in/www1/artemis/ContentVersionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/ContentVersionIntegrationTest.java index 90f05369fcbd..98e1a3805c85 100644 --- a/src/test/java/de/tum/in/www1/artemis/ContentVersionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/ContentVersionIntegrationTest.java @@ -15,7 +15,7 @@ import de.tum.in.www1.artemis.config.ApiVersionFilter; import de.tum.in.www1.artemis.user.UserUtilService; -class ContentVersionIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ContentVersionIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "contentversion"; diff --git a/src/test/java/de/tum/in/www1/artemis/DatabaseQueryCountTest.java b/src/test/java/de/tum/in/www1/artemis/DatabaseQueryCountTest.java index db17e3683e4c..060dac060f74 100644 --- a/src/test/java/de/tum/in/www1/artemis/DatabaseQueryCountTest.java +++ b/src/test/java/de/tum/in/www1/artemis/DatabaseQueryCountTest.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis; +import java.util.Set; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -10,11 +12,12 @@ import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.exam.StudentExam; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.user.UserUtilService; -class DatabaseQueryCountTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class DatabaseQueryCountTest extends AbstractSpringIntegrationIndependentTest { private final Logger log = LoggerFactory.getLogger(this.getClass()); @@ -35,6 +38,8 @@ class DatabaseQueryCountTest extends AbstractSpringIntegrationBambooBitbucketJir void setup() { participantScoreScheduleService.shutdown(); userUtilService.addUsers(TEST_PREFIX, 1, NUMBER_OF_TUTORS, 0, 0); + User student = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); + student.setGroups(Set.of(TEST_PREFIX + "tumuser")); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/FileIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/FileIntegrationTest.java index 7bb99c70f786..ab1cef3d3679 100644 --- a/src/test/java/de/tum/in/www1/artemis/FileIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/FileIntegrationTest.java @@ -41,7 +41,7 @@ import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.user.UserUtilService; -class FileIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class FileIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "fileintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/GuidedTourSettingResourceTest.java b/src/test/java/de/tum/in/www1/artemis/GuidedTourSettingResourceTest.java index d7caee4aafc1..38c70b38b40a 100644 --- a/src/test/java/de/tum/in/www1/artemis/GuidedTourSettingResourceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/GuidedTourSettingResourceTest.java @@ -15,7 +15,7 @@ import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.user.UserUtilService; -class GuidedTourSettingResourceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class GuidedTourSettingResourceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "gtsettingtest"; diff --git a/src/test/java/de/tum/in/www1/artemis/ImprintResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/ImprintResourceIntegrationTest.java index e8a8625f0d8d..bca382ba465c 100644 --- a/src/test/java/de/tum/in/www1/artemis/ImprintResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/ImprintResourceIntegrationTest.java @@ -2,14 +2,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.mockStatic; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; +import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -21,7 +21,7 @@ import de.tum.in.www1.artemis.domain.enumeration.Language; import net.minidev.json.JSONObject; -class ImprintResourceIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ImprintResourceIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "ir"; // only lower case is supported @@ -52,10 +52,9 @@ void testGetImprintForUpdate_cannotReadFileInternalServerError() throws Exceptio @Test @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") void testUpdatePrivacyStatement_cannotWriteFileInternalServerError() throws Exception { - try (MockedStatic mockedFiles = mockStatic(Files.class)) { + try (MockedStatic mockedFiles = mockStatic(Files.class); MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { mockedFiles.when(() -> Files.exists(argThat(path -> path.toString().contains("_de")))).thenReturn(true); - mockedFiles.when( - () -> Files.writeString(argThat(path -> path.toString().contains("_de")), anyString(), eq(StandardOpenOption.CREATE), eq(StandardOpenOption.TRUNCATE_EXISTING))) + mockedFileUtils.when(() -> FileUtils.writeStringToFile(argThat(file -> file.toString().contains("_de")), anyString(), eq(StandardCharsets.UTF_8))) .thenThrow(new IOException()); request.putWithResponseBody("/api/admin/imprint", new Imprint("text", Language.GERMAN), Imprint.class, HttpStatus.INTERNAL_SERVER_ERROR); } @@ -65,13 +64,12 @@ void testUpdatePrivacyStatement_cannotWriteFileInternalServerError() throws Exce @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") void testUpdatePrivacyStatement_directoryDoesntExist_createsDirectoryAndSavesFile() throws Exception { Imprint response; - try (MockedStatic mockedFiles = mockStatic(Files.class)) { + try (MockedStatic mockedFiles = mockStatic(Files.class); MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(false); response = request.putWithResponseBody("/api/admin/imprint", new Imprint("updatedText", Language.GERMAN), Imprint.class, HttpStatus.OK); mockedFiles.verify(() -> Files.createDirectories(any())); - mockedFiles.verify(() -> Files.writeString(argThat(path -> path.toString().contains("_de")), anyString(), eq(StandardOpenOption.CREATE), - eq(StandardOpenOption.TRUNCATE_EXISTING))); + mockedFileUtils.verify(() -> FileUtils.writeStringToFile(argThat(file -> file.toString().contains("_de")), anyString(), eq(StandardCharsets.UTF_8))); } assertThat(response.getText()).isEqualTo("updatedText"); assertThat(response.getLanguage()).isEqualTo(Language.GERMAN); @@ -201,12 +199,10 @@ void testUpdateImprint_writesFile_ReturnsUpdatedFileContent() throws Exception { Imprint response; Imprint requestBody = new Imprint(Language.GERMAN); requestBody.setText("Impressum"); - try (MockedStatic mockedFiles = mockStatic(Files.class)) { + try (MockedStatic mockedFiles = mockStatic(Files.class); MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { mockedFiles.when(() -> Files.exists(any())).thenReturn(true); response = request.putWithResponseBody("/api/admin/imprint", requestBody, Imprint.class, HttpStatus.OK); - mockedFiles.verify(() -> Files.writeString(argThat(path -> path.toString().contains("_de")), anyString(), eq(StandardOpenOption.CREATE), - eq(StandardOpenOption.TRUNCATE_EXISTING))); - + mockedFileUtils.verify(() -> FileUtils.writeStringToFile(argThat(file -> file.toString().contains("_de")), anyString(), eq(StandardCharsets.UTF_8))); } assertThat(response.getLanguage()).isEqualTo(Language.GERMAN); assertThat(response.getText()).isEqualTo("Impressum"); diff --git a/src/test/java/de/tum/in/www1/artemis/LogResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/LogResourceIntegrationTest.java index 9499fcfd370f..0a324351bdd3 100644 --- a/src/test/java/de/tum/in/www1/artemis/LogResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/LogResourceIntegrationTest.java @@ -10,7 +10,7 @@ import de.tum.in.www1.artemis.web.rest.vm.LoggerVM; -class LogResourceIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class LogResourceIntegrationTest extends AbstractSpringIntegrationIndependentTest { @Test @WithMockUser(roles = "ADMIN") diff --git a/src/test/java/de/tum/in/www1/artemis/LongFeedbackResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/LongFeedbackResourceIntegrationTest.java index ba7f057256e7..fcea52814b21 100644 --- a/src/test/java/de/tum/in/www1/artemis/LongFeedbackResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/LongFeedbackResourceIntegrationTest.java @@ -15,7 +15,7 @@ import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.user.UserUtilService; -class LongFeedbackResourceIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class LongFeedbackResourceIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "longfeedbackintegration"; @@ -49,7 +49,7 @@ void getLongFeedbackAsStudent() throws Exception { final Feedback feedback = addLongFeedbackToResult(resultStudent1); final LongFeedbackText longFeedbackText = request.get(getUrl(resultStudent1.getId(), feedback.getId()), HttpStatus.OK, LongFeedbackText.class); - assertThat(longFeedbackText.getId()).isEqualTo(feedback.getId()); + assertThat(longFeedbackText.getFeedback().getId()).isEqualTo(feedback.getId()); } @Test @@ -58,7 +58,7 @@ void getLongFeedbackAsTutor() throws Exception { final Feedback feedback = addLongFeedbackToResult(resultStudent1); final LongFeedbackText longFeedbackText = request.get(getUrl(resultStudent1.getId(), feedback.getId()), HttpStatus.OK, LongFeedbackText.class); - assertThat(longFeedbackText.getId()).isEqualTo(feedback.getId()); + assertThat(longFeedbackText.getFeedback().getId()).isEqualTo(feedback.getId()); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/Lti13LaunchIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/Lti13LaunchIntegrationTest.java index 0a8cedd60144..03c497955843 100644 --- a/src/test/java/de/tum/in/www1/artemis/Lti13LaunchIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/Lti13LaunchIntegrationTest.java @@ -37,7 +37,7 @@ * see LTI message general details * see OpenId Connect launch flow */ -class Lti13LaunchIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class Lti13LaunchIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final Key SIGNING_KEY = new SecretKeySpec("a".repeat(100).getBytes(), SignatureAlgorithm.HS256.getJcaName()); diff --git a/src/test/java/de/tum/in/www1/artemis/LtiIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/LtiIntegrationTest.java index 8c40b6665b1b..83f74b5025da 100644 --- a/src/test/java/de/tum/in/www1/artemis/LtiIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/LtiIntegrationTest.java @@ -4,9 +4,12 @@ import static org.mockito.Mockito.*; import java.net.URI; +import java.net.URISyntaxException; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -170,6 +173,23 @@ private void addJiraMocks(String email, String existingUser) throws Exception { jiraRequestMockProvider.mockAddUserToGroup("tumuser", false); } + private void addBitbucketMock(String requestBody) throws URISyntaxException { + bitbucketRequestMockProvider.enableMockingOfRequests(); + + String username = "prefix_"; + Matcher matcher = Pattern.compile("lis_person_sourcedid=([^&#]+)").matcher(requestBody); + if (matcher.find() && !matcher.group(1).isEmpty()) { + username += matcher.group(1); + } + else { + matcher = Pattern.compile("ext_user_username=([^&#]+)").matcher(requestBody); + if (matcher.find()) { + username += matcher.group(1); + } + } + bitbucketRequestMockProvider.mockUserExists(username); + } + @ParameterizedTest @ValueSource(strings = { EDX_REQUEST_BODY, MOODLE_REQUEST_BODY }) @WithAnonymousUser @@ -184,12 +204,13 @@ void launchAsAnonymousUser_noOnlineCourseConfigurationException(String requestBo } @ParameterizedTest - @ValueSource(strings = { EDX_REQUEST_BODY }) // To be readded when LtiUserId is removed, MOODLE_REQUEST_BODY }) + @ValueSource(strings = { EDX_REQUEST_BODY, MOODLE_REQUEST_BODY }) @WithAnonymousUser void launchAsAnonymousUser_WithoutExistingEmail(String requestBody) throws Exception { String email = generateEmail("launchAsAnonymousUser_WithoutExistingEmail"); requestBody = replaceEmail(requestBody, email); addJiraMocks(email, null); + addBitbucketMock(requestBody); Long exerciseId = programmingExercise.getId(); Long courseId = programmingExercise.getCourseViaExerciseGroupOrCourseMember().getId(); @@ -218,7 +239,7 @@ void launchAsAnonymousUser_WithExistingEmail(String requestBody) throws Exceptio } @ParameterizedTest - @ValueSource(strings = { EDX_REQUEST_BODY }) // To be readded when LtiUserId is removed, MOODLE_REQUEST_BODY }) + @ValueSource(strings = { EDX_REQUEST_BODY, MOODLE_REQUEST_BODY }) @WithAnonymousUser void launchAsAnonymousUser_RequireExistingUser(String requestBody) throws Exception { String email = generateEmail("launchAsAnonymousUser_RequireExistingUser"); diff --git a/src/test/java/de/tum/in/www1/artemis/OAuth2JWKSIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/OAuth2JWKSIntegrationTest.java index 3a27ad298350..9fa488862cd8 100644 --- a/src/test/java/de/tum/in/www1/artemis/OAuth2JWKSIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/OAuth2JWKSIntegrationTest.java @@ -18,7 +18,7 @@ import de.tum.in.www1.artemis.repository.OnlineCourseConfigurationRepository; import de.tum.in.www1.artemis.security.OAuth2JWKSService; -class OAuth2JWKSIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class OAuth2JWKSIntegrationTest extends AbstractSpringIntegrationIndependentTest { @Autowired private CourseRepository courseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/PrivacyStatementResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/PrivacyStatementResourceIntegrationTest.java index 6f8eaadb8c09..8bb40f27e639 100644 --- a/src/test/java/de/tum/in/www1/artemis/PrivacyStatementResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/PrivacyStatementResourceIntegrationTest.java @@ -5,10 +5,11 @@ import static org.mockito.Mockito.mockStatic; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; +import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -20,7 +21,7 @@ import de.tum.in.www1.artemis.domain.enumeration.Language; import net.minidev.json.JSONObject; -class PrivacyStatementResourceIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class PrivacyStatementResourceIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "psr"; // only lower case is supported @@ -53,10 +54,9 @@ void testGetPrivacyStatementForUpdate_cannotReadFileInternalServerError() throws @Test @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") void testUpdatePrivacyStatement_cannotWriteFileInternalServerError() throws Exception { - try (MockedStatic mockedFiles = mockStatic(Files.class)) { + try (MockedStatic mockedFiles = mockStatic(Files.class); MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { mockedFiles.when(() -> Files.exists(argThat(path -> path.toString().contains("_de")))).thenReturn(true); - mockedFiles.when( - () -> Files.writeString(argThat(path -> path.toString().contains("_de")), anyString(), eq(StandardOpenOption.CREATE), eq(StandardOpenOption.TRUNCATE_EXISTING))) + mockedFileUtils.when(() -> FileUtils.writeStringToFile(argThat(file -> file.toString().contains("_de")), anyString(), eq(StandardCharsets.UTF_8))) .thenThrow(new IOException()); request.putWithResponseBody("/api/admin/privacy-statement", new PrivacyStatement("text", Language.GERMAN), PrivacyStatement.class, HttpStatus.INTERNAL_SERVER_ERROR); } @@ -67,14 +67,12 @@ void testUpdatePrivacyStatement_cannotWriteFileInternalServerError() throws Exce @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") void testUpdatePrivacyStatement_directoryDoesntExist_createsDirectoryAndSavesFile() throws Exception { PrivacyStatement response; - try (MockedStatic mockedFiles = mockStatic(Files.class)) { + try (MockedStatic mockedFiles = mockStatic(Files.class); MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(false); response = request.putWithResponseBody("/api/admin/privacy-statement", new PrivacyStatement("updatedText", Language.GERMAN), PrivacyStatement.class, HttpStatus.OK); mockedFiles.verify(() -> Files.createDirectories(any())); - mockedFiles.verify(() -> Files.writeString(argThat(path -> path.toString().contains("_de")), anyString(), eq(StandardOpenOption.CREATE), - eq(StandardOpenOption.TRUNCATE_EXISTING))); - + mockedFileUtils.verify(() -> FileUtils.writeStringToFile(argThat(file -> file.toString().contains("_de")), anyString(), eq(StandardCharsets.UTF_8))); } assertThat(response.getText()).isEqualTo("updatedText"); assertThat(response.getLanguage()).isEqualTo(Language.GERMAN); @@ -205,16 +203,14 @@ void testUpdatePrivacyStatement_writesFile_ReturnsUpdatedFileContent() throws Ex PrivacyStatement response; PrivacyStatement requestBody = new PrivacyStatement(Language.GERMAN); requestBody.setText("Datenschutzerklärung"); - try (MockedStatic mockedFiles = mockStatic(Files.class)) { + try (MockedStatic mockedFiles = mockStatic(Files.class); MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { mockedFiles.when(() -> Files.exists(any())).thenReturn(true); response = request.putWithResponseBody("/api/admin/privacy-statement", requestBody, PrivacyStatement.class, HttpStatus.OK); - mockedFiles.verify(() -> Files.writeString(argThat(path -> path.toString().contains("_de")), anyString(), eq(StandardOpenOption.CREATE), - eq(StandardOpenOption.TRUNCATE_EXISTING))); + mockedFileUtils.verify(() -> FileUtils.writeStringToFile(argThat(file -> file.toString().contains("_de")), anyString(), eq(StandardCharsets.UTF_8))); // we explicitly check the method calls to ensure createDirectories is not called when the directory exists mockedFiles.verify(() -> Files.exists(any())); mockedFiles.verifyNoMoreInteractions(); - } assertThat(response.getLanguage()).isEqualTo(Language.GERMAN); assertThat(response.getText()).isEqualTo("Datenschutzerklärung"); diff --git a/src/test/java/de/tum/in/www1/artemis/StatisticsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/StatisticsIntegrationTest.java index 05565d100a63..185e049968f1 100644 --- a/src/test/java/de/tum/in/www1/artemis/StatisticsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/StatisticsIntegrationTest.java @@ -46,7 +46,7 @@ import de.tum.in.www1.artemis.web.rest.dto.CourseManagementStatisticsDTO; import de.tum.in.www1.artemis.web.rest.dto.ExerciseManagementStatisticsDTO; -class StatisticsIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class StatisticsIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "statisticsintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentComplaintIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentComplaintIntegrationTest.java index 131f6742512c..1e9595153988 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentComplaintIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentComplaintIntegrationTest.java @@ -16,7 +16,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; @@ -40,7 +40,7 @@ import de.tum.in.www1.artemis.util.FileUtils; import de.tum.in.www1.artemis.web.rest.dto.SubmissionWithComplaintDTO; -class AssessmentComplaintIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class AssessmentComplaintIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "assessmentcomplaintintegration"; @@ -364,8 +364,12 @@ void submitComplaintResponse_examExercise() throws Exception { complaintResponse.setResponseText("abcdefghijklmnopqrstuvwxyz"); request.putWithResponseBody("/api/complaint-responses/complaint/" + examExerciseComplaint.getId() + "/resolve", complaintResponse, ComplaintResponse.class, HttpStatus.OK); - TextSubmission finalTextSubmission = textSubmission; - await().untilAsserted(() -> assertThat(complaintRepo.findByResultId(finalTextSubmission.getId())).isPresent()); + + assertThat(textSubmission.getLatestResult()).isNotNull(); + assertThat(complaintRepo.findByResultId(textSubmission.getLatestResult().getId())).isPresent(); + + Complaint finalExamExerciseComplaint = examExerciseComplaint; + await().untilAsserted(() -> assertThat(complaintResponseRepo.findByComplaint_Id(finalExamExerciseComplaint.getId())).isPresent()); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentTeamComplaintIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentTeamComplaintIntegrationTest.java index eb411380c3d9..4a482f1c05ac 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentTeamComplaintIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentTeamComplaintIntegrationTest.java @@ -13,7 +13,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.ComplaintType; import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; @@ -30,7 +30,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.FileUtils; -class AssessmentTeamComplaintIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class AssessmentTeamComplaintIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "assmentteamcomplaint"; // only lower case is supported diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ComplaintResponseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ComplaintResponseIntegrationTest.java index c9e9d7d90b46..73ea06bc48b2 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ComplaintResponseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ComplaintResponseIntegrationTest.java @@ -14,7 +14,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; @@ -27,7 +27,7 @@ import de.tum.in.www1.artemis.service.ParticipationService; import de.tum.in.www1.artemis.user.UserUtilService; -class ComplaintResponseIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ComplaintResponseIntegrationTest extends AbstractSpringIntegrationIndependentTest { private final Logger log = LoggerFactory.getLogger(ComplaintResponseIntegrationTest.class); diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ExampleSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ExampleSubmissionIntegrationTest.java index 739677d584c0..406e25f51e81 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ExampleSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ExampleSubmissionIntegrationTest.java @@ -15,7 +15,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; @@ -34,7 +34,7 @@ import de.tum.in.www1.artemis.util.FileUtils; import de.tum.in.www1.artemis.web.rest.dto.TextAssessmentDTO; -class ExampleSubmissionIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExampleSubmissionIntegrationTest extends AbstractSpringIntegrationIndependentTest { private final Logger log = LoggerFactory.getLogger(ExampleSubmissionIntegrationTest.class); diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ExerciseScoresChartIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ExerciseScoresChartIntegrationTest.java index e5335333637f..b9c1839d4d05 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ExerciseScoresChartIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ExerciseScoresChartIntegrationTest.java @@ -14,18 +14,20 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; -import de.tum.in.www1.artemis.repository.*; +import de.tum.in.www1.artemis.repository.ParticipantScoreRepository; +import de.tum.in.www1.artemis.repository.TeamRepository; +import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.service.scheduled.ParticipantScoreScheduleService; import de.tum.in.www1.artemis.team.TeamUtilService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.rest.dto.ExerciseScoresDTO; -class ExerciseScoresChartIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExerciseScoresChartIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "exercisescoreschart"; @@ -111,6 +113,7 @@ void setupTestScenario() { participationUtilService.createParticipationSubmissionAndResult(idOfTeamTextExercise, team2, 10.0, 10.0, 90, true); participantScoreScheduleService.executeScheduledTasks(); + await().until(participantScoreScheduleService::isIdle); await().until(() -> participantScoreRepository.findAllByExercise(textExercise).size() == 3); await().until(() -> participantScoreRepository.findAllByExercise(teamExercise).size() == 2); } diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/GradeStepIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/GradeStepIntegrationTest.java index a2afe884c270..d3e45e4736a4 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/GradeStepIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/GradeStepIntegrationTest.java @@ -12,7 +12,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.exam.Exam; @@ -29,7 +29,7 @@ import de.tum.in.www1.artemis.web.rest.dto.GradeDTO; import de.tum.in.www1.artemis.web.rest.dto.GradeStepsDTO; -class GradeStepIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class GradeStepIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "gradestep"; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/GradingScaleIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/GradingScaleIntegrationTest.java index 474f5d38546c..dc857b4506d2 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/GradingScaleIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/GradingScaleIntegrationTest.java @@ -12,7 +12,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.GradeStep; @@ -28,7 +28,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.PageableSearchUtilService; -class GradingScaleIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class GradingScaleIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "gradingscale"; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ParticipantScoreIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ParticipantScoreIntegrationTest.java index c66ab688cdf8..86bb975e28b9 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ParticipantScoreIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ParticipantScoreIntegrationTest.java @@ -14,7 +14,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.exam.Exam; @@ -30,7 +30,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.rest.dto.ScoreDTO; -class ParticipantScoreIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ParticipantScoreIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "participantscoreintegrationtest"; @@ -137,6 +137,8 @@ void setupTestScenario() { long getIdOfIndividualTextExerciseOfExam = examTextExercise.getId(); participationUtilService.createParticipationSubmissionAndResult(getIdOfIndividualTextExerciseOfExam, student1, 10.0, 10.0, 50, true); + participantScoreScheduleService.executeScheduledTasks(); + await().until(participantScoreScheduleService::isIdle); await().until(() -> participantScoreRepository.findAllByExercise(textExercise).size() == 1); await().until(() -> participantScoreRepository.findAllByExercise(teamExercise).size() == 1); await().until(() -> participantScoreRepository.findAllByExercise(examTextExercise).size() == 1); diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/RatingResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/RatingResourceIntegrationTest.java index 9ff42e2be9f9..6c200b0e88ef 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/RatingResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/RatingResourceIntegrationTest.java @@ -11,7 +11,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.Language; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.service.RatingService; import de.tum.in.www1.artemis.user.UserUtilService; -class RatingResourceIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class RatingResourceIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "ratingresourceintegrationtest"; // only lower case is supported diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/TutorEffortIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/TutorEffortIntegrationTest.java index 7d7bf44213ba..7e31bbae4836 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/TutorEffortIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/TutorEffortIntegrationTest.java @@ -13,7 +13,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Exercise; @@ -28,7 +28,7 @@ import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class TutorEffortIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TutorEffortIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "tutoreffort"; // only lower case is supported diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/TutorLeaderboardServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/TutorLeaderboardServiceIntegrationTest.java index 455499098824..c85e0eb84d29 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/TutorLeaderboardServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/TutorLeaderboardServiceIntegrationTest.java @@ -12,7 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; @@ -25,7 +25,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.rest.dto.TutorLeaderboardDTO; -class TutorLeaderboardServiceIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TutorLeaderboardServiceIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "tlbsitest"; // only lower case is supported diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/TutorParticipationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/TutorParticipationIntegrationTest.java index 2ae138e14657..a17159213d22 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/TutorParticipationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/TutorParticipationIntegrationTest.java @@ -15,7 +15,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.FeedbackType; @@ -35,7 +35,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.FileUtils; -class TutorParticipationIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TutorParticipationIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "tutorparticipationintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/TutorParticipationResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/TutorParticipationResourceIntegrationTest.java index 2eb4f1f47dca..42384f7bd58c 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/TutorParticipationResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/TutorParticipationResourceIntegrationTest.java @@ -10,7 +10,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ExampleSubmission; @@ -23,7 +23,7 @@ import de.tum.in.www1.artemis.repository.TutorParticipationRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class TutorParticipationResourceIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TutorParticipationResourceIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "tutorparticipationresource"; diff --git a/src/test/java/de/tum/in/www1/artemis/authentication/UserBambooBitbucketJiraIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/authentication/UserBambooBitbucketJiraIntegrationTest.java index 446160f03ece..1fb35562d582 100644 --- a/src/test/java/de/tum/in/www1/artemis/authentication/UserBambooBitbucketJiraIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/authentication/UserBambooBitbucketJiraIntegrationTest.java @@ -51,6 +51,12 @@ void updateUser_asAdmin_isSuccessful() throws Exception { userTestService.updateUser_asAdmin_isSuccessful(); } + @Test + @WithMockUser(username = "admin", roles = "ADMIN") + void updateUserEmptyRoles() throws Exception { + userTestService.updateUserWithEmptyRoles(); + } + @Test @WithMockUser(username = "admin", roles = "ADMIN") void updateUserInvalidId() throws Exception { @@ -126,6 +132,15 @@ void createInternalUser_asAdmin_isSuccessful() throws Exception { userTestService.createInternalUser_asAdmin_isSuccessful(); } + @Test + @WithMockUser(username = "admin", roles = "ADMIN") + void createInternalUserWithoutRoles_asAdmin_isSuccessful() throws Exception { + bitbucketRequestMockProvider.mockUserDoesNotExist("batman"); + bitbucketRequestMockProvider.mockCreateUser("batman", "foobar1234", "batman@secret.invalid", TEST_PREFIX + "student1First " + TEST_PREFIX + "student1Last"); + bitbucketRequestMockProvider.mockAddUserToGroups(); + userTestService.createInternalUserWithoutRoles_asAdmin_isSuccessful(); + } + @Test @WithMockUser(username = "admin", roles = "ADMIN") void createUser_asAdmin_hasId() throws Exception { diff --git a/src/test/java/de/tum/in/www1/artemis/authentication/UserJenkinsGitlabIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/authentication/UserJenkinsGitlabIntegrationTest.java index 8a2ec915317b..322427593291 100644 --- a/src/test/java/de/tum/in/www1/artemis/authentication/UserJenkinsGitlabIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/authentication/UserJenkinsGitlabIntegrationTest.java @@ -177,6 +177,15 @@ void createInternalUser_asAdmin_with_vcsAccessToken_isSuccessful() throws Except ReflectionTestUtils.setField(gitLabUserManagementService, "versionControlAccessToken", false); } + @Test + @WithMockUser(username = "admin", roles = "ADMIN") + void createInternalUserWithoutRoles_isSuccessful() throws Exception { + gitlabRequestMockProvider.mockCreationOfUser("batman"); + ReflectionTestUtils.setField(gitLabUserManagementService, "versionControlAccessToken", true); + userTestService.createInternalUserWithoutRoles_asAdmin_isSuccessful(); + ReflectionTestUtils.setField(gitLabUserManagementService, "versionControlAccessToken", false); + } + @Test @WithMockUser(username = "admin", roles = "ADMIN") void createUser_asAdmin_hasId() throws Exception { @@ -392,6 +401,12 @@ void updateUserLogin() throws Exception { userTestService.updateUserLogin(); } + @Test + @WithMockUser(username = "admin", roles = "ADMIN") + void updateUserEmptyRoles() throws Exception { + userTestService.updateUserWithEmptyRoles(); + } + @Test @WithMockUser(username = "admin", roles = "ADMIN") void shouldFailIfCannotUpdateActivatedUserInGitlab() throws Exception { diff --git a/src/test/java/de/tum/in/www1/artemis/bonus/BonusIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/bonus/BonusIntegrationTest.java index 6d2a0e9fdf84..f80bc1ea4342 100644 --- a/src/test/java/de/tum/in/www1/artemis/bonus/BonusIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/bonus/BonusIntegrationTest.java @@ -14,7 +14,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.assessment.GradingScaleFactory; import de.tum.in.www1.artemis.assessment.GradingScaleUtilService; import de.tum.in.www1.artemis.course.CourseUtilService; @@ -27,7 +27,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.rest.dto.BonusExampleDTO; -class BonusIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class BonusIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "bonusintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/config/MetricsBeanTest.java b/src/test/java/de/tum/in/www1/artemis/config/MetricsBeanTest.java index 9f3332a8d751..452203bea36c 100644 --- a/src/test/java/de/tum/in/www1/artemis/config/MetricsBeanTest.java +++ b/src/test/java/de/tum/in/www1/artemis/config/MetricsBeanTest.java @@ -3,9 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.time.ZonedDateTime; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; +import java.util.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -13,6 +11,7 @@ import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; import de.tum.in.www1.artemis.course.CourseUtilService; +import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.enumeration.ExerciseType; import de.tum.in.www1.artemis.domain.enumeration.QuizMode; @@ -162,8 +161,7 @@ void testPublicMetricsActiveUsers() { @Test void testPublicMetricsCourses() { var activeCourse = courseUtilService.createCourse(); - activeCourse.setStartDate(ZonedDateTime.now().minusDays(1)); - activeCourse.setEndDate(ZonedDateTime.now().plusDays(1)); + activateCourse(activeCourse); courseRepository.save(activeCourse); var inactiveCourse = courseUtilService.createCourse(); @@ -173,8 +171,8 @@ void testPublicMetricsCourses() { metricsBean.updatePublicArtemisMetrics(); - var totalNumberOfCourses = courseRepository.count(); - var numberOfActiveCourses = courseRepository.findAllActive(ZonedDateTime.now()).size(); + long totalNumberOfCourses = courseRepository.count(); + long numberOfActiveCourses = countActiveCourses(); // Assert that there is at least one non-active course in the database so that the values returned from the metrics are different assertThat(numberOfActiveCourses).isNotEqualTo(totalNumberOfCourses); @@ -183,6 +181,33 @@ void testPublicMetricsCourses() { assertMetricEquals(numberOfActiveCourses, "artemis.statistics.public.active_courses"); } + @Test + void testPublicMetricsFilterTestCourses() { + var activeCourse = courseUtilService.createCourse(); + activateCourse(activeCourse); + courseRepository.save(activeCourse); + + var testCourse = courseUtilService.createCourse(); + activateCourse(testCourse); + testCourse.setTestCourse(true); + courseRepository.save(testCourse); + + metricsBean.updatePublicArtemisMetrics(); + + long totalNumberOfCourses = courseRepository.count(); + long numberOfActiveCourses = countActiveCourses(); + + assertMetricEquals(totalNumberOfCourses, "artemis.statistics.public.courses"); + assertMetricEquals(numberOfActiveCourses, "artemis.statistics.public.active_courses"); + } + + private long countActiveCourses() { + final List activeCourses = courseRepository.findAllActive(ZonedDateTime.now()); + // the test courses are only filtered for the metrics since for instructors/tutors/editors using Artemis + // test courses count as active, but they never contain active students/exams relevant for the metrics + return activeCourses.stream().filter(course -> !course.isTestCourse()).count(); + } + @Test void testPublicMetricsExams() { var users = userUtilService.addUsers(TEST_PREFIX, 1, 0, 0, 0); @@ -455,4 +480,9 @@ private void assertMetricEquals(double expectedValue, String metricName, String. var gauge = meterRegistry.get(metricName).tags(tags).gauge(); assertThat(gauge.value()).isEqualTo(expectedValue); } + + private void activateCourse(final Course course) { + course.setStartDate(ZonedDateTime.now().minusDays(1)); + course.setEndDate(ZonedDateTime.now().plusDays(1)); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/config/TopicSubscriptionInterceptorTest.java b/src/test/java/de/tum/in/www1/artemis/config/TopicSubscriptionInterceptorTest.java index 81025f1fc268..1d3c2672a013 100644 --- a/src/test/java/de/tum/in/www1/artemis/config/TopicSubscriptionInterceptorTest.java +++ b/src/test/java/de/tum/in/www1/artemis/config/TopicSubscriptionInterceptorTest.java @@ -12,14 +12,14 @@ import org.springframework.messaging.simp.stomp.StompCommand; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.config.websocket.WebsocketConfiguration; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.user.UserUtilService; @SuppressWarnings("unchecked") -class TopicSubscriptionInterceptorTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TopicSubscriptionInterceptorTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "topicsubscriptioninterceptor"; diff --git a/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java b/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java index d59c2f991c4d..dd5d80795a2d 100644 --- a/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java @@ -28,7 +28,9 @@ import de.tum.in.www1.artemis.security.ArtemisAuthenticationProvider; import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.security.jwt.JWTCookieService; +import de.tum.in.www1.artemis.service.connectors.ci.CIUserManagementService; import de.tum.in.www1.artemis.service.connectors.lti.LtiService; +import de.tum.in.www1.artemis.service.connectors.vcs.VcsUserManagementService; import de.tum.in.www1.artemis.service.user.UserCreationService; class LtiServiceTest { @@ -45,6 +47,12 @@ class LtiServiceTest { @Mock private JWTCookieService jwtCookieService; + @Mock + private Optional optionalVcsUserManagementService; + + @Mock + private Optional optionalCIUserManagementService; + private Exercise exercise; private LtiService ltiService; @@ -61,7 +69,8 @@ class LtiServiceTest { void init() { closeable = MockitoAnnotations.openMocks(this); SecurityContextHolder.clearContext(); - ltiService = new LtiService(userCreationService, userRepository, artemisAuthenticationProvider, jwtCookieService); + ltiService = new LtiService(userCreationService, userRepository, artemisAuthenticationProvider, jwtCookieService, optionalVcsUserManagementService, + optionalCIUserManagementService); Course course = new Course(); course.setId(100L); course.setStudentGroupName(courseStudentGroupName); diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java index 6135065201d5..17598432a202 100644 --- a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java @@ -5,10 +5,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.mockStatic; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.IOException; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.time.DayOfWeek; @@ -67,12 +69,14 @@ import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; import de.tum.in.www1.artemis.repository.metis.conversation.ConversationRepository; import de.tum.in.www1.artemis.security.SecurityUtils; -import de.tum.in.www1.artemis.service.*; +import de.tum.in.www1.artemis.service.FilePathService; +import de.tum.in.www1.artemis.service.ParticipationService; import de.tum.in.www1.artemis.service.dto.StudentDTO; import de.tum.in.www1.artemis.service.dto.UserDTO; import de.tum.in.www1.artemis.service.dto.UserPublicInfoDTO; import de.tum.in.www1.artemis.service.export.CourseExamExportService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; +import de.tum.in.www1.artemis.service.scheduled.ParticipantScoreScheduleService; import de.tum.in.www1.artemis.team.TeamUtilService; import de.tum.in.www1.artemis.user.UserFactory; import de.tum.in.www1.artemis.user.UserUtilService; @@ -129,7 +133,7 @@ public class CourseTestService { private CourseExamExportService courseExamExportService; @Autowired - private FileService fileService; + private FilePathService filePathService; @Autowired private FileUploadExerciseRepository fileUploadExerciseRepository; @@ -209,6 +213,9 @@ public class CourseTestService { @Autowired private LearningPathRepository learningPathRepository; + @Autowired + private ParticipantScoreScheduleService participantScoreScheduleService; + private static final int numberOfStudents = 8; private static final int numberOfTutors = 5; @@ -1470,8 +1477,8 @@ public void testGetCourse() throws Exception { assertThat(courseOnly.getNumberOfInstructors()).as("Amount of instructors is correct").isEqualTo(1); // Assert that course properties on courseWithExercises and courseWithExercisesAndRelevantParticipations match those of courseOnly - String[] ignoringFields = { "exercises", "tutorGroups", "lectures", "exams", "fileService", "numberOfInstructorsTransient", "numberOfStudentsTransient", - "numberOfTeachingAssistantsTransient", "numberOfEditorsTransient" }; + String[] ignoringFields = { "exercises", "tutorGroups", "lectures", "exams", "fileService", "filePathService", "entityFileService", "numberOfInstructorsTransient", + "numberOfStudentsTransient", "numberOfTeachingAssistantsTransient", "numberOfEditorsTransient" }; assertThat(courseWithExercises).as("courseWithExercises same as courseOnly").usingRecursiveComparison().ignoringFields(ignoringFields).isEqualTo(courseOnly); // Verify presence of exercises in mock courses @@ -1522,8 +1529,8 @@ public void testEnrollInCourse() throws Exception { assertThat(updatedGroups).as("User is enrolled in course").contains(course1.getStudentGroupName()); List auditEvents = auditEventRepo.find("ab12cde", Instant.now().minusSeconds(20), Constants.ENROLL_IN_COURSE); - assertThat(auditEvents).as("Audit Event for course enrollment added").hasSize(1); - AuditEvent auditEvent = auditEvents.get(0); + AuditEvent auditEvent = auditEvents.stream().max(Comparator.comparing(AuditEvent::getTimestamp)).orElse(null); + assertThat(auditEvent).as("Audit Event for course enrollment added").isNotNull(); assertThat(auditEvent.getData()).as("Correct Event Data").containsEntry("course", course1.getTitle()); request.postWithResponseBody("/api/courses/" + course2.getId() + "/enroll", null, Set.class, HttpStatus.FORBIDDEN); @@ -2702,6 +2709,7 @@ public void testGetCourseManagementDetailData() throws Exception { request.putWithResponseBody("/api/participations/" + result2.getSubmission().getParticipation().getId() + "/submissions/" + result2.getSubmission().getId() + "/text-assessment-after-complaint", feedbackUpdate, Result.class, HttpStatus.OK); + await().until(participantScoreScheduleService::isIdle); TextExercise finalExercise1 = exercise1; await().until(() -> participantScoreRepository.findAllByExercise(finalExercise1).size() == 2); TextExercise finalExercise2 = exercise2; @@ -3071,21 +3079,18 @@ public void testEditCourseRemoveExistingIcon() throws Exception { ZonedDateTime futureTimestamp = ZonedDateTime.now().plusDays(5); Course course = CourseFactory.generateCourse(null, pastTimestamp, futureTimestamp, new HashSet<>(), "tumuser", "tutor", "editor", "instructor"); + Course savedCourse = courseRepo.save(course); byte[] iconBytes = "icon".getBytes(); MockMultipartFile iconFile = new MockMultipartFile("file", "icon.png", MediaType.APPLICATION_JSON_VALUE, iconBytes); - String iconPath = fileService.handleSaveFile(iconFile, false, false); - iconPath = fileService.manageFilesForUpdatedFilePath(null, iconPath, FilePathService.getCourseIconFilePath(), course.getId()); - course.setCourseIcon(iconPath); - course = courseRepo.save(course); - iconPath = iconPath.replace(Constants.FILEPATH_ID_PLACEHOLDER, course.getId().toString()); - assertThat(course.getCourseIcon()).as("course icon was set correctly").isEqualTo(iconPath); - - course.setCourseIcon(null); - request.putWithMultipartFile("/api/courses/" + course.getId(), course, "course", null, Course.class, HttpStatus.OK, null); + Course savedCourseWithFile = request.putWithMultipartFile("/api/courses/" + savedCourse.getId(), savedCourse, "course", iconFile, Course.class, HttpStatus.OK, null); + Path path = filePathService.actualPathForPublicPath(URI.create(savedCourseWithFile.getCourseIcon())); + savedCourseWithFile.setCourseIcon(null); + request.putWithMultipartFile("/api/courses/" + savedCourseWithFile.getId(), savedCourseWithFile, "course", null, Course.class, HttpStatus.OK, null); + await().until(() -> !Files.exists(path)); course = courseRepo.findByIdElseThrow(course.getId()); assertThat(course.getCourseIcon()).as("course icon was deleted correctly").isNull(); - assertThat(fileService.getFileForPath(fileService.actualPathForPublicPath(iconPath))).as("course icon file was deleted correctly").isNull(); + assertThat(path).as("course icon file was deleted correctly").doesNotExist(); } private String getUpdateOnlineCourseConfigurationPath(String courseId) { diff --git a/src/test/java/de/tum/in/www1/artemis/dataexport/DataExportResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/dataexport/DataExportResourceIntegrationTest.java index f9a7599e5eff..68f7ef85899e 100644 --- a/src/test/java/de/tum/in/www1/artemis/dataexport/DataExportResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/dataexport/DataExportResourceIntegrationTest.java @@ -4,11 +4,11 @@ import static org.mockito.Mockito.*; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.time.ZonedDateTime; import java.util.Optional; +import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -86,14 +86,14 @@ private DataExport prepareDataExportForDownload() throws IOException { dataExport.setCreationFinishedDate(ZonedDateTime.now().minusDays(1)); // rename file to avoid duplicates in the temp directory var newFilePath = TEST_DATA_EXPORT_BASE_FILE_PATH + ZonedDateTime.now().toEpochSecond(); - Files.move(Path.of(TEST_DATA_EXPORT_BASE_FILE_PATH), Path.of(newFilePath)); + FileUtils.moveFile(Path.of(TEST_DATA_EXPORT_BASE_FILE_PATH).toFile(), Path.of(newFilePath).toFile()); dataExport.setFilePath(newFilePath); return dataExportRepository.save(dataExport); } private void restoreTestDataInitState(DataExport dataExport) throws IOException { // undo file renaming - Files.move(Path.of(dataExport.getFilePath()), Path.of(TEST_DATA_EXPORT_BASE_FILE_PATH)); + FileUtils.moveFile(Path.of(dataExport.getFilePath()).toFile(), Path.of(TEST_DATA_EXPORT_BASE_FILE_PATH).toFile()); } @Test @@ -296,7 +296,7 @@ void testRequestingDataExportCreatesCorrectDataExportObject() throws Exception { @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testDeleteDataExportSchedulesDirectoryForDeletion_setsCorrectState(DataExportState state) { var dataExport = initDataExport(state); - doNothing().when(fileService).scheduleForDirectoryDeletion(any(Path.class), anyInt()); + doNothing().when(fileService).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), anyInt()); dataExportService.deleteDataExportAndSetDataExportState(dataExport); var dataExportFromDb = dataExportRepository.findByIdElseThrow(dataExport.getId()); if (state == DataExportState.DOWNLOADED) { @@ -305,7 +305,7 @@ void testDeleteDataExportSchedulesDirectoryForDeletion_setsCorrectState(DataExpo else { assertThat(dataExportFromDb.getDataExportState()).isEqualTo(DataExportState.DELETED); } - verify(fileService).scheduleForDeletion(Path.of(dataExportFromDb.getFilePath()), 2); + verify(fileService).schedulePathForDeletion(Path.of(dataExportFromDb.getFilePath()), 2); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/domain/ExerciseTest.java b/src/test/java/de/tum/in/www1/artemis/domain/ExerciseTest.java index e62051329a60..2e9c330c015c 100644 --- a/src/test/java/de/tum/in/www1/artemis/domain/ExerciseTest.java +++ b/src/test/java/de/tum/in/www1/artemis/domain/ExerciseTest.java @@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseFactory; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.DiagramType; @@ -24,7 +24,7 @@ import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.service.ExerciseService; -class ExerciseTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExerciseTest extends AbstractSpringIntegrationIndependentTest { private Course course; diff --git a/src/test/java/de/tum/in/www1/artemis/domain/ResultTest.java b/src/test/java/de/tum/in/www1/artemis/domain/ResultTest.java index 336fa73962fb..bb98e07371f7 100644 --- a/src/test/java/de/tum/in/www1/artemis/domain/ResultTest.java +++ b/src/test/java/de/tum/in/www1/artemis/domain/ResultTest.java @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.enumeration.FeedbackType; import de.tum.in.www1.artemis.domain.enumeration.Visibility; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; @@ -18,7 +18,7 @@ import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.service.AssessmentService; -class ResultTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ResultTest extends AbstractSpringIntegrationIndependentTest { Result result = new Result(); diff --git a/src/test/java/de/tum/in/www1/artemis/entitylistener/ResultListenerIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/entitylistener/ResultListenerIntegrationTest.java index fee1e001dbb4..fe602b3564b4 100644 --- a/src/test/java/de/tum/in/www1/artemis/entitylistener/ResultListenerIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/entitylistener/ResultListenerIntegrationTest.java @@ -17,7 +17,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.participation.Participant; @@ -34,7 +34,7 @@ import de.tum.in.www1.artemis.team.TeamUtilService; import de.tum.in.www1.artemis.user.UserUtilService; -class ResultListenerIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ResultListenerIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "resultlistenerintegrationtest"; diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamSessionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamSessionIntegrationTest.java index 26df61ed14b8..f77f31d8bbf9 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExamSessionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamSessionIntegrationTest.java @@ -7,7 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.exam.Exam; @@ -18,7 +18,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import inet.ipaddr.IPAddressString; -class ExamSessionIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExamSessionIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "examsessionintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamUserIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamUserIntegrationTest.java index 9c93d719942a..79843172db90 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExamUserIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamUserIntegrationTest.java @@ -118,6 +118,9 @@ void initTestCase() throws Exception { @AfterEach void tearDown() throws Exception { programmingExerciseTestService.tearDown(); + for (LocalRepository studentRepo : studentRepos) { + studentRepo.resetLocalRepo(); + } } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/exam/StudentExamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/StudentExamIntegrationTest.java index ac6b8d9c783b..cfbf77cda095 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/StudentExamIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/StudentExamIntegrationTest.java @@ -473,6 +473,7 @@ void testGetStudentExamForConduction_testExam() throws Exception { // TODO: test the conduction / submission of the test exams, in particular that the summary includes all submissions deleteExamWithInstructor(testExam1); + repo.resetLocalRepo(); } private void assertParticipationAndSubmissions(StudentExam response, User user) { diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseIntegrationTest.java index f7db2779160a..7931609ccc71 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseIntegrationTest.java @@ -14,7 +14,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.*; @@ -37,7 +37,7 @@ import de.tum.in.www1.artemis.web.rest.dto.StatsForDashboardDTO; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; -class ExerciseIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExerciseIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "exerciseintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java index b9f5e5a0071e..a50184014df5 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java @@ -15,7 +15,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.assessment.ComplaintUtilService; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.*; @@ -35,7 +35,7 @@ import de.tum.in.www1.artemis.web.rest.dto.ResultDTO; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -class FileUploadAssessmentIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class FileUploadAssessmentIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "fileuploadassessment"; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseIntegrationTest.java index c7d9c08dcff2..6f979cbc7d97 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseIntegrationTest.java @@ -16,7 +16,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; @@ -35,7 +35,7 @@ import de.tum.in.www1.artemis.web.rest.dto.CourseForDashboardDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; -class FileUploadExerciseIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class FileUploadExerciseIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "fileuploaderxercise"; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseUtilService.java b/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseUtilService.java index afb26d6a7cc4..ff3bcef043d2 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseUtilService.java @@ -210,7 +210,7 @@ public void createFileUploadSubmissionWithFile(String loginPrefix, FileUploadExe fileUploadSubmission = addFileUploadSubmission(fileUploadExercise, fileUploadSubmission, loginPrefix + "student1"); // Create a dummy file - var uploadedFileDir = Path.of("./", FileUploadSubmission.buildFilePath(fileUploadExercise.getId(), fileUploadSubmission.getId())); + var uploadedFileDir = Path.of("./").resolve(FileUploadSubmission.buildFilePath(fileUploadExercise.getId(), fileUploadSubmission.getId())); var uploadedFilePath = Path.of(uploadedFileDir.toString(), filename); if (!Files.exists(uploadedFilePath)) { Files.createDirectories(uploadedFileDir); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadSubmissionIntegrationTest.java index 0c28793d2441..99c5e9065fad 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadSubmissionIntegrationTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.*; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.time.ZonedDateTime; @@ -17,7 +18,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.InitializationState; @@ -25,17 +26,17 @@ import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; import de.tum.in.www1.artemis.domain.participation.Participation; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; -import de.tum.in.www1.artemis.exception.FilePathParsingException; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.FileUploadSubmissionRepository; import de.tum.in.www1.artemis.repository.ParticipationRepository; +import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; -class FileUploadSubmissionIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class FileUploadSubmissionIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "fileuploadsubmission"; @@ -60,6 +61,9 @@ class FileUploadSubmissionIntegrationTest extends AbstractSpringIntegrationBambo @Autowired private ModelingExerciseUtilService modelingExerciseUtilService; + @Autowired + private FilePathService filePathService; + private FileUploadExercise releasedFileUploadExercise; private FileUploadExercise finishedFileUploadExercise; @@ -146,27 +150,27 @@ private void submitFile(String filename, boolean differentFilePath) throws Excep } FileUploadSubmission returnedSubmission = performInitialSubmission(releasedFileUploadExercise.getId(), submission, filename); - String actualFilePath; + Path actualFilePath; if (differentFilePath) { - actualFilePath = Path.of(FileUploadSubmission.buildFilePath(releasedFileUploadExercise.getId(), returnedSubmission.getId()), filename).toString(); + actualFilePath = FileUploadSubmission.buildFilePath(releasedFileUploadExercise.getId(), returnedSubmission.getId()).resolve(filename); } else { if (filename.length() < 5) { - actualFilePath = Path.of(FileUploadSubmission.buildFilePath(releasedFileUploadExercise.getId(), returnedSubmission.getId()), "file" + filename).toString(); + actualFilePath = FileUploadSubmission.buildFilePath(releasedFileUploadExercise.getId(), returnedSubmission.getId()).resolve(Path.of("file" + filename)); } else if (filename.contains("\\")) { - actualFilePath = Path.of(FileUploadSubmission.buildFilePath(releasedFileUploadExercise.getId(), returnedSubmission.getId()), "file.png").toString(); + actualFilePath = FileUploadSubmission.buildFilePath(releasedFileUploadExercise.getId(), returnedSubmission.getId()).resolve("file.png"); } else { - actualFilePath = Path.of(FileUploadSubmission.buildFilePath(releasedFileUploadExercise.getId(), returnedSubmission.getId()), filename).toString(); + actualFilePath = FileUploadSubmission.buildFilePath(releasedFileUploadExercise.getId(), returnedSubmission.getId()).resolve(filename); } } - String publicFilePath = fileService.publicPathForActualPath(actualFilePath, returnedSubmission.getId()); + URI publicFilePath = filePathService.publicPathForActualPath(actualFilePath, returnedSubmission.getId()); assertThat(returnedSubmission).as("submission correctly posted").isNotNull(); - assertThat(returnedSubmission.getFilePath()).isEqualTo(publicFilePath); - var fileBytes = Files.readAllBytes(Path.of(actualFilePath)); + assertThat(returnedSubmission.getFilePath()).isEqualTo(publicFilePath.toString()); + var fileBytes = Files.readAllBytes(actualFilePath); assertThat(fileBytes.length > 0).as("Stored file has content").isTrue(); checkDetailsHidden(returnedSubmission, true); } @@ -301,7 +305,8 @@ void getSubmissionWithoutAssessment() throws Exception { FileUploadSubmission storedSubmission = request.get("/api/exercises/" + releasedFileUploadExercise.getId() + "/file-upload-submission-without-assessment", HttpStatus.OK, FileUploadSubmission.class); - assertThat(storedSubmission).as("in-time submission was found").isEqualToIgnoringGivenFields(submission, "results", "submissionDate", "fileService"); + assertThat(storedSubmission).as("in-time submission was found").isEqualToIgnoringGivenFields(submission, "results", "submissionDate", "fileService", "filePathService", + "entityFileService"); assertThat(storedSubmission.getSubmissionDate()).as("submission date is correct").isEqualToIgnoringNanos(submission.getSubmissionDate()); assertThat(storedSubmission.getLatestResult()).as("result is not set").isNull(); checkDetailsHidden(storedSubmission, false); @@ -322,7 +327,8 @@ void getLateSubmissionWithoutAssessment() throws Exception { FileUploadSubmission storedSubmission = request.get("/api/exercises/" + releasedFileUploadExercise.getId() + "/file-upload-submission-without-assessment", HttpStatus.OK, FileUploadSubmission.class); - assertThat(storedSubmission).as("submission was found").isEqualToIgnoringGivenFields(lateSubmission, "result", "submissionDate", "fileService"); + assertThat(storedSubmission).as("submission was found").isEqualToIgnoringGivenFields(lateSubmission, "result", "submissionDate", "fileService", "filePathService", + "entityFileService"); assertThat(storedSubmission.getLatestResult()).as("result is not set").isNull(); checkDetailsHidden(storedSubmission, false); } @@ -343,7 +349,8 @@ void testGetLateSubmissionWithoutAssessmentLock() throws Exception { FileUploadSubmission storedSubmission = request.get("/api/exercises/" + releasedFileUploadExercise.getId() + "/file-upload-submission-without-assessment?lock=true", HttpStatus.OK, FileUploadSubmission.class); - assertThat(storedSubmission).as("submission was found").isEqualToIgnoringGivenFields(lateSubmission, "results", "submissionDate", "fileService"); + assertThat(storedSubmission).as("submission was found").isEqualToIgnoringGivenFields(lateSubmission, "results", "submissionDate", "fileService", "filePathService", + "entityFileService"); assertThat(storedSubmission.getLatestResult()).as("result is set").isNotNull(); checkDetailsHidden(storedSubmission, false); } @@ -695,12 +702,4 @@ void testOnDeleteSubmission() { fileUploadSubmissionRepository.save(submittedFileUploadSubmission); assertThatNoException().isThrownBy(() -> submittedFileUploadSubmission.onDelete()); } - - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testOnDeleteSubmissionWithException() { - submittedFileUploadSubmission.setFilePath("/api/files/file-upload-exercises"); - fileUploadSubmissionRepository.save(submittedFileUploadSubmission); - assertThatExceptionOfType(FilePathParsingException.class).isThrownBy(() -> submittedFileUploadSubmission.onDelete()); - } } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ApollonDiagramResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ApollonDiagramResourceIntegrationTest.java index 8b0bb122ad28..1f707aa15233 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ApollonDiagramResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ApollonDiagramResourceIntegrationTest.java @@ -13,7 +13,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseFactory; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.enumeration.DiagramType; @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class ApollonDiagramResourceIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ApollonDiagramResourceIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "repositoryintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java index 57996459c6dd..4fb60c5b33c4 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java @@ -15,7 +15,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.*; @@ -41,7 +41,7 @@ import de.tum.in.www1.artemis.util.FileUtils; import de.tum.in.www1.artemis.web.rest.dto.ResultDTO; -class ModelingAssessmentIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ModelingAssessmentIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "modelingassessment"; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingExerciseIntegrationTest.java index 1f49d93c02fd..f58bd0ab3cc8 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingExerciseIntegrationTest.java @@ -18,7 +18,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.*; @@ -39,7 +39,7 @@ import de.tum.in.www1.artemis.util.InvalidExamExerciseDatesArgumentProvider.InvalidExamExerciseDateConfiguration; import de.tum.in.www1.artemis.web.rest.dto.CourseForDashboardDTO; -class ModelingExerciseIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ModelingExerciseIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "modelingexerciseintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingSubmissionIntegrationTest.java index e7770a975fb0..5b6ec29e766d 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingSubmissionIntegrationTest.java @@ -17,7 +17,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.DiagramType; @@ -47,7 +47,7 @@ import de.tum.in.www1.artemis.util.FileUtils; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; -class ModelingSubmissionIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ModelingSubmissionIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "modelingsubmissionintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java index ef5cf3a90efd..1b94cc3cdf81 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java @@ -678,7 +678,7 @@ void testArchiveCourseWithTestModelingAndFileUploadExercises() throws Exception void testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportModelingExercise() throws Exception { doThrow(new IOException("Error")).when(fileService).writeObjectToJsonFile(any(), any(ObjectMapper.class), any(Path.class)); courseTestService.testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportModelingExercise(); - verify(fileService).scheduleForDirectoryDeletion(any(Path.class), anyLong()); + verify(fileService).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), anyLong()); } @Test @@ -687,7 +687,7 @@ void testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportFileUplo doThrow(new IOException("Error")).when(fileService).writeObjectToJsonFile(any(), any(ObjectMapper.class), any(Path.class)); courseTestService.testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportFileUploadExercise(); // the temp directory should be deleted - verify(fileService).scheduleForDirectoryDeletion(any(Path.class), anyLong()); + verify(fileService).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), anyLong()); } @Test @@ -695,7 +695,7 @@ void testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportFileUplo void testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportTextExercise() throws Exception { doThrow(new IOException("Error")).when(fileService).writeObjectToJsonFile(any(), any(ObjectMapper.class), any(Path.class)); courseTestService.testArchiveCourseWithTestModelingAndFileUploadExercisesFailToExportTextExercise(); - verify(fileService).scheduleForDirectoryDeletion(any(Path.class), anyLong()); + verify(fileService).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), anyLong()); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/GitServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/GitServiceTest.java index a8f4cd3eb5e6..a8953ef58b6f 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/GitServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/GitServiceTest.java @@ -26,13 +26,13 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.File; import de.tum.in.www1.artemis.domain.FileType; import de.tum.in.www1.artemis.domain.Repository; import de.tum.in.www1.artemis.util.GitUtilService; -class GitServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class GitServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private GitUtilService gitUtilService; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/PlantUmlIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/PlantUmlIntegrationTest.java index a30be74d26df..29e7fb51be26 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/PlantUmlIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/PlantUmlIntegrationTest.java @@ -19,12 +19,12 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.user.UserUtilService; import net.sourceforge.plantuml.SourceStringReader; import net.sourceforge.plantuml.core.DiagramDescription; -class PlantUmlIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class PlantUmlIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "plantumlintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java index 74b3eec32ed4..3db8760bc506 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java @@ -19,7 +19,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.assessment.ComplaintUtilService; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.*; @@ -37,7 +37,7 @@ import de.tum.in.www1.artemis.util.FileUtils; import de.tum.in.www1.artemis.web.rest.dto.ResultDTO; -class ProgrammingAssessmentIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ProgrammingAssessmentIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "programmingassessment"; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseBitbucketBambooIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseBitbucketBambooIntegrationTest.java index 711baa4c219a..e92117ce8a58 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseBitbucketBambooIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseBitbucketBambooIntegrationTest.java @@ -322,7 +322,7 @@ void configureRepository_testBadRequestError() throws Exception { void exportInstructorRepositories() throws Exception { programmingExerciseTestService.exportInstructorRepositories_shouldReturnFile(); // we export three repositories (template, solution, tests) and for each repository the temp directory and the directory with the zip file should be deleted - verify(fileService, times(6)).scheduleForDirectoryDeletion(any(Path.class), eq(5L)); + verify(fileService, times(6)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); } @Test @@ -330,7 +330,7 @@ void exportInstructorRepositories() throws Exception { void exportAuxiliaryRepository_shouldReturnFile() throws Exception { programmingExerciseTestService.exportInstructorAuxiliaryRepository_shouldReturnFile(); // once for the temp directory and once for the directory with the zip file - verify(fileService, times(2)).scheduleForDirectoryDeletion(any(Path.class), eq(5L)); + verify(fileService, times(2)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); } @Test @@ -350,8 +350,8 @@ void exportInstructorRepositories_forbidden() throws Exception { void exportProgrammingExerciseInstructorMaterial() throws Exception { programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial_shouldReturnFile(true); // we have a working directory and one directory for each repository - verify(fileService, times(4)).scheduleForDirectoryDeletion(any(Path.class), eq(5L)); - verify(fileService).scheduleForDeletion(any(Path.class), eq(5L)); + verify(fileService, times(4)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); + verify(fileService).schedulePathForDeletion(any(Path.class), eq(5L)); } @Test @@ -383,7 +383,7 @@ void importExerciseFromFileMissingExerciseDetailsJson_badRequest() throws Except void importExerciseFromFile_exception_directoryDeleted() throws Exception { doThrow(new RuntimeException("Error")).when(zipFileService).extractZipFileRecursively(any(Path.class)); programmingExerciseTestService.importFromFile_exception_DirectoryDeleted(); - verify(fileService).scheduleForDirectoryDeletion(any(Path.class), eq(5L)); + verify(fileService).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); } @Test @@ -525,7 +525,7 @@ void testExportSolutionRepository_shouldReturnFileOrForbidden() throws Exception programmingExerciseTestService.exportSolutionRepository_shouldReturnFileOrForbidden(); // the test has two successful cases, the other times the operation is forbidden --> one successful case has one repository, // the other one has two because the tests repository is also included. - verify(fileService, times(3)).scheduleForDirectoryDeletion(any(Path.class), eq(5L)); + verify(fileService, times(3)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); } @Test @@ -534,7 +534,7 @@ void testExportExamSolutionRepository_shouldReturnFileOrForbidden() throws Excep programmingExerciseTestService.exportExamSolutionRepository_shouldReturnFileOrForbidden(); // the test has two successful cases, the other times the operation is forbidden --> one successful case has one repository, // the other one has two because the tests repository is also included. - verify(fileService, times(3)).scheduleForDirectoryDeletion(any(Path.class), eq(5L)); + verify(fileService, times(3)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitIntegrationTest.java index 745852518f42..334b336cd6ea 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitIntegrationTest.java @@ -22,7 +22,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; @@ -35,7 +35,7 @@ import de.tum.in.www1.artemis.web.rest.ProgrammingExerciseResourceEndpoints; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; -class ProgrammingExerciseGitIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ProgrammingExerciseGitIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "progexgitintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitlabJenkinsIntegrationTest.java index ce90007f3c9e..4afb801e464e 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitlabJenkinsIntegrationTest.java @@ -205,7 +205,7 @@ void importExerciseFromFile_NoZip_badRequest() throws Exception { void importExerciseFromFile_exception_directoryDeleted() throws Exception { doThrow(new RuntimeException("Error")).when(zipFileService).extractZipFileRecursively(any(Path.class)); programmingExerciseTestService.importFromFile_exception_DirectoryDeleted(); - verify(fileService).scheduleForDirectoryDeletion(any(Path.class), eq(5L)); + verify(fileService).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); } @Test @@ -377,7 +377,7 @@ void configureRepository_testBadRequestError() throws Exception { void exportInstructorRepositories() throws Exception { programmingExerciseTestService.exportInstructorRepositories_shouldReturnFile(); // we export three repositories (template, solution, tests) and for each repository the temp directory and the directory with the zip file should be deleted - verify(fileService, times(6)).scheduleForDirectoryDeletion(any(Path.class), eq(5L)); + verify(fileService, times(6)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); } @Test @@ -385,7 +385,7 @@ void exportInstructorRepositories() throws Exception { void exportAuxiliaryRepository_shouldReturnFile() throws Exception { programmingExerciseTestService.exportInstructorAuxiliaryRepository_shouldReturnFile(); // once for the temp directory and once for the directory with the zip file - verify(fileService, times(2)).scheduleForDirectoryDeletion(any(Path.class), eq(5L)); + verify(fileService, times(2)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); } @Test @@ -405,8 +405,8 @@ void exportInstructorRepositories_forbidden() throws Exception { void exportProgrammingExerciseInstructorMaterial() throws Exception { programmingExerciseTestService.exportProgrammingExerciseInstructorMaterial_shouldReturnFile(true); // we have a working directory and one directory for each repository - verify(fileService, times(4)).scheduleForDirectoryDeletion(any(Path.class), eq(5L)); - verify(fileService).scheduleForDeletion(any(Path.class), eq(5L)); + verify(fileService, times(4)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); + verify(fileService).schedulePathForDeletion(any(Path.class), eq(5L)); } @Test @@ -508,7 +508,7 @@ void testExportSolutionRepository_shouldReturnFileOrForbidden() throws Exception programmingExerciseTestService.exportSolutionRepository_shouldReturnFileOrForbidden(); // the test has two successful cases, the other times the operation is forbidden --> one successful case has one repository, // the other one has two because the tests repository is also included. - verify(fileService, times(3)).scheduleForDirectoryDeletion(any(Path.class), eq(5L)); + verify(fileService, times(3)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); } @Test @@ -517,7 +517,7 @@ void testExportExamSolutionRepository_shouldReturnFileOrForbidden() throws Excep programmingExerciseTestService.exportExamSolutionRepository_shouldReturnFileOrForbidden(); // the test has two successful cases, the other times the operation is forbidden --> one successful case has one repository, // the other one has two because the tests repository is also included. - verify(fileService, times(3)).scheduleForDirectoryDeletion(any(Path.class), eq(5L)); + verify(fileService, times(3)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); } // TODO: add startProgrammingExerciseStudentSubmissionFailedWithBuildlog & copyRepository_testConflictError diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGradingServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGradingServiceTest.java index 88dc5b760cc3..75b5a615da1c 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGradingServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGradingServiceTest.java @@ -3,6 +3,8 @@ import static de.tum.in.www1.artemis.config.Constants.TEST_CASES_DUPLICATE_NOTIFICATION; import static de.tum.in.www1.artemis.web.rest.ProgrammingExerciseResourceEndpoints.ROOT; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import java.time.ZonedDateTime; @@ -10,6 +12,8 @@ import java.util.function.Function; import java.util.stream.Collectors; +import javax.mail.internet.MimeMessage; + import org.assertj.core.data.Offset; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -30,10 +34,7 @@ import de.tum.in.www1.artemis.domain.enumeration.Visibility; import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; -import de.tum.in.www1.artemis.domain.participation.Participation; -import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; -import de.tum.in.www1.artemis.domain.participation.SolutionProgrammingExerciseParticipation; -import de.tum.in.www1.artemis.domain.participation.StudentParticipation; +import de.tum.in.www1.artemis.domain.participation.*; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; @@ -257,6 +258,7 @@ void shouldAddFeedbackForDuplicateTestCases() { assertThat(result.getFeedbacks()).hasSize(countOfNewFeedbacks); String notificationText = TEST_CASES_DUPLICATE_NOTIFICATION + "test3, test1"; verify(groupNotificationService).notifyEditorAndInstructorGroupAboutDuplicateTestCasesForExercise(programmingExercise, notificationText); + verify(javaMailSender, timeout(4000)).send(any(MimeMessage.class)); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationBambooBitbucketJiraTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationBambooBitbucketJiraTest.java index ef68076801af..d4eb0920f74a 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationBambooBitbucketJiraTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationBambooBitbucketJiraTest.java @@ -809,8 +809,8 @@ void testCheckPlagiarism() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testCheckPlagiarismJplagReport() throws Exception { programmingExerciseIntegrationTestService.testCheckPlagiarismJplagReport(); - verify(fileService).scheduleForDeletion(any(Path.class), eq(1L)); - verify(fileService).scheduleForDirectoryDeletion(any(Path.class), eq(5L)); + verify(fileService).schedulePathForDeletion(any(Path.class), eq(1L)); + verify(fileService).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationTestService.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationTestService.java index 658e55429f87..70b9c8fd60ef 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationTestService.java @@ -1633,12 +1633,13 @@ void lockAllRepositories() throws Exception { verify(versionControlService, timeout(300)).setRepositoryPermissionsToReadOnly(participation2.getVcsRepositoryUrl(), programmingExercise.getProjectKey(), participation2.getStudents()); - userUtilService.changeUser(userPrefix + "instructor1"); - - var notifications = request.getList("/api/notifications", HttpStatus.OK, Notification.class); - assertThat(notifications).as("Instructor get notified that lock operations were successful") - .anyMatch(n -> n.getText().contains(Constants.PROGRAMMING_EXERCISE_SUCCESSFUL_LOCK_OPERATION_NOTIFICATION)) - .noneMatch(n -> n.getText().contains(Constants.PROGRAMMING_EXERCISE_FAILED_LOCK_OPERATIONS_NOTIFICATION)); + await().untilAsserted(() -> { + userUtilService.changeUser(userPrefix + "instructor1"); + var notifications = request.getList("/api/notifications", HttpStatus.OK, Notification.class); + assertThat(notifications).as("Instructor get notified that lock operations were successful") + .anyMatch(n -> n.getText().contains(Constants.PROGRAMMING_EXERCISE_SUCCESSFUL_LOCK_OPERATION_NOTIFICATION)) + .noneMatch(n -> n.getText().contains(Constants.PROGRAMMING_EXERCISE_FAILED_LOCK_OPERATIONS_NOTIFICATION)); + }); } void unlockAllRepositories_asStudent_forbidden() throws Exception { @@ -1772,11 +1773,11 @@ private int calculateMagicNumber() { Files.createDirectories(jPlagReposDir.resolve(projectKey)); Path file1 = Files.createFile(jPlagReposDir.resolve(projectKey).resolve("Submission-1.java")); - Files.writeString(file1, exampleProgram); + FileUtils.writeStringToFile(file1.toFile(), exampleProgram, StandardCharsets.UTF_8); Path file2 = Files.createFile(jPlagReposDir.resolve(projectKey).resolve("Submission-2.java")); - Files.writeString(file2, exampleProgram); + FileUtils.writeStringToFile(file2.toFile(), exampleProgram, StandardCharsets.UTF_8); - doReturn(jPlagReposDir).when(fileService).getTemporaryUniquePath(any(Path.class), eq(60L)); + doReturn(jPlagReposDir).when(fileService).getTemporaryUniqueSubfolderPath(any(Path.class), eq(60L)); doReturn(null).when(urlService).getRepositorySlugFromRepositoryUrl(any()); var repository1 = gitService.getExistingCheckedOutRepositoryByLocalPath(localRepoFile.toPath(), null); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseParticipationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseParticipationIntegrationTest.java index 79ebb1edaadf..0d91f8e94c74 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseParticipationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseParticipationIntegrationTest.java @@ -20,7 +20,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.participation.*; @@ -29,7 +29,7 @@ import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.user.UserUtilService; -class ProgrammingExerciseParticipationIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ProgrammingExerciseParticipationIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "programmingexerciseparticipation"; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseRepositoryServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseRepositoryServiceTest.java index 4b0711b07d34..780bf90380e5 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseRepositoryServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseRepositoryServiceTest.java @@ -8,13 +8,13 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseRepositoryService; -class ProgrammingExerciseRepositoryServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ProgrammingExerciseRepositoryServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private ProgrammingExerciseRepository programmingExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseScheduleServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseScheduleServiceTest.java index cdbb6775e78c..6b546e374c79 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseScheduleServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseScheduleServiceTest.java @@ -1,16 +1,8 @@ package de.tum.in.www1.artemis.exercise.programmingexercise; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; - -import java.net.URISyntaxException; +import static org.mockito.Mockito.*; + import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.List; @@ -18,6 +10,7 @@ import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.ObjectId; +import org.gitlab4j.api.GitLabApiException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,9 +20,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationGitlabCIGitlabSamlTest; import de.tum.in.www1.artemis.config.Constants; -import de.tum.in.www1.artemis.connector.BitbucketRequestMockProvider; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; @@ -47,7 +39,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.LocalRepository; -class ProgrammingExerciseScheduleServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ProgrammingExerciseScheduleServiceTest extends AbstractSpringIntegrationGitlabCIGitlabSamlTest { private static final String TEST_PREFIX = "programmingexercisescheduleservice"; @@ -63,9 +55,6 @@ class ProgrammingExerciseScheduleServiceTest extends AbstractSpringIntegrationBa @Autowired private ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository; - @Autowired - private BitbucketRequestMockProvider bitbucketRequestMockProvider; - @Autowired private ExamRepository examRepository; @@ -95,10 +84,14 @@ class ProgrammingExerciseScheduleServiceTest extends AbstractSpringIntegrationBa // TODO: This could be improved by e.g. manually setting the system time instead of waiting for actual time to pass. private static final long SCHEDULER_TASK_TRIGGER_DELAY_MS = 1000; + private static final long DELAY_MS = 300; + + private static final long TIMEOUT_MS = 5000; + @BeforeEach void init() throws Exception { studentRepository.configureRepos("studentLocalRepo", "studentOriginRepo"); - bitbucketRequestMockProvider.enableMockingOfRequests(true); + gitlabRequestMockProvider.enableMockingOfRequests(); doReturn(ObjectId.fromString("fffb09455885349da6e19d3ad7fd9c3404c5a0df")).when(gitService).getLastCommitHash(any()); userUtilService.addUsers(TEST_PREFIX, 3, 1, 0, 1); @@ -113,17 +106,10 @@ void init() throws Exception { } @AfterEach - void tearDown() throws InterruptedException { - // not yet finished scheduled futures may otherwise affect following tests + void tearDown() throws Exception { scheduleService.clearAllTasks(); - - // TODO: find a better solution in the future, because this makes the tests slower - // Some futures might already run while all tasks are cancelled. Waiting a bit makes sure the mocks are not called by the futures after the reset. - // Otherwise, the following test might fail. - Thread.sleep(500); // ok - - bambooRequestMockProvider.reset(); - bitbucketRequestMockProvider.reset(); + gitlabRequestMockProvider.reset(); + studentRepository.resetLocalRepo(); } private void verifyLockStudentRepositoryAndParticipationOperation(boolean wasCalled, long timeoutInMs) { @@ -145,10 +131,10 @@ private void verifyLockStudentRepositoryAndParticipationOperation(boolean wasCal } } - private void mockStudentRepoLocks() throws URISyntaxException, GitAPIException { + private void mockStudentRepoLocks() throws GitAPIException, GitLabApiException { for (final var participation : programmingExercise.getStudentParticipations()) { - final var repositorySlug = (programmingExercise.getProjectKey() + "-" + participation.getParticipantIdentifier()).toLowerCase(); - bitbucketRequestMockProvider.mockSetRepositoryPermissionsToReadOnly(repositorySlug, programmingExercise.getProjectKey(), participation.getStudents()); + final VcsRepositoryUrl repositoryUrl = ((ProgrammingExerciseParticipation) participation).getVcsRepositoryUrl(); + gitlabRequestMockProvider.setRepositoryPermissionsToReadOnly(repositoryUrl, participation.getStudents()); doReturn(gitService.getExistingCheckedOutRepositoryByLocalPath(studentRepository.localRepoFile.toPath(), null)).when(gitService) .getOrCheckoutRepository((ProgrammingExerciseParticipation) participation); } @@ -158,84 +144,75 @@ private void mockStudentRepoLocks() throws URISyntaxException, GitAPIException { @WithMockUser(username = "admin", roles = "ADMIN") void shouldExecuteScheduledBuildAndTestAfterDueDate() throws Exception { mockStudentRepoLocks(); - final var dueDateDelayMS = 200; - programmingExercise.setDueDate(ZonedDateTime.now().plus(dueDateDelayMS / 2, ChronoUnit.MILLIS)); - programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(plusMillis(ZonedDateTime.now(), dueDateDelayMS)); - programmingExerciseRepository.save(programmingExercise); + programmingExercise.setDueDate(nowPlusMillis(DELAY_MS)); + programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(nowPlusMillis(DELAY_MS)); + programmingExerciseRepository.saveAndFlush(programmingExercise); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); // Lock student repository must be called once per participation. - verifyLockStudentRepositoryAndParticipationOperation(true, dueDateDelayMS); + verifyLockStudentRepositoryAndParticipationOperation(true, TIMEOUT_MS); // Instructor build should have been triggered. - verify(programmingTriggerService, timeout(dueDateDelayMS)).triggerInstructorBuildForExercise(programmingExercise.getId()); + verify(programmingTriggerService, timeout(TIMEOUT_MS)).triggerInstructorBuildForExercise(programmingExercise.getId()); } @Test @WithMockUser(username = "admin", roles = "ADMIN") - void shouldNotExecuteScheduledIfBuildAndTestAfterDueDateHasPassed() throws InterruptedException { + void shouldNotExecuteScheduledIfBuildAndTestAfterDueDateHasPassed() { programmingExercise.setDueDate(ZonedDateTime.now().minusHours(1L)); programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(ZonedDateTime.now().minusHours(1L)); - programmingExerciseRepository.save(programmingExercise); + programmingExerciseRepository.saveAndFlush(programmingExercise); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - Thread.sleep(SCHEDULER_TASK_TRIGGER_DELAY_MS); // ok - // Lock student repository must not be called. - verifyLockStudentRepositoryAndParticipationOperation(false, 0); + verifyLockStudentRepositoryAndParticipationOperation(false, TIMEOUT_MS); verify(programmingTriggerService, never()).triggerInstructorBuildForExercise(programmingExercise.getId()); } @Test @WithMockUser(username = "admin", roles = "ADMIN") - void shouldNotExecuteScheduledIfBuildAndTestAfterDueDateIsNull() throws InterruptedException { + void shouldNotExecuteScheduledIfBuildAndTestAfterDueDateIsNull() { instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - Thread.sleep(SCHEDULER_TASK_TRIGGER_DELAY_MS); // ok - - // Lock student repository must not be called. - verifyLockStudentRepositoryAndParticipationOperation(false, 0); - verify(programmingTriggerService, never()).triggerInstructorBuildForExercise(programmingExercise.getId()); + verify(programmingTriggerService, after(SCHEDULER_TASK_TRIGGER_DELAY_MS).never()).triggerInstructorBuildForExercise(programmingExercise.getId()); // Update all scores should not have been triggered. verify(programmingExerciseGradingService, never()).updateAllResults(programmingExercise); + // Lock student repository must not be called. + verifyLockStudentRepositoryAndParticipationOperation(false, 0); } @Test @WithMockUser(username = "admin", roles = "ADMIN") void shouldNotExecuteScheduledTwiceIfSameExercise() throws Exception { mockStudentRepoLocks(); - long delayMS = 200; // 200 ms. - programmingExercise.setDueDate(plusMillis(ZonedDateTime.now(), delayMS / 2)); + programmingExercise.setDueDate(nowPlusMillis(DELAY_MS)); // Setting it the first time. - programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(plusMillis(ZonedDateTime.now(), delayMS)); - programmingExercise = programmingExerciseRepository.save(programmingExercise); + programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(nowPlusMillis(DELAY_MS)); + programmingExercise = programmingExerciseRepository.saveAndFlush(programmingExercise); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); // Setting it the second time. - programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(plusMillis(ZonedDateTime.now(), delayMS * 2)); - programmingExercise = programmingExerciseRepository.save(programmingExercise); + programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(nowPlusMillis(DELAY_MS)); + programmingExercise = programmingExerciseRepository.saveAndFlush(programmingExercise); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); // Lock student repository must be called once per participation. - verifyLockStudentRepositoryAndParticipationOperation(true, delayMS * 2); - verify(programmingTriggerService, timeout(delayMS * 2)).triggerInstructorBuildForExercise(programmingExercise.getId()); + verifyLockStudentRepositoryAndParticipationOperation(true, TIMEOUT_MS); + verify(programmingTriggerService, timeout(TIMEOUT_MS)).triggerInstructorBuildForExercise(programmingExercise.getId()); } @Test @WithMockUser(username = "admin", roles = "ADMIN") - void shouldNotExecuteScheduledIfBuildAndTestAfterDueDateChangesToNull() throws InterruptedException { - long delayMS = 200; + void shouldNotExecuteScheduledIfBuildAndTestAfterDueDateChangesToNull() { // Setting it the first time. - programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(plusMillis(ZonedDateTime.now(), delayMS)); + programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(nowPlusMillis(DELAY_MS)); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); // Now setting the date to null - this must also clear the old scheduled task! programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(null); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - Thread.sleep(delayMS + SCHEDULER_TASK_TRIGGER_DELAY_MS); // ok - + verify(programmingTriggerService, after(SCHEDULER_TASK_TRIGGER_DELAY_MS).never()).triggerInstructorBuildForExercise(programmingExercise.getId()); verifyLockStudentRepositoryAndParticipationOperation(false, 0); - verify(programmingTriggerService, never()).triggerInstructorBuildForExercise(programmingExercise.getId()); verify(programmingExerciseGradingService, never()).updateAllResults(programmingExercise); } @@ -243,55 +220,48 @@ void shouldNotExecuteScheduledIfBuildAndTestAfterDueDateChangesToNull() throws I @WithMockUser(username = "admin", roles = "ADMIN") void shouldScheduleExercisesWithBuildAndTestDateInFuture() throws Exception { mockStudentRepoLocks(); - long delayMS = 800; - programmingExercise.setDueDate(plusMillis(ZonedDateTime.now(), delayMS / 2)); - programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(plusMillis(ZonedDateTime.now(), delayMS)); - programmingExerciseRepository.save(programmingExercise); + programmingExercise.setDueDate(nowPlusMillis(DELAY_MS)); + programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(nowPlusMillis(DELAY_MS * 2)); + programmingExerciseRepository.saveAndFlush(programmingExercise); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verifyLockStudentRepositoryAndParticipationOperation(true, delayMS); - verify(programmingTriggerService, timeout(5000)).triggerInstructorBuildForExercise(programmingExercise.getId()); + verifyLockStudentRepositoryAndParticipationOperation(true, TIMEOUT_MS); + verify(programmingTriggerService, timeout(TIMEOUT_MS)).triggerInstructorBuildForExercise(programmingExercise.getId()); } @Test @WithMockUser(username = "admin", roles = "ADMIN") void shouldScheduleExercisesWithManualAssessment() throws Exception { mockStudentRepoLocks(); - long delayMS = 200; - programmingExercise.setDueDate(plusMillis(ZonedDateTime.now(), delayMS / 2)); + programmingExercise.setDueDate(nowPlusMillis(DELAY_MS)); programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(null); programmingExercise.setAssessmentType(AssessmentType.SEMI_AUTOMATIC); - programmingExerciseRepository.save(programmingExercise); + programmingExerciseRepository.saveAndFlush(programmingExercise); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - Thread.sleep(delayMS + SCHEDULER_TASK_TRIGGER_DELAY_MS); // ok - - // Only lock participations - verifyLockStudentRepositoryAndParticipationOperation(true, delayMS); // but do not build all - verify(programmingTriggerService, never()).triggerInstructorBuildForExercise(programmingExercise.getId()); + verify(programmingTriggerService, after(SCHEDULER_TASK_TRIGGER_DELAY_MS).never()).triggerInstructorBuildForExercise(programmingExercise.getId()); + // Only lock participations + verifyLockStudentRepositoryAndParticipationOperation(true, TIMEOUT_MS); } @Test @WithMockUser(username = "admin", roles = "ADMIN") void shouldUpdateScoresIfHasTestsAfterDueDateAndNoBuildAfterDueDate() throws Exception { mockStudentRepoLocks(); - final var dueDateDelayMS = 500; - programmingExercise.setDueDate(ZonedDateTime.now().plus(dueDateDelayMS / 2, ChronoUnit.MILLIS)); + programmingExercise.setDueDate(nowPlusMillis(DELAY_MS)); programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(null); - programmingExerciseRepository.save(programmingExercise); + programmingExerciseRepository.saveAndFlush(programmingExercise); var testCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExercise.getId()); testCases.stream().findFirst().orElseThrow().setVisibility(Visibility.AFTER_DUE_DATE); - programmingExerciseTestCaseRepository.saveAll(testCases); + programmingExerciseTestCaseRepository.saveAllAndFlush(testCases); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - Thread.sleep(dueDateDelayMS + SCHEDULER_TASK_TRIGGER_DELAY_MS); // ok - - verifyLockStudentRepositoryAndParticipationOperation(true, dueDateDelayMS); - verify(programmingTriggerService, never()).triggerInstructorBuildForExercise(programmingExercise.getId()); + verify(programmingTriggerService, after(SCHEDULER_TASK_TRIGGER_DELAY_MS).never()).triggerInstructorBuildForExercise(programmingExercise.getId()); + verifyLockStudentRepositoryAndParticipationOperation(true, TIMEOUT_MS); // has AFTER_DUE_DATE tests and no additional build after due date => update the scores to show those test cases in it verify(programmingExerciseGradingService, timeout(5000)).updateResultsOnlyRegularDueDateParticipations(programmingExercise); // make sure to trigger the update only for participants who do not have got an individual due date @@ -302,22 +272,19 @@ void shouldUpdateScoresIfHasTestsAfterDueDateAndNoBuildAfterDueDate() throws Exc @WithMockUser(username = "admin", roles = "ADMIN") void shouldNotUpdateScoresIfHasTestsAfterDueDateAndBuildAfterDueDate() throws Exception { mockStudentRepoLocks(); - final var dueDateDelayMS = 500; - programmingExercise.setDueDate(ZonedDateTime.now().plus(dueDateDelayMS / 2, ChronoUnit.MILLIS)); - programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(plusMillis(ZonedDateTime.now(), dueDateDelayMS)); - programmingExerciseRepository.save(programmingExercise); + programmingExercise.setDueDate(nowPlusMillis(DELAY_MS)); + programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(nowPlusMillis(DELAY_MS * 2)); + programmingExerciseRepository.saveAndFlush(programmingExercise); var testCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExercise.getId()); testCases.stream().findFirst().orElseThrow().setVisibility(Visibility.AFTER_DUE_DATE); - programmingExerciseTestCaseRepository.saveAll(testCases); + programmingExerciseTestCaseRepository.saveAllAndFlush(testCases); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - Thread.sleep(dueDateDelayMS + SCHEDULER_TASK_TRIGGER_DELAY_MS); // ok - - verifyLockStudentRepositoryAndParticipationOperation(true, dueDateDelayMS / 2); - verify(programmingTriggerService, timeout(dueDateDelayMS)).triggerInstructorBuildForExercise(programmingExercise.getId()); // has AFTER_DUE_DATE tests, but also buildAfterDueDate => do not update results, but use the results created on additional build run - verify(programmingExerciseGradingService, never()).updateAllResults(programmingExercise); + verify(programmingExerciseGradingService, after(SCHEDULER_TASK_TRIGGER_DELAY_MS).never()).updateAllResults(programmingExercise); + verifyLockStudentRepositoryAndParticipationOperation(true, TIMEOUT_MS); + verify(programmingTriggerService, timeout(TIMEOUT_MS)).triggerInstructorBuildForExercise(programmingExercise.getId()); } @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @@ -325,32 +292,29 @@ void shouldNotUpdateScoresIfHasTestsAfterDueDateAndBuildAfterDueDate() throws Ex @WithMockUser(username = "admin", roles = "ADMIN") void shouldNotUpdateScoresIfHasNoTestsAfterDueDate(boolean hasBuildAndTestAfterDueDate) throws Exception { mockStudentRepoLocks(); - final var dueDateDelayMS = 200; - programmingExercise.setDueDate(ZonedDateTime.now().plus(dueDateDelayMS / 2, ChronoUnit.MILLIS)); + programmingExercise.setDueDate(nowPlusMillis(DELAY_MS)); if (hasBuildAndTestAfterDueDate) { - programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(plusMillis(ZonedDateTime.now(), dueDateDelayMS)); + programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(nowPlusMillis(DELAY_MS * 2)); } else { programmingExercise.setBuildAndTestStudentSubmissionsAfterDueDate(null); } - programmingExerciseRepository.save(programmingExercise); + programmingExerciseRepository.saveAndFlush(programmingExercise); var testCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExercise.getId()); testCases.forEach(testCase -> testCase.setVisibility(Visibility.ALWAYS)); - programmingExerciseTestCaseRepository.saveAll(testCases); + programmingExerciseTestCaseRepository.saveAllAndFlush(testCases); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - Thread.sleep(dueDateDelayMS + SCHEDULER_TASK_TRIGGER_DELAY_MS); // ok - - verifyLockStudentRepositoryAndParticipationOperation(true, dueDateDelayMS / 2); + // no tests marked as AFTER_DUE_DATE => do not update scores on due date + verify(programmingExerciseGradingService, after(SCHEDULER_TASK_TRIGGER_DELAY_MS).never()).updateAllResults(programmingExercise); + verifyLockStudentRepositoryAndParticipationOperation(true, TIMEOUT_MS); if (hasBuildAndTestAfterDueDate) { - verify(programmingTriggerService, timeout(dueDateDelayMS)).triggerInstructorBuildForExercise(programmingExercise.getId()); + verify(programmingTriggerService, timeout(TIMEOUT_MS)).triggerInstructorBuildForExercise(programmingExercise.getId()); } else { verify(programmingTriggerService, never()).triggerInstructorBuildForExercise(programmingExercise.getId()); } - // no tests marked as AFTER_DUE_DATE => do not update scores on due date - verify(programmingExerciseGradingService, never()).updateAllResults(programmingExercise); } @Test @@ -360,23 +324,22 @@ void testCombineTemplateBeforeRelease() throws Exception { VcsRepositoryUrl repositoryUrl = programmingExerciseWithTemplate.getVcsTemplateRepositoryUrl(); doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(repositoryUrl); - programmingExercise.setReleaseDate(ZonedDateTime.now().plusSeconds(Constants.SECONDS_BEFORE_RELEASE_DATE_FOR_COMBINING_TEMPLATE_COMMITS + 1)); - programmingExerciseRepository.save(programmingExercise); + programmingExercise.setReleaseDate(nowPlusMillis(DELAY_MS).plusSeconds(Constants.SECONDS_BEFORE_RELEASE_DATE_FOR_COMBINING_TEMPLATE_COMMITS)); + programmingExerciseRepository.saveAndFlush(programmingExercise); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(gitService, timeout(5000)).combineAllCommitsOfRepositoryIntoOne(repositoryUrl); + verify(gitService, timeout(TIMEOUT_MS)).combineAllCommitsOfRepositoryIntoOne(repositoryUrl); } @Test @WithMockUser(username = "admin", roles = "ADMIN") void scheduleIndividualDueDateNoBuildAndTestDateLock() throws Exception { mockStudentRepoLocks(); - final long delayMS = 400; final ZonedDateTime now = ZonedDateTime.now(); - setupProgrammingExerciseDates(now, delayMS / 2, null); + setupProgrammingExerciseDates(now, DELAY_MS, null); var login = TEST_PREFIX + "student3"; - var participationIndividualDueDate = setupParticipationIndividualDueDate(now, 3 * delayMS + SCHEDULER_TASK_TRIGGER_DELAY_MS, login); + var participationIndividualDueDate = setupParticipationIndividualDueDate(now, DELAY_MS * 2 + SCHEDULER_TASK_TRIGGER_DELAY_MS, login); programmingExercise = programmingExerciseRepository.findWithAllParticipationsById(programmingExercise.getId()).orElseThrow(); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); @@ -389,88 +352,80 @@ void scheduleIndividualDueDateNoBuildAndTestDateLock() throws Exception { assertThat(studentParticipationIndividualDueDate.getIndividualDueDate()).isNotEqualTo(programmingExercise.getDueDate()); // the repo-lock for the participation with a later due date should only have been called after that individual due date has passed - verifyLockStudentRepositoryAndParticipationOperation(true, studentParticipationsRegularDueDate, delayMS); + verifyLockStudentRepositoryAndParticipationOperation(true, studentParticipationsRegularDueDate, DELAY_MS * 2); // first the operation should not be called verifyLockStudentRepositoryAndParticipationOperation(false, participationIndividualDueDate, 0); // after some time the operation should be called as well (verify waits up to 5s until this condition is fulfilled) - verifyLockStudentRepositoryAndParticipationOperation(true, participationIndividualDueDate, 3 * delayMS + SCHEDULER_TASK_TRIGGER_DELAY_MS); + verifyLockStudentRepositoryAndParticipationOperation(true, participationIndividualDueDate, TIMEOUT_MS); } @Test @WithMockUser(username = "admin", roles = "ADMIN") void scheduleIndividualDueDateBetweenDueDateAndBuildAndTestDate() throws Exception { - bitbucketRequestMockProvider.reset(); mockStudentRepoLocks(); - final long delayMS = 200; final ZonedDateTime now = ZonedDateTime.now(); - setupProgrammingExerciseDates(now, delayMS / 2, 2 * SCHEDULER_TASK_TRIGGER_DELAY_MS); + setupProgrammingExerciseDates(now, DELAY_MS, 2 * SCHEDULER_TASK_TRIGGER_DELAY_MS); // individual due date between regular due date and build and test date - var participationIndividualDueDate = setupParticipationIndividualDueDate(now, 2 * delayMS + SCHEDULER_TASK_TRIGGER_DELAY_MS, TEST_PREFIX + "student3"); + var participationIndividualDueDate = setupParticipationIndividualDueDate(now, DELAY_MS * 2 + SCHEDULER_TASK_TRIGGER_DELAY_MS, TEST_PREFIX + "student3"); programmingExercise = programmingExerciseRepository.findWithAllParticipationsById(programmingExercise.getId()).orElseThrow(); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(5000)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); + verify(scheduleService, timeout(DELAY_MS * 2)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any()); - Thread.sleep(delayMS + SCHEDULER_TASK_TRIGGER_DELAY_MS); // ok - // not yet locked on regular due date + verify(programmingTriggerService, after(DELAY_MS * 2).never()).triggerInstructorBuildForExercise(programmingExercise.getId()); verifyLockStudentRepositoryAndParticipationOperation(false, participationIndividualDueDate, 0); - verify(programmingTriggerService, never()).triggerInstructorBuildForExercise(programmingExercise.getId()); // locked after individual due date - verifyLockStudentRepositoryAndParticipationOperation(true, participationIndividualDueDate, 2 * delayMS + SCHEDULER_TASK_TRIGGER_DELAY_MS); - - Thread.sleep(delayMS + SCHEDULER_TASK_TRIGGER_DELAY_MS); // ok + verifyLockStudentRepositoryAndParticipationOperation(true, participationIndividualDueDate, 2 * DELAY_MS + SCHEDULER_TASK_TRIGGER_DELAY_MS); // after build and test date: no individual build and test actions are scheduled - verify(programmingTriggerService, never()).triggerBuildForParticipations(List.of(participationIndividualDueDate)); - verify(programmingTriggerService, timeout(2 * SCHEDULER_TASK_TRIGGER_DELAY_MS)).triggerInstructorBuildForExercise(programmingExercise.getId()); + verify(programmingTriggerService, after(DELAY_MS + SCHEDULER_TASK_TRIGGER_DELAY_MS).never()).triggerBuildForParticipations(List.of(participationIndividualDueDate)); + verify(programmingTriggerService, timeout(TIMEOUT_MS)).triggerInstructorBuildForExercise(programmingExercise.getId()); } @Test @WithMockUser(username = "admin", roles = "ADMIN") void scheduleIndividualDueDateAfterBuildAndTestDate() throws Exception { mockStudentRepoLocks(); - final long delayMS = 200; final ZonedDateTime now = ZonedDateTime.now(); - setupProgrammingExerciseDates(now, delayMS / 2, delayMS); + setupProgrammingExerciseDates(now, DELAY_MS, DELAY_MS); // individual due date after build and test date - var participationIndividualDueDate = setupParticipationIndividualDueDate(now, 2 * delayMS, TEST_PREFIX + "student3"); + var participationIndividualDueDate = setupParticipationIndividualDueDate(now, DELAY_MS * 2, TEST_PREFIX + "student3"); programmingExercise = programmingExerciseRepository.findWithAllParticipationsById(programmingExercise.getId()).orElseThrow(); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); // special scheduling for both lock and build and test - verify(scheduleService, timeout(5000)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); - verify(scheduleService, timeout(5000)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any()); } @Test @WithMockUser(username = "admin", roles = "ADMIN") void scheduleIndividualDueDateTestsAfterDueDateNoBuildAndTestDate() throws Exception { mockStudentRepoLocks(); - final long delayMS = 500; final ZonedDateTime now = ZonedDateTime.now(); // no build and test date, but after_due_date tests ⇒ score update needed - setupProgrammingExerciseDates(now, delayMS / 2, null); + setupProgrammingExerciseDates(now, DELAY_MS, null); var testCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExercise.getId()); testCases.stream().findFirst().orElseThrow().setVisibility(Visibility.AFTER_DUE_DATE); - programmingExerciseTestCaseRepository.saveAll(testCases); + programmingExerciseTestCaseRepository.saveAllAndFlush(testCases); - var participationIndividualDueDate = setupParticipationIndividualDueDate(now, 2 * delayMS, TEST_PREFIX + "student3"); + var participationIndividualDueDate = setupParticipationIndividualDueDate(now, DELAY_MS * 2, TEST_PREFIX + "student3"); programmingExercise = programmingExerciseRepository.findWithAllParticipationsById(programmingExercise.getId()).orElseThrow(); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(5000)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any()); - verify(scheduleService, timeout(5000)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); verify(scheduleService, never()).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(Runnable.class)); } @@ -478,39 +433,38 @@ void scheduleIndividualDueDateTestsAfterDueDateNoBuildAndTestDate() throws Excep @WithMockUser(username = "admin", roles = "ADMIN") void cancelAllSchedulesOnRemovingExerciseDueDate() throws Exception { mockStudentRepoLocks(); - final long delayMS = 500; final ZonedDateTime now = ZonedDateTime.now(); - setupProgrammingExerciseDates(now, delayMS / 2, null); + setupProgrammingExerciseDates(now, DELAY_MS, null); var testCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExercise.getId()); testCases.stream().findFirst().orElseThrow().setVisibility(Visibility.AFTER_DUE_DATE); programmingExerciseTestCaseRepository.saveAll(testCases); - var participationIndividualDueDate = setupParticipationIndividualDueDate(now, 2 * delayMS, TEST_PREFIX + "student3"); + var participationIndividualDueDate = setupParticipationIndividualDueDate(now, DELAY_MS * 2, TEST_PREFIX + "student3"); programmingExercise = programmingExerciseRepository.findWithAllParticipationsById(programmingExercise.getId()).orElseThrow(); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(5000)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any()); - verify(scheduleService, timeout(5000)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); verify(scheduleService, never()).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(Runnable.class)); // remove due date and schedule again programmingExercise.setDueDate(null); - programmingExercise = programmingExerciseRepository.save(programmingExercise); + programmingExercise = programmingExerciseRepository.saveAndFlush(programmingExercise); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); // all schedules are cancelled InOrder cancelCalls = inOrder(scheduleService); - cancelCalls.verify(scheduleService, timeout(5000)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.DUE); - cancelCalls.verify(scheduleService, timeout(5000)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE); + cancelCalls.verify(scheduleService, timeout(TIMEOUT_MS)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.DUE); + cancelCalls.verify(scheduleService, timeout(TIMEOUT_MS)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE); for (final var participation : programmingExercise.getStudentParticipations()) { - cancelCalls.verify(scheduleService, timeout(5000)).cancelScheduledTaskForParticipationLifecycle(programmingExercise.getId(), participation.getId(), + cancelCalls.verify(scheduleService, timeout(TIMEOUT_MS)).cancelScheduledTaskForParticipationLifecycle(programmingExercise.getId(), participation.getId(), ParticipationLifecycle.DUE); - cancelCalls.verify(scheduleService, timeout(5000)).cancelScheduledTaskForParticipationLifecycle(programmingExercise.getId(), participation.getId(), + cancelCalls.verify(scheduleService, timeout(TIMEOUT_MS)).cancelScheduledTaskForParticipationLifecycle(programmingExercise.getId(), participation.getId(), ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE); } } @@ -519,91 +473,88 @@ void cancelAllSchedulesOnRemovingExerciseDueDate() throws Exception { @WithMockUser(username = "admin", roles = "ADMIN") void cancelIndividualSchedulesOnRemovingIndividualDueDate() throws Exception { mockStudentRepoLocks(); - final long delayMS = 200; final ZonedDateTime now = ZonedDateTime.now(); - setupProgrammingExerciseDates(now, delayMS, null); + setupProgrammingExerciseDates(now, DELAY_MS, null); - var participationIndividualDueDate = setupParticipationIndividualDueDate(now, 2 * delayMS, TEST_PREFIX + "student3"); + var participationIndividualDueDate = setupParticipationIndividualDueDate(now, 2 * DELAY_MS, TEST_PREFIX + "student3"); programmingExercise = programmingExerciseRepository.findWithAllParticipationsById(programmingExercise.getId()).orElseThrow(); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(5000)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); - verify(scheduleService, timeout(5000)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); // remove individual due date and schedule again participationIndividualDueDate.setIndividualDueDate(null); - participationIndividualDueDate = participationRepository.save(participationIndividualDueDate); + participationIndividualDueDate = participationRepository.saveAndFlush(participationIndividualDueDate); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); // called twice: first time when removing potential old schedules before scheduling, second time only cancelling - verify(scheduleService, timeout(5000).times(2)).cancelScheduledTaskForParticipationLifecycle(programmingExercise.getId(), participationIndividualDueDate.getId(), + verify(scheduleService, timeout(TIMEOUT_MS).times(2)).cancelScheduledTaskForParticipationLifecycle(programmingExercise.getId(), participationIndividualDueDate.getId(), ParticipationLifecycle.DUE); - verify(scheduleService, timeout(5000)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); } @Test @WithMockUser(username = "admin", roles = "ADMIN") void updateIndividualScheduleOnIndividualDueDateChange() throws Exception { mockStudentRepoLocks(); - final long delayMS = 500; final ZonedDateTime now = ZonedDateTime.now(); - setupProgrammingExerciseDates(now, delayMS / 2, null); + setupProgrammingExerciseDates(now, DELAY_MS, null); - var participationIndividualDueDate = setupParticipationIndividualDueDate(now, 2 * delayMS, TEST_PREFIX + "student3"); + var participationIndividualDueDate = setupParticipationIndividualDueDate(now, 2 * DELAY_MS, TEST_PREFIX + "student3"); programmingExercise = programmingExerciseRepository.findWithAllParticipationsById(programmingExercise.getId()).orElseThrow(); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(5000)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); - verify(scheduleService, timeout(5000)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); // change individual due date and schedule again - participationIndividualDueDate.setIndividualDueDate(plusMillis(now, 3 * delayMS)); - participationIndividualDueDate = participationRepository.save(participationIndividualDueDate); + participationIndividualDueDate.setIndividualDueDate(nowPlusMillis(DELAY_MS)); + participationIndividualDueDate = participationRepository.saveAndFlush(participationIndividualDueDate); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); // scheduling called twice, each scheduling cancels potential old schedules - verify(scheduleService, timeout(5000).times(2)).cancelScheduledTaskForParticipationLifecycle(programmingExercise.getId(), participationIndividualDueDate.getId(), + verify(scheduleService, timeout(TIMEOUT_MS).times(2)).cancelScheduledTaskForParticipationLifecycle(programmingExercise.getId(), participationIndividualDueDate.getId(), ParticipationLifecycle.DUE); - verify(scheduleService, timeout(5000).times(2)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); + verify(scheduleService, timeout(TIMEOUT_MS).times(2)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); } @Test @WithMockUser(username = "admin", roles = "ADMIN") void keepIndividualScheduleOnExerciseDueDateChange() throws Exception { mockStudentRepoLocks(); - final long delayMS = 500; final ZonedDateTime now = ZonedDateTime.now(); - setupProgrammingExerciseDates(now, delayMS / 2, null); + setupProgrammingExerciseDates(now, DELAY_MS, null); var testCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExercise.getId()); testCases.stream().findFirst().orElseThrow().setVisibility(Visibility.AFTER_DUE_DATE); - programmingExerciseTestCaseRepository.saveAll(testCases); + programmingExerciseTestCaseRepository.saveAllAndFlush(testCases); - var participationIndividualDueDate = setupParticipationIndividualDueDate(now, 2 * delayMS, TEST_PREFIX + "student3"); + var participationIndividualDueDate = setupParticipationIndividualDueDate(now, DELAY_MS * 2, TEST_PREFIX + "student3"); programmingExercise = programmingExerciseRepository.findWithAllParticipationsById(programmingExercise.getId()).orElseThrow(); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(5000)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any()); - verify(scheduleService, timeout(5000)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); verify(scheduleService, never()).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(Runnable.class)); // change exercise due date and schedule again - programmingExercise.setDueDate(plusMillis(now, delayMS)); - programmingExercise = programmingExerciseRepository.save(programmingExercise); + programmingExercise.setDueDate(nowPlusMillis(DELAY_MS)); + programmingExercise = programmingExerciseRepository.saveAndFlush(programmingExercise); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(5000).times(2)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); + verify(scheduleService, timeout(TIMEOUT_MS).times(2)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); verify(scheduleService, never()).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any()); - verify(scheduleService, timeout(5000).times(2)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS).times(2)).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); verify(scheduleService, never()).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE), any(Runnable.class)); } @@ -611,19 +562,18 @@ void keepIndividualScheduleOnExerciseDueDateChange() throws Exception { @WithMockUser(username = "admin", roles = "ADMIN") void shouldScheduleExerciseIfAnyIndividualDueDateInFuture() throws Exception { mockStudentRepoLocks(); - final long delayMS = 200; final ZonedDateTime now = ZonedDateTime.now(); - setupProgrammingExerciseDates(now, -1 * delayMS / 2, null); + setupProgrammingExerciseDates(now, -DELAY_MS, null); programmingExercise.setReleaseDate(ZonedDateTime.now().minusHours(1)); - programmingExercise = programmingExerciseRepository.save(programmingExercise); + programmingExercise = programmingExerciseRepository.saveAndFlush(programmingExercise); - var participationIndividualDueDate = setupParticipationIndividualDueDate(now, 2 * delayMS, TEST_PREFIX + "student3"); + var participationIndividualDueDate = setupParticipationIndividualDueDate(now, DELAY_MS, TEST_PREFIX + "student3"); programmingExercise = programmingExerciseRepository.findWithAllParticipationsById(programmingExercise.getId()).orElseThrow(); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, timeout(5000)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); + verify(scheduleService, timeout(TIMEOUT_MS)).scheduleParticipationTask(eq(participationIndividualDueDate), eq(ParticipationLifecycle.DUE), any()); verify(scheduleService, never()).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); } @@ -631,22 +581,21 @@ void shouldScheduleExerciseIfAnyIndividualDueDateInFuture() throws Exception { @WithMockUser(username = "admin", roles = "ADMIN") void shouldCancelAllTasksIfSchedulingNoLongerNeeded() throws Exception { mockStudentRepoLocks(); - final long delayMS = 200; final ZonedDateTime now = ZonedDateTime.now(); - setupProgrammingExerciseDates(now, -1 * delayMS / 2, null); + setupProgrammingExerciseDates(now, -DELAY_MS, null); programmingExercise.setReleaseDate(ZonedDateTime.now().minusHours(1)); programmingExercise.setAssessmentType(AssessmentType.AUTOMATIC); programmingExercise.setAllowComplaintsForAutomaticAssessments(false); - programmingExercise = programmingExerciseRepository.save(programmingExercise); + programmingExercise = programmingExerciseRepository.saveAndFlush(programmingExercise); instanceMessageReceiveService.processScheduleProgrammingExercise(programmingExercise.getId()); - verify(scheduleService, never()).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); - verify(scheduleService, timeout(5000)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.RELEASE); - verify(scheduleService, timeout(5000)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.DUE); - verify(scheduleService, timeout(5000)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE); - verify(scheduleService, timeout(5000)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.ASSESSMENT_DUE); + verify(scheduleService, after(SCHEDULER_TASK_TRIGGER_DELAY_MS).never()).scheduleTask(eq(programmingExercise), eq(ExerciseLifecycle.DUE), any(Runnable.class)); + verify(scheduleService, timeout(TIMEOUT_MS)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.RELEASE); + verify(scheduleService, timeout(TIMEOUT_MS)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.DUE); + verify(scheduleService, timeout(TIMEOUT_MS)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.BUILD_AND_TEST_AFTER_DUE_DATE); + verify(scheduleService, timeout(TIMEOUT_MS)).cancelScheduledTaskForLifecycle(programmingExercise.getId(), ExerciseLifecycle.ASSESSMENT_DUE); } @Test @@ -655,18 +604,18 @@ void testExamWorkingTimeChangeDuringConduction() { ProgrammingExercise examExercise = programmingExerciseUtilService.addCourseExamExerciseGroupWithOneProgrammingExercise(); Exam exam = examExercise.getExamViaExerciseGroupOrCourseMember(); exam.setStartDate(ZonedDateTime.now().minusMinutes(1)); - exam = examRepository.save(exam); + exam = examRepository.saveAndFlush(exam); User user = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); StudentExam studentExam = examUtilService.addStudentExamWithUser(exam, user); ProgrammingExerciseStudentParticipation participation = (ProgrammingExerciseStudentParticipation) participationUtilService .addProgrammingParticipationWithResultForExercise(examExercise, TEST_PREFIX + "student1").getParticipation(); studentExam.setExercises(List.of(examExercise)); studentExam.setWorkingTime(1); - studentExamRepository.save(studentExam); + studentExamRepository.saveAndFlush(studentExam); instanceMessageReceiveService.processStudentExamIndividualWorkingTimeChangeDuringConduction(studentExam.getId()); - verify(versionControlService, timeout(200)).setRepositoryPermissionsToReadOnly(participation.getVcsRepositoryUrl(), examExercise.getProjectKey(), + verify(versionControlService, timeout(TIMEOUT_MS)).setRepositoryPermissionsToReadOnly(participation.getVcsRepositoryUrl(), examExercise.getProjectKey(), participation.getStudents()); } @@ -692,7 +641,7 @@ private void setupProgrammingExerciseDates(final ZonedDateTime reference, Long d } programmingExercise.setAssessmentType(AssessmentType.SEMI_AUTOMATIC); - programmingExercise = programmingExerciseRepository.save(programmingExercise); + programmingExercise = programmingExerciseRepository.saveAndFlush(programmingExercise); } private ProgrammingExerciseStudentParticipation setupParticipationIndividualDueDate(final ZonedDateTime reference, Long individualDueDateDelayMillis, String login) { @@ -705,7 +654,7 @@ private ProgrammingExerciseStudentParticipation setupParticipationIndividualDueD participationIndividualDueDate.setIndividualDueDate(null); } - return participationRepository.save((ProgrammingExerciseStudentParticipation) participationIndividualDueDate); + return participationRepository.saveAndFlush((ProgrammingExerciseStudentParticipation) participationIndividualDueDate); } private StudentParticipation getParticipation(String login) { @@ -720,4 +669,8 @@ private List getParticipationsWithoutIndividualDueDate() { private ZonedDateTime plusMillis(final ZonedDateTime reference, long millis) { return reference.plus(millis, ChronoUnit.MILLIS); } + + private ZonedDateTime nowPlusMillis(long millis) { + return ZonedDateTime.now().plus(millis, ChronoUnit.MILLIS); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseServiceTest.java index 808c63ffc237..f24dc8ec5d00 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseServiceTest.java @@ -10,14 +10,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class ProgrammingExerciseServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ProgrammingExerciseServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "progexservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTemplateIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTemplateIntegrationTest.java index b0c874f9f2c9..e8d86320e8f4 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTemplateIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTemplateIntegrationTest.java @@ -3,7 +3,6 @@ import static de.tum.in.www1.artemis.web.rest.ProgrammingExerciseResourceEndpoints.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Fail.fail; -import static org.mockito.Mockito.reset; import java.io.*; import java.nio.file.Files; @@ -14,6 +13,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.commons.io.FileUtils; import org.apache.maven.plugin.surefire.log.api.PrintStreamLogger; import org.apache.maven.plugins.surefire.report.ReportTestCase; import org.apache.maven.plugins.surefire.report.ReportTestSuite; @@ -33,7 +33,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationJenkinsGitlabTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; @@ -43,7 +43,7 @@ import de.tum.in.www1.artemis.util.LocalRepository; @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ProgrammingExerciseTemplateIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ProgrammingExerciseTemplateIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { private final Logger log = LoggerFactory.getLogger(this.getClass()); @@ -109,8 +109,8 @@ void setup() throws Exception { programmingExerciseTestService.setupTestUsers(TEST_PREFIX, 1, 1, 0, 1); Course course = courseUtilService.addEmptyCourse(); exercise = ProgrammingExerciseFactory.generateProgrammingExercise(ZonedDateTime.now().minusDays(1), ZonedDateTime.now().plusDays(7), course); - bambooRequestMockProvider.enableMockingOfRequests(); - bitbucketRequestMockProvider.enableMockingOfRequests(true); + jenkinsRequestMockProvider.enableMockingOfRequests(jenkinsServer); + gitlabRequestMockProvider.enableMockingOfRequests(); exerciseRepo.configureRepos("exerciseLocalRepo", "exerciseOriginRepo"); testRepo.configureRepos("testLocalRepo", "testOriginRepo"); @@ -123,11 +123,9 @@ void setup() throws Exception { @AfterEach void tearDown() throws Exception { - reset(gitService); - reset(bambooServer); + jenkinsRequestMockProvider.enableMockingOfRequests(jenkinsServer); + gitlabRequestMockProvider.enableMockingOfRequests(); programmingExerciseTestService.tearDown(); - bitbucketRequestMockProvider.reset(); - bambooRequestMockProvider.reset(); exerciseRepo.resetLocalRepo(); testRepo.resetLocalRepo(); solutionRepo.resetLocalRepo(); @@ -152,6 +150,10 @@ private Stream languageTypeBuilder() { argumentBuilder.add(Arguments.of(language, null, false)); } for (ProjectType projectType : projectTypes) { + // TODO: MAVEN_BLACKBOX Templates should be tested in the future! + if (projectType == ProjectType.MAVEN_BLACKBOX) { + continue; + } argumentBuilder.add(Arguments.of(language, projectType, false)); } @@ -160,6 +162,9 @@ private Stream languageTypeBuilder() { argumentBuilder.add(Arguments.of(language, null, true)); } for (ProjectType projectType : projectTypes) { + if (projectType == ProjectType.MAVEN_BLACKBOX) { + continue; + } argumentBuilder.add(Arguments.of(language, projectType, true)); } } @@ -254,7 +259,7 @@ private void moveAssignmentSourcesOf(LocalRepository localRepository) throws IOE Path sourceSrc = localRepository.localRepoFile.toPath().resolve("src"); Path assignment = testRepo.localRepoFile.toPath().resolve("assignment"); Files.createDirectories(assignment); - Files.move(sourceSrc, assignment.resolve("src")); + FileUtils.moveDirectory(sourceSrc.toFile(), assignment.resolve("src").toFile()); } private Map readTestReports(String testResultPath) { diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java index 90b0c1e52785..5a416432e9c4 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java @@ -24,6 +24,7 @@ import javax.validation.constraints.NotNull; +import org.apache.commons.io.FileUtils; import org.awaitility.Awaitility; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.CanceledException; @@ -490,8 +491,8 @@ void importFromFile_validExercise_isSuccessfullyImported(ProgrammingLanguage lan void importFromFile_embeddedFiles_embeddedFilesCopied() throws Exception { String embeddedFileName1 = "Markdown_2023-05-06T16-17-46-410_ad323711.jpg"; String embeddedFileName2 = "Markdown_2023-05-06T16-17-46-822_b921f475.jpg"; - Path fileSystemPathEmbeddedFile1 = Path.of(FilePathService.getMarkdownFilePath(), embeddedFileName1); - Path fileSystemPathEmbeddedFile2 = Path.of(FilePathService.getMarkdownFilePath(), embeddedFileName2); + Path fileSystemPathEmbeddedFile1 = FilePathService.getMarkdownFilePath().resolve(embeddedFileName1); + Path fileSystemPathEmbeddedFile2 = FilePathService.getMarkdownFilePath().resolve(embeddedFileName2); // clean up to make sure the test doesn't pass because it has passed previously if (Files.exists(fileSystemPathEmbeddedFile1)) { Files.delete(fileSystemPathEmbeddedFile1); @@ -507,7 +508,7 @@ void importFromFile_embeddedFiles_embeddedFilesCopied() throws Exception { request.postWithMultipartFile(ROOT + "/courses/" + course.getId() + "/programming-exercises/import-from-file", exercise, "programmingExercise", file, ProgrammingExercise.class, HttpStatus.OK); - assertThat(Path.of(FilePathService.getMarkdownFilePath())).isDirectoryContaining(path -> embeddedFileName1.equals(path.getFileName().toString())) + assertThat(FilePathService.getMarkdownFilePath()).isDirectoryContaining(path -> embeddedFileName1.equals(path.getFileName().toString())) .isDirectoryContaining(path -> embeddedFileName2.equals(path.getFileName().toString())); } @@ -1391,8 +1392,8 @@ void exportProgrammingExerciseInstructorMaterial_shouldReturnFile(boolean saveEm String embeddedFileName1 = "Markdown_2023-05-06T16-17-46-410_ad323711.jpg"; String embeddedFileName2 = "Markdown_2023-05-06T16-17-46-822_b921f475.jpg"; // delete the files to not only make a test pass because a previous test run succeeded - Path embeddedFilePath1 = Path.of(FilePathService.getMarkdownFilePath(), embeddedFileName1); - Path embeddedFilePath2 = Path.of(FilePathService.getMarkdownFilePath(), embeddedFileName2); + Path embeddedFilePath1 = FilePathService.getMarkdownFilePath().resolve(embeddedFileName1); + Path embeddedFilePath2 = FilePathService.getMarkdownFilePath().resolve(embeddedFileName2); if (Files.exists(embeddedFilePath1)) { Files.delete(embeddedFilePath1); } @@ -1497,10 +1498,10 @@ private void generateProgrammingExerciseForExport(boolean saveEmbeddedFiles) thr """, embeddedFileName1, embeddedFileName2)); if (saveEmbeddedFiles) { - Files.write(Path.of(FilePathService.getMarkdownFilePath(), embeddedFileName1), - new ClassPathResource("test-data/repository-export/" + embeddedFileName1).getInputStream().readAllBytes()); - Files.write(Path.of(FilePathService.getMarkdownFilePath(), embeddedFileName2), - new ClassPathResource("test-data/repository-export/" + embeddedFileName2).getInputStream().readAllBytes()); + FileUtils.copyToFile(new ClassPathResource("test-data/repository-export/" + embeddedFileName1).getInputStream(), + FilePathService.getMarkdownFilePath().resolve(embeddedFileName1).toFile()); + FileUtils.copyToFile(new ClassPathResource("test-data/repository-export/" + embeddedFileName2).getInputStream(), + FilePathService.getMarkdownFilePath().resolve(embeddedFileName2).toFile()); } exercise = programmingExerciseRepository.save(exercise); exercise = programmingExerciseUtilService.addTemplateParticipationForProgrammingExercise(exercise); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionIntegrationTest.java index 243c7edcc9f6..1202044ed886 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionIntegrationTest.java @@ -16,7 +16,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; @@ -34,19 +33,13 @@ import de.tum.in.www1.artemis.domain.enumeration.SubmissionType; import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; -import de.tum.in.www1.artemis.domain.participation.Participation; -import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; -import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; -import de.tum.in.www1.artemis.domain.participation.StudentParticipation; +import de.tum.in.www1.artemis.domain.participation.*; import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; -import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; -import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; -import de.tum.in.www1.artemis.repository.ProgrammingSubmissionRepository; -import de.tum.in.www1.artemis.repository.StudentParticipationRepository; +import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.service.connectors.bamboo.dto.BambooBuildPlanDTO; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.FileUtils; @@ -231,7 +224,6 @@ void triggerBuildStudentForbidden() throws Exception { } @Test - @Timeout(5) @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void triggerBuildForExerciseAsInstructor() throws Exception { bambooRequestMockProvider.enableMockingOfRequests(); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryIntegrationTest.java index f2f2f9cc0494..23c82edaeec6 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryIntegrationTest.java @@ -136,6 +136,8 @@ class RepositoryIntegrationTest extends AbstractSpringIntegrationBambooBitbucket private final LocalRepository studentRepository = new LocalRepository(defaultBranch); + private LocalRepository templateRepository; + private final List logs = new ArrayList<>(); private final BuildLogEntry buildLogEntry = new BuildLogEntry(ZonedDateTime.now(), "Checkout to revision e65aa77cc0380aeb9567ccceb78aca416d86085b has failed."); @@ -184,7 +186,7 @@ void setup() throws Exception { programmingExercise.setTestRepositoryUrl(localRepoUrl.toString()); // Create template repo - LocalRepository templateRepository = new LocalRepository(defaultBranch); + templateRepository = new LocalRepository(defaultBranch); templateRepository.configureRepos("templateLocalRepo", "templateOriginRepo"); // add file to the template repo folder @@ -240,6 +242,7 @@ void setup() throws Exception { void tearDown() throws IOException { reset(gitService); studentRepository.resetLocalRepo(); + templateRepository.resetLocalRepo(); } @Test @@ -631,6 +634,7 @@ void testSaveFilesAfterDueDateAsInstructor() throws Exception { .getOrCheckoutRepository(instructorAssignmentParticipation.getVcsRepositoryUrl(), true, defaultBranch); request.put(studentRepoBaseUrl + instructorAssignmentParticipation.getId() + "/files?commit=true", List.of(), HttpStatus.OK); + instructorAssignmentRepository.resetLocalRepo(); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseFactory.java b/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseFactory.java index 3d542fa3e9d4..2b6fe03b6a68 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseFactory.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseFactory.java @@ -1,7 +1,9 @@ package de.tum.in.www1.artemis.exercise.quizexercise; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import java.io.IOException; import java.time.ZonedDateTime; import java.util.Set; import java.util.UUID; @@ -9,6 +11,9 @@ import javax.validation.constraints.NotNull; +import org.apache.commons.io.FileUtils; +import org.springframework.util.ResourceUtils; + import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.enumeration.QuizMode; import de.tum.in.www1.artemis.domain.enumeration.ScoringType; @@ -16,6 +21,7 @@ import de.tum.in.www1.artemis.domain.quiz.*; import de.tum.in.www1.artemis.exercise.ExerciseFactory; import de.tum.in.www1.artemis.participation.ParticipationFactory; +import de.tum.in.www1.artemis.service.FilePathService; /** * Factory for creating QuizExercises and related objects. @@ -354,6 +360,13 @@ public static DragAndDropQuestion createDragAndDropQuestionWithAllTypesOfMapping dragItem3.setTempID(generateTempId()); var dragItem4 = new DragItem().text("invalid drag item"); dragItem4.setTempID(generateTempId()); + try { + FileUtils.copyFile(ResourceUtils.getFile("classpath:test-data/attachment/placeholder.jpg"), + FilePathService.getDragItemFilePath().resolve("10").resolve("drag_item.jpg").toFile()); + } + catch (IOException ex) { + fail("Failed while copying test attachment files", ex); + } var dragItem5 = new DragItem().pictureFilePath("/api/files/drag-and-drop/drag-items/10/drag_item.jpg"); dragItem4.setInvalid(true); dnd.addDragItem(dragItem1); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseIntegrationTest.java index c1aae99010df..4dbd1b9914c5 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseIntegrationTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.byLessThan; -import java.security.Principal; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.*; @@ -17,12 +16,11 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.enumeration.*; @@ -41,9 +39,8 @@ import de.tum.in.www1.artemis.util.PageableSearchUtilService; import de.tum.in.www1.artemis.web.rest.dto.QuizBatchJoinDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; -import de.tum.in.www1.artemis.web.websocket.QuizSubmissionWebsocketService; -class QuizExerciseIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class QuizExerciseIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "quizexerciseintegration"; @@ -52,9 +49,6 @@ class QuizExerciseIntegrationTest extends AbstractSpringIntegrationBambooBitbuck @Autowired private QuizExerciseService quizExerciseService; - @Autowired - private QuizSubmissionWebsocketService quizSubmissionWebsocketService; - @Autowired private StudentParticipationRepository studentParticipationRepository; @@ -70,9 +64,6 @@ class QuizExerciseIntegrationTest extends AbstractSpringIntegrationBambooBitbuck @Autowired private SubmittedAnswerRepository submittedAnswerRepository; - @Autowired - private QuizExerciseUtilService quizUtilService; - @Autowired private TeamRepository teamRepository; @@ -332,19 +323,10 @@ void testDeleteQuizExerciseWithSubmittedAnswers(QuizMode quizMode) throws Except QuizExercise quizExercise = quizExerciseUtilService.createAndSaveQuiz(ZonedDateTime.now(), ZonedDateTime.now().plusMinutes(1), quizMode); assertThat(quizExerciseRepository.findOneWithQuestionsAndStatistics(quizExercise.getId())).as("Exercise is created correctly").isNotNull(); - String username = TEST_PREFIX + "student1"; - final Principal principal = () -> username; QuizSubmission quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, 1, true, null); - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - quizUtilService.prepareBatchForSubmitting(quizExercise, authentication, SecurityUtils.makeAuthorizationObject(username)); - quizSubmissionWebsocketService.saveSubmission(quizExercise.getId(), quizSubmission, principal); - SecurityContextHolder.getContext().setAuthentication(authentication); - - // Quiz submissions are not yet in database - assertThat(quizSubmissionRepository.findByParticipation_Exercise_Id(quizExercise.getId())).isEmpty(); - - quizScheduleService.processCachedQuizSubmissions(); + quizSubmission.submitted(true); + participationUtilService.addSubmission(quizExercise, quizSubmission, TEST_PREFIX + "student1"); + participationUtilService.addResultToSubmission(quizSubmission, AssessmentType.AUTOMATIC, null, quizExercise.getScoreForSubmission(quizSubmission), true); // Quiz submissions are now in database assertThat(quizSubmissionRepository.findByParticipation_Exercise_Id(quizExercise.getId())).hasSize(1); @@ -841,6 +823,24 @@ void testReEvaluateQuizQuestionWithMoreSolutions() throws Exception { assertThat(receivedShortAnswerQuestion.getCorrectMappings()).hasSize(3); } + @Test + @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") + void testAddAndStartQuizBatch() throws Exception { + QuizExercise quizExercise = quizExerciseUtilService.createAndSaveQuiz(ZonedDateTime.now().plusHours(5), null, QuizMode.BATCHED); + + QuizBatch batch = request.putWithResponseBody("/api/quiz-exercises/" + quizExercise.getId() + "/add-batch", null, QuizBatch.class, HttpStatus.OK); + request.put("/api/quiz-exercises/" + batch.getId() + "/start-batch", null, HttpStatus.OK); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testAddAndStartQuizBatch_AsStudentNotAllowed() throws Exception { + QuizExercise quizExercise = quizExerciseUtilService.createAndSaveQuiz(ZonedDateTime.now().plusHours(5), null, QuizMode.BATCHED); + + request.putWithResponseBody("/api/quiz-exercises/" + quizExercise.getId() + "/add-batch", null, QuizBatch.class, HttpStatus.FORBIDDEN); + request.put("/api/quiz-exercises/" + null + "/start-batch", null, HttpStatus.BAD_REQUEST); + } + @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testPerformStartNow() throws Exception { @@ -912,12 +912,21 @@ void testPerformJoin(QuizMode quizMode, ZonedDateTime release, ZonedDateTime due SecurityContextHolder.getContext().setAuthentication(SecurityUtils.makeAuthorizationObject(TEST_PREFIX + "student1")); request.postWithResponseBody("/api/quiz-exercises/" + quizExercise.getId() + "/join", new QuizBatchJoinDTO(password), QuizBatch.class, result); - if (result == HttpStatus.OK) { - // if joining was successful repeating the request should fail, otherwise with the same reason as the first attempt - result = HttpStatus.BAD_REQUEST; - } + } - request.postWithResponseBody("/api/quiz-exercises/" + quizExercise.getId() + "/join", new QuizBatchJoinDTO(password), QuizBatch.class, result); + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + @EnumSource(QuizMode.class) + void testCannotPerformJoinTwice(QuizMode quizMode) throws Exception { + QuizExercise quizExercise = quizExerciseUtilService.createAndSaveQuiz(ZonedDateTime.now().minusMinutes(2), ZonedDateTime.now().plusMinutes(2), quizMode); + QuizBatch batch = new QuizBatch(); + batch.setStartTime(ZonedDateTime.now().minusMinutes(1)); + batch.setPassword("1234"); + + quizExerciseUtilService.setQuizBatchExerciseAndSave(batch, quizExercise); + quizScheduleService.joinQuizBatch(quizExercise, batch, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + + request.postWithResponseBody("/api/quiz-exercises/" + quizExercise.getId() + "/join", new QuizBatchJoinDTO("1234"), QuizBatch.class, HttpStatus.BAD_REQUEST); } /** diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseUtilService.java b/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseUtilService.java index 76429a21d3a5..909215386516 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseUtilService.java @@ -4,27 +4,23 @@ import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; import java.time.ZonedDateTime; import java.util.HashSet; import java.util.Set; import javax.validation.constraints.NotNull; +import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.ClassPathResource; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import org.springframework.util.ResourceUtils; import de.tum.in.www1.artemis.course.CourseFactory; import de.tum.in.www1.artemis.course.CourseUtilService; -import de.tum.in.www1.artemis.domain.Course; -import de.tum.in.www1.artemis.domain.Team; -import de.tum.in.www1.artemis.domain.TeamAssignmentConfig; -import de.tum.in.www1.artemis.domain.User; -import de.tum.in.www1.artemis.domain.enumeration.*; +import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; +import de.tum.in.www1.artemis.domain.enumeration.QuizMode; +import de.tum.in.www1.artemis.domain.enumeration.SubmissionType; import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; @@ -35,8 +31,6 @@ import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.scheduled.cache.quiz.QuizScheduleService; import de.tum.in.www1.artemis.user.UserUtilService; -import de.tum.in.www1.artemis.util.RequestUtilService; -import de.tum.in.www1.artemis.web.rest.dto.QuizBatchJoinDTO; /** * Service responsible for initializing the database with specific testdata related to quiz exercises for use in integration tests. @@ -77,9 +71,6 @@ public class QuizExerciseUtilService { @Autowired private CourseUtilService courseUtilService; - @Autowired - private RequestUtilService requestUtilService; - @Autowired private SubmittedAnswerRepository submittedAnswerRepository; @@ -98,29 +89,6 @@ public class QuizExerciseUtilService { @Autowired private QuizScheduleService quizScheduleService; - /** - * Create, join and start a batch for student by tutor - */ - public void prepareBatchForSubmitting(QuizExercise quizExercise, Authentication tutor, Authentication student) throws Exception { - var authentication = SecurityContextHolder.getContext().getAuthentication(); - switch (quizExercise.getQuizMode()) { - case SYNCHRONIZED -> { - } - case BATCHED -> { - SecurityContextHolder.getContext().setAuthentication(tutor); - var batch = requestUtilService.putWithResponseBody("/api/quiz-exercises/" + quizExercise.getId() + "/add-batch", null, QuizBatch.class, HttpStatus.OK); - requestUtilService.put("/api/quiz-exercises/" + batch.getId() + "/start-batch", null, HttpStatus.OK); - SecurityContextHolder.getContext().setAuthentication(student); - requestUtilService.postWithoutLocation("/api/quiz-exercises/" + quizExercise.getId() + "/join", new QuizBatchJoinDTO(batch.getPassword()), HttpStatus.OK, null); - } - case INDIVIDUAL -> { - SecurityContextHolder.getContext().setAuthentication(student); - requestUtilService.postWithoutLocation("/api/quiz-exercises/" + quizExercise.getId() + "/join", new QuizBatchJoinDTO(null), HttpStatus.OK, null); - } - } - SecurityContextHolder.getContext().setAuthentication(authentication); - } - public Course addCourseWithOneQuizExercise() { return addCourseWithOneQuizExercise("Title"); } @@ -322,16 +290,16 @@ public QuizSubmission addQuizExerciseToCourseWithParticipationAndSubmissionForUs var submittedDragAndDropAnswer = new DragAndDropSubmittedAnswer(); DragAndDropQuestion dragAndDropQuestion = (DragAndDropQuestion) (quizExercise.getQuizQuestions().get(1)); - var backgroundPathInFileSystem = Path.of(FilePathService.getDragAndDropBackgroundFilePath(), "drag_and_drop_background.jpg"); - var dragItemPathInFileSystem = Path.of(FilePathService.getDragItemFilePath(), "drag_item.jpg"); + var backgroundPathInFileSystem = FilePathService.getDragAndDropBackgroundFilePath().resolve("drag_and_drop_background.jpg"); + var dragItemPathInFileSystem = FilePathService.getDragItemFilePath().resolve("drag_item.jpg"); if (Files.exists(backgroundPathInFileSystem)) { Files.delete(backgroundPathInFileSystem); } if (Files.exists(dragItemPathInFileSystem)) { Files.delete(dragItemPathInFileSystem); } - Files.copy(new ClassPathResource("test-data/data-export/drag_and_drop_background.jpg").getInputStream(), backgroundPathInFileSystem); - Files.copy(new ClassPathResource("test-data/data-export/drag_item.jpg").getInputStream(), dragItemPathInFileSystem); + FileUtils.copyFile(ResourceUtils.getFile("classpath:test-data/data-export/drag_and_drop_background.jpg"), backgroundPathInFileSystem.toFile()); + FileUtils.copyFile(ResourceUtils.getFile("classpath:test-data/data-export/drag_item.jpg"), dragItemPathInFileSystem.toFile()); dragAndDropQuestion.setBackgroundFilePath("/api/files/drag-and-drop/backgrounds/3/drag_and_drop_background.jpg"); submittedDragAndDropAnswer.setQuizQuestion(dragAndDropQuestion); dragAndDropQuestion.setExercise(quizExercise); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizSubmissionIntegrationTest.java index 12eba7fcb04e..c5290695b2b2 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizSubmissionIntegrationTest.java @@ -12,9 +12,8 @@ import java.util.Arrays; import java.util.List; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.parallel.Isolated; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; @@ -24,23 +23,26 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Result; +import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.QuizMode; import de.tum.in.www1.artemis.domain.enumeration.ScoringType; import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; import de.tum.in.www1.artemis.domain.quiz.*; import de.tum.in.www1.artemis.exam.ExamUtilService; +import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.service.QuizBatchService; import de.tum.in.www1.artemis.service.QuizExerciseService; +import de.tum.in.www1.artemis.service.QuizStatisticService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.websocket.QuizSubmissionWebsocketService; -class QuizSubmissionIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class QuizSubmissionIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "quizsubmissiontest"; @@ -62,9 +64,6 @@ class QuizSubmissionIntegrationTest extends AbstractSpringIntegrationBambooBitbu @Autowired private QuizExerciseRepository quizExerciseRepository; - @Autowired - private QuizSubmissionWebsocketService quizSubmissionWebsocketService; - @Autowired private QuizSubmissionRepository quizSubmissionRepository; @@ -92,6 +91,15 @@ class QuizSubmissionIntegrationTest extends AbstractSpringIntegrationBambooBitbu @Autowired private ExamUtilService examUtilService; + @Autowired + QuizStatisticService quizStatisticService; + + @Autowired + ParticipationUtilService participationUtilService; + + @Autowired + QuizSubmissionWebsocketService quizSubmissionWebsocketService; + @BeforeEach void init() { // do not use the schedule service based on a time interval in the tests, because this would result in flaky tests that run much slower @@ -106,51 +114,69 @@ protected void resetSpyBeans() { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testQuizSubmit() { + void testQuizSubmitWebsocket() { QuizExercise quizExercise = setupQuizExerciseParameters(); quizExercise = quizExerciseService.save(quizExercise); - QuizSubmission quizSubmission; + QuizSubmission quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, 1, false, null); - for (int i = 1; i <= NUMBER_OF_STUDENTS; i++) { - quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, i, false, null); - final var username = TEST_PREFIX + "student" + i; - final Principal principal = () -> username; - // save - quizSubmissionWebsocketService.saveSubmission(quizExercise.getId(), quizSubmission, principal); - } + String username = TEST_PREFIX + "student1"; + Principal principal = () -> username; + + quizSubmissionWebsocketService.saveSubmission(quizExercise.getId(), quizSubmission, principal); + verify(websocketMessagingService, never()).sendMessageToUser(eq(username), any(), any()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testQuizSubmitUnactiveQuizWebsocket() { + QuizExercise quizExercise = quizExerciseUtilService.createQuiz(ZonedDateTime.now().plusDays(1), null, QuizMode.SYNCHRONIZED); + quizExercise.duration(240); + quizExerciseRepository.save(quizExercise); + + QuizSubmission quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, 1, false, null); + + String username = TEST_PREFIX + "student1"; + Principal principal = () -> username; + + quizSubmissionWebsocketService.saveSubmission(quizExercise.getId(), quizSubmission, principal); + verify(websocketMessagingService).sendMessageToUser(eq(username), any(), any()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testQuizSubmit_CalculateScore() { + QuizExercise quizExercise = setupQuizExerciseParameters(); + quizExercise = quizExerciseService.save(quizExercise); + + QuizSubmission quizSubmission; - // only half of the students submit manually + // only half of the students submit for (int i = 1; i <= NUMBER_OF_STUDENTS / 2; i++) { - quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, i, true, null); - final var username = TEST_PREFIX + "student" + i; - final Principal principal = () -> username; - // submit - quizSubmissionWebsocketService.saveSubmission(quizExercise.getId(), quizSubmission, principal); + quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, i, false, null); + quizSubmission.setSubmitted(true); + participationUtilService.addSubmission(quizExercise, quizSubmission, TEST_PREFIX + "student" + i); + participationUtilService.addResultToSubmission(quizSubmission, AssessmentType.AUTOMATIC, null, quizExercise.getScoreForSubmission(quizSubmission), true); } - // before the quiz submissions are processed, none of them ends up in the database - assertThat(submissionRepository.countByExerciseIdSubmitted(quizExercise.getId())).isZero(); - - // process first half of the submissions - quizScheduleService.processCachedQuizSubmissions(); + // check first half of the submissions assertThat(submissionRepository.countByExerciseIdSubmitted(quizExercise.getId())).isEqualTo(NUMBER_OF_STUDENTS / 2); - // End the quiz right now so that results can be processed - quizExercise = quizExerciseRepository.findOneWithQuestionsAndStatistics(quizExercise.getId()); - final var exercise = quizExercise; - assertThat(quizExercise).isNotNull(); - quizExercise.setDueDate(ZonedDateTime.now()); - quizExercise.getQuizBatches().forEach(batch -> batch.setStartTime(quizBatchService.quizBatchStartDate(exercise, batch.getStartTime()))); - exerciseRepository.saveAndFlush(quizExercise); - - quizScheduleService.processCachedQuizSubmissions(); + for (int i = NUMBER_OF_STUDENTS / 2 + 1; i <= NUMBER_OF_STUDENTS; i++) { + quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, i, false, null); + quizSubmission.setSubmitted(true); + participationUtilService.addSubmission(quizExercise, quizSubmission, TEST_PREFIX + "student" + i); + participationUtilService.addResultToSubmission(quizSubmission, AssessmentType.AUTOMATIC, null, quizExercise.getScoreForSubmission(quizSubmission), true); + } - // after the quiz submissions have been processed, all submission are saved to the database + // all submission are saved to the database assertThat(submissionRepository.countByExerciseIdSubmitted(quizExercise.getId())).isEqualTo(NUMBER_OF_STUDENTS); - // Test the statistics directly from the database - QuizExercise quizExerciseWithStatistic = quizExerciseRepository.findOneWithQuestionsAndStatistics(quizExercise.getId()); + // update the statistics + QuizExercise quizExerciseWithStatistic = quizExerciseRepository.findByIdWithQuestionsAndStatisticsElseThrow(quizExercise.getId()); + quizStatisticService.recalculateStatistics(quizExerciseWithStatistic); + + // Test the statistics assertThat(quizExerciseWithStatistic).isNotNull(); assertThat(quizExerciseWithStatistic.getQuizPointStatistic().getParticipantsUnrated()).isZero(); assertThat(quizExerciseWithStatistic.getQuizPointStatistic().getParticipantsRated()).isEqualTo(NUMBER_OF_STUDENTS); @@ -193,11 +219,6 @@ else if (question instanceof DragAndDropQuestion) { assertThat(question.getQuizQuestionStatistic().getParticipantsRated()).isEqualTo(NUMBER_OF_STUDENTS); assertThat(question.getQuizQuestionStatistic().getParticipantsUnrated()).isZero(); } - - // execute the scheduler again, this should remove the quiz exercise from the cache - quizScheduleService.processCachedQuizSubmissions(); - // but of course keep all submissions - assertThat(submissionRepository.countByExerciseIdSubmitted(quizExercise.getId())).isEqualTo(NUMBER_OF_STUDENTS); } @Test @@ -259,14 +280,14 @@ void testQuizSubmit_partial_points() { submissions.add(student3Submission); for (int i = 0; i < 3; i++) { - var username = TEST_PREFIX + "student" + (i + 1); - final Principal principal = () -> username; - quizSubmissionWebsocketService.saveSubmission(quizExercise.getId(), submissions.get(i), principal); + participationUtilService.addSubmission(quizExercise, submissions.get(i), TEST_PREFIX + "student" + (i + 1)); + participationUtilService.addResultToSubmission(submissions.get(i), AssessmentType.AUTOMATIC, null, quizExercise.getScoreForSubmission(submissions.get(i)), true); } - quizScheduleService.processCachedQuizSubmissions(); + // update the statistics + QuizExercise quizExerciseWithStatistic = quizExerciseRepository.findByIdWithQuestionsAndStatisticsElseThrow(quizExercise.getId()); + quizStatisticService.recalculateStatistics(quizExerciseWithStatistic); - QuizExercise quizExerciseWithStatistic = quizExerciseRepository.findOneWithQuestionsAndStatistics(quizExercise.getId()); var quizPointStatistic = quizExerciseWithStatistic.getQuizPointStatistic(); assertThat(quizExerciseWithStatistic).isNotNull(); @@ -284,51 +305,7 @@ else if (pointCounter.getPoints() == 6.0) { else { assertThat(pointCounter.getRatedCounter()).as("All other buckets contain 0 rated submissions").isZero(); } - - } - } - - @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") - @WithMockUser(username = TEST_PREFIX + "student3", roles = "USER") - @EnumSource(QuizMode.class) - void testQuizSubmitLiveMode(QuizMode quizMode) throws Exception { - QuizExercise quizExercise = quizExerciseUtilService.createQuiz(ZonedDateTime.now().minusSeconds(10), null, quizMode); - quizExercise.setDuration(600); - quizExercise = quizExerciseService.save(quizExercise); - - // at the beginning there are no submissions and no participants - assertThat(quizSubmissionRepository.findByParticipation_Exercise_Id(quizExercise.getId())).isEmpty(); - assertThat(participationRepository.findByExerciseId(quizExercise.getId())).isEmpty(); - - if (quizMode != QuizMode.SYNCHRONIZED) { - var batch = quizBatchService.save(QuizExerciseFactory.generateQuizBatch(quizExercise, ZonedDateTime.now().minusSeconds(10))); - for (int i = 1; i <= NUMBER_OF_STUDENTS; i++) { - quizExerciseUtilService.joinQuizBatch(quizExercise, batch, TEST_PREFIX + "student" + i); - } - } - - for (int i = 1; i <= NUMBER_OF_STUDENTS; i++) { - userUtilService.changeUser(TEST_PREFIX + "student" + i); - QuizSubmission quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, i, false, null); - assertThat(quizSubmission.getSubmittedAnswers()).hasSize(3); - assertThat(quizSubmission.isSubmitted()).isFalse(); - assertThat(quizSubmission.getSubmissionDate()).isNull(); - QuizSubmission updatedSubmission = request.postWithResponseBody("/api/exercises/" + quizExercise.getId() + "/submissions/live", quizSubmission, QuizSubmission.class, - HttpStatus.OK); - // check whether submission flag was updated - assertThat(updatedSubmission.isSubmitted()).isTrue(); - // check whether all answers were submitted properly - assertThat(updatedSubmission.getSubmittedAnswers()).hasSameSizeAs(quizSubmission.getSubmittedAnswers()); - // check whether submission date was set - assertThat(updatedSubmission.getSubmissionDate()).isNotNull(); } - - // process cached submissions - quizScheduleService.processCachedQuizSubmissions(); - - // check whether all submissions were saved to the database - assertThat(quizSubmissionRepository.findByParticipation_Exercise_Id(quizExercise.getId())).hasSize(NUMBER_OF_STUDENTS); - assertThat(participationRepository.findByExerciseId(quizExercise.getId())).hasSize(NUMBER_OF_STUDENTS); } @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @@ -353,26 +330,6 @@ void testQuizSubmitLiveMode_badRequest_notActive(QuizMode quizMode) throws Excep } } - @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") - @WithMockUser(username = TEST_PREFIX + "student3", roles = "USER") - @EnumSource(QuizMode.class) - void testQuizSubmitLiveMode_badRequest_alreadySubmitted(QuizMode quizMode) throws Exception { - QuizExercise quizExercise = quizExerciseUtilService.createQuiz(ZonedDateTime.now().minusSeconds(5), ZonedDateTime.now().plusSeconds(10), quizMode); - quizExercise.setDuration(10); - quizExercise = quizExerciseService.save(quizExercise); - - if (quizMode != QuizMode.SYNCHRONIZED) { - var batch = quizBatchService.save(QuizExerciseFactory.generateQuizBatch(quizExercise, ZonedDateTime.now().minusSeconds(5))); - quizExerciseUtilService.joinQuizBatch(quizExercise, batch, TEST_PREFIX + "student3"); - } - - QuizSubmission quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, 1, false, ZonedDateTime.now()); - // submit quiz for the first time, expected status = OK - request.postWithResponseBody("/api/exercises/" + quizExercise.getId() + "/submissions/live", quizSubmission, Result.class, HttpStatus.OK); - // submit quiz for the second time, expected status = BAD_REQUEST - request.postWithResponseBody("/api/exercises/" + quizExercise.getId() + "/submissions/live", quizSubmission, Result.class, HttpStatus.BAD_REQUEST); - } - @Test @WithMockUser(username = TEST_PREFIX + "student3", roles = "USER") void testQuizSubmitEmptyQuizInLiveMode() throws Exception { @@ -414,15 +371,15 @@ void testQuizSubmitPractice(QuizMode quizMode) throws Exception { assertThat(((QuizSubmission) receivedResult.getSubmission()).getSubmittedAnswers()).hasSameSizeAs(quizSubmission.getSubmittedAnswers()); } - // after the quiz has ended, all submission are saved to the database + // all submission are saved to the database assertThat(quizSubmissionRepository.findByParticipation_Exercise_Id(quizExercise.getId())).hasSize(NUMBER_OF_STUDENTS); assertThat(participationRepository.findByExerciseId(quizExercise.getId())).hasSize(NUMBER_OF_STUDENTS); - // processing the quiz submissions will update the statistics - quizScheduleService.processCachedQuizSubmissions(); + // update the statistics + QuizExercise quizExerciseWithStatistic = quizExerciseRepository.findByIdWithQuestionsAndStatisticsElseThrow(quizExercise.getId()); + quizStatisticService.recalculateStatistics(quizExerciseWithStatistic); - // Test the statistics directly from the database - QuizExercise quizExerciseWithStatistic = quizExerciseRepository.findOneWithQuestionsAndStatistics(quizExercise.getId()); + // Test the statistics assertThat(quizExerciseWithStatistic).isNotNull(); assertThat(quizExerciseWithStatistic.getQuizPointStatistic().getParticipantsRated()).isZero(); assertThat(quizExerciseWithStatistic.getQuizPointStatistic().getParticipantsUnrated()).isEqualTo(NUMBER_OF_STUDENTS); @@ -592,11 +549,12 @@ void testQuizSubmitPreview(QuizMode quizMode) throws Exception { // in the preview the submission will not be saved to the database assertThat(quizSubmissionRepository.findByParticipation_Exercise_Id(quizExercise.getId())).isEmpty(); - quizScheduleService.processCachedQuizSubmissions(); + // update the statistics + QuizExercise quizExerciseWithStatistic = quizExerciseRepository.findByIdWithQuestionsAndStatisticsElseThrow(quizExercise.getId()); + quizStatisticService.recalculateStatistics(quizExerciseWithStatistic); // all stats must be 0 because we have a preview here - // Test the statistics directly from the database - QuizExercise quizExerciseWithStatistic = quizExerciseRepository.findOneWithQuestionsAndStatistics(quizExercise.getId()); + // Test the statistics assertThat(quizExerciseWithStatistic).isNotNull(); assertThat(quizExerciseWithStatistic.getQuizPointStatistic().getParticipantsRated()).isZero(); assertThat(quizExerciseWithStatistic.getQuizPointStatistic().getParticipantsUnrated()).isZero(); @@ -618,8 +576,9 @@ void testQuizSubmitPreview(QuizMode quizMode) throws Exception { } @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testQuizSubmitScheduledAndDeleted() { + + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testQuizSubmitScheduledAndDeleted() throws Exception { Course course = courseUtilService.createCourse(); String publishQuizPath = "/topic/courses/" + course.getId() + "/quizExercises"; log.debug("// Creating the quiz exercise 2s in the future"); @@ -635,9 +594,7 @@ void testQuizSubmitScheduledAndDeleted() { // check that submission fails QuizSubmission quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, 1, true, null); - quizSubmissionWebsocketService.saveSubmission(quizExercise.getId(), quizSubmission, () -> TEST_PREFIX + "student3"); - - quizScheduleService.processCachedQuizSubmissions(); + request.postWithResponseBody("/api/exercises/" + quizExercise.getId() + "/submissions/live", quizSubmission, Result.class, HttpStatus.BAD_REQUEST); assertThat(submissionRepository.countByExerciseIdSubmitted(quizExercise.getId())).isZero(); // reschedule @@ -650,42 +607,27 @@ void testQuizSubmitScheduledAndDeleted() { assertThat(quizExercise.isQuizStarted()).isTrue(); assertThat(quizExercise.getQuizBatches()).allMatch(QuizBatch::isStarted); - // process cached submissions - quizScheduleService.processCachedQuizSubmissions(); - // save submissions for (int i = 1; i <= 2; i++) { quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, i, false, null); - final var username = TEST_PREFIX + "student" + i; - final Principal principal = () -> username; - // save - quizSubmissionWebsocketService.saveSubmission(quizExercise.getId(), quizSubmission, principal); + participationUtilService.addSubmission(quizExercise, quizSubmission, TEST_PREFIX + "student" + i); + participationUtilService.addResultToSubmission(quizSubmission, AssessmentType.AUTOMATIC, null, quizExercise.getScoreForSubmission(quizSubmission), true); } - // process the saved but not submitted quiz submissions - quizScheduleService.processCachedQuizSubmissions(); - - // before the quiz submissions are submitted, none of them ends up in the database - assertThat(submissionRepository.countByExerciseIdSubmitted(quizExercise.getId())).isZero(); - // set the quiz end to now and ... log.debug("// End the quiz and delete it"); quizExercise = quizExerciseRepository.findOneWithQuestionsAndStatistics(quizExercise.getId()); assertThat(quizExercise).isNotNull(); quizExercise.setDuration((int) Duration.between(quizExercise.getReleaseDate(), ZonedDateTime.now()).getSeconds() - Constants.QUIZ_GRACE_PERIOD_IN_SECONDS); quizExercise = exerciseRepository.saveAndFlush(quizExercise); - quizScheduleService.updateQuizExercise(quizExercise); - // ... directly delete the quiz - exerciseRepository.delete(quizExercise); + + // ...delete the quiz + request.delete("/api/quiz-exercises/" + quizExercise.getId(), HttpStatus.OK); QuizExercise finalQuizExercise = quizExercise; await().until(() -> exerciseRepository.findById(finalQuizExercise.getId()).isEmpty()); - // the deleted quiz should get removed, no submissions should be saved - quizScheduleService.processCachedQuizSubmissions(); - // quiz is not cached anymore - assertThat(quizScheduleService.getQuizExercise(quizExercise.getId())).isNull(); - // no submissions were marked as submitted and saved + // no submissions left assertThat(submissionRepository.countByExerciseIdSubmitted(quizExercise.getId())).isZero(); } @@ -702,18 +644,10 @@ void testQuizScoringTypes() { quizSubmission.addSubmittedAnswers(QuizExerciseFactory.generateSubmittedAnswerForQuizWithCorrectAndFalseAnswers(question)); } quizSubmission.submitted(true); - quizSubmissionWebsocketService.saveSubmission(quizExercise.getId(), quizSubmission, () -> TEST_PREFIX + "student4"); - - quizScheduleService.processCachedQuizSubmissions(); - - // End the quiz right now so that results can be processed - quizExercise = quizExerciseRepository.findOneWithQuestionsAndStatistics(quizExercise.getId()); - assertThat(quizExercise).isNotNull(); - quizExercise.setDuration((int) Duration.between(quizExercise.getReleaseDate(), ZonedDateTime.now()).getSeconds() - Constants.QUIZ_GRACE_PERIOD_IN_SECONDS); - exerciseRepository.saveAndFlush(quizExercise); - - quizScheduleService.processCachedQuizSubmissions(); + participationUtilService.addSubmission(quizExercise, quizSubmission, TEST_PREFIX + "student4"); + participationUtilService.addResultToSubmission(quizSubmission, AssessmentType.AUTOMATIC, null, quizExercise.getScoreForSubmission(quizSubmission), true); + quizExerciseService.reEvaluate(quizExercise, quizExercise); assertThat(quizSubmissionRepository.findByQuizExerciseId(quizExercise.getId())).isPresent(); List results = resultRepository.findByParticipationExerciseIdOrderByCompletionDateAsc(quizExercise.getId()); @@ -752,18 +686,10 @@ void testQuizScoringType(ScoringType scoringType) { quizSubmission.addSubmittedAnswers(QuizExerciseFactory.generateSubmittedAnswerForQuizWithCorrectAndFalseAnswers(question)); } quizSubmission.submitted(true); - quizSubmissionWebsocketService.saveSubmission(quizExercise.getId(), quizSubmission, () -> TEST_PREFIX + "student3"); - - quizScheduleService.processCachedQuizSubmissions(); - - // End the quiz right now so that results can be processed - quizExercise = quizExerciseRepository.findOneWithQuestionsAndStatistics(quizExercise.getId()); - assertThat(quizExercise).isNotNull(); - quizExercise.setDuration((int) Duration.between(quizExercise.getReleaseDate(), ZonedDateTime.now()).getSeconds() - Constants.QUIZ_GRACE_PERIOD_IN_SECONDS); - exerciseRepository.saveAndFlush(quizExercise); - - quizScheduleService.processCachedQuizSubmissions(); + participationUtilService.addSubmission(quizExercise, quizSubmission, TEST_PREFIX + "student3"); + participationUtilService.addResultToSubmission(quizSubmission, AssessmentType.AUTOMATIC, null, quizExercise.getScoreForSubmission(quizSubmission), true); + quizExerciseService.reEvaluate(quizExercise, quizExercise); assertThat(submissionRepository.countByExerciseIdSubmitted(quizExercise.getId())).isEqualTo(1); List results = resultRepository.findByParticipationExerciseIdOrderByCompletionDateAsc(quizExercise.getId()); @@ -848,4 +774,57 @@ private QuizExercise setupQuizExerciseParameters() { quizExercise.duration(240); return quizExercise; } + + @Nested + @Isolated + class QuizSubmitLiveModeIsolatedTest { + + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + @EnumSource(QuizMode.class) + void testQuizSubmitLiveMode(QuizMode quizMode) throws Exception { + QuizExercise quizExercise = quizExerciseUtilService.createQuiz(ZonedDateTime.now().minusMinutes(2), null, quizMode); + quizExercise.setDuration(600); + quizExercise = quizExerciseService.save(quizExercise); + + // at the beginning there are no submissions and no participants + assertThat(quizSubmissionRepository.findByParticipation_Exercise_Id(quizExercise.getId())).isEmpty(); + assertThat(participationRepository.findByExerciseId(quizExercise.getId())).isEmpty(); + + if (quizMode != QuizMode.SYNCHRONIZED) { + var batch = quizBatchService.save(QuizExerciseFactory.generateQuizBatch(quizExercise, ZonedDateTime.now().minusSeconds(10))); + quizExerciseUtilService.joinQuizBatch(quizExercise, batch, TEST_PREFIX + "student1"); + } + + QuizSubmission quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, 1, false, null); + QuizSubmission updatedSubmission = request.postWithResponseBody("/api/exercises/" + quizExercise.getId() + "/submissions/live", quizSubmission, QuizSubmission.class, + HttpStatus.OK); + // check whether submission flag was updated + assertThat(updatedSubmission.isSubmitted()).isTrue(); + // check whether all answers were submitted properly + assertThat(updatedSubmission.getSubmittedAnswers()).hasSameSizeAs(quizSubmission.getSubmittedAnswers()); + // check whether submission date was set + assertThat(updatedSubmission.getSubmissionDate()).isNotNull(); + } + + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") + @WithMockUser(username = TEST_PREFIX + "student3", roles = "USER") + @EnumSource(QuizMode.class) + void testQuizSubmitLiveMode_badRequest_alreadySubmitted(QuizMode quizMode) throws Exception { + QuizExercise quizExercise = quizExerciseUtilService.createQuiz(ZonedDateTime.now().minusSeconds(5), ZonedDateTime.now().plusSeconds(10), quizMode); + quizExercise.setDuration(10); + quizExercise = quizExerciseService.save(quizExercise); + + if (quizMode != QuizMode.SYNCHRONIZED) { + var batch = quizBatchService.save(QuizExerciseFactory.generateQuizBatch(quizExercise, ZonedDateTime.now().minusSeconds(5))); + quizExerciseUtilService.joinQuizBatch(quizExercise, batch, TEST_PREFIX + "student3"); + } + + // create a submission for the first time + QuizSubmission quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, 1, true, ZonedDateTime.now()); + quizScheduleService.updateSubmission(quizExercise.getId(), TEST_PREFIX + "student3", quizSubmission); + // submit quiz for the second time, expected status = BAD_REQUEST + request.postWithResponseBody("/api/exercises/" + quizExercise.getId() + "/submissions/live", quizSubmission, Result.class, HttpStatus.BAD_REQUEST); + } + } } diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintIntegrationTest.java index a485b98a04a6..0f3822466f44 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintIntegrationTest.java @@ -13,7 +13,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingExerciseTestCase; @@ -26,7 +26,7 @@ import de.tum.in.www1.artemis.repository.hestia.ProgrammingExerciseSolutionEntryRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class CodeHintIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class CodeHintIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "codehint"; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintServiceTest.java index fc31fe05fe6f..a3556cc65288 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintServiceTest.java @@ -10,7 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingExerciseTestCase; @@ -27,7 +27,7 @@ import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @SuppressWarnings("ArraysAsListWithZeroOrOneArgument") -class CodeHintServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class CodeHintServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "codehintservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintIntegrationTest.java index dbaf0eb83581..e05647ec8480 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintIntegrationTest.java @@ -11,7 +11,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.FeedbackType; @@ -30,7 +30,7 @@ import de.tum.in.www1.artemis.service.hestia.ProgrammingExerciseTaskService; import de.tum.in.www1.artemis.user.UserUtilService; -class ExerciseHintIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExerciseHintIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "exercisehintintegration"; @@ -193,9 +193,10 @@ void rateActivatedHintForAnExerciseBadRequest() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void rateNotActivatedHintForAnExerciseForbidden() throws Exception { + long sizeBefore = exerciseHintActivationRepository.count(); request.postWithoutLocation("/api/programming-exercises/" + exercise.getId() + "/exercise-hints/" + exerciseHint.getId() + "/rating/" + 4, null, HttpStatus.NOT_FOUND, null); - assertThat(exerciseHintActivationRepository.count()).isZero(); + assertThat(exerciseHintActivationRepository.count()).isEqualTo(sizeBefore); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintServiceTest.java index d25c4b1ca992..5dc1c38c1deb 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintServiceTest.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.FeedbackType; @@ -32,7 +32,7 @@ import de.tum.in.www1.artemis.service.hestia.ProgrammingExerciseTaskService; import de.tum.in.www1.artemis.user.UserUtilService; -class ExerciseHintServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExerciseHintServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "exercisehintservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/HestiaDatabaseTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/HestiaDatabaseTest.java index c35d9e94ab31..3bdcdad52da6 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/HestiaDatabaseTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/HestiaDatabaseTest.java @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingExerciseTestCase; @@ -30,7 +30,7 @@ * This currently includes ProgrammingExerciseTask, ProgrammingExerciseSolutionEntry and CodeHint. * It tests if the addition and deletion of these models works as expected. */ -class HestiaDatabaseTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class HestiaDatabaseTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "hestiadatabase"; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java index 6d8250aa7c6d..a7a069d4b7a0 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java @@ -2,6 +2,7 @@ import java.time.ZonedDateTime; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -53,6 +54,12 @@ void initTestCase() throws Exception { exercise = ProgrammingExerciseFactory.generateProgrammingExercise(ZonedDateTime.now().minusDays(1), ZonedDateTime.now().plusDays(7), course); } + @AfterEach + void cleanup() throws Exception { + solutionRepo.resetLocalRepo(); + templateRepo.resetLocalRepo(); + } + @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void getGitDiffAsAStudent() throws Exception { diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportServiceTest.java index 459253e1f85b..c54eb20f1c49 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportServiceTest.java @@ -2,9 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -67,6 +69,12 @@ void initTestCase() { exercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); } + @AfterEach + void cleanup() throws IOException { + templateRepo.resetLocalRepo(); + solutionRepo.resetLocalRepo(); + } + @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateGitDiffNoChanges() throws Exception { diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseSolutionEntryIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseSolutionEntryIntegrationTest.java index aa9fa024f0ab..b8a2864e6d76 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseSolutionEntryIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseSolutionEntryIntegrationTest.java @@ -12,7 +12,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingExerciseTestCase; import de.tum.in.www1.artemis.domain.hestia.CodeHint; @@ -26,7 +26,7 @@ import de.tum.in.www1.artemis.repository.hestia.ProgrammingExerciseTaskRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class ProgrammingExerciseSolutionEntryIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ProgrammingExerciseSolutionEntryIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "progexsolutionentry"; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskIntegrationTest.java index 2df5b20c9fa7..20b60d78d642 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskIntegrationTest.java @@ -11,7 +11,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.DomainObject; import de.tum.in.www1.artemis.domain.ProgrammingExercise; @@ -27,7 +27,7 @@ import de.tum.in.www1.artemis.service.hestia.ProgrammingExerciseTaskService; import de.tum.in.www1.artemis.user.UserUtilService; -class ProgrammingExerciseTaskIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ProgrammingExerciseTaskIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "progextask"; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskServiceTest.java index fcb42d0b6d0d..6af86de04963 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskServiceTest.java @@ -10,7 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingExerciseTestCase; @@ -25,7 +25,7 @@ import de.tum.in.www1.artemis.service.hestia.ProgrammingExerciseTaskService; import de.tum.in.www1.artemis.user.UserUtilService; -class ProgrammingExerciseTaskServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ProgrammingExerciseTaskServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "progextaskservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/StructuralTestCaseServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/StructuralTestCaseServiceTest.java index 8fd23c42138f..d82424a3d18f 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/StructuralTestCaseServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/StructuralTestCaseServiceTest.java @@ -3,8 +3,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import java.io.IOException; import java.time.ZonedDateTime; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -61,6 +63,12 @@ void initTestCase() { exercise = ProgrammingExerciseFactory.generateProgrammingExercise(ZonedDateTime.now().minusDays(1), ZonedDateTime.now().plusDays(7), course); } + @AfterEach + void cleanup() throws IOException { + solutionRepo.resetLocalRepo(); + testRepo.resetLocalRepo(); + } + private void addTestCaseToExercise(String name) { var testCase = new ProgrammingExerciseTestCase(); testCase.setTestName(name); diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageIntegrationTest.java index 6ea13cb715ce..2e0b2b5f61c7 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageIntegrationTest.java @@ -11,7 +11,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingExerciseTestCase; @@ -31,7 +31,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.RequestUtilService; -class TestwiseCoverageIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TestwiseCoverageIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "testwisecoverageint"; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageReportServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageReportServiceTest.java index 9b62ede865ba..59aa9be00e27 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageReportServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageReportServiceTest.java @@ -2,9 +2,11 @@ import static org.assertj.core.api.Assertions.*; +import java.io.IOException; import java.util.Map; import java.util.Set; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -80,6 +82,11 @@ void setup() throws Exception { programmingExercise = programmingExerciseRepository.findByIdElseThrow(programmingExercise.getId()); } + @AfterEach + void cleanup() throws IOException { + solutionRepo.resetLocalRepo(); + } + @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void shouldCreateFullTestwiseCoverageReport() { diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/behavioral/BehavioralTestCaseServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/behavioral/BehavioralTestCaseServiceTest.java index e0f94c1d7eb7..4eece5351289 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/behavioral/BehavioralTestCaseServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/behavioral/BehavioralTestCaseServiceTest.java @@ -2,11 +2,12 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; import java.util.HashSet; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; @@ -19,7 +20,6 @@ import de.tum.in.www1.artemis.domain.hestia.*; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.SolutionProgrammingExerciseParticipationRepository; import de.tum.in.www1.artemis.repository.hestia.*; @@ -43,9 +43,6 @@ class BehavioralTestCaseServiceTest extends AbstractSpringIntegrationBambooBitbu @Autowired private ProgrammingExerciseTestCaseRepository testCaseRepository; - @Autowired - private ProgrammingExerciseRepository programmingExerciseRepository; - @Autowired private ProgrammingExerciseGitDiffReportRepository programmingExerciseGitDiffReportRepository; @@ -73,13 +70,18 @@ class BehavioralTestCaseServiceTest extends AbstractSpringIntegrationBambooBitbu private ProgrammingExercise exercise; @BeforeEach - void initTestCase() throws Exception { + void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 0, 0, 0, 1); final Course course = programmingExerciseUtilService.addCourseWithOneProgrammingExercise(false, true, ProgrammingLanguage.JAVA); exercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); exercise.setTestwiseCoverageEnabled(true); } + @AfterEach + void cleanup() throws IOException { + solutionRepo.resetLocalRepo(); + } + private ProgrammingExerciseTestCase addTestCaseToExercise(String name) { var testCase = new ProgrammingExerciseTestCase(); testCase.setTestName(name); @@ -144,7 +146,6 @@ private TestwiseCoverageReportEntry newCoverageReportEntry(int startLine, int li } @Test - @Timeout(1000) @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGenerationForSimpleExample() throws Exception { exercise = hestiaUtilTestService.setupSolution("Test.java", "A\nB\nC\nD\nE\nF\nG\nH", exercise, solutionRepo); diff --git a/src/test/java/de/tum/in/www1/artemis/iris/AbstractIrisIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/AbstractIrisIntegrationTest.java index d6893a13f9f8..f2ccfa6cdfab 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/AbstractIrisIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/AbstractIrisIntegrationTest.java @@ -55,6 +55,8 @@ public abstract class AbstractIrisIntegrationTest extends AbstractSpringIntegrat @Autowired protected ProgrammingExerciseUtilService programmingExerciseUtilService; + private static final long TIMEOUT_MS = 200; + @BeforeEach void setup() { irisRequestMockProvider.enableMockingOfRequests(); @@ -99,15 +101,6 @@ protected IrisTemplate createDummyTemplate() { return template; } - /** - * Wait for the iris message to be processed by Iris, the LLM mock and the websocket service. - * - * @throws InterruptedException if the thread is interrupted - */ - protected void waitForIrisMessageToBeProcessed() throws InterruptedException { - Thread.sleep(100); - } - /** * Verify that the message was sent through the websocket. * @@ -116,7 +109,7 @@ protected void waitForIrisMessageToBeProcessed() throws InterruptedException { * @param message the content of the message */ protected void verifyMessageWasSentOverWebsocket(String user, Long sessionId, String message) { - verify(websocketMessagingService, times(1)).sendMessageToUser(eq(user), eq("/topic/iris/sessions/" + sessionId), + verify(websocketMessagingService, timeout(TIMEOUT_MS).times(1)).sendMessageToUser(eq(user), eq("/topic/iris/sessions/" + sessionId), ArgumentMatchers.argThat(object -> object instanceof IrisWebsocketService.IrisWebsocketDTO websocketDTO && websocketDTO.getType() == IrisWebsocketService.IrisWebsocketDTO.IrisWebsocketMessageType.MESSAGE && Objects.equals(websocketDTO.getMessage().getContent().stream().map(IrisMessageContent::getTextContent).collect(Collectors.joining("\n")), message))); @@ -130,7 +123,7 @@ protected void verifyMessageWasSentOverWebsocket(String user, Long sessionId, St * @param message the message */ protected void verifyMessageWasSentOverWebsocket(String user, Long sessionId, IrisMessage message) { - verify(websocketMessagingService, times(1)).sendMessageToUser(eq(user), eq("/topic/iris/sessions/" + sessionId), + verify(websocketMessagingService, timeout(TIMEOUT_MS).times(1)).sendMessageToUser(eq(user), eq("/topic/iris/sessions/" + sessionId), ArgumentMatchers.argThat(object -> object instanceof IrisWebsocketService.IrisWebsocketDTO websocketDTO && websocketDTO.getType() == IrisWebsocketService.IrisWebsocketDTO.IrisWebsocketMessageType.MESSAGE && Objects.equals(websocketDTO.getMessage().getContent().stream().map(IrisMessageContent::getTextContent).toList(), @@ -144,7 +137,7 @@ protected void verifyMessageWasSentOverWebsocket(String user, Long sessionId, Ir * @param sessionId the session id */ protected void verifyErrorWasSentOverWebsocket(String user, Long sessionId) { - verify(websocketMessagingService, times(1)).sendMessageToUser(eq(user), eq("/topic/iris/sessions/" + sessionId), + verify(websocketMessagingService, timeout(TIMEOUT_MS).times(1)).sendMessageToUser(eq(user), eq("/topic/iris/sessions/" + sessionId), ArgumentMatchers.argThat(object -> object instanceof IrisWebsocketService.IrisWebsocketDTO websocketDTO && websocketDTO.getType() == IrisWebsocketService.IrisWebsocketDTO.IrisWebsocketMessageType.ERROR)); } diff --git a/src/test/java/de/tum/in/www1/artemis/iris/IrisMessageIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/IrisMessageIntegrationTest.java index b4a72b067632..6ce1fea32261 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/IrisMessageIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/IrisMessageIntegrationTest.java @@ -51,6 +51,8 @@ class IrisMessageIntegrationTest extends AbstractIrisIntegrationTest { private ProgrammingExercise exercise; + private LocalRepository repository; + @BeforeEach void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 2, 0, 0, 0); @@ -59,6 +61,7 @@ void initTestCase() { exercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); activateIrisFor(course); activateIrisFor(exercise); + repository = new LocalRepository("main"); } @Test @@ -69,9 +72,9 @@ void sendOneMessage() throws Exception { messageToSend.setMessageDifferentiator(1453); irisRequestMockProvider.mockMessageResponse("Hello World"); - var savedExercise = irisUtilTestService.setupTemplate(exercise, new LocalRepository("main")); + var savedExercise = irisUtilTestService.setupTemplate(exercise, repository); var exerciseParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(savedExercise, TEST_PREFIX + "student1"); - irisUtilTestService.setupStudentParticipation(exerciseParticipation, new LocalRepository("main")); + irisUtilTestService.setupStudentParticipation(exerciseParticipation, repository); activateIrisFor(savedExercise); var irisMessage = request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, IrisMessage.class, HttpStatus.CREATED); @@ -81,9 +84,7 @@ void sendOneMessage() throws Exception { // Compare contents of messages by only comparing the textContent field assertThat(irisMessage.getContent()).hasSize(3).map(IrisMessageContent::getTextContent) .isEqualTo(messageToSend.getContent().stream().map(IrisMessageContent::getTextContent).toList()); - var irisSessionFromDb = irisSessionRepository.findByIdWithMessagesElseThrow(irisSession.getId()); - assertThat(irisSessionFromDb.getMessages()).hasSize(1).isEqualTo(List.of(irisMessage)); - await().until(() -> irisSessionRepository.findByIdWithMessagesElseThrow(irisSession.getId()).getMessages().size() == 2); + await().untilAsserted(() -> assertThat(irisSessionRepository.findByIdWithMessagesElseThrow(irisSession.getId()).getMessages()).hasSize(2).contains(irisMessage)); verifyMessageWasSentOverWebsocket(TEST_PREFIX + "student1", irisSession.getId(), messageToSend); verifyMessageWasSentOverWebsocket(TEST_PREFIX + "student1", irisSession.getId(), "Hello World"); @@ -115,9 +116,9 @@ void sendTwoMessages() throws Exception { var irisSession = irisSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); IrisMessage messageToSend1 = createDefaultMockMessage(irisSession); - var savedExercise = irisUtilTestService.setupTemplate(exercise, new LocalRepository("main")); + var savedExercise = irisUtilTestService.setupTemplate(exercise, repository); var exerciseParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(savedExercise, TEST_PREFIX + "student1"); - irisUtilTestService.setupStudentParticipation(exerciseParticipation, new LocalRepository("main")); + irisUtilTestService.setupStudentParticipation(exerciseParticipation, repository); activateIrisFor(savedExercise); var irisMessage1 = request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend1, IrisMessage.class, HttpStatus.CREATED); @@ -239,14 +240,13 @@ void sendOneMessageBadRequest() throws Exception { IrisMessage messageToSend = createDefaultMockMessage(irisSession); irisRequestMockProvider.mockMessageError(); - var savedExercise = irisUtilTestService.setupTemplate(exercise, new LocalRepository("main")); + var savedExercise = irisUtilTestService.setupTemplate(exercise, repository); var exerciseParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(savedExercise, TEST_PREFIX + "student1"); - irisUtilTestService.setupStudentParticipation(exerciseParticipation, new LocalRepository("main")); + irisUtilTestService.setupStudentParticipation(exerciseParticipation, repository); activateIrisFor(savedExercise); request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, IrisMessage.class, HttpStatus.CREATED); - waitForIrisMessageToBeProcessed(); verifyMessageWasSentOverWebsocket(TEST_PREFIX + "student1", irisSession.getId(), messageToSend); verifyErrorWasSentOverWebsocket(TEST_PREFIX + "student1", irisSession.getId()); verifyNothingElseWasSentOverWebsocket(TEST_PREFIX + "student1", irisSession.getId()); @@ -259,14 +259,13 @@ void sendOneMessageEmptyBody() throws Exception { IrisMessage messageToSend = createDefaultMockMessage(irisSession); irisRequestMockProvider.mockMessageResponse(null); - var savedExercise = irisUtilTestService.setupTemplate(exercise, new LocalRepository("main")); + var savedExercise = irisUtilTestService.setupTemplate(exercise, repository); var exerciseParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(savedExercise, TEST_PREFIX + "student1"); - irisUtilTestService.setupStudentParticipation(exerciseParticipation, new LocalRepository("main")); + irisUtilTestService.setupStudentParticipation(exerciseParticipation, repository); activateIrisFor(savedExercise); request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, IrisMessage.class, HttpStatus.CREATED); - waitForIrisMessageToBeProcessed(); verifyMessageWasSentOverWebsocket(TEST_PREFIX + "student1", irisSession.getId(), messageToSend); verifyErrorWasSentOverWebsocket(TEST_PREFIX + "student1", irisSession.getId()); verifyNothingElseWasSentOverWebsocket(TEST_PREFIX + "student1", irisSession.getId()); diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentResourceIntegrationTest.java index 1365901b2fd3..3cbe1a819a11 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentResourceIntegrationTest.java @@ -14,14 +14,14 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.user.UserUtilService; -class AttachmentResourceIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class AttachmentResourceIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "attachmentresourceintegrationtest"; @@ -54,7 +54,7 @@ void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 0, 1, 0, 1); attachment = LectureFactory.generateAttachment(null); - attachment.setLink("files/temp/example.txt"); + attachment.setLink("/api/files/temp/example.txt"); var course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); textExercise = exerciseUtilService.getFirstExerciseWithType(course, TextExercise.class); @@ -96,7 +96,8 @@ void updateAttachment(boolean fileUpdate) throws Exception { var expectedAttachment = attachmentRepository.findById(actualAttachment.getId()).orElseThrow(); assertThat(actualAttachment.getName()).isEqualTo("new name"); - var ignoringFields = new String[] { "name", "fileService", "prevLink", "lecture.lectureUnits", "lecture.posts", "lecture.course", "lecture.attachments" }; + var ignoringFields = new String[] { "name", "fileService", "filePathService", "entityFileService", "prevLink", "lecture.lectureUnits", "lecture.posts", "lecture.course", + "lecture.attachments" }; assertThat(actualAttachment).usingRecursiveComparison().ignoringFields(ignoringFields).isEqualTo(expectedAttachment); verify(groupNotificationService).notifyStudentGroupAboutAttachmentChange(actualAttachment, notificationText); } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentUnitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentUnitIntegrationTest.java index 5fd380e9bf79..859e98c44c64 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentUnitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentUnitIntegrationTest.java @@ -28,7 +28,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Attachment; import de.tum.in.www1.artemis.domain.Lecture; import de.tum.in.www1.artemis.domain.lecture.AttachmentUnit; @@ -38,7 +38,7 @@ import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.user.UserUtilService; -class AttachmentUnitIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class AttachmentUnitIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "attachmentunitintegrationtest"; // only lower case is supported @@ -76,7 +76,7 @@ void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 1, 1, 0, 1); this.attachment = LectureFactory.generateAttachment(null); this.attachment.setName(" LoremIpsum "); - this.attachment.setLink("files/temp/example.txt"); + this.attachment.setLink("/api/files/temp/example.txt"); this.lecture1 = lectureUtilService.createCourseWithLecture(true); this.attachmentUnit = new AttachmentUnit(); this.attachmentUnit.setDescription("Lorem Ipsum"); diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentUnitsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentUnitsIntegrationTest.java index dea2195856dd..16ee82a1dbe2 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentUnitsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentUnitsIntegrationTest.java @@ -20,7 +20,7 @@ import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Lecture; import de.tum.in.www1.artemis.domain.lecture.AttachmentUnit; import de.tum.in.www1.artemis.repository.AttachmentUnitRepository; @@ -30,7 +30,7 @@ import de.tum.in.www1.artemis.web.rest.dto.LectureUnitInformationDTO; import de.tum.in.www1.artemis.web.rest.dto.LectureUnitSplitDTO; -class AttachmentUnitsIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class AttachmentUnitsIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "attachmentunitsintegrationtest"; diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java index ee3c632b2a28..eb3a1cefd33e 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java @@ -19,7 +19,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; @@ -45,7 +45,7 @@ import de.tum.in.www1.artemis.util.PageableSearchUtilService; import de.tum.in.www1.artemis.web.rest.dto.CourseCompetencyProgressDTO; -class CompetencyIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class CompetencyIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "competencyintegrationtest"; diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/ExerciseUnitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/ExerciseUnitIntegrationTest.java index 19386db708ac..aab5f77c5bbd 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/ExerciseUnitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/ExerciseUnitIntegrationTest.java @@ -12,7 +12,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; @@ -21,7 +21,7 @@ import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.user.UserUtilService; -class ExerciseUnitIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExerciseUnitIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "exerciseunitintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LectureFactory.java b/src/test/java/de/tum/in/www1/artemis/lecture/LectureFactory.java index 378f795052c9..b61e4a6e5c6d 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LectureFactory.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LectureFactory.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.fail; -import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.time.ZonedDateTime; @@ -79,7 +78,7 @@ public static Attachment generateAttachmentWithFile(ZonedDateTime startDate) { Attachment attachment = generateAttachment(startDate); String testFileName = "test_" + UUID.randomUUID().toString().substring(0, 8) + ".jpg"; try { - FileUtils.copyFile(ResourceUtils.getFile("classpath:test-data/attachment/placeholder.jpg"), new File(FilePathService.getTempFilePath(), testFileName)); + FileUtils.copyFile(ResourceUtils.getFile("classpath:test-data/attachment/placeholder.jpg"), FilePathService.getTempFilePath().resolve(testFileName).toFile()); } catch (IOException ex) { fail("Failed while copying test attachment files", ex); diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LectureIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LectureIntegrationTest.java index 9c4d1c790f4f..39c7bad09090 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LectureIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LectureIntegrationTest.java @@ -14,7 +14,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.lecture.*; @@ -25,7 +25,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.PageableSearchUtilService; -class LectureIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class LectureIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "lectureintegrationtest"; @@ -105,7 +105,7 @@ void initTestCase() throws Exception { private void addAttachmentToLecture() { this.attachmentDirectOfLecture = LectureFactory.generateAttachment(null); - this.attachmentDirectOfLecture.setLink("files/temp/example2.txt"); + this.attachmentDirectOfLecture.setLink("/api/files/temp/example2.txt"); this.attachmentDirectOfLecture.setLecture(this.lecture1); this.attachmentDirectOfLecture = attachmentRepository.save(this.attachmentDirectOfLecture); this.lecture1.addAttachments(this.attachmentDirectOfLecture); diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LectureUnitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LectureUnitIntegrationTest.java index dbd732522b5e..4a89a196cbdd 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LectureUnitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LectureUnitIntegrationTest.java @@ -12,7 +12,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; @@ -23,7 +23,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.rest.dto.lectureunit.LectureUnitForLearningPathNodeDetailsDTO; -class LectureUnitIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class LectureUnitIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "lectureunitintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LectureUtilService.java b/src/test/java/de/tum/in/www1/artemis/lecture/LectureUtilService.java index 7d17446b858f..95160ce2893e 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LectureUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LectureUtilService.java @@ -1,10 +1,15 @@ package de.tum.in.www1.artemis.lecture; +import static org.assertj.core.api.Assertions.fail; + +import java.io.IOException; import java.time.ZonedDateTime; import java.util.*; +import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.ResourceUtils; import de.tum.in.www1.artemis.course.CourseFactory; import de.tum.in.www1.artemis.course.CourseUtilService; @@ -15,6 +20,7 @@ import de.tum.in.www1.artemis.post.ConversationFactory; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.repository.metis.conversation.ConversationRepository; +import de.tum.in.www1.artemis.service.FilePathService; /** * Service responsible for initializing the database with specific testdata related to lectures for use in integration tests. @@ -157,7 +163,14 @@ public AttachmentUnit createAttachmentUnitWithSlides(int numberOfSlides) { for (int i = 1; i <= numberOfSlides; i++) { Slide slide = new Slide(); slide.setSlideNumber(i); - slide.setSlideImagePath("path/to/slide" + i + ".png"); + String testFileName = "slide" + i + ".png"; + try { + FileUtils.copyFile(ResourceUtils.getFile("classpath:test-data/attachment/placeholder.jpg"), FilePathService.getTempFilePath().resolve(testFileName).toFile()); + } + catch (IOException ex) { + fail("Failed while copying test attachment files", ex); + } + slide.setSlideImagePath("/api/files/temp/" + testFileName); slide.setAttachmentUnit(attachmentUnit); slideRepository.save(slide); } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/OnlineUnitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/OnlineUnitIntegrationTest.java index 3447da7411a1..a09199424c15 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/OnlineUnitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/OnlineUnitIntegrationTest.java @@ -20,7 +20,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Lecture; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.domain.lecture.OnlineUnit; @@ -29,7 +29,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.rest.dto.OnlineResourceDTO; -class OnlineUnitIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class OnlineUnitIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "onlineunitintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/TextUnitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/TextUnitIntegrationTest.java index 99eb0e2328c3..09715dba3702 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/TextUnitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/TextUnitIntegrationTest.java @@ -10,7 +10,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Lecture; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.domain.lecture.TextUnit; @@ -18,7 +18,7 @@ import de.tum.in.www1.artemis.repository.TextUnitRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class TextUnitIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TextUnitIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "textunitintegrationtest"; diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/VideoUnitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/VideoUnitIntegrationTest.java index 01b3e1efd40b..aea03a64a163 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/VideoUnitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/VideoUnitIntegrationTest.java @@ -10,7 +10,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Lecture; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.domain.lecture.VideoUnit; @@ -18,7 +18,7 @@ import de.tum.in.www1.artemis.repository.VideoUnitRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class VideoUnitIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class VideoUnitIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "videounitintegrationtest"; diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCIntegrationTest.java index 6d91ed555d30..658901c776d0 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCIntegrationTest.java @@ -86,8 +86,7 @@ void testFetchPush_repositoryDoesNotExist() throws IOException, GitAPIException, localVCLocalCITestService.testPushReturnsError(someRepository.localGit, student1Login, projectKey, repositorySlug, NOT_FOUND); // Cleanup - someRepository.localGit.close(); - FileUtils.deleteDirectory(someRepository.localRepoFile); + someRepository.resetLocalRepo(); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java index 2b763da0c2fc..059efcea2e4b 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java @@ -369,6 +369,7 @@ void testFetch_studentAssignmentRepository_teamMode_afterDueDate() throws Except // Instructor should be able to read and write. localVCLocalCITestService.testFetchSuccessful(teamLocalRepository.localGit, instructor1Login, projectKey1, teamRepositorySlug); localVCLocalCITestService.testPushSuccessful(teamLocalRepository.localGit, instructor1Login, projectKey1, teamRepositorySlug); + teamLocalRepository.resetLocalRepo(); } private LocalRepository prepareTeamExerciseAndRepository() throws GitAPIException, IOException, URISyntaxException { diff --git a/src/test/java/de/tum/in/www1/artemis/metis/AbstractConversationTest.java b/src/test/java/de/tum/in/www1/artemis/metis/AbstractConversationTest.java index 6707bc6680d7..e26368f0e914 100644 --- a/src/test/java/de/tum/in/www1/artemis/metis/AbstractConversationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/metis/AbstractConversationTest.java @@ -1,7 +1,6 @@ package de.tum.in.www1.artemis.metis; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.util.Arrays; @@ -12,7 +11,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.User; @@ -36,7 +35,7 @@ /** * Contains useful methods for testing the conversations futures */ -abstract class AbstractConversationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +abstract class AbstractConversationTest extends AbstractSpringIntegrationIndependentTest { @Autowired CourseRepository courseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/metis/AnswerMessageIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/metis/AnswerMessageIntegrationTest.java index 927c34d2e253..aefa041fef28 100644 --- a/src/test/java/de/tum/in/www1/artemis/metis/AnswerMessageIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/metis/AnswerMessageIntegrationTest.java @@ -11,7 +11,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.enumeration.CourseInformationSharingConfiguration; @@ -24,7 +24,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.websocket.dto.metis.PostDTO; -class AnswerMessageIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class AnswerMessageIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "answermessageint"; diff --git a/src/test/java/de/tum/in/www1/artemis/metis/AnswerPostIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/metis/AnswerPostIntegrationTest.java index 70866f2704dd..fc9a1ba726bc 100644 --- a/src/test/java/de/tum/in/www1/artemis/metis/AnswerPostIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/metis/AnswerPostIntegrationTest.java @@ -12,7 +12,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.User; @@ -28,7 +28,7 @@ import de.tum.in.www1.artemis.repository.metis.PostRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class AnswerPostIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class AnswerPostIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "answerpostintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/metis/MessageIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/metis/MessageIntegrationTest.java index b20df00ae957..551fb0134afb 100644 --- a/src/test/java/de/tum/in/www1/artemis/metis/MessageIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/metis/MessageIntegrationTest.java @@ -33,7 +33,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.enumeration.CourseInformationSharingConfiguration; @@ -55,7 +55,7 @@ import de.tum.in.www1.artemis.web.rest.dto.PostContextFilter; import de.tum.in.www1.artemis.web.websocket.dto.metis.PostDTO; -class MessageIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class MessageIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "messageintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/metis/PostIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/metis/PostIntegrationTest.java index a2007694c886..2565bbae5729 100644 --- a/src/test/java/de/tum/in/www1/artemis/metis/PostIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/metis/PostIntegrationTest.java @@ -7,10 +7,8 @@ import java.util.*; import java.util.stream.Collectors; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; +import javax.mail.internet.MimeMessage; +import javax.validation.*; import javax.validation.constraints.NotNull; import org.junit.jupiter.api.AfterEach; @@ -24,19 +22,13 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; -import de.tum.in.www1.artemis.domain.Course; -import de.tum.in.www1.artemis.domain.Exercise; -import de.tum.in.www1.artemis.domain.Lecture; -import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; +import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.CourseInformationSharingConfiguration; import de.tum.in.www1.artemis.domain.enumeration.DisplayPriority; import de.tum.in.www1.artemis.domain.enumeration.SortingOrder; import de.tum.in.www1.artemis.domain.exam.Exam; -import de.tum.in.www1.artemis.domain.metis.CourseWideContext; -import de.tum.in.www1.artemis.domain.metis.Post; -import de.tum.in.www1.artemis.domain.metis.PostSortCriterion; -import de.tum.in.www1.artemis.domain.metis.UserRole; +import de.tum.in.www1.artemis.domain.metis.*; import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismCase; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.post.ConversationUtilService; @@ -48,7 +40,7 @@ import de.tum.in.www1.artemis.web.rest.dto.PostContextFilter; import de.tum.in.www1.artemis.web.websocket.dto.metis.PostDTO; -class PostIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class PostIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "postintegration"; @@ -303,9 +295,10 @@ void testCreateAnnouncement() throws Exception { postToSave.setDisplayPriority(DisplayPriority.PINNED); checkCreatedPost(postToSave, createdPost); - List updatedCourseWidePosts = postRepository.findPosts(postContextFilter, null, false, null).stream().filter(post -> post.getCourseWideContext() != null).toList(); + postRepository.findPosts(postContextFilter, null, false, null).stream().filter(post -> post.getCourseWideContext() != null).toList(); assertThat(postRepository.findPosts(postContextFilter, null, false, null)).hasSize(numberOfPostsBefore + 1); verify(groupNotificationService).notifyAllGroupsAboutNewAnnouncement(createdPost, course); + verify(javaMailSender, timeout(4000).times(4)).send(any(MimeMessage.class)); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/metis/ReactionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/metis/ReactionIntegrationTest.java index 5d19715f8d7e..1484c025dd32 100644 --- a/src/test/java/de/tum/in/www1/artemis/metis/ReactionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/metis/ReactionIntegrationTest.java @@ -20,7 +20,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.User; @@ -35,7 +35,7 @@ import de.tum.in.www1.artemis.repository.metis.ReactionRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class ReactionIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ReactionIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "reactionintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/migration/MigrationIntegrityTest.java b/src/test/java/de/tum/in/www1/artemis/migration/MigrationIntegrityTest.java index 334a38704447..e54b9155663b 100644 --- a/src/test/java/de/tum/in/www1/artemis/migration/MigrationIntegrityTest.java +++ b/src/test/java/de/tum/in/www1/artemis/migration/MigrationIntegrityTest.java @@ -7,12 +7,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.config.migration.MigrationEntry; import de.tum.in.www1.artemis.config.migration.MigrationRegistry; import de.tum.in.www1.artemis.config.migration.MigrationService; -class MigrationIntegrityTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class MigrationIntegrityTest extends AbstractSpringIntegrationIndependentTest { @Autowired private MigrationRegistry migrationRegistry; diff --git a/src/test/java/de/tum/in/www1/artemis/migration/MigrationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/migration/MigrationServiceTest.java index 80cd24fe66c4..baeecfeea6c8 100644 --- a/src/test/java/de/tum/in/www1/artemis/migration/MigrationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/migration/MigrationServiceTest.java @@ -20,7 +20,7 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Profiles; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.config.migration.MigrationEntry; import de.tum.in.www1.artemis.config.migration.MigrationIntegrityException; import de.tum.in.www1.artemis.config.migration.MigrationRegistry; @@ -31,7 +31,7 @@ import de.tum.in.www1.artemis.migration.entries.TestChangeEntry20211216_231800; import de.tum.in.www1.artemis.repository.MigrationChangeRepository; -class MigrationServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class MigrationServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private MigrationRegistry registry; diff --git a/src/test/java/de/tum/in/www1/artemis/notification/GroupNotificationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/notification/GroupNotificationServiceTest.java index 763e5cdf0842..23c1f6f6e5ac 100644 --- a/src/test/java/de/tum/in/www1/artemis/notification/GroupNotificationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/notification/GroupNotificationServiceTest.java @@ -17,7 +17,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.QuizMode; @@ -34,7 +34,7 @@ import de.tum.in.www1.artemis.service.notifications.GroupNotificationScheduleService; import de.tum.in.www1.artemis.user.UserUtilService; -class GroupNotificationServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class GroupNotificationServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "groupnotificationservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/notification/NotificationResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/notification/NotificationResourceIntegrationTest.java index 9a03abf776b6..3b77aaf360d7 100644 --- a/src/test/java/de/tum/in/www1/artemis/notification/NotificationResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/notification/NotificationResourceIntegrationTest.java @@ -12,7 +12,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.NotificationSetting; import de.tum.in.www1.artemis.domain.User; @@ -26,7 +26,7 @@ import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.user.UserUtilService; -class NotificationResourceIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class NotificationResourceIntegrationTest extends AbstractSpringIntegrationIndependentTest { @Autowired private CourseRepository courseRepository; @@ -37,9 +37,6 @@ class NotificationResourceIntegrationTest extends AbstractSpringIntegrationBambo @Autowired private NotificationRepository notificationRepository; - @Autowired - private SystemNotificationRepository systemNotificationRepository; - @Autowired private NotificationSettingRepository notificationSettingRepository; @@ -60,7 +57,7 @@ void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 2, 1, 1, 1); course1 = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); course2 = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - systemNotificationRepository.deleteAll(); + notificationRepository.deleteAll(); User student1 = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); student1.setLastNotificationRead(ZonedDateTime.now().minusDays(1)); @@ -69,7 +66,6 @@ void initTestCase() { @AfterEach void tearDown() { - systemNotificationRepository.deleteAll(); notificationRepository.deleteAll(); } diff --git a/src/test/java/de/tum/in/www1/artemis/notification/NotificationScheduleServiceTest.java b/src/test/java/de/tum/in/www1/artemis/notification/NotificationScheduleServiceTest.java index 1db66bc4a57d..6571cb625394 100644 --- a/src/test/java/de/tum/in/www1/artemis/notification/NotificationScheduleServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/notification/NotificationScheduleServiceTest.java @@ -12,24 +12,20 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; -import de.tum.in.www1.artemis.exercise.ExerciseUtilService; +import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; -import de.tum.in.www1.artemis.repository.ExerciseRepository; -import de.tum.in.www1.artemis.repository.NotificationRepository; -import de.tum.in.www1.artemis.repository.NotificationSettingRepository; -import de.tum.in.www1.artemis.repository.ResultRepository; +import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.service.messaging.InstanceMessageReceiveService; import de.tum.in.www1.artemis.user.UserUtilService; -class NotificationScheduleServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class NotificationScheduleServiceTest extends AbstractSpringIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "notificationschedserv"; @@ -54,9 +50,6 @@ class NotificationScheduleServiceTest extends AbstractSpringIntegrationBambooBit @Autowired private CourseUtilService courseUtilService; - @Autowired - private ExerciseUtilService exerciseUtilService; - @Autowired private ParticipationUtilService participationUtilService; @@ -64,35 +57,42 @@ class NotificationScheduleServiceTest extends AbstractSpringIntegrationBambooBit private User user; + private long sizeBefore; + + // TODO: This could be improved by e.g. manually setting the system time instead of waiting for actual time to pass. + private static final long DELAY_MS = 200; + + private static final long TIMEOUT_MS = 5000; + @BeforeEach void init() { userUtilService.addUsers(TEST_PREFIX, 1, 1, 1, 1); user = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); - final Course course = courseUtilService.addCourseWithModelingAndTextExercise(); - exercise = exerciseUtilService.getFirstExerciseWithType(course, TextExercise.class); - exercise.setReleaseDate(now().plus(500, ChronoUnit.MILLIS)); - exercise.setAssessmentDueDate(now().plus(2, ChronoUnit.SECONDS)); - exerciseRepository.save(exercise); + final Course course = courseUtilService.addEmptyCourse(); + exercise = TextExerciseFactory.generateTextExercise(null, null, null, course); + exercise.setMaxPoints(5.0); + exerciseRepository.saveAndFlush(exercise); + doNothing().when(javaMailSender).send(any(MimeMessage.class)); + sizeBefore = notificationRepository.count(); } @Test - @Timeout(10) @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void shouldCreateNotificationAndEmailAtReleaseDate() { - long sizeBefore = notificationRepository.count(); - notificationSettingRepository.save(new NotificationSetting(user, true, true, true, NOTIFICATION__EXERCISE_NOTIFICATION__EXERCISE_RELEASED)); + notificationSettingRepository.saveAndFlush(new NotificationSetting(user, true, true, true, NOTIFICATION__EXERCISE_NOTIFICATION__EXERCISE_RELEASED)); + exercise.setReleaseDate(now().plus(DELAY_MS, ChronoUnit.MILLIS)); + exerciseRepository.saveAndFlush(exercise); + instanceMessageReceiveService.processScheduleExerciseReleasedNotification(exercise.getId()); await().until(() -> notificationRepository.count() > sizeBefore); - verify(groupNotificationService, timeout(4000)).notifyAllGroupsAboutReleasedExercise(exercise); - verify(mailService, timeout(4000).atLeastOnce()).sendNotification(any(), anySet(), any()); + verify(groupNotificationService, timeout(TIMEOUT_MS)).notifyAllGroupsAboutReleasedExercise(exercise); + verify(mailService, timeout(TIMEOUT_MS).atLeastOnce()).sendNotification(any(), anySet(), any()); } @Test - @Timeout(10) @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void shouldCreateNotificationAndEmailAtAssessmentDueDate() { - long sizeBefore = notificationRepository.count(); TextSubmission textSubmission = new TextSubmission(); textSubmission.text("Text"); textSubmission.submitted(true); @@ -101,14 +101,15 @@ void shouldCreateNotificationAndEmailAtAssessmentDueDate() { Result manualResult = participationUtilService.createParticipationSubmissionAndResult(exercise.getId(), userUtilService.getUserByLogin(TEST_PREFIX + "student1"), 10.0, 10.0, 50, true); manualResult.setAssessmentType(AssessmentType.MANUAL); - resultRepository.save(manualResult); - - notificationSettingRepository.save(new NotificationSetting(user, true, true, true, NOTIFICATION__EXERCISE_NOTIFICATION__EXERCISE_SUBMISSION_ASSESSED)); + resultRepository.saveAndFlush(manualResult); + notificationSettingRepository.saveAndFlush(new NotificationSetting(user, true, true, true, NOTIFICATION__EXERCISE_NOTIFICATION__EXERCISE_SUBMISSION_ASSESSED)); + exercise.setAssessmentDueDate(now().plus(DELAY_MS, ChronoUnit.MILLIS)); + exerciseRepository.saveAndFlush(exercise); instanceMessageReceiveService.processScheduleAssessedExerciseSubmittedNotification(exercise.getId()); await().until(() -> notificationRepository.count() > sizeBefore); - verify(singleUserNotificationService, timeout(4000)).notifyUsersAboutAssessedExerciseSubmission(exercise); - verify(javaMailSender, timeout(4000)).send(any(MimeMessage.class)); + verify(singleUserNotificationService, timeout(TIMEOUT_MS)).notifyUsersAboutAssessedExerciseSubmission(exercise); + verify(javaMailSender, timeout(TIMEOUT_MS)).send(any(MimeMessage.class)); } } diff --git a/src/test/java/de/tum/in/www1/artemis/notification/NotificationSettingsResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/notification/NotificationSettingsResourceIntegrationTest.java index 10124449f739..781366b1c1ab 100644 --- a/src/test/java/de/tum/in/www1/artemis/notification/NotificationSettingsResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/notification/NotificationSettingsResourceIntegrationTest.java @@ -12,13 +12,13 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.NotificationSetting; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.user.UserUtilService; -class NotificationSettingsResourceIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class NotificationSettingsResourceIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "notificationsettingsresourrce"; diff --git a/src/test/java/de/tum/in/www1/artemis/notification/NotificationSettingsServiceTest.java b/src/test/java/de/tum/in/www1/artemis/notification/NotificationSettingsServiceTest.java index 234e5d810c54..210025e43b83 100644 --- a/src/test/java/de/tum/in/www1/artemis/notification/NotificationSettingsServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/notification/NotificationSettingsServiceTest.java @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.NotificationSetting; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.enumeration.NotificationType; @@ -26,7 +26,7 @@ import de.tum.in.www1.artemis.service.notifications.NotificationSettingsService; import de.tum.in.www1.artemis.user.UserUtilService; -class NotificationSettingsServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class NotificationSettingsServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "notificationsettingsservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/notification/PushNotificationResourceTest.java b/src/test/java/de/tum/in/www1/artemis/notification/PushNotificationResourceTest.java index 29480a01bc34..cc285c3a5a51 100644 --- a/src/test/java/de/tum/in/www1/artemis/notification/PushNotificationResourceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/notification/PushNotificationResourceTest.java @@ -13,7 +13,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.push_notification.PushNotificationDeviceConfiguration; import de.tum.in.www1.artemis.domain.push_notification.PushNotificationDeviceType; @@ -25,7 +25,7 @@ import de.tum.in.www1.artemis.web.rest.push_notification.PushNotificationUnregisterRequest; @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class PushNotificationResourceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class PushNotificationResourceTest extends AbstractSpringIntegrationIndependentTest { @Autowired UserRepository userRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/notification/SingleUserNotificationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/notification/SingleUserNotificationServiceTest.java index 85b8bd5a1aaa..527781793ed0 100644 --- a/src/test/java/de/tum/in/www1/artemis/notification/SingleUserNotificationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/notification/SingleUserNotificationServiceTest.java @@ -29,7 +29,7 @@ import org.mockito.Captor; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; @@ -60,7 +60,7 @@ import de.tum.in.www1.artemis.service.notifications.SingleUserNotificationService; import de.tum.in.www1.artemis.user.UserUtilService; -class SingleUserNotificationServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class SingleUserNotificationServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "singleusernotification"; diff --git a/src/test/java/de/tum/in/www1/artemis/notification/SystemNotificationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/notification/SystemNotificationIntegrationTest.java index f2339c306a2d..caad0cffd0af 100644 --- a/src/test/java/de/tum/in/www1/artemis/notification/SystemNotificationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/notification/SystemNotificationIntegrationTest.java @@ -13,11 +13,11 @@ import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.notification.SystemNotification; import de.tum.in.www1.artemis.repository.SystemNotificationRepository; -class SystemNotificationIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class SystemNotificationIntegrationTest extends AbstractSpringIntegrationIndependentTest { @Autowired private SystemNotificationRepository systemNotificationRepo; diff --git a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java index d8095fc6e6fd..4a3a054f64cc 100644 --- a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java @@ -7,21 +7,16 @@ import java.time.ZonedDateTime; import java.util.*; import java.util.stream.IntStream; -import java.util.stream.Stream; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.parallel.Isolated; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; @@ -45,9 +40,9 @@ import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.*; -import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.service.GradingScaleService; import de.tum.in.www1.artemis.service.ParticipationService; +import de.tum.in.www1.artemis.service.QuizBatchService; import de.tum.in.www1.artemis.service.feature.Feature; import de.tum.in.www1.artemis.service.feature.FeatureToggleService; import de.tum.in.www1.artemis.service.scheduled.cache.quiz.QuizScheduleService; @@ -83,7 +78,7 @@ class ParticipationIntegrationTest extends AbstractSpringIntegrationBambooBitbuc private ParticipationService participationService; @Autowired - private QuizExerciseUtilService quizUtilService; + private QuizBatchService quizBatchService; @Autowired protected QuizScheduleService quizScheduleService; @@ -373,6 +368,7 @@ private void prepareMocksForProgrammingExercise(String userLogin, boolean practi var repo = new LocalRepository(defaultBranch); repo.configureRepos("studentRepo", "studentOriginRepo"); programmingExerciseTestService.setupRepositoryMocksParticipant(programmingExercise, userLogin, repo, practiceMode); + repo.resetLocalRepo(); } @Test @@ -492,6 +488,7 @@ void requestFeedbackScoreNotFull() throws Exception { request.putWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, ProgrammingExerciseStudentParticipation.class, HttpStatus.BAD_REQUEST); + localRepo.resetLocalRepo(); } @Test @@ -547,6 +544,7 @@ void requestFeedbackSuccess() throws Exception { assertThat(response.getIndividualDueDate()).isNotNull().isBefore(ZonedDateTime.now()); verify(programmingExerciseParticipationService).lockStudentRepositoryAndParticipation(programmingExercise, participation); + localRepo.resetLocalRepo(); } @Test @@ -562,6 +560,7 @@ void resumeProgrammingExerciseParticipation() throws Exception { var updatedParticipation = request.putWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/resume-programming-participation/" + participation.getId(), null, ProgrammingExerciseStudentParticipation.class, HttpStatus.OK); assertThat(updatedParticipation.getInitializationState()).isEqualTo(InitializationState.INITIALIZED); + localRepo.resetLocalRepo(); } @Test @@ -1235,20 +1234,6 @@ void getParticipation_quizExerciseStartedAndNoParticipation(QuizMode quizMode) t request.getNullable("/api/exercises/" + quizEx.getId() + "/participation", HttpStatus.NO_CONTENT, StudentParticipation.class); } - @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - @EnumSource(QuizMode.class) - void getParticipation_quizExerciseStartedAndSubmissionAllowed(QuizMode quizMode) throws Exception { - var quizEx = QuizExerciseFactory.generateQuizExercise(ZonedDateTime.now().minusMinutes(1), ZonedDateTime.now().plusMinutes(5), quizMode, course).duration(360); - quizEx = exerciseRepo.save(quizEx); - quizUtilService.prepareBatchForSubmitting(quizEx, SecurityUtils.makeAuthorizationObject(TEST_PREFIX + "instructor1"), - SecurityContextHolder.getContext().getAuthentication()); - var participation = request.get("/api/exercises/" + quizEx.getId() + "/participation", HttpStatus.OK, StudentParticipation.class); - assertThat(participation.getExercise()).as("Participation contains exercise").isEqualTo(quizEx); - assertThat(participation.getResults()).as("New result was added to the participation").hasSize(1); - assertThat(participation.getInitializationState()).as("Participation was initialized").isEqualTo(InitializationState.INITIALIZED); - } - @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void getParticipation_quizBatchNotPresent() throws Exception { @@ -1288,17 +1273,14 @@ void getParticipation_notStudentInCourse() throws Exception { @ParameterizedTest @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - @MethodSource("getGetParticipationsubmittedNotEndedQuizParameters") - void getParticipation_submittedNotEndedQuiz(QuizMode quizMode, boolean isSubmissionAllowed) throws Exception { - QuizExercise quizExercise = QuizExerciseFactory.generateQuizExercise(ZonedDateTime.now().minusMinutes(10), ZonedDateTime.now().plusMinutes(10), quizMode, course); + @EnumSource(QuizMode.class) + void testCheckQuizParticipation(QuizMode quizMode) throws Exception { + QuizExercise quizExercise = QuizExerciseFactory.generateQuizExercise(ZonedDateTime.now().minusMinutes(10), ZonedDateTime.now().minusMinutes(8), quizMode, course); quizExercise.addQuestions(QuizExerciseFactory.createShortAnswerQuestion()); quizExercise.setDuration(600); quizExercise.setQuizPointStatistic(new QuizPointStatistic()); quizExercise = exerciseRepo.save(quizExercise); - quizUtilService.prepareBatchForSubmitting(quizExercise, SecurityUtils.makeAuthorizationObject(TEST_PREFIX + "instructor1"), - SecurityContextHolder.getContext().getAuthentication()); - ShortAnswerQuestion saQuestion = (ShortAnswerQuestion) quizExercise.getQuizQuestions().get(0); List spots = saQuestion.getSpots(); ShortAnswerSubmittedAnswer submittedAnswer = new ShortAnswerSubmittedAnswer(); @@ -1311,24 +1293,15 @@ void getParticipation_submittedNotEndedQuiz(QuizMode quizMode, boolean isSubmiss QuizSubmission quizSubmission = new QuizSubmission(); quizSubmission.addSubmittedAnswers(submittedAnswer); - request.postWithResponseBody("/api/exercises/" + quizExercise.getId() + "/submissions/live", quizSubmission, QuizSubmission.class, HttpStatus.OK); - - quizScheduleService.processCachedQuizSubmissions(); - - if (!isSubmissionAllowed) { - // Duration is set to 0 so that QuizBatch.isSubmissionAllowed() will be false - quizExercise.setDuration(0); - quizExercise = exerciseRepo.save(quizExercise); - } + quizSubmission.submitted(true); + participationUtilService.addSubmission(quizExercise, quizSubmission, TEST_PREFIX + "student1"); + participationUtilService.addResultToSubmission(quizSubmission, AssessmentType.AUTOMATIC, null, quizExercise.getScoreForSubmission(quizSubmission), true); var actualParticipation = request.get("/api/exercises/" + quizExercise.getId() + "/participation", HttpStatus.OK, StudentParticipation.class); - assertThat(actualParticipation.getInitializationState()).isEqualTo(InitializationState.FINISHED); - var actualResults = actualParticipation.getResults(); assertThat(actualResults).hasSize(1); var actualSubmission = (QuizSubmission) actualResults.stream().findFirst().get().getSubmission(); - assertThat(actualSubmission.getType()).isEqualTo(SubmissionType.MANUAL); assertThat(actualSubmission.isSubmitted()).isTrue(); var actualSubmittedAnswers = actualSubmission.getSubmittedAnswers(); @@ -1343,8 +1316,25 @@ void getParticipation_submittedNotEndedQuiz(QuizMode quizMode, boolean isSubmiss assertThat(actualSubmittedAnswerText.isIsCorrect()).isFalse(); } - private static Stream getGetParticipationsubmittedNotEndedQuizParameters() { - return Stream.of(Arguments.of(QuizMode.SYNCHRONIZED, true), Arguments.of(QuizMode.SYNCHRONIZED, false), Arguments.of(QuizMode.BATCHED, true), - Arguments.of(QuizMode.BATCHED, false), Arguments.of(QuizMode.INDIVIDUAL, true), Arguments.of(QuizMode.INDIVIDUAL, false)); + @Nested + @Isolated + class ParticipationIntegrationIsolatedTest { + + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + @EnumSource(QuizMode.class) + void getParticipation_quizExerciseStartedAndSubmissionAllowed(QuizMode quizMode) throws Exception { + var quizEx = QuizExerciseFactory.generateQuizExercise(ZonedDateTime.now().minusMinutes(1), ZonedDateTime.now().plusMinutes(5), quizMode, course).duration(360); + quizEx = exerciseRepo.save(quizEx); + + if (quizMode != QuizMode.SYNCHRONIZED) { + var batch = quizBatchService.save(QuizExerciseFactory.generateQuizBatch(quizEx, ZonedDateTime.now().minusSeconds(10))); + quizExerciseUtilService.joinQuizBatch(quizEx, batch, TEST_PREFIX + "student1"); + } + var participation = request.get("/api/exercises/" + quizEx.getId() + "/participation", HttpStatus.OK, StudentParticipation.class); + assertThat(participation.getExercise()).as("Participation contains exercise").isEqualTo(quizEx); + assertThat(participation.getResults()).as("New result was added to the participation").hasSize(1); + assertThat(participation.getInitializationState()).as("Participation was initialized").isEqualTo(InitializationState.INITIALIZED); + } } } diff --git a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationSubmissionIntegrationTest.java index 199aaa45dfd2..7fdbd64f88e2 100644 --- a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationSubmissionIntegrationTest.java @@ -10,7 +10,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; @@ -18,7 +18,7 @@ import de.tum.in.www1.artemis.repository.SubmissionRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class ParticipationSubmissionIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ParticipationSubmissionIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "psitest"; // only lower case is supported diff --git a/src/test/java/de/tum/in/www1/artemis/participation/SubmissionExportIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/participation/SubmissionExportIntegrationTest.java index c765b30921fb..aed64f17d927 100644 --- a/src/test/java/de/tum/in/www1/artemis/participation/SubmissionExportIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/participation/SubmissionExportIntegrationTest.java @@ -5,8 +5,8 @@ import java.io.File; import java.io.IOException; -import java.nio.file.Path; import java.time.ZonedDateTime; +import java.util.regex.Pattern; import java.util.zip.ZipFile; import org.junit.jupiter.api.AfterEach; @@ -16,7 +16,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.Language; @@ -29,7 +29,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.rest.dto.SubmissionExportOptionsDTO; -class SubmissionExportIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class SubmissionExportIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "submissionexportintegration"; @@ -139,7 +139,9 @@ else if (exercise instanceof FileUploadExercise) { private void saveEmptySubmissionFile(Exercise exercise, FileUploadSubmission submission) throws IOException { - File file = Path.of(FileUploadSubmission.buildFilePath(exercise.getId(), submission.getId()), submission.getFilePath()).toFile(); + String[] parts = submission.getFilePath().split(Pattern.quote(File.separator)); + String fileName = parts[parts.length - 1]; + File file = FileUploadSubmission.buildFilePath(exercise.getId(), submission.getId()).resolve(fileName).toFile(); File parent = file.getParentFile(); if (!parent.exists() && !parent.mkdirs()) { diff --git a/src/test/java/de/tum/in/www1/artemis/participation/SubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/participation/SubmissionIntegrationTest.java index bcc04db80603..b51e9e75d7e2 100644 --- a/src/test/java/de/tum/in/www1/artemis/participation/SubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/participation/SubmissionIntegrationTest.java @@ -8,7 +8,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.util.PageableSearchUtilService; import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; -class SubmissionIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class SubmissionIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "submissionintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismCaseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismCaseIntegrationTest.java index 1e31ee3e488a..c73610e57567 100644 --- a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismCaseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismCaseIntegrationTest.java @@ -13,7 +13,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.exam.Exam; @@ -30,7 +30,7 @@ import de.tum.in.www1.artemis.web.rest.dto.PlagiarismCaseInfoDTO; import de.tum.in.www1.artemis.web.rest.dto.PlagiarismVerdictDTO; -class PlagiarismCaseIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class PlagiarismCaseIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "plagiarismcaseintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismCheckIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismCheckIntegrationTest.java index eee25e61421d..55bf34dbf463 100644 --- a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismCheckIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismCheckIntegrationTest.java @@ -11,7 +11,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; @@ -20,7 +20,7 @@ import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.util.FileUtils; -class PlagiarismCheckIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class PlagiarismCheckIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "plagiarismcheck"; diff --git a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismIntegrationTest.java index b93cd24f6567..6be418637f43 100644 --- a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismIntegrationTest.java @@ -11,7 +11,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.TextExercise; @@ -31,7 +31,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.rest.dto.PlagiarismComparisonStatusDTO; -class PlagiarismIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class PlagiarismIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "plagiarismintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/repository/StatisticsRepositoryTest.java b/src/test/java/de/tum/in/www1/artemis/repository/StatisticsRepositoryTest.java index 4b5ef06893e4..5634ce479334 100644 --- a/src/test/java/de/tum/in/www1/artemis/repository/StatisticsRepositoryTest.java +++ b/src/test/java/de/tum/in/www1/artemis/repository/StatisticsRepositoryTest.java @@ -15,7 +15,7 @@ import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.PersistentAuditEvent; import de.tum.in.www1.artemis.domain.enumeration.GraphType; import de.tum.in.www1.artemis.domain.enumeration.SpanType; @@ -24,7 +24,7 @@ import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.user.UserUtilService; -class StatisticsRepositoryTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class StatisticsRepositoryTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "statisticsrepository"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/AssessmentServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/AssessmentServiceTest.java index 53895cf69960..822ee8487c62 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/AssessmentServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/AssessmentServiceTest.java @@ -15,7 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseFactory; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.DiagramType; @@ -36,7 +36,7 @@ import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class AssessmentServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class AssessmentServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "assessmentservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/BuildLogEntryServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/BuildLogEntryServiceTest.java index cbd61c178b7d..65831858ec52 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/BuildLogEntryServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/BuildLogEntryServiceTest.java @@ -14,11 +14,11 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.BuildLogEntry; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; -class BuildLogEntryServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class BuildLogEntryServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String GRADLE_SCENARIO = """ Build ABC23H01E01 - AB12345 - Default Job #5 (MY-JOB) started building on agent ls1Agent-test.artemistest.in.tum.de, bamboo version: 8.2.5 diff --git a/src/test/java/de/tum/in/www1/artemis/service/ComplaintResponseServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ComplaintResponseServiceTest.java index 879ac8ad47da..f14b3f369639 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ComplaintResponseServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ComplaintResponseServiceTest.java @@ -9,7 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.assessment.ComplaintUtilService; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.team.TeamUtilService; import de.tum.in.www1.artemis.user.UserUtilService; -class ComplaintResponseServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ComplaintResponseServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "complaintresponseservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/CourseScoreCalculationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/CourseScoreCalculationServiceTest.java index eff0610fdfcb..9b7b549de9ba 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/CourseScoreCalculationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/CourseScoreCalculationServiceTest.java @@ -12,7 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.assessment.GradingScaleFactory; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; @@ -28,7 +28,7 @@ import de.tum.in.www1.artemis.web.rest.dto.CourseScoresDTO; import de.tum.in.www1.artemis.web.rest.dto.StudentScoresDTO; -class CourseScoreCalculationServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class CourseScoreCalculationServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "cscservicetest"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/DataExportCreationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/DataExportCreationServiceTest.java index d5ddd2c30e30..942f96a169f3 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/DataExportCreationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/DataExportCreationServiceTest.java @@ -528,7 +528,7 @@ private void addOnlyAnswerPostInCourse(Course course) { void testDataExportCreationError_handlesErrorAndInformsUserAndAdmin() { var dataExport = initDataExport(); Exception exception = new RuntimeException("error"); - doThrow(exception).when(fileService).scheduleForDirectoryDeletion(any(Path.class), anyLong()); + doThrow(exception).when(fileService).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), anyLong()); doNothing().when(mailService).sendDataExportFailedEmailToAdmin(any(), any(), any()); doNothing().when(singleUserNotificationService).notifyUserAboutDataExportCreation(any(DataExport.class)); dataExportCreationService.createDataExport(dataExport); diff --git a/src/test/java/de/tum/in/www1/artemis/service/EmailSummaryServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/EmailSummaryServiceTest.java index 1a74cc9d8646..0945cd521c5f 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/EmailSummaryServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/EmailSummaryServiceTest.java @@ -17,7 +17,7 @@ import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.DifficultyLevel; @@ -27,7 +27,7 @@ import de.tum.in.www1.artemis.repository.NotificationSettingRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class EmailSummaryServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class EmailSummaryServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "emailsummaryservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/ExerciseDateServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ExerciseDateServiceTest.java index 6ea4e0ca6eb4..fc452f2ba014 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ExerciseDateServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ExerciseDateServiceTest.java @@ -11,7 +11,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.enumeration.DiagramType; import de.tum.in.www1.artemis.domain.exam.Exam; @@ -31,7 +31,7 @@ import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.user.UserUtilService; -class ExerciseDateServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExerciseDateServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "exercisedateservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/ExerciseLifecycleServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ExerciseLifecycleServiceTest.java index 50232ec49d7c..1c2b035a781f 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ExerciseLifecycleServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ExerciseLifecycleServiceTest.java @@ -11,12 +11,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.enumeration.ExerciseLifecycle; -class ExerciseLifecycleServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExerciseLifecycleServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private ExerciseLifecycleService exerciseLifecycleService; diff --git a/src/test/java/de/tum/in/www1/artemis/service/FeatureToggleServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/FeatureToggleServiceTest.java index 758b8fc2e204..4e07f0b2153a 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/FeatureToggleServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/FeatureToggleServiceTest.java @@ -9,11 +9,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.service.feature.Feature; import de.tum.in.www1.artemis.service.feature.FeatureToggleService; -class FeatureToggleServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class FeatureToggleServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private FeatureToggleService featureToggleService; diff --git a/src/test/java/de/tum/in/www1/artemis/service/FeedbackServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/FeedbackServiceTest.java index 5259def297ac..a9e48217c5e1 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/FeedbackServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/FeedbackServiceTest.java @@ -5,13 +5,13 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.Feedback; import de.tum.in.www1.artemis.domain.LongFeedbackText; import de.tum.in.www1.artemis.repository.FeedbackRepository; -class FeedbackServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class FeedbackServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private FeedbackService feedbackService; diff --git a/src/test/java/de/tum/in/www1/artemis/service/FilePathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/FilePathServiceTest.java new file mode 100644 index 000000000000..477d35feb7e4 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/service/FilePathServiceTest.java @@ -0,0 +1,80 @@ +package de.tum.in.www1.artemis.service; + +import static org.assertj.core.api.Assertions.*; + +import java.net.URI; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.exception.FilePathParsingException; + +class FilePathServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { + + @Autowired + private FilePathService filePathService; + + @Test + void testActualPathForPublicPath() { + Path actualPath = filePathService.actualPathForPublicPath(URI.create("/api/files/drag-and-drop/backgrounds/background.jpeg")); + assertThat(actualPath).isEqualTo(Path.of("uploads", "images", "drag-and-drop", "backgrounds", "background.jpeg")); + + actualPath = filePathService.actualPathForPublicPath(URI.create("/api/files/drag-and-drop/drag-items/image.jpeg")); + assertThat(actualPath).isEqualTo(Path.of("uploads", "images", "drag-and-drop", "drag-items", "image.jpeg")); + + actualPath = filePathService.actualPathForPublicPath(URI.create("/api/files/course/icons/icon.png")); + assertThat(actualPath).isEqualTo(Path.of("uploads", "images", "course", "icons", "icon.png")); + + actualPath = filePathService.actualPathForPublicPath(URI.create("/api/files/attachments/lecture/4/slides.pdf")); + assertThat(actualPath).isEqualTo(Path.of("uploads", "attachments", "lecture", "4", "slides.pdf")); + + actualPath = filePathService.actualPathForPublicPath(URI.create("/api/files/attachments/attachment-unit/4/download.pdf")); + assertThat(actualPath).isEqualTo(Path.of("uploads", "attachments", "attachment-unit", "4", "download.pdf")); + + actualPath = filePathService.actualPathForPublicPath(URI.create("/api/files/attachments/attachment-unit/4/slide/1/1.jpg")); + assertThat(actualPath).isEqualTo(Path.of("uploads", "attachments", "attachment-unit", "4", "slide", "1", "1.jpg")); + } + + @Test + void testActualPathForPublicFileUploadExercisePath_shouldReturnNull() { + Path path = filePathService.actualPathForPublicPath(URI.create("/api/unknown-path/unknown-file.pdf")); + assertThat(path).isNull(); + } + + @Test + void testActualPathForPublicFileUploadExercisePathOrThrow_shouldThrowException() { + assertThatExceptionOfType(FilePathParsingException.class) + .isThrownBy(() -> filePathService.actualPathForPublicPathOrThrow(URI.create("/api/files/file-upload-exercises/file.pdf"))) + .withMessageStartingWith("Public path does not contain correct exerciseId or submissionId:"); + + assertThatExceptionOfType(FilePathParsingException.class).isThrownBy(() -> filePathService.actualPathForPublicPathOrThrow(URI.create("/api/unknown-path/unknown-file.pdf"))) + .withMessageStartingWith("Unknown Filepath:"); + } + + @Test + void testPublicPathForActualTempFilePath() { + Path actualPath = FilePathService.getTempFilePath().resolve("test"); + URI publicPath = filePathService.publicPathForActualPath(actualPath, 1L); + assertThat(publicPath).isEqualTo(URI.create(FileService.DEFAULT_FILE_SUBPATH + actualPath.getFileName())); + } + + @Test + void testPublicPathForActualPath_shouldReturnNull() { + URI otherPath = filePathService.publicPathForActualPath(Path.of("unknown-path", "unknown-file.pdf"), 1L); + assertThat(otherPath).isNull(); + } + + @Test + void testPublicPathForActualPath_shouldThrowException() { + assertThatExceptionOfType(FilePathParsingException.class).isThrownBy(() -> { + Path actualFileUploadPath = FilePathService.getFileUploadExercisesFilePath(); + filePathService.publicPathForActualPathOrThrow(actualFileUploadPath, 1L); + + }).withMessageStartingWith("Unexpected String in upload file path. Exercise ID should be present here:"); + + assertThatExceptionOfType(FilePathParsingException.class).isThrownBy(() -> filePathService.publicPathForActualPathOrThrow(Path.of("unknown-path", "unknown-file.pdf"), 1L)) + .withMessageStartingWith("Unknown Filepath:"); + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/service/FileServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/FileServiceTest.java index 9608e4605370..c38321adb4a1 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/FileServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/FileServiceTest.java @@ -3,9 +3,11 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; -import java.io.*; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -23,15 +25,18 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.util.ResourceUtils; +import org.springframework.web.multipart.MultipartFile; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; -import de.tum.in.www1.artemis.exception.FilePathParsingException; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; -class FileServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class FileServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private ResourceLoaderService resourceLoaderService; + @Autowired + private FileService fileService; + private final Path javaPath = Path.of("templates", "java", "java.txt"); // the resource loader allows to load resources from the file system for this prefix @@ -101,6 +106,54 @@ void deleteFiles() throws IOException { FileUtils.deleteDirectory(Path.of(".", "exportTest").toFile()); } + @Test + void testGetFileForPath() throws IOException { + writeFile("testFile.txt", FILE_WITH_UNIX_LINE_ENDINGS); + byte[] result = fileService.getFileForPath(Path.of(".", "exportTest", "testFile.txt")); + assertThat(result).containsExactly(FILE_WITH_UNIX_LINE_ENDINGS.getBytes(StandardCharsets.UTF_8)); + } + + @Test + void testGetFileFOrPath_notFound() throws IOException { + writeFile("testFile.txt", FILE_WITH_UNIX_LINE_ENDINGS); + byte[] result = fileService.getFileForPath(Path.of(".", "exportTest", UUID.randomUUID() + ".txt")); + assertThat(result).isNull(); + } + + @Test + void testHandleSaveFile_noOriginalFilename() { + MultipartFile file = mock(MultipartFile.class); + doAnswer(invocation -> null).when(file).getOriginalFilename(); + assertThatThrownBy(() -> fileService.handleSaveFile(file, false, false)).isInstanceOf(IllegalArgumentException.class); + verify(file, times(1)).getOriginalFilename(); + } + + @Test + void testCopyExistingFileToTarget() throws IOException { + String payload = "test"; + Path filePath = Path.of(".", "exportTest", "testFile.txt"); + FileUtils.writeStringToFile(filePath.toFile(), payload, StandardCharsets.UTF_8); + Path newFolder = Path.of(".", "exportTest", "newFolder"); + + Path newPath = fileService.copyExistingFileToTarget(filePath, newFolder); + assertThat(newPath).isNotNull(); + + assertThat(FileUtils.readFileToString(newPath.toFile(), StandardCharsets.UTF_8)).isEqualTo(payload); + } + + @Test + void testCopyExistingFileToTarget_newFile() { + assertThat(fileService.copyExistingFileToTarget(null, Path.of(".", "exportTest"))).isNull(); + } + + @Test + void testCopyExistingFileToTarget_temporaryFile() { + // We don't need to create a file here as we expect the method to terminate early + Path tempPath = Path.of(".", "uploads", "files", "temp", "testFile.txt"); + Path newPath = Path.of(".", "exportTest"); + assertThat(fileService.copyExistingFileToTarget(tempPath, newPath)).isNull(); + } + @Test void normalizeFileEndingsUnix_noChange() throws IOException { writeFile("LineEndingsUnix.java", FILE_WITH_UNIX_LINE_ENDINGS); @@ -233,70 +286,6 @@ void testMergePdf() throws IOException { assertThat(mergedDoc.getNumberOfPages()).isEqualTo(5); } - @Test - void testManageFilesForUpdatedFilePath_shouldNotThrowException() { - assertThatNoException().isThrownBy(() -> fileService.manageFilesForUpdatedFilePath("oldFilePath", "newFilePath", "targetFolder", 1L, true)); - } - - @Test - void testActualPathForPublicPath() { - String actualPath = fileService.actualPathForPublicPath("asdasdfiles/drag-and-drop/backgrounds"); - assertThat(actualPath).isEqualTo(Path.of("uploads", "images", "drag-and-drop", "backgrounds", "backgrounds").toString()); - - actualPath = fileService.actualPathForPublicPath("asdasdfiles/drag-and-drop/drag-items"); - assertThat(actualPath).isEqualTo(Path.of("uploads", "images", "drag-and-drop", "drag-items", "drag-items").toString()); - - actualPath = fileService.actualPathForPublicPath("asdasdfiles/course/icons"); - assertThat(actualPath).isEqualTo(Path.of("uploads", "images", "course", "icons", "icons").toString()); - - actualPath = fileService.actualPathForPublicPath("asdasdfiles/attachments/lecture"); - assertThat(actualPath).isEqualTo(Path.of("uploads", "attachments", "lecture", "asdasdfiles", "attachments", "lecture").toString()); - - actualPath = fileService.actualPathForPublicPath("asdasdfiles/attachments/attachment-unit"); - assertThat(actualPath).isEqualTo(Path.of("uploads", "attachments", "attachment-unit", "asdasdfiles", "attachments", "attachment-unit").toString()); - } - - @Test - void testActualPathForPublicFileUploadExercisePath_shouldReturnNull() { - String path = fileService.actualPathForPublicPath("asdasdfiles/file-asd-exercises"); - assertThat(path).isNull(); - } - - @Test - void testActualPathForPublicFileUploadExercisePathOrThrow_shouldThrowException() { - assertThatExceptionOfType(FilePathParsingException.class).isThrownBy(() -> fileService.actualPathForPublicPathOrThrow("asdasdfiles/file-upload-exercises")) - .withMessageStartingWith("Public path does not contain correct exerciseId or submissionId:"); - - assertThatExceptionOfType(FilePathParsingException.class).isThrownBy(() -> fileService.actualPathForPublicPathOrThrow("asdasdfiles/file-asd-exercises")) - .withMessageStartingWith("Unknown Filepath:"); - } - - @Test - void testPublicPathForActualTempFilePath() { - Path actualPath = Path.of(FilePathService.getTempFilePath(), "test"); - String publicPath = fileService.publicPathForActualPath(actualPath.toString(), 1L); - assertThat(publicPath).isEqualTo(FileService.DEFAULT_FILE_SUBPATH + actualPath.getFileName()); - } - - @Test - void testPublicPathForActualPath_shouldReturnNull() { - String otherPath = fileService.publicPathForActualPath(Path.of("asdasdfiles", "file-asd-exercises").toString(), 1L); - assertThat(otherPath).isNull(); - } - - @Test - void testPublicPathForActualPathOrThrow_shouldThrowException() { - assertThatExceptionOfType(FilePathParsingException.class).isThrownBy(() -> { - Path actualFileUploadPath = Path.of(FilePathService.getFileUploadExercisesFilePath()); - fileService.publicPathForActualPathOrThrow(actualFileUploadPath.toString(), 1L); - - }).withMessageStartingWith("Unexpected String in upload file path. Exercise ID should be present here:"); - - assertThatExceptionOfType(FilePathParsingException.class) - .isThrownBy(() -> fileService.publicPathForActualPathOrThrow(Path.of("asdasdfiles", "file-asd-exercises").toString(), 1L)) - .withMessageStartingWith("Unknown Filepath:"); - } - @Test void testReplaceVariablesInFileRecursive_shouldThrowException() { assertThatRuntimeException().isThrownBy(() -> fileService.replaceVariablesInFileRecursive(Path.of("some-path"), new HashMap<>())) @@ -311,7 +300,7 @@ void testNormalizeLineEndingsDirectory_shouldThrowException() { @Test void testConvertToUTF8Directory_shouldThrowException() { - assertThatRuntimeException().isThrownBy(() -> fileService.convertToUTF8Directory(Path.of("some-path"))) + assertThatRuntimeException().isThrownBy(() -> fileService.convertFilesInDirectoryToUtf8(Path.of("some-path"))) .withMessageEndingWith("should be converted to UTF-8 but the directory does not exist."); } @@ -319,18 +308,12 @@ void testConvertToUTF8Directory_shouldThrowException() { @Test void testGetUniqueTemporaryPath_shouldNotThrowException() { assertThatNoException().isThrownBy(() -> { - var uniquePath = fileService.getTemporaryUniquePath(Path.of("some-random-path-which-does-not-exist"), 1); + var uniquePath = fileService.getTemporaryUniqueSubfolderPath(Path.of("some-random-path-which-does-not-exist"), 1); assertThat(uniquePath.toString()).isNotEmpty(); - verify(fileService).scheduleForDirectoryDeletion(any(Path.class), eq(1L)); + verify(fileService).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(1L)); }); } - @Test - void testCreateDirectory_shouldNotThrowException() { - Path path = Path.of("some-random-path-which-does-not-exist"); - assertThatNoException().isThrownBy(() -> fileService.createDirectory(path)); - } - @Test void testDeleteFiles_shouldNotThrowException() { Path path = Path.of("some-random-path-which-does-not-exist"); diff --git a/src/test/java/de/tum/in/www1/artemis/service/GradingScaleServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/GradingScaleServiceTest.java index c8d7df1d058d..fb0aca636f3b 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/GradingScaleServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/GradingScaleServiceTest.java @@ -15,7 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.assessment.GradingScaleUtilService; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; @@ -30,7 +30,7 @@ import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; -class GradingScaleServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class GradingScaleServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private GradingScaleService gradingScaleService; diff --git a/src/test/java/de/tum/in/www1/artemis/service/LectureImportServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LectureImportServiceTest.java index 28f20a5ccd45..264fa1646105 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LectureImportServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LectureImportServiceTest.java @@ -10,7 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Attachment; import de.tum.in.www1.artemis.domain.Course; @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.repository.LectureRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class LectureImportServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class LectureImportServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "lectureimport"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/LectureServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LectureServiceTest.java index bed4e1855747..b5fe10cefade 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LectureServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LectureServiceTest.java @@ -13,7 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.lecture.LectureFactory; import de.tum.in.www1.artemis.lecture.LectureUtilService; @@ -24,7 +24,7 @@ import de.tum.in.www1.artemis.web.rest.dto.PageableSearchDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; -class LectureServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class LectureServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "lservicetest"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/ParticipationAuthorizationCheckServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ParticipationAuthorizationCheckServiceTest.java index ac2fc33c1825..44009ed315dc 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ParticipationAuthorizationCheckServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ParticipationAuthorizationCheckServiceTest.java @@ -9,7 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.participation.ParticipationInterface; @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; -class ParticipationAuthorizationCheckServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ParticipationAuthorizationCheckServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "participationauthservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/ParticipationLifecycleServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ParticipationLifecycleServiceTest.java index eba717ea7718..61d5b54735f5 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ParticipationLifecycleServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ParticipationLifecycleServiceTest.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.enumeration.ParticipationLifecycle; @@ -23,7 +23,7 @@ import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.user.UserUtilService; -class ParticipationLifecycleServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ParticipationLifecycleServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "partlcservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/ParticipationTeamWebsocketServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ParticipationTeamWebsocketServiceTest.java index 73bd3015cca2..0bf22e90f7c3 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ParticipationTeamWebsocketServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ParticipationTeamWebsocketServiceTest.java @@ -13,7 +13,7 @@ import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.participation.Participation; @@ -24,7 +24,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.websocket.team.ParticipationTeamWebsocketService; -class ParticipationTeamWebsocketServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ParticipationTeamWebsocketServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "participationteamwebsocket"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/PresentationPointsCalculationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/PresentationPointsCalculationServiceTest.java index 09e1f34130b5..5cea54fee584 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/PresentationPointsCalculationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/PresentationPointsCalculationServiceTest.java @@ -7,7 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.assessment.GradingScaleFactory; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.repository.StudentParticipationRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class PresentationPointsCalculationServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class PresentationPointsCalculationServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "ppcservicetest"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/ResourceLoaderServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ResourceLoaderServiceTest.java index 755c428e1af3..549d78dd50c6 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ResourceLoaderServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ResourceLoaderServiceTest.java @@ -25,9 +25,9 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; -class ResourceLoaderServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ResourceLoaderServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private ResourceLoaderService resourceLoaderService; @@ -132,9 +132,8 @@ void testGetResourceFilePathFromJar() throws IOException, URISyntaxException { // Mock the getResource() method. doReturn(true).when(resource).exists(); doReturn(resourceUrl).when(resource).getURL(); - doReturn(mock(InputStream.class)).when(resource).getInputStream(); + doReturn(InputStream.nullInputStream()).when(resource).getInputStream(); - // ResourcePatternResolver resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); doReturn(resource).when(resourceLoader).getResource(anyString()); // Instantiate the class under test and invoke the method. diff --git a/src/test/java/de/tum/in/www1/artemis/service/ResultServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ResultServiceTest.java index eeafd1404e59..a595692b57b2 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ResultServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ResultServiceTest.java @@ -12,7 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Feedback; import de.tum.in.www1.artemis.domain.ProgrammingExercise; @@ -31,7 +31,7 @@ import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class ResultServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ResultServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "resultservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/SubmissionServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/SubmissionServiceTest.java index 4b039d29e335..e2f74d8cbed4 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/SubmissionServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/SubmissionServiceTest.java @@ -13,7 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.assessment.ComplaintUtilService; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; @@ -31,7 +31,7 @@ import de.tum.in.www1.artemis.web.rest.dto.SubmissionWithComplaintDTO; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; -class SubmissionServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class SubmissionServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "submissionservicetest"; // only lower case is supported diff --git a/src/test/java/de/tum/in/www1/artemis/service/TeamWebsocketServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/TeamWebsocketServiceTest.java index 45d150a0f2c8..9152aae542dd 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/TeamWebsocketServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/TeamWebsocketServiceTest.java @@ -14,7 +14,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; @@ -27,7 +27,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.websocket.dto.TeamAssignmentPayload; -class TeamWebsocketServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TeamWebsocketServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "teamwebsocketservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/TitleCacheEvictionServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/TitleCacheEvictionServiceTest.java index 5b9fe49d9702..ee2faa517e08 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/TitleCacheEvictionServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/TitleCacheEvictionServiceTest.java @@ -9,7 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.CacheManager; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.enumeration.DiagramType; @@ -30,7 +30,7 @@ * The service is not directly injected / used here as it listens to Hibernate events, so we just apply * CRUD operations on the entities it supports. */ -class TitleCacheEvictionServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TitleCacheEvictionServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private CacheManager cacheManager; diff --git a/src/test/java/de/tum/in/www1/artemis/service/UrlServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/UrlServiceTest.java index 29554d104d68..3c80424663c4 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/UrlServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/UrlServiceTest.java @@ -8,14 +8,14 @@ import org.junit.jupiter.api.Test; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.domain.participation.SolutionProgrammingExerciseParticipation; import de.tum.in.www1.artemis.domain.participation.TemplateProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exception.VersionControlException; -class UrlServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class UrlServiceTest extends AbstractSpringIntegrationIndependentTest { private final VcsRepositoryUrl repositoryUrl1 = new VcsRepositoryUrl("https://ab12cde@bitbucket.ase.in.tum.de/scm/EIST2016RME/RMEXERCISE-ab12cde"); diff --git a/src/test/java/de/tum/in/www1/artemis/service/ZipFileServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ZipFileServiceTest.java index 82d5a8b0a7be..b296f3eb4e9c 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ZipFileServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ZipFileServiceTest.java @@ -12,9 +12,9 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; -class ZipFileServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ZipFileServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private ZipFileService zipFileService; @@ -50,7 +50,7 @@ void testCreateTemporaryZipFileSchedulesFileForDeletion() throws IOException { var tempZipFile = Files.createTempFile("test", ".zip"); zipFileService.createTemporaryZipFile(tempZipFile, List.of(), 5); assertThat(tempZipFile).exists(); - verify(fileService).scheduleForDeletion(tempZipFile, 5L); + verify(fileService).schedulePathForDeletion(tempZipFile, 5L); } } diff --git a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamAccessServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamAccessServiceTest.java index c949623d621f..7879349a1bb7 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamAccessServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamAccessServiceTest.java @@ -13,7 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.User; @@ -31,7 +31,7 @@ import de.tum.in.www1.artemis.web.rest.errors.ConflictException; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; -class ExamAccessServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExamAccessServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "examaccessservicetest"; // only lower case is supported diff --git a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamQuizServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamQuizServiceTest.java index 6b256958b001..4ebf28198f31 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamQuizServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamQuizServiceTest.java @@ -14,7 +14,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.enumeration.InitializationState; @@ -29,7 +29,7 @@ import de.tum.in.www1.artemis.service.QuizExerciseService; import de.tum.in.www1.artemis.user.UserUtilService; -class ExamQuizServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExamQuizServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "eqservicetest"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamServiceTest.java index d4f24384c817..0b067e1267b9 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamServiceTest.java @@ -13,7 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.TextExercise; @@ -29,7 +29,7 @@ import de.tum.in.www1.artemis.web.rest.dto.ExamChecklistDTO; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; -class ExamServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExamServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private ExamService examService; diff --git a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionServiceTest.java index dbd138dcb7e8..f2217fb1b452 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionServiceTest.java @@ -10,7 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Exercise; @@ -32,7 +32,7 @@ import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; -class ExamSubmissionServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ExamSubmissionServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "esstest"; // only lower case is supported diff --git a/src/test/java/de/tum/in/www1/artemis/service/exam/StudentExamAccessServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/exam/StudentExamAccessServiceTest.java index b5200442b455..27de96ba7b44 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/exam/StudentExamAccessServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/exam/StudentExamAccessServiceTest.java @@ -10,7 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.User; @@ -24,7 +24,7 @@ import de.tum.in.www1.artemis.web.rest.errors.ConflictException; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; -class StudentExamAccessServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class StudentExamAccessServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "seastest"; // only lower case is supported diff --git a/src/test/java/de/tum/in/www1/artemis/service/notifications/ConversationNotificationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/notifications/ConversationNotificationServiceTest.java index 614c41470b7e..3af87124c965 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/notifications/ConversationNotificationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/notifications/ConversationNotificationServiceTest.java @@ -15,7 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.DomainObject; @@ -33,7 +33,7 @@ import de.tum.in.www1.artemis.repository.metis.conversation.ConversationRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class ConversationNotificationServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ConversationNotificationServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "conversationnotificationservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/notifications/TutorialGroupNotificationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/notifications/TutorialGroupNotificationServiceTest.java index 6e2ea3d4930c..a6dd74b3baca 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/notifications/TutorialGroupNotificationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/notifications/TutorialGroupNotificationServiceTest.java @@ -21,7 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.Language; @@ -38,7 +38,7 @@ import de.tum.in.www1.artemis.repository.tutorialgroups.TutorialGroupRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class TutorialGroupNotificationServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TutorialGroupNotificationServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "tutorialgroupnotifservice"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseFeedbackCreationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseFeedbackCreationServiceTest.java index b198dbd50b7f..460fd2662a6f 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseFeedbackCreationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseFeedbackCreationServiceTest.java @@ -9,7 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.Feedback; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; @@ -18,7 +18,7 @@ import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; import de.tum.in.www1.artemis.service.dto.StaticCodeAnalysisReportDTO; -class ProgrammingExerciseFeedbackCreationServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class ProgrammingExerciseFeedbackCreationServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "progexfeedbackcreaiontest"; diff --git a/src/test/java/de/tum/in/www1/artemis/service/scheduled/DataExportScheduleServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/scheduled/DataExportScheduleServiceTest.java index 196f0ac906d2..e3e98a1a9a40 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/scheduled/DataExportScheduleServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/scheduled/DataExportScheduleServiceTest.java @@ -75,7 +75,7 @@ void testScheduledCronTaskSendsEmailToAdminAboutSuccessfulDataExports() throws I createDataExportWithState(DataExportState.REQUESTED); createDataExportWithState(DataExportState.REQUESTED); // first data export creation should fail, the subsequent ones should succeed - doThrow(new RuntimeException("error")).doNothing().doNothing().when(fileService).scheduleForDirectoryDeletion(any(Path.class), anyLong()); + doThrow(new RuntimeException("error")).doNothing().doNothing().when(fileService).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), anyLong()); dataExportScheduleService.createDataExportsAndDeleteOldOnes(); var dataExportsAfterCreation = dataExportRepository.findAllSuccessfullyCreatedDataExports(); verify(mailService).sendSuccessfulDataExportsEmailToAdmin(any(User.class), anyString(), anyString(), eq(Set.copyOf(dataExportsAfterCreation))); @@ -91,7 +91,7 @@ private static Stream provideDataExportStatesAndExpectedToBeCreated() @MethodSource("provideCreationDatesAndExpectedToDelete") void testScheduledCronTaskDeletesOldDataExports(ZonedDateTime creationDate, DataExportState state, boolean shouldDelete) throws InterruptedException { var dataExport = createDataExportWithCreationDateAndState(creationDate, state); - doNothing().when(fileService).scheduleForDeletion(any(), anyLong()); + doNothing().when(fileService).schedulePathForDeletion(any(), anyLong()); var dataExportId = dataExport.getId(); dataExportScheduleService.createDataExportsAndDeleteOldOnes(); var dataExportFromDb = dataExportRepository.findByIdElseThrow(dataExportId); diff --git a/src/test/java/de/tum/in/www1/artemis/service/scheduled/PushNotificationDeviceConfigurationCleanupServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/scheduled/PushNotificationDeviceConfigurationCleanupServiceTest.java index 67695bdffd5a..70f760a28185 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/scheduled/PushNotificationDeviceConfigurationCleanupServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/scheduled/PushNotificationDeviceConfigurationCleanupServiceTest.java @@ -10,14 +10,14 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.push_notification.PushNotificationDeviceConfiguration; import de.tum.in.www1.artemis.domain.push_notification.PushNotificationDeviceType; import de.tum.in.www1.artemis.repository.PushNotificationDeviceConfigurationRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class PushNotificationDeviceConfigurationCleanupServiceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class PushNotificationDeviceConfigurationCleanupServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private PushNotificationDeviceConfigurationRepository deviceConfigurationRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizCacheTest.java b/src/test/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizCacheTest.java index 37ae07649b47..4789b09607f9 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizCacheTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizCacheTest.java @@ -1,14 +1,22 @@ package de.tum.in.www1.artemis.service.scheduled.cache.quiz; import static de.tum.in.www1.artemis.service.scheduled.cache.quiz.QuizCache.HAZELCAST_CACHED_EXERCISE_UPDATE_TOPIC; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import java.security.Principal; import java.time.ZonedDateTime; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -16,21 +24,26 @@ import com.hazelcast.core.HazelcastInstance; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Result; import de.tum.in.www1.artemis.domain.enumeration.QuizMode; +import de.tum.in.www1.artemis.domain.quiz.QuizBatch; import de.tum.in.www1.artemis.domain.quiz.QuizExercise; import de.tum.in.www1.artemis.domain.quiz.QuizSubmission; import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseUtilService; +import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.QuizExerciseRepository; +import de.tum.in.www1.artemis.repository.QuizSubmissionRepository; import de.tum.in.www1.artemis.service.QuizBatchService; import de.tum.in.www1.artemis.service.QuizExerciseService; import de.tum.in.www1.artemis.user.UserUtilService; +import de.tum.in.www1.artemis.web.websocket.QuizSubmissionWebsocketService; -class QuizCacheTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +@Isolated +class QuizCacheTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "quizcachetest"; @@ -55,6 +68,15 @@ class QuizCacheTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @Autowired private QuizExerciseRepository quizExerciseRepository; + @Autowired + QuizSubmissionWebsocketService quizSubmissionWebsocketService; + + @Autowired + QuizSubmissionRepository submissionRepository; + + @Autowired + ParticipationUtilService participationUtilService; + @BeforeEach void init() { // do not use the schedule service based on a time interval in the tests, because this would result in flaky tests that run much slower @@ -97,4 +119,55 @@ void testQuizSubmitNoDatabaseRequests(QuizMode quizMode) throws Exception { assertThatDb(() -> request.postWithResponseBody("/api/exercises/" + exerciseId + "/submissions/live", quizSubmission, Result.class, HttpStatus.OK)) .hasBeenCalledTimes(quizMode == QuizMode.SYNCHRONIZED ? 0 : 1); } + + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + @CsvSource({ "true,true", "true,false", "false,true", "false,false" }) + void testProcessSubmission(boolean submitted, boolean deleted) { + QuizExercise quizExercise = quizExerciseUtilService.createQuiz(ZonedDateTime.now().minusMinutes(1), null, QuizMode.SYNCHRONIZED); + quizExercise.duration(240); + quizExerciseRepository.save(quizExercise); + + QuizSubmission quizSubmission = QuizExerciseFactory.generateSubmissionForThreeQuestions(quizExercise, 1, submitted, null); + String username = TEST_PREFIX + "student1"; + Principal principal = () -> username; + + quizSubmissionWebsocketService.saveSubmission(quizExercise.getId(), quizSubmission, principal); + if (deleted) { + quizExerciseRepository.delete(quizExercise); + } + quizScheduleService.processCachedQuizSubmissions(); + assertThat(submissionRepository.findByParticipation_Exercise_Id(quizExercise.getId())).hasSize(submitted && !deleted ? 1 : 0); + } + + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + @CsvSource({ "true,true", "true,false", "false,true", "false,false" }) + void testQuizBatchEnded(boolean quizEnded, boolean batchEnded) { + QuizExercise quizExercise = quizExerciseUtilService.createQuiz(ZonedDateTime.now().minusMinutes(2), quizEnded ? ZonedDateTime.now().minusMinutes(1) : null, + QuizMode.BATCHED); + quizExercise.duration(batchEnded ? 5 : 3600); + quizExerciseRepository.save(quizExercise); + + var batch = quizBatchService.save(QuizExerciseFactory.generateQuizBatch(quizExercise, ZonedDateTime.now().minusMinutes(1))); + quizExerciseUtilService.joinQuizBatch(quizExercise, batch, TEST_PREFIX + "student1"); + + quizScheduleService.processCachedQuizSubmissions(); + + assertThat(submissionRepository.findByParticipation_Exercise_Id(quizExercise.getId())).hasSize(batchEnded ? 1 : 0); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testQuizNewParticipationAndStatistics() { + QuizExercise quizExercise = quizExerciseUtilService.createQuiz(ZonedDateTime.now().minusMinutes(2), ZonedDateTime.now().minusMinutes(1), QuizMode.SYNCHRONIZED); + quizExercise.duration(5); + quizExerciseService.save(quizExercise); + + QuizBatch batch = quizBatchService.save(QuizExerciseFactory.generateQuizBatch(quizExercise, ZonedDateTime.now().minusMinutes(1))); + quizExerciseUtilService.joinQuizBatch(quizExercise, batch, TEST_PREFIX + "student1"); + + quizScheduleService.processCachedQuizSubmissions(); + verify(websocketMessagingService, timeout(3000)).sendMessageToUser(any(), any(), any()); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/team/TeamImportIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/team/TeamImportIntegrationTest.java index 5a8d8fe9cab5..2efa091f3fc5 100644 --- a/src/test/java/de/tum/in/www1/artemis/team/TeamImportIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/team/TeamImportIntegrationTest.java @@ -14,7 +14,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; @@ -23,7 +23,7 @@ import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.user.UserUtilService; -class TeamImportIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TeamImportIntegrationTest extends AbstractSpringIntegrationIndependentTest { @Autowired private CourseRepository courseRepo; diff --git a/src/test/java/de/tum/in/www1/artemis/team/TeamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/team/TeamIntegrationTest.java index 4480cd86a5f3..b692c72f7f97 100644 --- a/src/test/java/de/tum/in/www1/artemis/team/TeamIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/team/TeamIntegrationTest.java @@ -13,7 +13,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; @@ -30,7 +30,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.rest.dto.CourseForDashboardDTO; -class TeamIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TeamIntegrationTest extends AbstractSpringIntegrationIndependentTest { @Autowired private CourseRepository courseRepo; diff --git a/src/test/java/de/tum/in/www1/artemis/text/AssessmentEventIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/text/AssessmentEventIntegrationTest.java index ce9541bb838b..febe53d52064 100644 --- a/src/test/java/de/tum/in/www1/artemis/text/AssessmentEventIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/text/AssessmentEventIntegrationTest.java @@ -10,7 +10,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Exercise; @@ -26,7 +26,7 @@ import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.user.UserUtilService; -class AssessmentEventIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class AssessmentEventIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "assessmentevent"; diff --git a/src/test/java/de/tum/in/www1/artemis/text/TextExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/text/TextExerciseIntegrationTest.java index 75c1cd83c89c..f7db1a2b9e44 100644 --- a/src/test/java/de/tum/in/www1/artemis/text/TextExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/text/TextExerciseIntegrationTest.java @@ -19,7 +19,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.*; @@ -48,7 +48,7 @@ import de.tum.in.www1.artemis.web.rest.dto.CourseForDashboardDTO; import de.tum.in.www1.artemis.web.rest.dto.PlagiarismComparisonStatusDTO; -class TextExerciseIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TextExerciseIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "textexerciseintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/text/TextSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/text/TextSubmissionIntegrationTest.java index 8f98694994fb..214c1028a87c 100644 --- a/src/test/java/de/tum/in/www1/artemis/text/TextSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/text/TextSubmissionIntegrationTest.java @@ -15,7 +15,7 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; @@ -41,7 +41,7 @@ import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; -class TextSubmissionIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class TextSubmissionIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "textsubmissionintegration"; diff --git a/src/test/java/de/tum/in/www1/artemis/tutorialgroups/AbstractTutorialGroupIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/tutorialgroups/AbstractTutorialGroupIntegrationTest.java index 1ecd0ae8f185..4d8b9266e026 100644 --- a/src/test/java/de/tum/in/www1/artemis/tutorialgroups/AbstractTutorialGroupIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/tutorialgroups/AbstractTutorialGroupIntegrationTest.java @@ -14,7 +14,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; import de.tum.in.www1.artemis.course.CourseTestService; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.User; @@ -35,7 +35,7 @@ /** * Contains useful methods for testing the tutorial groups feature. */ -abstract class AbstractTutorialGroupIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +abstract class AbstractTutorialGroupIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { @Autowired CourseTestService courseTestService; diff --git a/src/test/java/de/tum/in/www1/artemis/user/UserTestService.java b/src/test/java/de/tum/in/www1/artemis/user/UserTestService.java index ee48ff136114..d07485179853 100644 --- a/src/test/java/de/tum/in/www1/artemis/user/UserTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/user/UserTestService.java @@ -19,6 +19,7 @@ import de.tum.in.www1.artemis.config.Constants; import de.tum.in.www1.artemis.course.CourseUtilService; +import de.tum.in.www1.artemis.domain.Authority; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.exercise.programmingexercise.MockDelegate; @@ -120,8 +121,8 @@ private void assertThatUserWasSoftDeleted(User originalUser, User deletedUser) t assertThat(deletedUser.getLogin()).isNotEqualTo(originalUser.getLogin()); assertThat(deletedUser.getPassword()).isNotEqualTo(originalUser.getPassword()); assertThat(deletedUser.getEmail()).endsWith(Constants.USER_EMAIL_DOMAIN_AFTER_SOFT_DELETE); - assertThat(deletedUser.getRegistrationNumber()).isEqualTo(null); - assertThat(deletedUser.getImageUrl()).isEqualTo(null); + assertThat(deletedUser.getRegistrationNumber()).isNull(); + assertThat(deletedUser.getImageUrl()).isNull(); assertThat(deletedUser.getActivated()).isFalse(); } @@ -221,6 +222,23 @@ public void updateUser_asAdmin_isSuccessful() throws Exception { assertThat(student).as("Updated user in DB is equal to sent update").isEqualTo(updatedUserIndDB); } + // Test + public void updateUserWithEmptyRoles() throws Exception { + student.setInternal(true); + student.setAuthorities(null); + + mockDelegate.mockUpdateUserInUserManagement(student.getLogin(), student, "foobar1234", student.getGroups()); + + var managedUserVM = new ManagedUserVM(student, "foobar1234"); + + final var response = request.putWithResponseBody("/api/admin/users", managedUserVM, User.class, HttpStatus.OK); + assertThat(response).isNotNull(); + + // do not allow empty authorities + final var updatedUserInDB = userRepository.findOneWithGroupsAndAuthoritiesByLogin(student.getLogin()).orElseThrow(); + assertThat(updatedUserInDB.getAuthorities()).containsExactly(new Authority(Role.STUDENT.getAuthority())); + } + // Test public void updateUser_withNullPassword_oldPasswordNotChanged() throws Exception { student.setPassword(null); @@ -341,6 +359,15 @@ public void createExternalUser_asAdmin_withVcsToken_isSuccessful() throws Except // Test public void createInternalUser_asAdmin_isSuccessful() throws Exception { + createInternalUserIsSuccessful(Set.of(Role.STUDENT)); + } + + // Test + public void createInternalUserWithoutRoles_asAdmin_isSuccessful() throws Exception { + createInternalUserIsSuccessful(Collections.emptySet()); + } + + private void createInternalUserIsSuccessful(final Set roles) throws Exception { String password = "foobar1234"; student.setId(null); student.setLogin("batman"); @@ -348,6 +375,9 @@ public void createInternalUser_asAdmin_isSuccessful() throws Exception { student.setEmail("batman@secret.invalid"); student.setInternal(true); + final Set authorities = roles.stream().map(Role::getAuthority).map(auth -> authorityRepository.findById(auth).orElseThrow()).collect(Collectors.toSet()); + student.setAuthorities(authorities); + mockDelegate.mockCreateUserInUserManagement(student, false); final var response = request.postWithResponseBody("/api/admin/users", new ManagedUserVM(student, student.getPassword()), User.class, HttpStatus.CREATED); diff --git a/src/test/java/de/tum/in/www1/artemis/util/RequestUtilService.java b/src/test/java/de/tum/in/www1/artemis/util/RequestUtilService.java index 1f3fe7254ca9..bc47080491ca 100644 --- a/src/test/java/de/tum/in/www1/artemis/util/RequestUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/util/RequestUtilService.java @@ -14,6 +14,7 @@ import javax.annotation.Nullable; +import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -355,7 +356,7 @@ public File postWithResponseBodyFile(String path, Object body, HttpStatus expect return null; } final var tmpFile = File.createTempFile(res.getResponse().getHeader("filename"), null); - Files.write(tmpFile.toPath(), res.getResponse().getContentAsByteArray()); + FileUtils.writeByteArrayToFile(tmpFile, res.getResponse().getContentAsByteArray()); return tmpFile; } @@ -573,7 +574,7 @@ public File getFile(String path, HttpStatus expectedStatus, MultiValueMap + * This extension is used to add structural information to the logs, e.g. to indicate the start and end of a test class. At the end of the test class, the collected logs from + * {@link ParallelConsoleAppender} get printed to the console. + */ +public class ParallelLoggingExtension implements BeforeAllCallback, BeforeEachCallback, AfterAllCallback { + + @Override + public void beforeAll(ExtensionContext context) { + Class testClass = context.getRequiredTestClass(); + ParallelConsoleAppender.registerActiveTestGroup(testClass); + ParallelConsoleAppender.addStringToLogsForGroup("\nStarting logs for " + testClass.getSimpleName() + "\n"); + + // Wait until the logger is initialized + await().until(() -> LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) instanceof ch.qos.logback.classic.Logger); + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + ParallelConsoleAppender.addStringToLogsForGroup("\n Starting logs for " + context.getRequiredTestClass().getSimpleName() + " > " + context.getDisplayName() + "\n"); + } + + @Override + public void afterAll(ExtensionContext context) { + Class testClass = context.getRequiredTestClass(); + ParallelConsoleAppender.addStringToLogsForGroup("\nFinished logs for " + testClass.getSimpleName() + "\n"); + ParallelConsoleAppender.printLogsForGroup(testClass); + ParallelConsoleAppender.unregisterActiveTestGroup(testClass); + } + +} diff --git a/src/test/java/de/tum/in/www1/artemis/util/junit_parallel_logging/ParallelConsoleAppender.java b/src/test/java/de/tum/in/www1/artemis/util/junit_parallel_logging/ParallelConsoleAppender.java new file mode 100644 index 000000000000..c22c66e90dff --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/util/junit_parallel_logging/ParallelConsoleAppender.java @@ -0,0 +1,159 @@ +package de.tum.in.www1.artemis.util.junit_parallel_logging; + +import static org.assertj.core.api.Assertions.fail; + +import java.io.ByteArrayOutputStream; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import de.tum.in.www1.artemis.*; +import de.tum.in.www1.artemis.util.AbstractArtemisIntegrationTest; + +/** + * This custom appender is used to capture the logs of multiple tests running in parallel. + *

+ * It captures the logs for each test group separately and prints them to the console at the end of the test class. + * The test group is determined by the test's class. Each test class can only be assigned to one test group. + * If a log cannot be assigned to a test group, the logs are printed for all active test groups. + */ +public class ParallelConsoleAppender extends AppenderBase { + + private PatternLayoutEncoder encoder; + + private static final InheritableThreadLocal> LOCAL_TEST_GROUP = new InheritableThreadLocal<>(); + + private static final ConcurrentMap, ByteArrayOutputStream> TEST_GROUP_TO_ENCODED_LOGS = new ConcurrentHashMap<>(); + + private static final Set> TEST_GROUPS = Set.of(AbstractSpringIntegrationBambooBitbucketJiraTest.class, AbstractSpringIntegrationGitlabCIGitlabSamlTest.class, + AbstractSpringIntegrationJenkinsGitlabTest.class, AbstractSpringIntegrationLocalCILocalVCTest.class, AbstractSpringIntegrationIndependentTest.class); + + @Override + protected synchronized void append(ILoggingEvent loggingEvent) { + Class testClass = LOCAL_TEST_GROUP.get(); + + // Add the logging Event to the corresponding List in the Map + if (testClass != null && !loggingEvent.getThreadName().contains("event")) { + if (TEST_GROUP_TO_ENCODED_LOGS.containsKey(testClass)) { + TEST_GROUP_TO_ENCODED_LOGS.get(testClass).writeBytes(encoder.encode(loggingEvent)); + } + else { + ByteArrayOutputStream logs = new ByteArrayOutputStream(); + logs.writeBytes(encoder.encode(loggingEvent)); + TEST_GROUP_TO_ENCODED_LOGS.put(testClass, logs); + } + return; + } + + // If the thread id is not assigned to a TestGroup, we add the logging event for all active TestGroups + for (ByteArrayOutputStream logs : TEST_GROUP_TO_ENCODED_LOGS.values()) { + logs.writeBytes(encoder.encode(loggingEvent)); + } + } + + /** + * Prints the logs for the given test group to the console and removes them. + * This method should be called at the end of a test class. + * + * @param testClass the test's class for which the logs should be printed + */ + public static synchronized void printLogsForGroup(Class testClass) { + Class testGroupClass = groupFromClass(testClass); + ByteArrayOutputStream logs = TEST_GROUP_TO_ENCODED_LOGS.remove(testGroupClass); + if (logs == null) { + return; + } + + System.out.writeBytes(logs.toByteArray()); + logs.reset(); + System.out.flush(); + } + + /** + * Adds the given string to the logs for the current test group. + * + * @param string the string to add to the logs + */ + public static synchronized void addStringToLogsForGroup(String string) { + Class testClass = LOCAL_TEST_GROUP.get(); + + // Add the logging Event to the corresponding List in the Map + if (testClass != null) { + if (TEST_GROUP_TO_ENCODED_LOGS.containsKey(testClass)) { + TEST_GROUP_TO_ENCODED_LOGS.get(testClass).writeBytes(string.getBytes()); + } + else { + ByteArrayOutputStream logs = new ByteArrayOutputStream(); + logs.writeBytes(string.getBytes()); + TEST_GROUP_TO_ENCODED_LOGS.put(testClass, logs); + } + return; + } + + // If the thread id is not assigned to a TestGroup, we add the logging event for all active TestGroups + for (ByteArrayOutputStream logs : TEST_GROUP_TO_ENCODED_LOGS.values()) { + logs.writeBytes(string.getBytes()); + } + + } + + /** + * Registers the test group for the given test class. + * This method should be called at the beginning of a test class. + * + * @param testClass the test's class + */ + public static void registerActiveTestGroup(Class testClass) { + LOCAL_TEST_GROUP.set(groupFromClass(testClass)); + } + + /** + * Unregisters the test group for the given test class. + * This method should be called at the end of a test class. + * + * @param testClass the test's class + */ + public static void unregisterActiveTestGroup(Class testClass) { + TEST_GROUP_TO_ENCODED_LOGS.remove(testClass); + LOCAL_TEST_GROUP.remove(); + } + + /** + * Sets the encoder for this appender. This method is used by logback and should not be removed. + * This method is used to set the encoder's pattern in the logback.xml file. + * + * @param encoder the encoder used to encode the logging events + */ + @SuppressWarnings("unused") + public void setEncoder(PatternLayoutEncoder encoder) { + this.encoder = encoder; + } + + /** + * Returns the test group's class for the given class. + * If none of the groups' classes is assignable from the given class, the class itself is returned. + * + * @param clazz the class for which the test group's class should be returned + * @return the test group's class for the given class + */ + private static Class groupFromClass(Class clazz) { + if (clazz == null) { + return null; + } + + for (Class group : TEST_GROUPS) { + if (group.isAssignableFrom(clazz)) { + return group; + } + } + + if (AbstractArtemisIntegrationTest.class.isAssignableFrom(clazz)) { + fail("Test class " + clazz.getName() + " extends ArtemisIntegrationTest but is not assigned to a test group"); + } + + return clazz; + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/util/junit_parallel_logging/ThreadIdConverter.java b/src/test/java/de/tum/in/www1/artemis/util/junit_parallel_logging/ThreadIdConverter.java new file mode 100644 index 000000000000..cfd9d1653db7 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/util/junit_parallel_logging/ThreadIdConverter.java @@ -0,0 +1,17 @@ +package de.tum.in.www1.artemis.util.junit_parallel_logging; + +import ch.qos.logback.classic.pattern.ClassicConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; + +/** + * A custom Logback converter that can be used to display the thread id in the logs. + *

+ * This converter is used to distinguish logs from different threads when running tests in parallel. + */ +public class ThreadIdConverter extends ClassicConverter { + + @Override + public String convert(ILoggingEvent iLoggingEvent) { + return String.valueOf(Thread.currentThread().getId()); + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/web/rest/AndroidAppSiteAssociationResourceTest.java b/src/test/java/de/tum/in/www1/artemis/web/rest/AndroidAppSiteAssociationResourceTest.java index 3798e8fb431e..6762f1ab3503 100644 --- a/src/test/java/de/tum/in/www1/artemis/web/rest/AndroidAppSiteAssociationResourceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/web/rest/AndroidAppSiteAssociationResourceTest.java @@ -7,9 +7,9 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; -class AndroidAppSiteAssociationResourceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class AndroidAppSiteAssociationResourceTest extends AbstractSpringIntegrationIndependentTest { @Autowired AndroidAppSiteAssociationResource androidAppSiteAssociationResource; diff --git a/src/test/java/de/tum/in/www1/artemis/web/rest/AppleAppSiteAssociationResourceTest.java b/src/test/java/de/tum/in/www1/artemis/web/rest/AppleAppSiteAssociationResourceTest.java index 74ad5fe60bea..fc89211686d4 100644 --- a/src/test/java/de/tum/in/www1/artemis/web/rest/AppleAppSiteAssociationResourceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/web/rest/AppleAppSiteAssociationResourceTest.java @@ -5,9 +5,9 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest; +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; -class AppleAppSiteAssociationResourceTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { +class AppleAppSiteAssociationResourceTest extends AbstractSpringIntegrationIndependentTest { @Autowired AppleAppSiteAssociationResource appleAppSiteAssociationResource; diff --git a/src/test/javascript/spec/component/course/course-lti-configuration.component.spec.ts b/src/test/javascript/spec/component/course/course-lti-configuration.component.spec.ts index 5848c452146d..83bc1e617eb7 100644 --- a/src/test/javascript/spec/component/course/course-lti-configuration.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-lti-configuration.component.spec.ts @@ -117,11 +117,11 @@ describe('Course LTI Configuration Component', () => { expect(findWithExercisesStub).toHaveBeenCalledOnce(); expect(comp.getDynamicRegistrationUrl()).toBe(`${location.origin}/lti/dynamic-registration/${course.id}`); - expect(comp.getDeepLinkingUrl()).toBe(`${location.origin}/api/lti13/deep-linking/${course.id}`); + expect(comp.getDeepLinkingUrl()).toBe(`${location.origin}/api/public/lti13/deep-linking/${course.id}`); expect(comp.getToolUrl()).toBe(`${location.origin}/courses/${course.id}`); expect(comp.getKeysetUrl()).toBe(`${location.origin}/.well-known/jwks.json`); - expect(comp.getInitiateLoginUrl()).toBe(`${location.origin}/api/lti13/initiate-login/${course.onlineCourseConfiguration?.registrationId}`); - expect(comp.getRedirectUri()).toBe(`${location.origin}/api/lti13/auth-callback`); + expect(comp.getInitiateLoginUrl()).toBe(`${location.origin}/api/public/lti13/initiate-login/${course.onlineCourseConfiguration?.registrationId}`); + expect(comp.getRedirectUri()).toBe(`${location.origin}/api/public/lti13/auth-callback`); }); }); diff --git a/src/test/javascript/spec/component/exam/manage/exam-exercise-import.component.spec.ts b/src/test/javascript/spec/component/exam/manage/exam-exercise-import.component.spec.ts index ce6488144644..31aa300beba3 100644 --- a/src/test/javascript/spec/component/exam/manage/exam-exercise-import.component.spec.ts +++ b/src/test/javascript/spec/component/exam/manage/exam-exercise-import.component.spec.ts @@ -293,11 +293,11 @@ describe('Exam Exercise Import Component', () => { }); it('should correctly return the Exercise Icon', () => { - expect(component.getExerciseIcon(modelingExercise)).toEqual(faProjectDiagram); - expect(component.getExerciseIcon(textExercise)).toEqual(faFont); - expect(component.getExerciseIcon(programmingExercise)).toEqual(faKeyboard); - expect(component.getExerciseIcon(quizExercise)).toEqual(faCheckDouble); - expect(component.getExerciseIcon(fileUploadExercise)).toEqual(faFileUpload); + expect(component.getExerciseIcon(modelingExercise.type)).toEqual(faProjectDiagram); + expect(component.getExerciseIcon(textExercise.type)).toEqual(faFont); + expect(component.getExerciseIcon(programmingExercise.type)).toEqual(faKeyboard); + expect(component.getExerciseIcon(quizExercise.type)).toEqual(faCheckDouble); + expect(component.getExerciseIcon(fileUploadExercise.type)).toEqual(faFileUpload); }); describe('Programming exercise import validation', () => { diff --git a/src/test/javascript/spec/component/lti/lti13-exercise-launch.component.spec.ts b/src/test/javascript/spec/component/lti/lti13-exercise-launch.component.spec.ts index 1d756fbe3eb7..d6a84e31ae72 100644 --- a/src/test/javascript/spec/component/lti/lti13-exercise-launch.component.spec.ts +++ b/src/test/javascript/spec/component/lti/lti13-exercise-launch.component.spec.ts @@ -90,7 +90,7 @@ describe('Lti13ExerciseLaunchComponent', () => { expect(consoleSpy).toHaveBeenCalledOnce(); expect(consoleSpy).toHaveBeenCalledWith('No LTI targetLinkUri received for a successful launch'); expect(httpStub).toHaveBeenCalledOnce(); - expect(httpStub).toHaveBeenCalledWith('api/lti13/auth-login', expect.anything(), expect.anything()); + expect(httpStub).toHaveBeenCalledWith('api/public/lti13/auth-login', expect.anything(), expect.anything()); expect(comp.isLaunching).toBeFalse(); }); @@ -104,7 +104,7 @@ describe('Lti13ExerciseLaunchComponent', () => { comp.ngOnInit(); expect(httpStub).toHaveBeenCalledOnce(); - expect(httpStub).toHaveBeenCalledWith('api/lti13/auth-login', expect.anything(), expect.anything()); + expect(httpStub).toHaveBeenCalledWith('api/public/lti13/auth-login', expect.anything(), expect.anything()); }); it('onInit launch fails on error', () => { @@ -118,7 +118,7 @@ describe('Lti13ExerciseLaunchComponent', () => { comp.ngOnInit(); expect(httpStub).toHaveBeenCalledOnce(); - expect(httpStub).toHaveBeenCalledWith('api/lti13/auth-login', expect.anything(), expect.anything()); + expect(httpStub).toHaveBeenCalledWith('api/public/lti13/auth-login', expect.anything(), expect.anything()); expect(comp.isLaunching).toBeFalse(); }); diff --git a/src/test/javascript/spec/component/participation-submission/participation-submission.component.spec.ts b/src/test/javascript/spec/component/participation-submission/participation-submission.component.spec.ts index a29c93177975..87c6053f53f8 100644 --- a/src/test/javascript/spec/component/participation-submission/participation-submission.component.spec.ts +++ b/src/test/javascript/spec/component/participation-submission/participation-submission.component.spec.ts @@ -172,6 +172,7 @@ describe('ParticipationSubmissionComponent', () => { expect(findAllSubmissionsOfParticipationStub).toHaveBeenCalledOnce(); expect(comp.participation).toEqual(participation); expect(comp.submissions).toEqual(submissions); + expect(comp.participation?.submissions).toEqual(submissions); // check if delete button is available const deleteButton = debugElement.query(By.css('#deleteButton')); diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise.component.spec.ts index ccaa5888bf1a..7915f67f8f84 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise.component.spec.ts @@ -116,6 +116,29 @@ describe('ProgrammingExercise Management Component', () => { expect(mockSubscriber).toHaveBeenCalledOnce(); }); + it('should delete multiple exercises', () => { + const headers = new HttpHeaders().append('link', 'link;link'); + jest.spyOn(programmingExerciseService, 'delete').mockReturnValue( + of( + new HttpResponse({ + body: {}, + headers, + }), + ), + ); + const mockSubscriber = jest.fn(); + comp.dialogError$.subscribe(mockSubscriber); + + comp.course = course; + comp.ngOnInit(); + comp.deleteMultipleProgrammingExercises([{ id: 441 }, { id: 442 }, { id: 443 }] as ProgrammingExercise[], { + deleteStudentReposBuildPlans: true, + deleteBaseReposBuildPlans: true, + }); + expect(programmingExerciseService.delete).toHaveBeenCalledTimes(3); + expect(mockSubscriber).toHaveBeenCalledTimes(3); + }); + it('should not delete exercise on error', () => { const httpErrorResponse = new HttpErrorResponse({ error: 'Forbidden', status: 403 }); jest.spyOn(programmingExerciseService, 'delete').mockReturnValue(throwError(() => httpErrorResponse)); @@ -201,41 +224,41 @@ describe('ProgrammingExercise Management Component', () => { describe('ProgrammingExercise Select Exercises', () => { it('should add selected exercise to list', () => { // WHEN - comp.toggleProgrammingExercise(programmingExercise); + comp.toggleExercise(programmingExercise); // THEN - expect(comp.selectedProgrammingExercises[0]).toContainEntry(['id', programmingExercise.id]); + expect(comp.selectedExercises[0]).toContainEntry(['id', programmingExercise.id]); }); it('should remove selected exercise to list', () => { // WHEN - comp.toggleProgrammingExercise(programmingExercise); - comp.toggleProgrammingExercise(programmingExercise); + comp.toggleExercise(programmingExercise); + comp.toggleExercise(programmingExercise); // THEN - expect(comp.selectedProgrammingExercises).toHaveLength(0); + expect(comp.selectedExercises).toHaveLength(0); }); it('should select all', () => { // WHEN - comp.toggleAllProgrammingExercises(); + comp.toggleMultipleExercises(comp.programmingExercises); // THEN - expect(comp.selectedProgrammingExercises).toHaveLength(comp.programmingExercises.length); + expect(comp.selectedExercises).toHaveLength(comp.programmingExercises.length); }); it('should deselect all', () => { // WHEN - comp.toggleAllProgrammingExercises(); // Select all - comp.toggleAllProgrammingExercises(); // Deselect all + comp.toggleMultipleExercises(comp.programmingExercises); // Select all + comp.toggleMultipleExercises(comp.programmingExercises); // Deselect all // THEN - expect(comp.selectedProgrammingExercises).toHaveLength(0); + expect(comp.selectedExercises).toHaveLength(0); }); it('should check correctly if selected', () => { // WHEN - comp.toggleProgrammingExercise(programmingExercise); + comp.toggleExercise(programmingExercise); // THEN expect(comp.isExerciseSelected(programmingExercise)).toBeTrue(); diff --git a/src/test/javascript/spec/component/quiz-exercise/quiz-exercise.component.spec.ts b/src/test/javascript/spec/component/quiz-exercise/quiz-exercise.component.spec.ts index 374e9d692206..a2944fbe9a88 100644 --- a/src/test/javascript/spec/component/quiz-exercise/quiz-exercise.component.spec.ts +++ b/src/test/javascript/spec/component/quiz-exercise/quiz-exercise.component.spec.ts @@ -281,6 +281,22 @@ describe('QuizExercise Management Component', () => { expect(quizExerciseService.delete).toHaveBeenCalledOnce(); }); + it('should delete multiple quizzes', () => { + const headers = new HttpHeaders().append('link', 'link;link'); + jest.spyOn(quizExerciseService, 'delete').mockReturnValue( + of( + new HttpResponse({ + body: {}, + headers, + }), + ), + ); + + comp.ngOnInit(); + comp.deleteMultipleExercises([{ id: 1 }, { id: 2 }, { id: 3 }] as QuizExercise[], comp.quizExerciseService); + expect(quizExerciseService.delete).toHaveBeenCalledTimes(3); + }); + it('should export quiz', () => { const headers = new HttpHeaders().append('link', 'link;link'); jest.spyOn(quizExerciseService, 'find').mockReturnValue( diff --git a/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension index 7fc53e343616..8dfc3c5b68c1 100644 --- a/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension +++ b/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -1 +1,2 @@ de.tum.in.www1.artemis.util.junit_extensions.AwaitilityExtension +de.tum.in.www1.artemis.util.junit_extensions.ParallelLoggingExtension diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties index bbcb24682b30..a2754a0609bb 100644 --- a/src/test/resources/junit-platform.properties +++ b/src/test/resources/junit-platform.properties @@ -1,6 +1,8 @@ -#junit.jupiter.execution.parallel.enabled = true -#junit.jupiter.execution.parallel.mode.default = concurrent -#junit.jupiter.execution.parallel.mode.classes.default = same_thread +# Enables junit5 parallel test execution. Tests are run on one JVM instance. +junit.jupiter.execution.parallel.enabled = true + +# Enables ordering test-classes with JUnit5 by class name. +junit.jupiter.testclass.order.default = org.junit.jupiter.api.ClassOrderer$ClassName # Enables JUnit5 automatic detection of extensions. junit.jupiter.extensions.autodetection.enabled = true diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index 0adc98c942a1..f51816ce443b 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -2,7 +2,15 @@ - + + + + + + + %16.16d{HH:mm:ss.SSS} | %3.3threadId %-16.16thread | %-5level | %-36.36logger{36} : %msg%n + + @@ -37,7 +45,7 @@ WARN - + diff --git a/supporting_scripts/flaky_test_detection.sh b/supporting_scripts/flaky_test_detection.sh new file mode 100644 index 000000000000..ec941b0b240e --- /dev/null +++ b/supporting_scripts/flaky_test_detection.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# Check for the number of test runs argument. +if [ $# -eq 0 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Get the number of test runs from the command line argument. +NUM_RUNS="$1" + +# Define spring profiles. +SPRING_PROFILES=("none" "mysql" "postgres") + +DIRECTORY='./build/flaky-test-detection-results' + +# Create the directory, if it doesn't exist. +mkdir -p ${DIRECTORY} + +# ==================== # +# FLAKY TEST DETECTION # +# ==================== # + +for ((run = 1; run <= NUM_RUNS; run++)); do + # Generate a random number between 1 and 13. + spring_profile_chance=$((RANDOM % 13 + 1)) + + # Determine the active spring profile based on the random number: + # 10/13 chance of no profile, 2/13 chance of MYSQL, 1/13 chance of POSTGRES. + # (Should result in similar execution times for each profile). + profile_index=0 + if [[ $spring_profile_chance -gt 10 ]]; then + profile_index=1 + fi + + if [[ $spring_profile_chance -gt 12 ]]; then + profile_index=2 + fi + active_profile="${SPRING_PROFILES[$profile_index]}" + + # Generate output file name + TIME=$(date +"%Y-%m-%d_%H:%M:%S") + output_file="${active_profile}_${TIME}_run${run}.log" + + # Run tests with gradlew + echo "Running tests with Spring Profile: $active_profile (Run $run)" + set -o pipefail && SPRING_PROFILES_INCLUDE="$active_profile" ./gradlew --console=plain \ + test --rerun jacocoTestReport -x webapp jacocoTestCoverageVerification > "${DIRECTORY}/$output_file" + + # Check if tests were successful. If not, rename the output file to indicate failure. Delete the output file if tests were successful. + if grep -q "BUILD SUCCESSFUL" "${DIRECTORY}/$output_file"; then + rm "${DIRECTORY}/$output_file" + else + mv "${DIRECTORY}/$output_file" "${DIRECTORY}/FAILURE_${output_file}" + fi + + # Wait a bit before the next run + sleep 10 +done + +# ================== # +# FLAKY TEST SUMMARY # +# ================== # + +echo "Generating flaky test summary..." + +SUMMARY_DIRECTORY="${DIRECTORY}/summary" +mkdir -p "$SUMMARY_DIRECTORY" + +SUMMARY_FILE="${SUMMARY_DIRECTORY}/run-summary.txt" +printf "Logfile and Failed Tests\n" > "$SUMMARY_FILE" +for file in "$DIRECTORY"/*.log; do + if [ -f "$file" ]; then + printf "\nFailed tests in $(basename "$file"):\n" >> "$SUMMARY_FILE" + grep "Test >.* FAILED" "$file" >> "$SUMMARY_FILE" + fi +done + +COUNT_FILE="${SUMMARY_DIRECTORY}/failure-count.txt" +printf "Count of Failed Tests\n" > "$COUNT_FILE" +grep "Test >.* FAILED" "$SUMMARY_FILE" | sort | uniq -c | sort -nr >> "$COUNT_FILE" + +echo "Tests completed for $NUM_RUNS run(s)."