Repository: TQS-EcoCharger/TQS_EcoCharger
Pull request #15: Et 52 as a user i want to register my account into the system using my email and password so that i can begin using the platform
Author: login: Ricardo-Alexandre-Antunes
Created: 2025-05-20T16:49:17Z | Merged: 2025-05-20T17:35:34Z

# Description

<!--- Describe your changes in detail -->
Registration functionality implemented

# Checklist:

- [X] I have performed a self-review of my code
- [X] I have commented my code only when needed, particularly in hard-to-understand areas
- [X] I have made corresponding changes to the documentation
- [X] My changes generate no new warnings/don't affect other functionalities

## Related Issue

<!--- Please link to the issue here: -->
[ET-52](https://ua-deti-alof.atlassian.net/jira/software/projects/ET/boards/2?selectedIssue=ET-52)

Link to the issue:

## Motivation and Context

<!--- If it fixes an open issue, please link to the issue here. -->
<!--- Other keywords also work: Fixes, Resolves, https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue for others -->

Closes [ET-52](https://ua-deti-alof.atlassian.net/jira/software/projects/ET/boards/2?selectedIssue=ET-52)

<!--- Why is this change required? What problem does it solve? -->

This PR implements registration into the website, needed for users to create accounts

## How Has This Been Tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->

Tests ran:

5 Controller Unit Tests
5 Service Unit Tests
5 Scenarios

I also did exploratory testing

![image](https://github.com/user-attachments/assets/674edddd-0f19-4797-aea0-68537734e407)


[ET-52]: https://ua-deti-alof.atlassian.net/browse/ET-52?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[ET-52]: https://ua-deti-alof.atlassian.net/browse/ET-52?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/.github/workflows/build.yml b/.github/workflows/backend-build.yml
similarity index 100%
rename from .github/workflows/build.yml
rename to .github/workflows/backend-build.yml
diff --git a/.github/workflows/styling.yml b/.github/workflows/backend-formatter.yml
similarity index 70%
rename from .github/workflows/styling.yml
rename to .github/workflows/backend-formatter.yml
index a28604a2a..62ea97f8b 100644
--- a/.github/workflows/styling.yml
+++ b/.github/workflows/backend-formatter.yml
@@ -9,28 +9,6 @@ on:
     types: [opened, synchronize, reopened]
 
 jobs:
-  test-eslint:
-    name: Test ESLint
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
-        with:
-          fetch-depth: 0
-
-      - name: Set up Node.js
-        uses: actions/setup-node@v4
-        with:
-          node-version: '18'
-
-      - name: Install dependencies
-        working-directory: ./frontend
-        run: npm install
-
-      - name: Run ESLint
-        working-directory: ./frontend
-        run: npm run lint
-
   format-backend:
     name: Format Backend
     runs-on: ubuntu-latest
diff --git a/.github/workflows/frontend-styling.yml b/.github/workflows/frontend-styling.yml
new file mode 100644
index 000000000..97dde385c
--- /dev/null
+++ b/.github/workflows/frontend-styling.yml
@@ -0,0 +1,32 @@
+name: Enforce Coding Styling
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - main
+  pull_request:
+    types: [opened, synchronize, reopened]
+
+jobs:
+  test-eslint:
+    name: Test ESLint
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - name: Set up Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: '18'
+
+      - name: Install dependencies
+        working-directory: ./frontend
+        run: npm install
+
+      - name: Run ESLint
+        working-directory: ./frontend
+        run: npm run lint
diff --git a/backend/pom.xml b/backend/pom.xml
index c127038f6..c68308d46 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -200,13 +200,6 @@
             <artifactId>spring-boot-starter-cache</artifactId>
         </dependency>
 
-        <!-- Database -->
-        <dependency>
-            <groupId>mysql</groupId>
-            <artifactId>mysql-connector-java</artifactId>
-            <version>8.0.33</version>
-            <scope>runtime</scope>
-        </dependency>
 
         <!-- Documentation -->
         <dependency>
diff --git a/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/config/SecurityConfig.java b/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/config/SecurityConfig.java
index e541176ed..3308ecbed 100644
--- a/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/config/SecurityConfig.java
+++ b/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/config/SecurityConfig.java
@@ -6,11 +6,14 @@
 import org.springframework.core.env.Environment;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.web.cors.CorsConfiguration;
 import org.springframework.web.cors.CorsConfigurationSource;
 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
 
+import jakarta.servlet.http.HttpServletRequest;
+import java.util.regex.Pattern;
 
 import java.util.List;
 
@@ -23,6 +26,23 @@ public class SecurityConfig {
   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
     http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
+        .csrf(
+            csrf ->
+                csrf
+                    /*
+                     * The endpoints described in the regex below are not protected by CSRF. This is because they are publicly
+                     * accessible endpoints. When adding endpoints that do not require CSRF protection, add them to the regex.
+                     */
+                    .ignoringRequestMatchers(
+                    new RequestMatcher() {
+                      private final Pattern pattern =
+                          Pattern.compile("^/api/auth/.*|^/api/suggestions.*|^/actuator/health/.*");
+
+                      @Override
+                      public boolean matches(HttpServletRequest request) {
+                        return pattern.matcher(request.getRequestURI()).matches();
+                      }
+                    }))
         .authorizeHttpRequests(
             auth ->
                 auth.requestMatchers("/api/auth/**")
@@ -32,7 +52,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
                     .requestMatchers("/actuator/health/**")
                     .permitAll()
                     .anyRequest()
-                    .permitAll());
+                    .authenticated());
+
     return http.build();
   }
 
diff --git a/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/controller/AuthenticationController.java b/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/controller/AuthenticationController.java
index 493205776..0211340e5 100644
--- a/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/controller/AuthenticationController.java
+++ b/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/controller/AuthenticationController.java
@@ -1,5 +1,7 @@
 package pt.ua.tqs.ecocharger.ecocharger.controller;
 
+import java.util.Map;
+
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -29,4 +31,25 @@ public ResponseEntity<?> login(@RequestParam String email, @RequestParam String
 
     return ResponseEntity.ok(result);
   }
+
+  @PostMapping("/register")
+  public ResponseEntity<Object> register(@RequestBody Map<String, String> user) {
+    String email = user.get("email");
+    String password = user.get("password");
+    String name = user.get("name");
+    if (email == null || password == null || name == null) {
+      AuthResultDTO result = new AuthResultDTO(false, "Missing required fields", null);
+      return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result.getMessage());
+    }
+    email = email.strip();
+    password = password.strip();
+    name = name.strip();
+    AuthResultDTO result = authService.register(email, password, name);
+
+    if (!result.isSuccess()) {
+      return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result.getMessage());
+    }
+
+    return ResponseEntity.ok(result);
+  }
 }
diff --git a/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/service/AuthenticationServiceImpl.java b/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/service/AuthenticationServiceImpl.java
index 6542e287e..db7be1232 100644
--- a/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/service/AuthenticationServiceImpl.java
+++ b/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/service/AuthenticationServiceImpl.java
@@ -37,4 +37,36 @@ public AuthResultDTO authenticate(String email, String password) {
 
     return new AuthResultDTO(true, "Login successful", token);
   }
+
+  @Override
+  public AuthResultDTO register(String email, String password, String name) {
+    Optional<User> existingUserOpt = userRepository.findByEmail(email);
+    if (existingUserOpt.isPresent()) {
+      return new AuthResultDTO(false, "Email already in use", null);
+    }
+
+    if (password.length() < 6) {
+      return new AuthResultDTO(false, "Password must be at least 6 characters", null);
+    }
+
+    if (name.length() < 3) {
+      return new AuthResultDTO(false, "Name must be at least 3 characters", null);
+    }
+
+    if (!email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) {
+      return new AuthResultDTO(false, "Invalid email format", null);
+    }
+
+    User user = new User();
+    user.setEmail(email);
+    user.setPassword(password);
+    user.setName(name);
+    user.setEnabled(true);
+
+    userRepository.save(user);
+
+    String token = JwtUtil.generateToken(user.getEmail());
+
+    return new AuthResultDTO(true, "Registration successful", token);
+  }
 }
diff --git a/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/service/interfaces/AuthenticationService.java b/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/service/interfaces/AuthenticationService.java
index cb375d302..4780a3862 100644
--- a/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/service/interfaces/AuthenticationService.java
+++ b/backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/service/interfaces/AuthenticationService.java
@@ -1,8 +1,9 @@
 package pt.ua.tqs.ecocharger.ecocharger.service.interfaces;
 
-
 import pt.ua.tqs.ecocharger.ecocharger.dto.AuthResultDTO;
 
 public interface AuthenticationService {
   AuthResultDTO authenticate(String email, String password);
+
+  AuthResultDTO register(String email, String password, String name);
 }
diff --git a/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/controller/AuthenticationControllerTest.java b/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/controller/AuthenticationControllerTest.java
index f30a38cef..e928f178a 100644
--- a/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/controller/AuthenticationControllerTest.java
+++ b/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/controller/AuthenticationControllerTest.java
@@ -16,6 +16,8 @@
 import org.springframework.context.annotation.Primary;
 import org.springframework.test.web.servlet.MockMvc;
 
+import app.getxray.xray.junit.customjunitxml.annotations.Requirement;
+
 import static org.hamcrest.Matchers.containsString;
 import static org.mockito.Mockito.mock;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -67,4 +69,77 @@ void testLoginFailure() throws Exception {
         .andExpect(status().isUnauthorized())
         .andExpect(content().string(containsString("Invalid password")));
   }
+
+  @Test
+  @DisplayName("Register success returns 200 OK and token in JSON")
+  @Requirement("ET-52")
+  void testRegisterSuccess() throws Exception {
+    Mockito.when(authService.register("mariah@example.com", "123456", "Mariah"))
+        .thenReturn(new AuthResultDTO(true, "Registration successful", "token123"));
+
+    String requestBody =
+        "{ \"email\": \" mariah@example.com\", \"password\": \"123456\", \"name\": \"Mariah\" }";
+    mockMvc
+        .perform(post("/api/auth/register").content(requestBody).contentType("application/json"))
+        .andExpect(status().isOk())
+        .andExpect(jsonPath("$.success").value(true))
+        .andExpect(jsonPath("$.message").value("Registration successful"))
+        .andExpect(jsonPath("$.token").value("token123"));
+  }
+
+  @Test
+  @DisplayName("Registering with existing email returns 400 Bad Request and message only")
+  @Requirement("ET-52")
+  void testRegisterExistingEmail() throws Exception {
+    Mockito.when(authService.register("peter@example.com", "123456", "Peter"))
+        .thenReturn(new AuthResultDTO(false, "Email already in use", null));
+    String requestBody =
+        "{ \"email\": \" peter@example.com\", \"password\": \"123456\", \"name\": \"Peter\" }";
+    mockMvc
+        .perform(post("/api/auth/register").content(requestBody).contentType("application/json"))
+        .andExpect(status().isBadRequest())
+        .andExpect(content().string(containsString("Email already in use")));
+  }
+
+  @Test
+  @DisplayName("Registering with short password returns 400 Bad Request and message only")
+  @Requirement("ET-52")
+  void testRegisterShortPassword() throws Exception {
+    Mockito.when(authService.register("andrew@example.com", "123", "Andrew"))
+        .thenReturn(new AuthResultDTO(false, "Password must be at least 6 characters", null));
+    String requestBody =
+        "{ \"email\": \" andrew@example.com\", \"password\": \"123\", \"name\": \"Andrew\" }";
+    mockMvc
+        .perform(post("/api/auth/register").content(requestBody).contentType("application/json"))
+        .andExpect(status().isBadRequest())
+        .andExpect(content().string(containsString("Password must be at least 6 characters")));
+  }
+
+  @Test
+  @DisplayName("Registering with invalid email returns 400 Bad Request and message only")
+  @Requirement("ET-52")
+  void testRegisterInvalidEmail() throws Exception {
+    Mockito.when(authService.register("invalid-email", "123456", "Invalid"))
+        .thenReturn(new AuthResultDTO(false, "Invalid email format", null));
+    String requestBody =
+        "{ \"email\": \" invalid-email\", \"password\": \"123456\", \"name\": \"Invalid\" }";
+    mockMvc
+        .perform(post("/api/auth/register").content(requestBody).contentType("application/json"))
+        .andExpect(status().isBadRequest())
+        .andExpect(content().string(containsString("Invalid email format")));
+  }
+
+  @Test
+  @DisplayName("Registering with invalid name returns 400 Bad Request and message only")
+  @Requirement("ET-52")
+  void testRegisterInvalidName() throws Exception {
+    Mockito.when(authService.register("john@example.com", "123456", ""))
+        .thenReturn(new AuthResultDTO(false, "Name must be at least 3 characters", null));
+    String requestBody =
+        "{ \"email\": \" john@example.com\", \"password\": \"123456\", \"name\": \"\" }";
+    mockMvc
+        .perform(post("/api/auth/register").content(requestBody).contentType("application/json"))
+        .andExpect(status().isBadRequest())
+        .andExpect(content().string(containsString("Name must be at least 3 characters")));
+  }
 }
diff --git a/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/functional/steps/LoginSteps.java b/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/functional/steps/LoginSteps.java
index 337781b3b..21ee226d4 100644
--- a/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/functional/steps/LoginSteps.java
+++ b/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/functional/steps/LoginSteps.java
@@ -1,8 +1,8 @@
 package pt.ua.tqs.ecocharger.ecocharger.functional.steps;
 
+import io.cucumber.java.After;
 import io.cucumber.java.Before;
 import io.cucumber.java.en.*;
-import io.github.bonigarcia.wdm.WebDriverManager;
 
 import org.openqa.selenium.*;
 import org.openqa.selenium.firefox.FirefoxDriver;
@@ -21,15 +21,14 @@ public class LoginSteps {
 
   @Before
   public void setup() {
-    WebDriverManager.firefoxdriver().setup();
-
-    FirefoxOptions options = new FirefoxOptions();
-    options.addArguments("--headless");
-    options.addArguments("--no-sandbox");
-    options.addArguments("--disable-dev-shm-usage");
+    WebDriverSingleton.initialize();
+    driver = WebDriverSingleton.getDriver();
+    wait = WebDriverSingleton.getWait();
+  }
 
-    driver = new FirefoxDriver(options);
-    wait = new WebDriverWait(driver, Duration.ofSeconds(10));
+  @After
+  public void cleanUp() {
+    WebDriverSingleton.quit();
   }
 
   @Given("I am on the login page")
@@ -65,7 +64,5 @@ public void i_should_be_redirected_to_the_home_page() {
 
     String currentUrl = driver.getCurrentUrl();
     assertTrue(currentUrl.endsWith("/home"), "Expected to be on /home but was on " + currentUrl);
-
-    driver.quit();
   }
 }
diff --git a/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/functional/steps/RegistrationSteps.java b/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/functional/steps/RegistrationSteps.java
new file mode 100644
index 000000000..64888777c
--- /dev/null
+++ b/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/functional/steps/RegistrationSteps.java
@@ -0,0 +1,98 @@
+package pt.ua.tqs.ecocharger.ecocharger.functional.steps;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+import java.util.Map;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import io.cucumber.java.After;
+import io.cucumber.java.Before;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+public class RegistrationSteps {
+  private WebDriver driver;
+  private WebDriverWait wait;
+
+  @Before
+  public void setup() {
+    WebDriverSingleton.initialize();
+    driver = WebDriverSingleton.getDriver();
+    wait = WebDriverSingleton.getWait();
+  }
+
+  @After
+  public void cleanUp() {
+    WebDriverSingleton.quit();
+  }
+
+  @Given("I am on the registration page")
+  public void i_am_on_the_registration_page() {
+    driver.get("http://localhost:5000/register");
+    wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("register-form")));
+  }
+
+  @When("I fill in the registration form with:")
+  public void i_fill_in_the_registration_form_with(io.cucumber.datatable.DataTable dataTable) {
+    List<Map<String, String>> users = dataTable.asMaps(String.class, String.class);
+
+    for (Map<String, String> userData : users) {
+      WebElement nameField =
+          wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("name")));
+      WebElement emailField =
+          wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("email")));
+      WebElement passwordField =
+          wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("password")));
+      WebElement confirmPasswordField =
+          wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("confirmPassword")));
+
+      nameField.clear();
+      nameField.sendKeys(userData.get("name"));
+      emailField.clear();
+      emailField.sendKeys(userData.get("email"));
+      passwordField.clear();
+      passwordField.sendKeys(userData.get("password"));
+      confirmPasswordField.clear();
+      confirmPasswordField.sendKeys(userData.get("confirmPassword"));
+    }
+  }
+
+  @Given("I submit the form")
+  public void i_submit_the_form() {
+    WebElement submitButton =
+        wait.until(ExpectedConditions.elementToBeClickable(By.id("register-button")));
+    submitButton.click();
+  }
+
+  @Then("I should see an invalid email error message")
+  public void i_should_see_an_invalid_email_error_message() {
+    WebElement errorElement =
+        wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("invalid-email")));
+    String errorMessage = errorElement.getText();
+    assertEquals("Invalid email format", errorMessage);
+  }
+
+  @Then("I should see an error message saying {string}")
+  public void i_should_see_an_error_message_saying(String errorMessage) {
+    WebElement errorElement =
+        wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("register-error")));
+    String actualErrorMessage = errorElement.getText();
+    assertEquals(errorMessage, actualErrorMessage);
+  }
+
+  @Then("I should remain on the registration page")
+  public void i_should_remain_on_the_registration_page() {
+    wait.until(ExpectedConditions.urlContains("/register"));
+
+    String currentUrl = driver.getCurrentUrl();
+    assertTrue(
+        currentUrl.endsWith("/register"), "Expected to be on /register but was on " + currentUrl);
+  }
+}
diff --git a/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/functional/steps/WebDriverSingleton.java b/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/functional/steps/WebDriverSingleton.java
new file mode 100644
index 000000000..db7c08bba
--- /dev/null
+++ b/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/functional/steps/WebDriverSingleton.java
@@ -0,0 +1,39 @@
+package pt.ua.tqs.ecocharger.ecocharger.functional.steps;
+
+import io.github.bonigarcia.wdm.WebDriverManager;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.firefox.FirefoxDriver;
+import org.openqa.selenium.firefox.FirefoxOptions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+import java.time.Duration;
+
+public class WebDriverSingleton {
+  private static WebDriver driver;
+  private static WebDriverWait wait;
+
+  public static void initialize() {
+    if (driver == null) {
+      WebDriverManager.firefoxdriver().setup();
+      FirefoxOptions options = new FirefoxOptions();
+      options.addArguments("--headless", "--no-sandbox", "--disable-dev-shm-usage");
+      driver = new FirefoxDriver(options);
+      wait = new WebDriverWait(driver, Duration.ofSeconds(10));
+    }
+  }
+
+  public static WebDriver getDriver() {
+    return driver;
+  }
+
+  public static WebDriverWait getWait() {
+    return wait;
+  }
+
+  public static void quit() {
+    if (driver != null) {
+      driver.quit();
+      driver = null;
+      wait = null;
+    }
+  }
+}
diff --git a/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/service/AuthenticationServiceTest.java b/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/service/AuthenticationServiceTest.java
index 12a6e1e44..f6e45c18d 100644
--- a/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/service/AuthenticationServiceTest.java
+++ b/backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/service/AuthenticationServiceTest.java
@@ -55,4 +55,54 @@ void testNonExistentUser() {
     assertThat(result.getMessage()).isEqualTo("User not found");
     assertThat(result.getToken()).isNull();
   }
+
+  @Test
+  @DisplayName("ET-52: Should register successfully and receive JWT")
+  @Requirement("ET-52")
+  void testSuccessfulRegistration() {
+    AuthResultDTO result = authService.register("new_guy@example.com", "123456", "New Guy");
+    assertThat(result.isSuccess()).isTrue();
+    assertThat(result.getMessage()).isEqualTo("Registration successful");
+    assertThat(result.getToken()).isNotNull();
+  }
+
+  @Test
+  @DisplayName("ET-52: Should fail registration with existing email")
+  @Requirement("ET-52")
+  void testExistingEmail() {
+    AuthResultDTO result = authService.register("john@example.com", "123456", "John Doe");
+    assertThat(result.isSuccess()).isFalse();
+    assertThat(result.getMessage()).isEqualTo("Email already in use");
+    assertThat(result.getToken()).isNull();
+  }
+
+  @Test
+  @DisplayName("ET-52: Should fail registration with short password")
+  @Requirement("ET-52")
+  void testShortPassword() {
+    AuthResultDTO result = authService.register("new_guy1@example.com", "123", "New Guy");
+    assertThat(result.isSuccess()).isFalse();
+    assertThat(result.getMessage()).isEqualTo("Password must be at least 6 characters");
+    assertThat(result.getToken()).isNull();
+  }
+
+  @Test
+  @DisplayName("ET-52: Should fail registration with short name")
+  @Requirement("ET-52")
+  void testEmptyName() {
+    AuthResultDTO result = authService.register("new_guy2@example.com", "123456", "ab");
+    assertThat(result.isSuccess()).isFalse();
+    assertThat(result.getMessage()).isEqualTo("Name must be at least 3 characters");
+    assertThat(result.getToken()).isNull();
+  }
+
+  @Test
+  @DisplayName("ET-52: Should fail registration with invalid email format")
+  @Requirement("ET-52")
+  void testInvalidEmailFormat() {
+    AuthResultDTO result = authService.register("invalid-email", "123456", "New Guy");
+    assertThat(result.isSuccess()).isFalse();
+    assertThat(result.getMessage()).isEqualTo("Invalid email format");
+    assertThat(result.getToken()).isNull();
+  }
 }
diff --git a/backend/src/test/resources/db/migration/V2__inserts.sql b/backend/src/test/resources/db/migration/V2__inserts.sql
deleted file mode 100644
index e69de29bb..000000000
diff --git a/backend/src/test/resources/pt/ua/tqs/ecocharger/ecocharger/features/register.feature b/backend/src/test/resources/pt/ua/tqs/ecocharger/ecocharger/features/register.feature
new file mode 100644
index 000000000..e76e862d0
--- /dev/null
+++ b/backend/src/test/resources/pt/ua/tqs/ecocharger/ecocharger/features/register.feature
@@ -0,0 +1,47 @@
+Feature: Registering a new user
+  As a user
+  I want to register a new account
+  So that I can access the application
+
+  Background:
+    Given I am on the registration page
+
+    Scenario: Registering with valid data
+      When I fill in the registration form with:
+        | name   |  password | confirmPassword  | email                 |
+        | User 3 |  pass123    | pass123            | user3@gmail.com       |
+      And I submit the form
+      Then I should be redirected to the home page
+
+    Scenario: Registering with an already taken email
+      When I fill in the registration form with:
+        | name   |  password | confirmPassword  | email                 |
+        | User 1 |  pass123    | pass123            | afonso@gmail.com  |
+      And I submit the form
+      Then I should see an error message saying "Email already in use"
+      And I should remain on the registration page
+
+      Scenario: Registering with a weak password
+      When I fill in the registration form with:
+        | name   |  password | confirmPassword  | email                 |
+        | User 5 |  pas     | pas               | user5@gmail.com       |
+      And I submit the form
+      Then I should see an error message saying "Password must be at least 6 characters"
+      And I should remain on the registration page
+
+    Scenario: Registering with mismatched passwords
+      When I fill in the registration form with:
+        | name   |  password | confirmPassword  | email                 |
+        | User 6 |  pass6    | pass7            | user6@gmail.com       |
+      And I submit the form
+      Then I should see an error message saying "Passwords do not match."
+      And I should remain on the registration page
+
+    Scenario: Registering with an invalid email
+      When I fill in the registration form with:
+        | name   |  password | confirmPassword  | email                 |
+        | User 7 |  pass8    | pass8            | user7gmail.com        |
+      And I submit the form
+      Then I should see an invalid email error message
+      And I should remain on the registration page
+
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index d1d0f819a..29e4deec8 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -2,6 +2,7 @@ import React from "react";
 import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
 import LoginPage from "./pages/LoginPage";
 import HomePage from "./pages/HomePage";
+import RegisterPage from "./pages/RegisterPage";
 
 function App() {
   return (
@@ -9,6 +10,7 @@ function App() {
       <Routes>
         <Route path="/" element={<LoginPage />} />
         <Route path="/home" element={<HomePage />} />
+        <Route path="/register" element={<RegisterPage />} />
       </Routes>
     </Router>
   );
diff --git a/frontend/src/pages/HomePage.jsx b/frontend/src/pages/HomePage.jsx
index 9d323a269..e336b728b 100644
--- a/frontend/src/pages/HomePage.jsx
+++ b/frontend/src/pages/HomePage.jsx
@@ -1,3 +1,20 @@
-export default function HomePage() {
-    <></>
-}
\ No newline at end of file
+import React from "react";
+
+const HomePage = () => {
+  return (
+    <div id="home-page-content" style={styles.container}>
+      <h1>Welcome to EcoCharger</h1>
+      <p>You have successfully logged in or registered.</p>
+    </div>
+  );
+};
+
+const styles = {
+  container: {
+    padding: "2rem",
+    textAlign: "center",
+    fontFamily: "Arial, sans-serif",
+  },
+};
+
+export default HomePage;
diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx
index 00f33dc7c..e0b67c9d4 100644
--- a/frontend/src/pages/LoginPage.jsx
+++ b/frontend/src/pages/LoginPage.jsx
@@ -79,6 +79,12 @@ const LoginPage = () => {
               <button type="submit" className={styles.button} id="login-button">
                 Login
               </button>
+              <p className={styles.footerText}>
+                Don't have an account?{" "}
+                <a href="/register" className={styles.link}>
+                  Register
+                </a>
+              </p>
             </form>
         </div>
       </motion.div>
diff --git a/frontend/src/pages/RegisterPage.jsx b/frontend/src/pages/RegisterPage.jsx
new file mode 100644
index 000000000..8af4e9b9a
--- /dev/null
+++ b/frontend/src/pages/RegisterPage.jsx
@@ -0,0 +1,145 @@
+/* Done with the help of ChatGPT */
+
+import React, { useState } from "react";
+import axios from "axios";
+import styles from "../css/LoginPage.module.css";
+import { motion } from "framer-motion";
+import logo from "../assets/logo.png";
+import CONFIG from "../../config";
+import { useNavigate, Link } from "react-router-dom";
+
+const RegisterPage = () => {
+  const [name, setName] = useState("");
+  const [email, setEmail] = useState("");
+  const [isEmailValid, setIsEmailValid] = useState(true);
+  const [password, setPassword] = useState("");
+  const [confirmPassword, setConfirmPassword] = useState("");
+  const [error, setError] = useState("");
+  const navigate = useNavigate();
+
+  const validateEmail = (value) => {
+    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+    setIsEmailValid(emailRegex.test(value));
+  };
+
+  const handleSubmit = async (e) => {
+    e.preventDefault();
+    setError("");
+
+    if (password !== confirmPassword) {
+      setError("Passwords do not match.");
+      return;
+    }
+
+    if (!isEmailValid) {
+      setError("Please enter a valid email.");
+      return;
+    }
+
+    try {
+      const response = await axios.post(`${CONFIG.API_URL}auth/register`, {
+        name,
+        email,
+        password,
+      });
+
+      console.log("Registration successful:", response.data);
+      localStorage.setItem("token", response.data.token);
+      navigate("/home");
+    } catch (err) {
+      console.error("Registration failed:", err.response?.data || err.message);
+      setError(err.response?.data || "Registration failed. Please try again.");
+    }
+  };
+
+  return (
+    <div className={styles.container}>
+      <motion.div
+        initial={{ opacity: 0, scale: 0.95 }}
+        animate={{ opacity: 1, scale: 1 }}
+        transition={{ duration: 0.6, ease: "easeOut" }}
+        className={styles.cardWrapper}
+      >
+        <div className={styles.card}>
+          <motion.img
+            src={logo}
+            alt="Logo"
+            className={styles.logo}
+            initial={{ opacity: 0, y: -20 }}
+            animate={{ opacity: 1, y: 0 }}
+            transition={{ duration: 0.5, delay: 0.3 }}
+          />
+          <h2 className={styles.title}>Create an Account</h2>
+          <form className={styles.form} onSubmit={handleSubmit} id="register-form">
+            <div className={styles.inputGroup}>
+              <label htmlFor="name" className={styles.label}>Name</label>
+              <input
+                type="text"
+                id="name"
+                placeholder="John Doe"
+                className={styles.input}
+                value={name}
+                onChange={(e) => setName(e.target.value)}
+                required
+              />
+            </div>
+            <div className={styles.inputGroup}>
+              <label htmlFor="email" className={styles.label}>Email</label>
+              <input
+                type="email"
+                id="email"
+                placeholder="you@example.com"
+                className={`${styles.input} ${!isEmailValid ? styles.invalid : ""}`}
+                value={email}
+                onChange={(e) => {
+                  setEmail(e.target.value);
+                  validateEmail(e.target.value);
+                }}
+                required
+              />
+              {!isEmailValid && (
+                <small className={styles.error} id="invalid-email">Invalid email format</small>
+              )}
+            </div>
+            <div className={styles.inputGroup}>
+              <label htmlFor="password" className={styles.label}>Password</label>
+              <input
+                type="password"
+                id="password"
+                placeholder="••••••••"
+                className={styles.input}
+                value={password}
+                onChange={(e) => setPassword(e.target.value)}
+                required
+              />
+            </div>
+            <div className={styles.inputGroup}>
+              <label htmlFor="confirmPassword" className={styles.label}>Confirm Password</label>
+              <input
+                type="password"
+                id="confirmPassword"
+                placeholder="••••••••"
+                className={styles.input}
+                value={confirmPassword}
+                onChange={(e) => setConfirmPassword(e.target.value)}
+                required
+              />
+            </div>
+            {error && <p className={styles.error} id="register-error">{error}</p>}
+            <button type="submit" className={styles.button} id="register-button">
+              Register
+            </button>
+            <p className={styles.footerText}>
+              Already have an account?{" "}
+              <Link to="/" className={styles.link}>
+                Login
+              </Link>
+            </p>
+          </form>
+        </div>
+      </motion.div>
+    </div>
+  );
+};
+
+export default RegisterPage;


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

Implements user registration end-to-end, adding the frontend form and wiring it into React Router, extending the backend service/controller to handle `register`, and providing unit and functional tests.

- Add `RegisterPage` component and link from `LoginPage`, update routing in `App.jsx`
- Introduce `register` method in `AuthenticationService` and endpoint in `AuthenticationController` with input validation and JWT issuance
- Cover registration scenarios with unit tests, controller tests, and Cucumber feature/step definitions; update security config for CSRF and authorization

### Reviewed Changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 6 comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| frontend/src/pages/RegisterPage.jsx | New registration form component |
| frontend/src/pages/LoginPage.jsx | Added link to register on login page |
| frontend/src/pages/HomePage.jsx | Defined home page content after login/registration |
| frontend/src/App.jsx | Wired `/register` route |
| backend/src/main/java/…/AuthenticationService.java | Declared `register` method |
| backend/src/main/java/…/AuthenticationServiceImpl.java | Implemented registration logic and validation |
| backend/src/main/java/…/AuthenticationController.java | Added `/api/auth/register` endpoint |
| backend/src/main/java/…/SecurityConfig.java | Updated CSRF ignoring and authorization rules |
| backend/src/test/resources/…/register.feature | Feature scenarios for registration |
| backend/src/test/java/…/AuthenticationServiceTest.java | Unit tests for service registration |
| backend/src/test/java/…/AuthenticationControllerTest.java | Controller tests for register responses |
| backend/src/test/java/…/functional/steps/WebDriverSingleton.java | Provided WebDriver singleton for UI tests |
| backend/src/test/java/…/functional/steps/RegistrationSteps.java | Step definitions for registration feature |
| backend/src/test/java/…/functional/steps/LoginSteps.java | Updated login steps to use singleton |
| .github/workflows/frontend-styling.yml | Added ESLint workflow |
| .github/workflows/backend-formatter.yml | Cleaned up formatter workflow |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**frontend/src/pages/RegisterPage.jsx:1**
* [nitpick] Consider removing or clarifying the top-of-file comment crediting ChatGPT, as it may not be necessary for long-term maintainability.
```
/* Done with the help of ChatGPT */
```
**frontend/src/pages/RegisterPage.jsx:5**
* [nitpick] Importing a login-specific CSS module for the register page can be confusing; consider creating a dedicated RegisterPage.module.css or a shared styles module.
```
import styles from "../css/LoginPage.module.css";
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T16:54:37Z
user: login: alof2004
body: Code looks ok but i would like to see a screenshot the next time
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T16:56:39Z
user: login: alof2004
body: Thank you for the screenshot, looks good 
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T17:16:34Z
user: login: Ricardo-Alexandre-Antunes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T17:18:12Z
user: login: Ricardo-Alexandre-Antunes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T17:20:46Z
user: login: alof2004
body: Looks good.
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T17:25:03Z
user: login: alof2004
body: For the last time it looks good
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T17:29:36Z
user: login: alof2004
body: If i have to accept this one more time i might kms
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T17:35:24Z

Review comments:
diff_hunk: @@ -79,6 +79,12 @@ const LoginPage = () => {
               <button type="submit" className={styles.button} id="login-button">
                 Login
               </button>
+              <p className={styles.footerText}>
+                Don't have an account?{" "}
+                <a href="/register" className={styles.link}>
+                  Register
+                </a>
path: frontend/src/pages/LoginPage.jsx
user: login: Copilot
body: Use React Router’s <Link> component for internal navigation instead of <a> to avoid a full page reload and preserve SPA routing.
```suggestion
                <Link to="/register" className={styles.link}>
                  Register
                </Link>
```
created_at: 2025-05-20T16:54:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,4 +31,22 @@ public ResponseEntity<?> login(@RequestParam String email, @RequestParam String
 
     return ResponseEntity.ok(result);
   }
+
+  @PostMapping("/register")
+  public ResponseEntity<?> register(@RequestBody Map<String, String> user) {
+    String email = user.get("email").strip();
+    String password = user.get("password").strip();
+    String name = user.get("name").strip();
+    if (email == null || password == null || name == null) {
+      AuthResultDTO result = new AuthResultDTO(false, "Missing required fields", null);
+      return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result.getMessage());
+    }
path: backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/controller/AuthenticationController.java
user: login: Copilot
body: Calling strip() before checking for null can throw a NullPointerException if the 'email' field is missing; move null-check above the trim operation.
```suggestion
    String email = user.get("email");
    String password = user.get("password");
    String name = user.get("name");
    if (email == null || password == null || name == null) {
      AuthResultDTO result = new AuthResultDTO(false, "Missing required fields", null);
      return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result.getMessage());
    }
    email = email.strip();
    password = password.strip();
    name = name.strip();
```
created_at: 2025-05-20T16:54:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,4 +31,22 @@ public ResponseEntity<?> login(@RequestParam String email, @RequestParam String
 
     return ResponseEntity.ok(result);
   }
+
+  @PostMapping("/register")
+  public ResponseEntity<?> register(@RequestBody Map<String, String> user) {
+    String email = user.get("email").strip();
+    String password = user.get("password").strip();
+    String name = user.get("name").strip();
+    if (email == null || password == null || name == null) {
+      AuthResultDTO result = new AuthResultDTO(false, "Missing required fields", null);
+      return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result.getMessage());
+    }
+    AuthResultDTO result = authService.register(email, password, name);
+
+    if (!result.isSuccess()) {
+      return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result.getMessage());
path: backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/controller/AuthenticationController.java
user: login: Copilot
body: Error responses return a plain string rather than a consistent JSON structure; consider returning a JSON object with an error field to match the success response schema.
```suggestion
      return ResponseEntity.status(HttpStatus.BAD_REQUEST)
          .body(Map.of("error", result.getMessage()));
    }
    AuthResultDTO result = authService.register(email, password, name);

    if (!result.isSuccess()) {
      return ResponseEntity.status(HttpStatus.BAD_REQUEST)
          .body(Map.of("error", result.getMessage()));
```
created_at: 2025-05-20T16:54:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,4 +37,36 @@ public AuthResultDTO authenticate(String email, String password) {
 
     return new AuthResultDTO(true, "Login successful", token);
   }
+
+  @Override
+  public AuthResultDTO register(String email, String password, String name) {
+    Optional<User> existingUserOpt = userRepository.findByEmail(email);
+    if (existingUserOpt.isPresent()) {
+      return new AuthResultDTO(false, "Email already in use", null);
+    }
+
+    if (password.length() < 6) {
+      return new AuthResultDTO(false, "Password must be at least 6 characters", null);
+    }
+
+    if (name.length() < 3) {
+      return new AuthResultDTO(false, "Name must be at least 3 characters", null);
+    }
+
+    if (!email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
path: backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/service/AuthenticationServiceImpl.java
user: login: Copilot
body: The email regex limits top-level domains to 2–4 characters, which may reject valid longer TLDs; consider using a more comprehensive validation or a standard library.
created_at: 2025-05-20T16:54:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,4 +37,36 @@ public AuthResultDTO authenticate(String email, String password) {
 
     return new AuthResultDTO(true, "Login successful", token);
   }
+
+  @Override
+  public AuthResultDTO register(String email, String password, String name) {
+    Optional<User> existingUserOpt = userRepository.findByEmail(email);
+    if (existingUserOpt.isPresent()) {
+      return new AuthResultDTO(false, "Email already in use", null);
+    }
+
+    if (password.length() < 6) {
+      return new AuthResultDTO(false, "Password must be at least 6 characters", null);
+    }
+
+    if (name.length() < 3) {
+      return new AuthResultDTO(false, "Name must be at least 3 characters", null);
+    }
+
+    if (!email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
+      return new AuthResultDTO(false, "Invalid email format", null);
+    }
+
+    User user = new User();
+    user.setEmail(email);
+    user.setPassword(password);
path: backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/service/AuthenticationServiceImpl.java
user: login: Copilot
body: Storing raw passwords is insecure; integrate a password encoder (e.g., BCrypt) to hash passwords before persisting.
created_at: 2025-05-20T16:54:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,97 @@
+package pt.ua.tqs.ecocharger.ecocharger.functional.steps;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.firefox.FirefoxDriver;
+import org.openqa.selenium.firefox.FirefoxOptions;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import io.cucumber.java.After;
+import io.cucumber.java.Before;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import io.github.bonigarcia.wdm.WebDriverManager;
+
+public class RegistrationSteps {
+    private WebDriver driver;
+    private WebDriverWait wait;
+
+    @Before
+    public void setup() {
+        WebDriverSingleton.initialize();
+        driver = WebDriverSingleton.getDriver();
+        wait = WebDriverSingleton.getWait();
+    }
+
+    @After
+    public void cleanUp() {
+        WebDriverSingleton.quit();
+    }
+
+    @Given("I am on the registration page")
+    public void i_am_on_the_registration_page() {
+        driver.get("http://localhost:5000/register");
+        wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("register-form")));
+    }
+
+    @When("I fill in the registration form with:")
+    public void i_fill_in_the_registration_form_with(io.cucumber.datatable.DataTable dataTable) {
+        List<Map<String, String>> users = dataTable.asMaps(String.class, String.class);
+
+        for (Map<String, String> userData : users) {
+            WebElement nameField = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("name")));
+            WebElement emailField = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("email")));
+            WebElement passwordField = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("password")));
+            WebElement confirmPasswordField = wait.until(ExpectedConditions
+                    .visibilityOfElementLocated(By.id("confirmPassword")));
+
+            nameField.clear();
+            nameField.sendKeys(userData.get("name"));
+            emailField.clear();
+            emailField.sendKeys(userData.get("email"));
+            passwordField.clear();
+            passwordField.sendKeys(userData.get("password"));
+            confirmPasswordField.clear();
+            confirmPasswordField.sendKeys(userData.get("confirmPassword"));
+        }
+    }
+
+    @Given("I submit the form")
+    public void i_submit_the_form() {
+        WebElement submitButton = wait.until(ExpectedConditions.elementToBeClickable(By.id("register-button")));
+        submitButton.click();
+    }
+
+    @Then("I should see an invalid email error message")
+    public void i_should_see_an_invalid_email_error_message() {
+        WebElement errorElement = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("invalid-email")));
+        String errorMessage = errorElement.getText();
+        assertEquals("Invalid email format", errorMessage);
+    }
+
+    @Then("I should see an error message saying {string}")
+    public void i_should_see_an_error_message_saying(String errorMessage) {
+        WebElement errorElement = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("register-error")));
+        String actualErrorMessage = errorElement.getText();
+        assertEquals(errorMessage, actualErrorMessage);
+    }
+
+    @Then("I should remain on the registration page")
+    public void i_should_remain_on_the_registration_page() {
+        wait.until(ExpectedConditions.urlContains("/register"));
+
+        String currentUrl = driver.getCurrentUrl();
+        assertTrue(currentUrl.endsWith("/register"), "Expected to be on /register but was on " + currentUrl);
+
+        driver.quit();
path: backend/src/test/java/pt/ua/tqs/ecocharger/ecocharger/functional/steps/RegistrationSteps.java
user: login: Copilot
body: [nitpick] Driver cleanup should be centralized in the @After hook rather than inside a step; remove this call to prevent premature driver shutdown.
```suggestion

```
created_at: 2025-05-20T16:54:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,6 +79,12 @@ const LoginPage = () => {
               <button type="submit" className={styles.button} id="login-button">
                 Login
               </button>
+              <p className={styles.footerText}>
+                Don't have an account?{" "}
+                <a href="/register" className={styles.link}>
+                  Register
+                </a>
path: frontend/src/pages/LoginPage.jsx
user: login: Ricardo-Alexandre-Antunes
body: @alof2004 
created_at: 2025-05-20T17:18:12Z
author_association: COLLABORATOR
diff_hunk: @@ -37,4 +37,36 @@ public AuthResultDTO authenticate(String email, String password) {
 
     return new AuthResultDTO(true, "Login successful", token);
   }
+
+  @Override
+  public AuthResultDTO register(String email, String password, String name) {
+    Optional<User> existingUserOpt = userRepository.findByEmail(email);
+    if (existingUserOpt.isPresent()) {
+      return new AuthResultDTO(false, "Email already in use", null);
+    }
+
+    if (password.length() < 6) {
+      return new AuthResultDTO(false, "Password must be at least 6 characters", null);
+    }
+
+    if (name.length() < 3) {
+      return new AuthResultDTO(false, "Name must be at least 3 characters", null);
+    }
+
+    if (!email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
path: backend/src/main/java/pt/ua/tqs/ecocharger/ecocharger/service/AuthenticationServiceImpl.java
user: login: Ricardo-Alexandre-Antunes
body: we only use .com or .org or .pt others can suffer
created_at: 2025-05-20T17:20:46Z
author_association: COLLABORATOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-20T17:33:40Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=TQS-EcoCharger_TQS_EcoCharger&pullRequest=15) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=TQS-EcoCharger_TQS_EcoCharger&pullRequest=15&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=TQS-EcoCharger_TQS_EcoCharger&pullRequest=15&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=TQS-EcoCharger_TQS_EcoCharger&pullRequest=15&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [88.9% Coverage on New Code](https://sonarcloud.io/component_measures?id=TQS-EcoCharger_TQS_EcoCharger&pullRequest=15&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=TQS-EcoCharger_TQS_EcoCharger&pullRequest=15&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=TQS-EcoCharger_TQS_EcoCharger&pullRequest=15)


author_association: NONE
user: login: Ponte348
created_at: 2025-05-20T17:51:02Z
body: Very interesting name for a PR, I sure would love if my project colleagues followed your practices.
author_association: NONE