From ec70ea55963ceb518593c62e0099264ff9dd00a4 Mon Sep 17 00:00:00 2001
From: Andreas Svanberg <andreass@dsv.su.se>
Date: Mon, 3 Mar 2025 07:32:25 +0100
Subject: [PATCH 1/5] Make session serializable (#121)

When re-deploying the application, or restarting Tomcat, it will attempt to serialize the active sessions to prevent users from getting logged out and losing in-progess work. This requires that all attributes that are stored in the session implement `java.io.Serializable`. Spring stores the entire security context in the session which includes a reference to the principal, and that principal may be of type "WicketControlledPrincipal" and it must therefore be serializable.

## How to test
1. Be on the `develop` branch
2. Make sure session preservation is turned on (in IntelliJ check "Preserve sessions across restarts and redeploys", or read https://tomcat.apache.org/tomcat-10.0-doc/config/manager.html#Persistence_Across_Restarts)
3. Log in as the default admin `dev@localhost`
4. Switch to "Sture Student" under "Admin / Users / Switch user"
5. Restart Tomcat
6. Refresh page and you'll be prompted to log in again
7. Switch to this branch and repeat step 1-6

Reviewed-on: https://gitea.dsv.su.se/DMC/scipro/pulls/121
Reviewed-by: Nico Athanassiadis <nico@dsv.su.se>
Co-authored-by: Andreas Svanberg <andreass@dsv.su.se>
Co-committed-by: Andreas Svanberg <andreass@dsv.su.se>
---
 .../se/su/dsv/scipro/war/CurrentUserFromSpringSecurity.java    | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/war/src/main/java/se/su/dsv/scipro/war/CurrentUserFromSpringSecurity.java b/war/src/main/java/se/su/dsv/scipro/war/CurrentUserFromSpringSecurity.java
index 6f209f38aa..3d71fd12a3 100644
--- a/war/src/main/java/se/su/dsv/scipro/war/CurrentUserFromSpringSecurity.java
+++ b/war/src/main/java/se/su/dsv/scipro/war/CurrentUserFromSpringSecurity.java
@@ -4,6 +4,7 @@ import jakarta.inject.Inject;
 import jakarta.inject.Provider;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import java.io.Serializable;
 import java.security.Principal;
 import java.util.Collections;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -81,7 +82,7 @@ public class CurrentUserFromSpringSecurity implements AuthenticationContext {
         return authentication.getName();
     }
 
-    private static final class WicketControlledPrincipal implements Principal {
+    private static final class WicketControlledPrincipal implements Principal, Serializable {
 
         private final String username;
 

From a71eeb5e2ce35e38a39dfa9630f39fd41b093859 Mon Sep 17 00:00:00 2001
From: Andreas Svanberg <andreass@dsv.su.se>
Date: Mon, 3 Mar 2025 07:48:46 +0100
Subject: [PATCH 2/5] Fix crash when editing an application period (#117)

Fixes #68

## How to test
1. Log in as admin
2. Go to "Match / Application periods"
3. Click the edit icon (6th column)
4. Click "Save"

Co-authored-by: Nico Athanassiadis <nico@dsv.su.se>
Reviewed-on: https://gitea.dsv.su.se/DMC/scipro/pulls/117
Reviewed-by: Nico Athanassiadis <nico@dsv.su.se>
Co-authored-by: Andreas Svanberg <andreass@dsv.su.se>
Co-committed-by: Andreas Svanberg <andreass@dsv.su.se>
---
 .../se/su/dsv/scipro/match/ApplicationPeriod.java    | 12 +++++-------
 .../AdminApplicationPeriodsPanel.java                |  8 +-------
 .../scipro/datatables/target/AddTargetLinkPanel.java |  7 +------
 .../scipro/projectpartner/ProjectPartnerPage.java    |  8 +-------
 4 files changed, 8 insertions(+), 27 deletions(-)

diff --git a/core/src/main/java/se/su/dsv/scipro/match/ApplicationPeriod.java b/core/src/main/java/se/su/dsv/scipro/match/ApplicationPeriod.java
index 9dc48661d9..b21a586607 100755
--- a/core/src/main/java/se/su/dsv/scipro/match/ApplicationPeriod.java
+++ b/core/src/main/java/se/su/dsv/scipro/match/ApplicationPeriod.java
@@ -176,18 +176,16 @@ public class ApplicationPeriod extends DomainObject {
         return Collections.unmodifiableSet(answerSet);
     }
 
-    public void setProjectTypes(Iterable<ProjectType> projectTypes) {
-        this.projectTypes.clear();
+    public void setProjectTypes(Set<ProjectType> projectTypes) {
+        this.projectTypes.removeIf(appt -> !projectTypes.contains(appt.getProjectType()));
         for (ProjectType pt : projectTypes) {
-            this.projectTypes.add(new ApplicationPeriodProjectType(this, pt));
+            if (this.projectTypes.stream().noneMatch(appt -> appt.getProjectType().equals(pt))) {
+                addProjectType(pt);
+            }
         }
     }
 
     public void addProjectType(ProjectType projectType) {
         this.projectTypes.add(new ApplicationPeriodProjectType(this, projectType));
     }
-
-    public void removeProjectType(ProjectType projectType) {
-        this.projectTypes.removeIf(next -> next.getProjectType().equals(projectType));
-    }
 }
diff --git a/view/src/main/java/se/su/dsv/scipro/applicationperiod/AdminApplicationPeriodsPanel.java b/view/src/main/java/se/su/dsv/scipro/applicationperiod/AdminApplicationPeriodsPanel.java
index 307d7858f3..b8bb7d9e76 100755
--- a/view/src/main/java/se/su/dsv/scipro/applicationperiod/AdminApplicationPeriodsPanel.java
+++ b/view/src/main/java/se/su/dsv/scipro/applicationperiod/AdminApplicationPeriodsPanel.java
@@ -123,13 +123,7 @@ public class AdminApplicationPeriodsPanel extends Panel {
                     item.add(
                         new DisplayMultiplesPanel<>(
                             s,
-                            new ListAdapterModel<>(
-                                LambdaModel.of(
-                                    iModel,
-                                    ApplicationPeriod::getProjectTypes,
-                                    ApplicationPeriod::setProjectTypes
-                                )
-                            )
+                            new ListAdapterModel<>(iModel.map(ApplicationPeriod::getProjectTypes))
                         ) {
                             @Override
                             public Component getComponent(String componentId, IModel<ProjectType> t) {
diff --git a/view/src/main/java/se/su/dsv/scipro/datatables/target/AddTargetLinkPanel.java b/view/src/main/java/se/su/dsv/scipro/datatables/target/AddTargetLinkPanel.java
index 2f4d027af5..8c4c98a6d0 100644
--- a/view/src/main/java/se/su/dsv/scipro/datatables/target/AddTargetLinkPanel.java
+++ b/view/src/main/java/se/su/dsv/scipro/datatables/target/AddTargetLinkPanel.java
@@ -22,12 +22,7 @@ public class AddTargetLinkPanel extends Panel {
     public AddTargetLinkPanel(String id, final IModel<ApplicationPeriod> model) {
         super(id, model);
         add(
-            new ListView<>(
-                "list",
-                new ListAdapterModel<>(
-                    LambdaModel.of(model, ApplicationPeriod::getProjectTypes, ApplicationPeriod::setProjectTypes)
-                )
-            ) {
+            new ListView<>("list", new ListAdapterModel<>(model.map(ApplicationPeriod::getProjectTypes))) {
                 @Override
                 protected void populateItem(ListItem<ProjectType> item) {
                     item.add(new Label("pc", item.getModelObject().getName()));
diff --git a/view/src/main/java/se/su/dsv/scipro/projectpartner/ProjectPartnerPage.java b/view/src/main/java/se/su/dsv/scipro/projectpartner/ProjectPartnerPage.java
index a70588801a..ca3736e027 100755
--- a/view/src/main/java/se/su/dsv/scipro/projectpartner/ProjectPartnerPage.java
+++ b/view/src/main/java/se/su/dsv/scipro/projectpartner/ProjectPartnerPage.java
@@ -76,13 +76,7 @@ public class ProjectPartnerPage extends AbstractIdeaProjectPage implements MenuH
             }
         );
         final IModel<? extends List<ProjectType>> matchableTypes = getMatchableTypes(
-            new ListAdapterModel<>(
-                LambdaModel.of(
-                    applicationPeriod,
-                    ApplicationPeriod::getProjectTypes,
-                    ApplicationPeriod::setProjectTypes
-                )
-            )
+            new ListAdapterModel<>(applicationPeriod.map(ApplicationPeriod::getProjectTypes))
         );
         panelContainer.add(
             new ListView<>("ads", matchableTypes) {

From 7f9e72484aaf00e1550374af2f644ec33eb0c94c Mon Sep 17 00:00:00 2001
From: Andreas Svanberg <andreass@dsv.su.se>
Date: Mon, 3 Mar 2025 07:59:14 +0100
Subject: [PATCH 3/5] Remove unused javax.inject and jersey-hk2 dependencies
 (#118)

Co-authored-by: Nico Athanassiadis <nico@dsv.su.se>
Reviewed-on: https://gitea.dsv.su.se/DMC/scipro/pulls/118
Reviewed-by: Nico Athanassiadis <nico@dsv.su.se>
Co-authored-by: Andreas Svanberg <andreass@dsv.su.se>
Co-committed-by: Andreas Svanberg <andreass@dsv.su.se>
---
 core/pom.xml | 22 ----------------------
 pom.xml      |  7 -------
 2 files changed, 29 deletions(-)

diff --git a/core/pom.xml b/core/pom.xml
index e34cb44596..532b1d4d04 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -23,36 +23,14 @@
         <dependency>
             <groupId>org.glassfish.jersey.core</groupId>
             <artifactId>jersey-client</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.glassfish.hk2.external</groupId>
-                    <artifactId>javax.inject</artifactId>
-                </exclusion>
-            </exclusions>
         </dependency>
         <dependency>
             <groupId>org.glassfish.jersey.media</groupId>
             <artifactId>jersey-media-jaxb</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.glassfish.hk2.external</groupId>
-                    <artifactId>javax.inject</artifactId>
-                </exclusion>
-            </exclusions>
         </dependency>
         <dependency>
             <groupId>org.glassfish.jersey.media</groupId>
             <artifactId>jersey-media-json-jackson</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.glassfish.hk2.external</groupId>
-                    <artifactId>javax.inject</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>
-        <dependency>
-            <groupId>org.glassfish.jersey.inject</groupId>
-            <artifactId>jersey-hk2</artifactId>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
diff --git a/pom.xml b/pom.xml
index a43a6491cd..1940f2ed25 100755
--- a/pom.xml
+++ b/pom.xml
@@ -295,13 +295,6 @@
             <artifactId>slf4j-api</artifactId>
         </dependency>
 
-        <!-- Additional dependencies -->
-        <dependency>
-            <groupId>javax.inject</groupId>
-            <artifactId>javax.inject</artifactId>
-            <version>1</version>
-        </dependency>
-
         <!-- Test stuff -->
         <dependency>
             <groupId>org.junit.jupiter</groupId>

From a76b317b1cb48f178efb1071f6b3f81ad6c15648 Mon Sep 17 00:00:00 2001
From: Andreas Svanberg <andreass@dsv.su.se>
Date: Mon, 3 Mar 2025 12:38:35 +0100
Subject: [PATCH 4/5] Improve test data creation (#112)

Currently there is only one class used to add test data; [`DataInitializer`](https://gitea.dsv.su.se/DMC/scipro/src/commit/b9f7dd5a49aeebd64a5f44b66ac0775901550476/core/src/main/java/se/su/dsv/scipro/DataInitializer.java). That class is already very large and causes a lot of merge conflicts when multiple changes are in the pipeline as noted by #109.

This change makes it possible to have multiple classes adding test data so that each change adds its own class and thus there are no conflicts. It also has the benefit of making each class smaller and more self-contained for testing a specific feature.

Some additional infrastructure was added in the form of the `BaseData` and `Factory` (naming improvements notwithstanding) interfaces to help each class add its own test data and re-use common data.

Finally all test data related classes have been moved to their own module so they can be properly excluded when building for production but are included by default while developing.

Fixes #109

## Future work
* Add a mechanism to work with date and time.
    Many processes (and therefore service method implementations) rely on the time between certain events. For example a final seminar must be scheduled a set amount of days in advance. In the ideal world, the test data is populated using these service methods to more accurately represent an achievable real world state. Therefore there must be a way to manipulate time when adding test data.
* Add more methods to the `Factory` interface as we discover more common steps that many populators must take.
* Add more base data available through the `BaseData` interface as we discover more common data that many populators need
    Care must be taken that this data is final and useful in its base state since populators will rely on that state.

Co-authored-by: Nico Athanassiadis <nico@dsv.su.se>
Reviewed-on: https://gitea.dsv.su.se/DMC/scipro/pulls/112
Reviewed-by: Nico Athanassiadis <nico@dsv.su.se>
Co-authored-by: Andreas Svanberg <andreass@dsv.su.se>
Co-committed-by: Andreas Svanberg <andreass@dsv.su.se>
---
 .gitignore                                    |  1 +
 Dockerfile                                    |  2 +
 .../java/se/su/dsv/scipro/CoreConfig.java     |  5 --
 pom.xml                                       |  1 +
 test-data/pom.xml                             | 20 ++++++++
 .../se/su/dsv/scipro/testdata/BaseData.java   | 19 +++++++
 .../dsv/scipro/testdata}/DataInitializer.java | 49 +++++++++++++++++--
 .../se/su/dsv/scipro/testdata/Factory.java    | 46 +++++++++++++++++
 .../testdata/TestDataConfiguration.java       | 16 ++++++
 .../scipro/testdata/TestDataPopulator.java    | 11 +++++
 .../su/dsv/scipro/testdata/package-info.java  |  8 +++
 .../testdata/populators/package-info.java     | 13 +++++
 .../se.su.dsv.scipro.war.PluginConfiguration  |  1 +
 war/pom.xml                                   | 13 +++++
 14 files changed, 196 insertions(+), 9 deletions(-)
 create mode 100644 test-data/pom.xml
 create mode 100644 test-data/src/main/java/se/su/dsv/scipro/testdata/BaseData.java
 rename {core/src/main/java/se/su/dsv/scipro => test-data/src/main/java/se/su/dsv/scipro/testdata}/DataInitializer.java (99%)
 create mode 100644 test-data/src/main/java/se/su/dsv/scipro/testdata/Factory.java
 create mode 100644 test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataConfiguration.java
 create mode 100644 test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataPopulator.java
 create mode 100644 test-data/src/main/java/se/su/dsv/scipro/testdata/package-info.java
 create mode 100644 test-data/src/main/java/se/su/dsv/scipro/testdata/populators/package-info.java
 create mode 100644 test-data/src/main/resources/META-INF/services/se.su.dsv.scipro.war.PluginConfiguration

diff --git a/.gitignore b/.gitignore
index a4397c6105..8393ae6082 100755
--- a/.gitignore
+++ b/.gitignore
@@ -25,4 +25,5 @@ fitnesse/target/
 daisy-integration/target/
 war/target/
 api/target/
+test-data/target/
 node_modules/
diff --git a/Dockerfile b/Dockerfile
index ac53fd07f3..acf3c889a3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,12 +12,14 @@ COPY core/pom.xml core/pom.xml
 COPY view/pom.xml view/pom.xml
 COPY war/pom.xml war/pom.xml
 COPY daisy-integration/pom.xml daisy-integration/pom.xml
+COPY test-data/pom.xml test-data/pom.xml
 
 COPY api/src/ api/src/
 COPY core/src/ core/src/
 COPY view/src/ view/src/
 COPY war/src/ war/src/
 COPY daisy-integration/src/ daisy-integration/src/
+COPY test-data/src/ test-data/src/
 
 RUN ./mvnw package \
     --define skipTests \
diff --git a/core/src/main/java/se/su/dsv/scipro/CoreConfig.java b/core/src/main/java/se/su/dsv/scipro/CoreConfig.java
index b7c51b41f2..19b8f04a13 100644
--- a/core/src/main/java/se/su/dsv/scipro/CoreConfig.java
+++ b/core/src/main/java/se/su/dsv/scipro/CoreConfig.java
@@ -1017,11 +1017,6 @@ public class CoreConfig {
         );
     }
 
-    @Bean
-    public DataInitializer dataInitializer() {
-        return new DataInitializer();
-    }
-
     @Bean
     public FinalSeminarActivityHandler finalSeminarActivityHandler(
         ActivityPlanFacade activityPlanFacade,
diff --git a/pom.xml b/pom.xml
index 1940f2ed25..896b1269e5 100755
--- a/pom.xml
+++ b/pom.xml
@@ -15,6 +15,7 @@
         <module>daisy-integration</module>
         <module>war</module>
         <module>api</module>
+        <module>test-data</module>
     </modules>
 
     <properties>
diff --git a/test-data/pom.xml b/test-data/pom.xml
new file mode 100644
index 0000000000..127cef4d71
--- /dev/null
+++ b/test-data/pom.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>se.su.dsv.scipro</groupId>
+        <artifactId>SciPro</artifactId>
+        <version>0.1-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>test-data</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>se.su.dsv.scipro</groupId>
+            <artifactId>core</artifactId>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/BaseData.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/BaseData.java
new file mode 100644
index 0000000000..1dd6b18132
--- /dev/null
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/BaseData.java
@@ -0,0 +1,19 @@
+package se.su.dsv.scipro.testdata;
+
+import se.su.dsv.scipro.system.ProjectType;
+
+/// All the base test data that can be re-used in different test cases.
+///
+/// **Do not modify any of this data.** There are many
+/// [TestDataPopulator]s that rely on this data to be in a specific state.
+///
+/// In addition to the data that is available here there is also much additional
+/// data that has been created;
+///
+///   - A grading report template for each [ProjectType]
+///
+public interface BaseData {
+    ProjectType bachelor();
+    ProjectType magister();
+    ProjectType master();
+}
diff --git a/core/src/main/java/se/su/dsv/scipro/DataInitializer.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java
similarity index 99%
rename from core/src/main/java/se/su/dsv/scipro/DataInitializer.java
rename to test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java
index 15a093006b..8b157598c4 100644
--- a/core/src/main/java/se/su/dsv/scipro/DataInitializer.java
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java
@@ -1,4 +1,4 @@
-package se.su.dsv.scipro;
+package se.su.dsv.scipro.testdata;
 
 import jakarta.inject.Inject;
 import jakarta.inject.Provider;
@@ -34,13 +34,16 @@ import se.su.dsv.scipro.security.auth.roles.Roles;
 import se.su.dsv.scipro.system.*;
 import se.su.dsv.scipro.util.Pair;
 
-public class DataInitializer implements Lifecycle {
+public class DataInitializer implements Lifecycle, BaseData, Factory {
 
     public static final int APPLICATION_PERIOD_START_MINUS_DAYS = 1;
     public static final int APPLICATION_PERIOD_END_PLUS_DAYS = 3;
     public static final int APPLICATION_PERIOD_COURSE_START_PLUS_DAYS = 5;
     public static final long RESEARCH_AREA_ID = 12L;
 
+    @Inject
+    private Collection<TestDataPopulator> testDataPopulators = new ArrayList<>();
+
     @Inject
     private UserService userService;
 
@@ -120,6 +123,9 @@ public class DataInitializer implements Lifecycle {
             createTarget();
             createStudentIdea();
             createRoughDraftApproval();
+            for (TestDataPopulator testDataPopulator : testDataPopulators) {
+                testDataPopulator.populate(this, this);
+            }
         }
         if (profile.getCurrentProfile() == Profiles.DEV && noAdminUser()) {
             createAdmin();
@@ -243,13 +249,18 @@ public class DataInitializer implements Lifecycle {
         sofia_student = createStudent("Sofia", 17);
     }
 
-    private User createStudent(String firstName, int identifier) {
+    private User createStudent(String firstName) {
         User user = createUser(firstName, STUDENT_LAST);
-        user.setIdentifier(identifier);
         createBeta(user);
         return user;
     }
 
+    private User createStudent(String firstName, int identifier) {
+        User user = createStudent(firstName);
+        user.setIdentifier(identifier);
+        return user;
+    }
+
     private User createEmployee(String firstName) {
         User user = createUser(firstName, EMPLOYEE_LAST);
         Unit u = createUnit();
@@ -2087,6 +2098,36 @@ public class DataInitializer implements Lifecycle {
         return entity;
     }
 
+    @Override
+    public ProjectType bachelor() {
+        return bachelorClass;
+    }
+
+    @Override
+    public ProjectType magister() {
+        return magisterClass;
+    }
+
+    @Override
+    public ProjectType master() {
+        return masterClass;
+    }
+
+    @Override
+    public User createAuthor(String firstName) {
+        return createStudent(firstName);
+    }
+
+    @Override
+    public User createSupervisor(String firstName) {
+        return createEmployee(firstName);
+    }
+
+    @Override
+    public User createReviewer(String firstName) {
+        return createEmployee(firstName);
+    }
+
     private static final class SimpleTextFile implements FileUpload {
 
         private final User uploader;
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/Factory.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/Factory.java
new file mode 100644
index 0000000000..fc4c0910f1
--- /dev/null
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/Factory.java
@@ -0,0 +1,46 @@
+package se.su.dsv.scipro.testdata;
+
+import se.su.dsv.scipro.security.auth.roles.Roles;
+import se.su.dsv.scipro.system.User;
+
+/**
+ * A factory to help with repetitive tasks when populating test data.
+ */
+public interface Factory {
+    /**
+     * Creates a user with the given first name and last name "Student".
+     * The user is given the role {@link Roles#AUTHOR}.
+     * <p>
+     * A username is created of the form {@code <first_name>@example.com} that
+     * can be used to log in.
+     */
+    User createAuthor(String firstName);
+
+    /**
+     * Creates a user with the given first name and last name "Employee".
+     * <p>
+     * The user is given the role {@link Roles#SUPERVISOR}, {@link Roles#REVIEWER},
+     * and {@link Roles#EXAMINER}.
+     * <p>
+     * The user gets a default research area, unit, and language. It is also
+     * marked as {@link User#setActiveAsSupervisor(boolean) an active supervisor}.
+     * <p>
+     * A username is created of the form {@code <first_name>@example.com} that
+     * can be used to log in.
+     */
+    User createSupervisor(String firstName);
+
+    /**
+     * Creates a user with the given first name and last name "Employee".
+     * <p>
+     * The user is given the role {@link Roles#SUPERVISOR}, {@link Roles#REVIEWER},
+     * and {@link Roles#EXAMINER}.
+     * <p>
+     * The user gets a default research area, unit, and language. It is also
+     * marked as {@link User#setActiveAsSupervisor(boolean) an active supervisor}.
+     * <p>
+     * A username is created of the form {@code <first_name>@example.com} that
+     * can be used to log in.
+     */
+    User createReviewer(String firstName);
+}
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataConfiguration.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataConfiguration.java
new file mode 100644
index 0000000000..ffd372628b
--- /dev/null
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataConfiguration.java
@@ -0,0 +1,16 @@
+package se.su.dsv.scipro.testdata;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import se.su.dsv.scipro.war.PluginConfiguration;
+
+@Configuration(proxyBeanMethods = false)
+@ComponentScan(basePackages = "se.su.dsv.scipro.testdata.populators")
+public class TestDataConfiguration implements PluginConfiguration {
+
+    @Bean
+    public DataInitializer dataInitializer() {
+        return new DataInitializer();
+    }
+}
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataPopulator.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataPopulator.java
new file mode 100644
index 0000000000..41d250ac71
--- /dev/null
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/TestDataPopulator.java
@@ -0,0 +1,11 @@
+package se.su.dsv.scipro.testdata;
+
+public interface TestDataPopulator {
+    /**
+     * Add test data to the system to help with testing a specific feature.
+     *
+     * @param baseData the base data already populated
+     * @param factory helper object to make repetitive tasks easier (such as creating users)
+     */
+    void populate(BaseData baseData, Factory factory);
+}
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/package-info.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/package-info.java
new file mode 100644
index 0000000000..6b93353f29
--- /dev/null
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * This package contains the infrastructure that is used when generating test data for the application. To add new test
+ * data to the system, add a new class to the {@link se.su.dsv.scipro.testdata.populators} package that implements the
+ * {@link se.su.dsv.scipro.testdata.TestDataPopulator} interface and annotate it with
+ * {@link org.springframework.stereotype.Service @Service}. Inject dependencies as needed using
+ * {@link jakarta.inject.Inject @Inject}.
+ */
+package se.su.dsv.scipro.testdata;
diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/populators/package-info.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/populators/package-info.java
new file mode 100644
index 0000000000..c9d116ca5a
--- /dev/null
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/populators/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * Contains classes that populate the database with test data.
+ * <p>
+ * Prefer to use methods on the various services to create data, rather than directly interacting with the database
+ * using an {@link jakarta.persistence.EntityManager}. This is to make sure all business rules are enforced and that
+ * any additional logic is executed such as sending notifications or calculating statistics.
+ *
+ * @see se.su.dsv.scipro.testdata how to add new populators
+ * @see se.su.dsv.scipro.testdata.TestDataPopulator
+ * @see se.su.dsv.scipro.testdata.BaseData
+ * @see se.su.dsv.scipro.testdata.Factory
+ */
+package se.su.dsv.scipro.testdata.populators;
diff --git a/test-data/src/main/resources/META-INF/services/se.su.dsv.scipro.war.PluginConfiguration b/test-data/src/main/resources/META-INF/services/se.su.dsv.scipro.war.PluginConfiguration
new file mode 100644
index 0000000000..99b97f03e4
--- /dev/null
+++ b/test-data/src/main/resources/META-INF/services/se.su.dsv.scipro.war.PluginConfiguration
@@ -0,0 +1 @@
+se.su.dsv.scipro.testdata.TestDataConfiguration
diff --git a/war/pom.xml b/war/pom.xml
index c1dd889587..6a347c9b09 100644
--- a/war/pom.xml
+++ b/war/pom.xml
@@ -140,5 +140,18 @@
                 <spring.profile.active>branch</spring.profile.active>
             </properties>
         </profile>
+        <profile>
+            <id>DEV</id>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+            <dependencies>
+                <dependency>
+                    <groupId>se.su.dsv.scipro</groupId>
+                    <artifactId>test-data</artifactId>
+                    <version>${project.version}</version>
+                </dependency>
+            </dependencies>
+        </profile>
     </profiles>
 </project>

From 17192f9eb9b103c0f442450ae643da5c7ea95fb0 Mon Sep 17 00:00:00 2001
From: Andreas Svanberg <andreass@dsv.su.se>
Date: Tue, 4 Mar 2025 06:12:15 +0100
Subject: [PATCH 5/5] Handle the case with no test data populators (#122)

Since there is no populator yet Spring fails when trying to inject since it does not support empty collections. Mark the dependency as optional until we have at least one populator at which point we can simply the code again.

Reviewed-on: https://gitea.dsv.su.se/DMC/scipro/pulls/122
Reviewed-by: Nico Athanassiadis <nico@dsv.su.se>
Co-authored-by: Andreas Svanberg <andreass@dsv.su.se>
Co-committed-by: Andreas Svanberg <andreass@dsv.su.se>
---
 .../main/java/se/su/dsv/scipro/testdata/DataInitializer.java | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java
index 8b157598c4..b2fec13a8e 100644
--- a/test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java
+++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/DataInitializer.java
@@ -42,7 +42,7 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
     public static final long RESEARCH_AREA_ID = 12L;
 
     @Inject
-    private Collection<TestDataPopulator> testDataPopulators = new ArrayList<>();
+    private Optional<Collection<TestDataPopulator>> testDataPopulators = Optional.empty();
 
     @Inject
     private UserService userService;
@@ -123,7 +123,8 @@ public class DataInitializer implements Lifecycle, BaseData, Factory {
             createTarget();
             createStudentIdea();
             createRoughDraftApproval();
-            for (TestDataPopulator testDataPopulator : testDataPopulators) {
+            Collection<TestDataPopulator> availablePopulators = testDataPopulators.orElseGet(Collections::emptySet);
+            for (TestDataPopulator testDataPopulator : availablePopulators) {
                 testDataPopulator.populate(this, this);
             }
         }