From e98f0446af6031b7fc066f71bf251bd46b12eb9d Mon Sep 17 00:00:00 2001 From: Darren Ackers Date: Fri, 31 Jan 2025 18:24:15 +0000 Subject: [PATCH 1/7] docs(memorystore): added valkey session demo application --- memorystore/valkey/session/README.md | 101 +++++++ memorystore/valkey/session/app/.gitignore | 92 +++++++ memorystore/valkey/session/app/Dockerfile | 24 ++ .../valkey/session/app/docker-compose.yaml | 37 +++ memorystore/valkey/session/app/init.sql | 6 + memorystore/valkey/session/app/main.tf | 139 ++++++++++ memorystore/valkey/session/app/pom.xml | 129 +++++++++ .../src/main/java/app/AccountRepository.java | 78 ++++++ .../app/src/main/java/app/AuthController.java | 136 ++++++++++ .../src/main/java/app/BasketController.java | 73 ++++++ .../app/src/main/java/app/BasketItem.java | 20 ++ .../app/src/main/java/app/DataController.java | 73 ++++++ .../session/app/src/main/java/app/Global.java | 29 +++ .../app/src/main/java/app/HomeController.java | 25 ++ .../app/src/main/java/app/JdbcConfig.java | 49 ++++ .../app/src/main/java/app/JedisConfig.java | 56 ++++ .../app/src/main/java/app/LoginInfo.java | 16 ++ .../session/app/src/main/java/app/Main.java | 16 ++ .../app/src/main/java/app/RegisterInfo.java | 18 ++ .../session/app/src/main/java/app/Utils.java | 49 ++++ .../app/src/main/java/app/VerifyResponse.java | 27 ++ .../app/src/main/resources/public/items.json | 202 ++++++++++++++ .../app/src/main/resources/static/basket.js | 226 ++++++++++++++++ .../app/src/main/resources/static/main.js | 85 ++++++ .../app/src/main/resources/static/utils.js | 22 ++ .../app/src/main/resources/static/verify.js | 39 +++ .../src/main/resources/templates/index.html | 186 +++++++++++++ .../src/main/resources/templates/login.html | 113 ++++++++ .../main/resources/templates/register.html | 139 ++++++++++ .../src/test/java/app/AuthControllerTest.java | 218 ++++++++++++++++ .../src/test/java/app/DataControllerTest.java | 246 ++++++++++++++++++ .../valkey/session/sample-data/.gitignore | 84 ++++++ .../valkey/session/sample-data/Dockerfile | 24 ++ .../session/sample-data/docker-compose.yaml | 52 ++++ .../valkey/session/sample-data/pom.xml | 80 ++++++ .../sample-data/src/main/java/app/Main.java | 99 +++++++ 36 files changed, 3008 insertions(+) create mode 100644 memorystore/valkey/session/README.md create mode 100644 memorystore/valkey/session/app/.gitignore create mode 100644 memorystore/valkey/session/app/Dockerfile create mode 100644 memorystore/valkey/session/app/docker-compose.yaml create mode 100644 memorystore/valkey/session/app/init.sql create mode 100644 memorystore/valkey/session/app/main.tf create mode 100644 memorystore/valkey/session/app/pom.xml create mode 100644 memorystore/valkey/session/app/src/main/java/app/AccountRepository.java create mode 100644 memorystore/valkey/session/app/src/main/java/app/AuthController.java create mode 100644 memorystore/valkey/session/app/src/main/java/app/BasketController.java create mode 100644 memorystore/valkey/session/app/src/main/java/app/BasketItem.java create mode 100644 memorystore/valkey/session/app/src/main/java/app/DataController.java create mode 100644 memorystore/valkey/session/app/src/main/java/app/Global.java create mode 100644 memorystore/valkey/session/app/src/main/java/app/HomeController.java create mode 100644 memorystore/valkey/session/app/src/main/java/app/JdbcConfig.java create mode 100644 memorystore/valkey/session/app/src/main/java/app/JedisConfig.java create mode 100644 memorystore/valkey/session/app/src/main/java/app/LoginInfo.java create mode 100644 memorystore/valkey/session/app/src/main/java/app/Main.java create mode 100644 memorystore/valkey/session/app/src/main/java/app/RegisterInfo.java create mode 100644 memorystore/valkey/session/app/src/main/java/app/Utils.java create mode 100644 memorystore/valkey/session/app/src/main/java/app/VerifyResponse.java create mode 100644 memorystore/valkey/session/app/src/main/resources/public/items.json create mode 100644 memorystore/valkey/session/app/src/main/resources/static/basket.js create mode 100644 memorystore/valkey/session/app/src/main/resources/static/main.js create mode 100644 memorystore/valkey/session/app/src/main/resources/static/utils.js create mode 100644 memorystore/valkey/session/app/src/main/resources/static/verify.js create mode 100644 memorystore/valkey/session/app/src/main/resources/templates/index.html create mode 100644 memorystore/valkey/session/app/src/main/resources/templates/login.html create mode 100644 memorystore/valkey/session/app/src/main/resources/templates/register.html create mode 100644 memorystore/valkey/session/app/src/test/java/app/AuthControllerTest.java create mode 100644 memorystore/valkey/session/app/src/test/java/app/DataControllerTest.java create mode 100644 memorystore/valkey/session/sample-data/.gitignore create mode 100644 memorystore/valkey/session/sample-data/Dockerfile create mode 100644 memorystore/valkey/session/sample-data/docker-compose.yaml create mode 100644 memorystore/valkey/session/sample-data/pom.xml create mode 100644 memorystore/valkey/session/sample-data/src/main/java/app/Main.java diff --git a/memorystore/valkey/session/README.md b/memorystore/valkey/session/README.md new file mode 100644 index 00000000000..40c8145ae24 --- /dev/null +++ b/memorystore/valkey/session/README.md @@ -0,0 +1,101 @@ +# Session Management + +This demo shows how to use Valkey as an in-memory sessions to store user tokens for quick access in a session management application. By storing user tokens in Valkey, the application can quickly retrieve and validate tokens without having to query the database. + +## Running the application locally + +### 1. Run your database locally. You can download and install PostgresSQL via this [link](https://www.postgresql.org/download/) + +### 2. Run your Valkey server locally. You can download and install Valkey via this [link](https://valkey.io/download/) + +### 3. Ensure that you have a user created called `postgres` + +```bash +createuser -s postgres +``` + +### 4. Next, create the required database tables + +```bash +psql -U postgres -d postgres -f ./app/init.sql +``` + +### 5. Run the app + +```bash +mvn clean spring-boot:run +``` + +### 6. Navigate to the web url `http://localhost:8080` to view your application + +## How to run the application locally (via Docker) + +You can use [docker compose](https://docs.docker.com/compose/install/) to run the app locally. Run the following: + +```bash +cd app +docker-compose up --build +``` + +You can also run the app with sample data by running the following: + +```bash +cd sample-data +docker-compose up --build +``` + +## How to deploy the application to Google Cloud + +1. You can use [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli) to deploy the infrastructure to Google Cloud. Run the following: + + ```bash + cd app + terraform init + terraform apply + ``` + + It should fail to ceate the Google Cloud Run service, but don't worry, we'll fix that in the next series of steps. The message you get might look like this: + + ``` + Error waiting to create Service: Error waiting for Creating Service: Error code 13, message: Revision 'sessions-app-service-00001-9zj' is not ready and cannot serve traffic. Image 'gcr.io/cloudannot serve traffic. Image 'gcr.io/cloud-memorystore-demos/sessions-app:latest' not found. + ``` + +2. Once the infrastructure is created, you'll need to run the `init.sql` script in the Cloud SQL instance to create the necessary tables. You can use the Cloud Shell to do this. Run the following command in the Cloud Shell: + + ```bash + gcloud sql connect --database=sessions-app-db --user=admin # The admin and database were created in the Terraform script + ``` + + Note: Ensure that the instance name is the same as the one you used in the Terraform script. + + a. When prompted to enable the Cloud SQL Admin app, type `Y` and press `Enter`. + b. When prompted to enter the password, type the password you set in the Terraform script and press `Enter`. + c. Once you're connected to the Cloud SQL instance, run the following command to run the `init.sql` script: + + ```sql + \i init.sql + ``` + +3. Finally, redeploy the Cloud Run service using the local source code. Run the following command: + + ```bash + gcloud run deploy \ + --source=. \ + --region= \ + --update-env-vars=ALLOWED_ORIGINS=example.com \ + ``` + + Note: Ensure that the instance name and region are the same as the ones you used in the Terraform script. Also, replace `example.com` with the domain of your frontend application. + +Now you should have the application running on Google Cloud. + +### Endpoints + +- `POST /auth/register` - Registers a new user +- `POST /auth/login` - Logs in a user +- `POST /auth/logout` - Logs out a user +- `POST /auth/verify` - Verifies a user's token +- `GET /api/basket` - Get all items +- `POST /api/basket/add` - Add item with quantity +- `POST /api/basket/remove` - Remove item quantity +- `POST /api/basket/clear` - Clear entire basket diff --git a/memorystore/valkey/session/app/.gitignore b/memorystore/valkey/session/app/.gitignore new file mode 100644 index 00000000000..91618798d77 --- /dev/null +++ b/memorystore/valkey/session/app/.gitignore @@ -0,0 +1,92 @@ +############################## +## Java +############################## +.mtj.tmp/ +*.class +*.jar +*.war +*.ear +*.nar +hs_err_pid* + +############################## +## Maven +############################## +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +pom.xml.bak +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +############################## +## Gradle +############################## +bin/ +build/ +.gradle +.gradletasknamecache +gradle-app.setting +!gradle-wrapper.jar + +############################## +## IntelliJ +############################## +out/ +.idea/ +.idea_modules/ +*.iml +*.ipr +*.iws + +############################## +## Eclipse +############################## +.settings/ +bin/ +tmp/ +.metadata +.classpath +.project +*.tmp +*.bak +*.swp +*~.nib +local.properties +.loadpath +.factorypath + +############################## +## NetBeans +############################## +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +nbactions.xml +nb-configuration.xml + +############################## +## Visual Studio Code +############################## +.vscode/ +.code-workspace + +############################## +## OS X +############################## +.DS_Store + +############################## +## Terraform +############################## +.terraform/ +.terraform.lock.hcl +terraform.tfstate +terraform.tfstate.backup \ No newline at end of file diff --git a/memorystore/valkey/session/app/Dockerfile b/memorystore/valkey/session/app/Dockerfile new file mode 100644 index 00000000000..131e16d0504 --- /dev/null +++ b/memorystore/valkey/session/app/Dockerfile @@ -0,0 +1,24 @@ +# Use an OpenJDK base image +FROM openjdk:17-jdk-slim + +# Install Maven for building the project +RUN apt-get update && apt-get install -y maven + +# Set the working directory +WORKDIR /app + +# Copy Maven project files +COPY pom.xml ./ +COPY src ./src + +# Build the project +RUN mvn clean package -DskipTests + +# Copy the built JAR file to the container +RUN cp target/app-1.0-SNAPSHOT.jar app.jar + +# Expose the application port +EXPOSE 8080 + +# Run the application +CMD ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/memorystore/valkey/session/app/docker-compose.yaml b/memorystore/valkey/session/app/docker-compose.yaml new file mode 100644 index 00000000000..0dce93f9123 --- /dev/null +++ b/memorystore/valkey/session/app/docker-compose.yaml @@ -0,0 +1,37 @@ +name: sessions-app + +services: + valkey: + image: valkey/valkey:latest + ports: + - "6379:6379" + command: ["valkey-server", "--save", "60", "1", "--loglevel", "warning"] + + postgres: + image: postgres:latest + container_name: postgres + ports: + - "5432:5432" + environment: + - POSTGRES_USER=admin + - POSTGRES_PASSWORD=password + - POSTGRES_DB=postgres + volumes: + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + + app: + build: + context: . + dockerfile: Dockerfile + container_name: java-app + ports: + - "8080:8080" + depends_on: + - valkey + - postgres + environment: + - VALKEY_HOST=valkey + - VALKEY_PORT=6379 + - DB_URL=jdbc:postgresql://postgres:5432/postgres + - DB_USERNAME=admin + - DB_PASSWORD=password diff --git a/memorystore/valkey/session/app/init.sql b/memorystore/valkey/session/app/init.sql new file mode 100644 index 00000000000..a1254ea885e --- /dev/null +++ b/memorystore/valkey/session/app/init.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS account ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) NOT NULL, + username VARCHAR(20) NOT NULL, + password VARCHAR(255) NOT NULL +); \ No newline at end of file diff --git a/memorystore/valkey/session/app/main.tf b/memorystore/valkey/session/app/main.tf new file mode 100644 index 00000000000..79dccfede64 --- /dev/null +++ b/memorystore/valkey/session/app/main.tf @@ -0,0 +1,139 @@ +provider "google" { + project = "cloud-memorystore-demos" + region = "us-central1" +} + +data "google_project" "project" { + project_id = "cloud-memorystore-demos" +} + +resource "google_compute_network" "app_network" { + name = "sessions-app-network" +} + +resource "google_compute_firewall" "allow_http" { + name = "sessions-app-allow-http-8080" + network = google_compute_network.app_network.name + + allow { + protocol = "tcp" + ports = ["8080"] + } + + source_ranges = ["0.0.0.0/0"] + + depends_on = [google_compute_network.app_network] +} + +resource "google_cloud_run_v2_service" "api" { + name = "sessions-api-service" + location = "us-central1" + + template { + containers { + image = "replace" # Will be set at a later time + + env { + name = "VALKEY_HOST" + value = module.valkey.discovery_endpoints[0]["address"] + } + + env { + name = "VALKEY_PORT" + value = module.valkey.discovery_endpoints[0]["port"] + } + + env { + name = "DB_URL" + value = "jdbc:postgresql://${google_sql_database_instance.postgres.public_ip_address}/${google_sql_database.postgres_db.name}" + } + + env { + name = "DB_USERNAME" + value = google_sql_user.postgres_user.name + } + + env { + name = "DB_PASSWORD" + value = google_sql_user.postgres_user.password + } + + ports { + container_port = 8080 + } + } + + vpc_access { + network_interfaces { + network = google_compute_network.app_network.name + subnetwork = google_compute_network.app_network.name + tags = [] + } + } + } + + depends_on = [ + google_compute_network.app_network, + module.valkey, + google_sql_database_instance.postgres + ] +} + +module "valkey" { + source = "terraform-google-modules/memorystore/google//modules/valkey" + version = "12.0" + + instance_id = "sessions-app-valkey-instance" + project_id = data.google_project.project.project_id + location = "us-central1" + node_type = "SHARED_CORE_NANO" + shard_count = 1 + engine_version = "VALKEY_7_2" + + network = google_compute_network.app_network.name + + service_connection_policies = { + sessions_valkey_scp = { + subnet_names = [google_compute_network.app_network.name] + } + } + + depends_on = [google_compute_network.app_network] +} + +resource "google_sql_database_instance" "postgres" { + name = "sessions-app-postgres-instance" + database_version = "POSTGRES_16" + region = "us-central1" + + settings { + edition = "ENTERPRISE" + tier = "db-custom-1-3840" + + ip_configuration { + ipv4_enabled = true + + authorized_networks { + name = "sessions-app-access" + value = "0.0.0.0/0" + } + } + } + + depends_on = [google_compute_network.app_network] +} + +resource "google_sql_user" "postgres_user" { + name = "admin" + instance = google_sql_database_instance.postgres.name + password = "password123" # Set this to the password you want to use for the user + + depends_on = [google_sql_database_instance.postgres] +} + +resource "google_sql_database" "postgres_db" { + name = "sessions-app-db" + instance = google_sql_database_instance.postgres.name + + depends_on = [google_sql_database_instance.postgres] +} \ No newline at end of file diff --git a/memorystore/valkey/session/app/pom.xml b/memorystore/valkey/session/app/pom.xml new file mode 100644 index 00000000000..eac0e0254b8 --- /dev/null +++ b/memorystore/valkey/session/app/pom.xml @@ -0,0 +1,129 @@ + + + 4.0.0 + + org.example + app + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + + org.springframework.boot + spring-boot-starter-web + 3.3.6 + + + + + org.slf4j + slf4j-api + 2.0.16 + + + org.slf4j + slf4j-simple + 2.0.16 + + + + + org.springframework.security + spring-security-crypto + 5.6.0 + + + + + redis.clients + jedis + 4.3.0 + + + + + org.springframework.boot + spring-boot-starter-jdbc + 3.3.6 + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + 3.3.6 + + + + + org.postgresql + postgresql + 42.6.0 + + + + + + org.springframework.boot + spring-boot-starter-test + 3.3.6 + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 17 + 17 + + -parameters + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + true + + + app.Main + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M8 + + + + \ No newline at end of file diff --git a/memorystore/valkey/session/app/src/main/java/app/AccountRepository.java b/memorystore/valkey/session/app/src/main/java/app/AccountRepository.java new file mode 100644 index 00000000000..de28ed96c5c --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/AccountRepository.java @@ -0,0 +1,78 @@ +/** + * Handles CRUD operations for the account table. + */ + +package app; + +import java.util.Map; +import java.util.Optional; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.crypto.bcrypt.BCrypt; +import org.springframework.stereotype.Repository; + +@Repository +public class AccountRepository { + + private final JdbcTemplate jdbcTemplate; + + public AccountRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public Optional authenticateUser(String username, String password) { + try { + // Fetch hashedPassword and userId in a single query + Map accountData = jdbcTemplate.queryForMap( + "SELECT id, password FROM account WHERE username = ?", + username + ); + + String hashedPassword = (String) accountData.get("password"); + Integer userId = (Integer) accountData.get("id"); + + // Check password validity + if (hashedPassword != null && BCrypt.checkpw(password, hashedPassword)) { + return Optional.of(userId); // Authentication successful + } else { + return Optional.empty(); // Authentication failed + } + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); // No user found + } + } + + public void registerUser(String email, String username, String password) { + // Validate input + if (email == null || username == null || password == null) { + throw new IllegalArgumentException( + "Email, username, and password must not be null" + ); + } + + // Hash the password to securely store it + String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt()); + + // Insert user into the database + jdbcTemplate.update( + "INSERT INTO account (email, username, password) VALUES (?, ?, ?)", + email, + username, + hashedPassword + ); + } + + public boolean isEmailRegistered(String email) { + String sql = "SELECT EXISTS (SELECT 1 FROM account WHERE email = ?)"; + return Boolean.TRUE.equals( + jdbcTemplate.queryForObject(sql, Boolean.class, email) + ); + } + + public boolean isUsernameRegistered(String username) { + String sql = "SELECT EXISTS (SELECT 1 FROM account WHERE username = ?)"; + return Boolean.TRUE.equals( + jdbcTemplate.queryForObject(sql, Boolean.class, username) + ); + } +} diff --git a/memorystore/valkey/session/app/src/main/java/app/AuthController.java b/memorystore/valkey/session/app/src/main/java/app/AuthController.java new file mode 100644 index 00000000000..477c44cac1a --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/AuthController.java @@ -0,0 +1,136 @@ +/** + * The Auth controller for the application. + * + * The controller contains the following endpoints: + * - POST /auth/register - Registers a new user + * - POST /auth/login - Logs in a user + * - POST /auth/logout - Logs out a user + * - POST /auth/verify - Verifies a user's token + */ + +package app; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.sql.Timestamp; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + private final DataController dataController; + + public AuthController(DataController dataController) { + this.dataController = dataController; + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody RegisterInfo info) { + String email = info.email; + String username = info.username; + String password = info.password; + + // Validate email + if (!email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) { + return ResponseEntity.badRequest().body(Global.EMAIL_INVALID); + } + + // Validate username + if (!username.matches("^[a-zA-Z0-9._-]+$")) { + return ResponseEntity.badRequest().body(Global.USERNAME_INVALID); + } else if (username.length() < 3 || username.length() > 20) { + return ResponseEntity.badRequest().body(Global.USERNAME_LENGTH); + } + + // Validate password + if (password.length() < 8 || password.length() > 255) { + return ResponseEntity.badRequest().body(Global.PASSWORD_LENGTH); + } + + // Check if email or username is already taken + if (dataController.checkIfEmailExists(email)) { + return ResponseEntity.status(HttpStatus.CONFLICT).body( + Global.EMAIL_ALREADY_REGISTERED + ); + } + if (dataController.checkIfUsernameExists(username)) { + return ResponseEntity.status(HttpStatus.CONFLICT).body( + Global.USERNAME_TAKEN + ); + } + + // Register user + dataController.register(email, username, password); + return ResponseEntity.ok(Global.REGISTERED); + } + + @PostMapping("/login") + public ResponseEntity login( + @RequestBody LoginInfo info, + HttpServletResponse response + ) { + String username = info.username; + String password = info.password; + + // Attempt to log in + String token = dataController.login(username, password); + + // Invalid credentials + if (token == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( + Global.INVALID_CREDENTIALS + ); + } + + // Create and set a cookie + response.addCookie(Utils.createCookie(token)); + return ResponseEntity.ok(Global.LOGGED_IN); + } + + @PostMapping("/logout") + public ResponseEntity logout(HttpServletRequest request) { + String token = Utils.getTokenFromCookie(request.getCookies()); + if (token == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( + Global.INVALID_TOKEN + ); + } + + // Logout user + dataController.logout(token); + + return ResponseEntity.ok(Global.LOGGED_OUT); + } + + @PostMapping("/verify") + public ResponseEntity verify( + HttpServletRequest request, + HttpServletResponse response + ) { + String token = Utils.getTokenFromCookie(request.getCookies()); + if (token == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( + Global.INVALID_TOKEN + ); + } + + // Verify token and extend session + String username = dataController.verify(token); + if (username == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( + Global.INVALID_TOKEN + ); + } + + // Refresh cookie expiration + Cookie cookie = Utils.createCookie(token); + response.addCookie(cookie); + return ResponseEntity.ok( + new VerifyResponse(username, cookie.getMaxAge()).toJson().toString() + ); + } +} diff --git a/memorystore/valkey/session/app/src/main/java/app/BasketController.java b/memorystore/valkey/session/app/src/main/java/app/BasketController.java new file mode 100644 index 00000000000..23efee92a40 --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/BasketController.java @@ -0,0 +1,73 @@ +/** + * The Auth controller for the application. + * + *

The controller contains the following endpoints: - GET /api/basket - Get all items - POST + * /api/basket/add - Add item with quantity - POST /api/basket/remove - Remove item quantity - POST + * /api/basket/clear - Clear entire basket + */ +package app; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import redis.clients.jedis.Jedis; + +import java.util.Map; + +@RestController +@RequestMapping("/api/basket") +public class BasketController { + + private final Jedis jedis; + + public BasketController(Jedis jedis) { + this.jedis = jedis; + } + + // Get all items + @GetMapping + public ResponseEntity> getBasket(HttpServletRequest request) { + String basketKey = getBasketKey(request); + return ResponseEntity.ok(jedis.hgetAll(basketKey)); + } + + // Add item with quantity + @PostMapping("/add") + public ResponseEntity addItem( + @RequestParam String itemId, + @RequestParam(defaultValue = "1") int quantity, + HttpServletRequest request) { + String basketKey = getBasketKey(request); + long newQty = jedis.hincrBy(basketKey, itemId, quantity); + return ResponseEntity.ok("Quantity updated: " + newQty); + } + + // Remove item quantity + @PostMapping("/remove") + public ResponseEntity removeItem( + @RequestParam String itemId, + @RequestParam(defaultValue = "1") int quantity, + HttpServletRequest request) { + String basketKey = getBasketKey(request); + long newQty = jedis.hincrBy(basketKey, itemId, -quantity); + if (newQty <= 0) { + jedis.hdel(basketKey, itemId); + return ResponseEntity.ok("Item removed"); + } + return ResponseEntity.ok("Quantity updated: " + newQty); + } + + // Clear entire basket + @PostMapping("/clear") + public ResponseEntity clearBasket(HttpServletRequest request) { + jedis.del(getBasketKey(request)); + return ResponseEntity.ok("Basket cleared"); + } + + private String getBasketKey(HttpServletRequest request) { + String token = Utils.getTokenFromCookie(request.getCookies()); + return "basket:" + token; + } +} diff --git a/memorystore/valkey/session/app/src/main/java/app/BasketItem.java b/memorystore/valkey/session/app/src/main/java/app/BasketItem.java new file mode 100644 index 00000000000..a8a40edc188 --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/BasketItem.java @@ -0,0 +1,20 @@ +package app; + +public class BasketItem { + + private int id; + private int quantity; + + public BasketItem(int id, int quantity) { + this.id = id; + this.quantity = quantity; + } + + public int getId() { + return id; + } + + public int getQuantity() { + return quantity; + } +} diff --git a/memorystore/valkey/session/app/src/main/java/app/DataController.java b/memorystore/valkey/session/app/src/main/java/app/DataController.java new file mode 100644 index 00000000000..5872f012b15 --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/DataController.java @@ -0,0 +1,73 @@ +/** + * Responsible for handling the data operations between the API, Valkey, and the database. + */ + +package app; + +import java.util.Optional; +import org.springframework.stereotype.Controller; +import redis.clients.jedis.Jedis; + +@Controller +public class DataController { + + private final AccountRepository accountRepository; + private final Jedis jedis; + + public DataController(AccountRepository accountRepository, Jedis jedis) { + this.accountRepository = accountRepository; + this.jedis = jedis; + } + + public void register(String email, String username, String password) { + accountRepository.registerUser(email, username, password); + } + + public String login(String username, String password) { + // Authenticate user + Optional userId = accountRepository.authenticateUser( + username, + password + ); + + // No user found + if (userId.isEmpty()) { + return null; + } + + // Generate token for the user + String token = Utils.generateToken(Global.TOKEN_BYTE_LENGTH); + + // Store token in Valkey + jedis.setex(token, Global.TOKEN_EXPIRATION, username); + + return token; + } + + public void logout(String token) { + jedis.del(token); + } + + public String verify(String token) { + // Retrieve username from Valkey + String username = jedis.get(token); + + // No username found for the token + if (username == null) { + return null; + } + + // Extend token expiration + jedis.expire(token, Global.TOKEN_EXPIRATION); + + return username; + } + + public boolean checkIfEmailExists(String email) { + return accountRepository.isEmailRegistered(email); + } + + public boolean checkIfUsernameExists(String username) { + return accountRepository.isUsernameRegistered(username); + } +} diff --git a/memorystore/valkey/session/app/src/main/java/app/Global.java b/memorystore/valkey/session/app/src/main/java/app/Global.java new file mode 100644 index 00000000000..2474326a1fa --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/Global.java @@ -0,0 +1,29 @@ +/** + * Global constants for the application. + */ + +package app; + +public class Global { + + public static final String INVALID_CREDENTIALS = + "Invalid username or password"; + public static final String INVALID_TOKEN = "Invalid token"; + public static final String REGISTERED = "User registered successfully"; + public static final String EMAIL_INVALID = "Invalid email format"; + public static final String EMAIL_ALREADY_REGISTERED = + "Email is already registered"; + public static final String USERNAME_INVALID = + "Username must only contain letters, numbers, periods, underscores, and hyphens"; + public static final String USERNAME_LENGTH = + "Username must be between 3 and 20 characters"; + public static final String USERNAME_TAKEN = "Username is already taken"; + public static final String PASSWORD_LENGTH = + "Password must be between 8 and 255 characters"; + public static final String LOGGED_IN = "Logged in"; + public static final String LOGGED_OUT = "Logged out"; + + public static final Integer TOKEN_BYTE_LENGTH = 128; + public static final Integer TOKEN_EXPIRATION = 1800; // Token expiration time in seconds (30 minutes) + public static final String TOKEN_COOKIE_NAME = "token"; +} diff --git a/memorystore/valkey/session/app/src/main/java/app/HomeController.java b/memorystore/valkey/session/app/src/main/java/app/HomeController.java new file mode 100644 index 00000000000..5c3b826e931 --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/HomeController.java @@ -0,0 +1,25 @@ +package app; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class HomeController { + + @GetMapping("/") + public String home(Model model) { + return "index"; // Refers to templates/index.html + } + + @GetMapping("/login") + public String login(Model model) { + return "login"; // Refers to templates/login.html + } + + @GetMapping("/register") + public String logout(Model model) { + return "register"; // Refers to templates/register.html + } +} diff --git a/memorystore/valkey/session/app/src/main/java/app/JdbcConfig.java b/memorystore/valkey/session/app/src/main/java/app/JdbcConfig.java new file mode 100644 index 00000000000..9b658dd5982 --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/JdbcConfig.java @@ -0,0 +1,49 @@ +/** + * Configuration for the JDBC DataSource to connect to the PostgreSQL server. + */ + +package app; + +import javax.sql.DataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +@Configuration +public class JdbcConfig { + + // Database configuration properties with environment variable fallback + @Value("${DB_URL:jdbc:postgresql://localhost:5432/default_db}") + private String url; + + @Value("${DB_USERNAME:postgres}") + private String username; + + @Value("${DB_PASSWORD:}") + private String password; + + @Bean + public DataSource dataSource() { + // Validate mandatory properties + if (url == null || url.isEmpty()) { + throw new IllegalArgumentException( + "Database URL (DB_URL) is not configured" + ); + } + if (username == null || username.isEmpty()) { + throw new IllegalArgumentException( + "Database username (DB_USERNAME) is not configured" + ); + } + + // Set up the DataSource + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.postgresql.Driver"); + dataSource.setUrl(url); + dataSource.setUsername(username); + dataSource.setPassword(password); + + return dataSource; + } +} diff --git a/memorystore/valkey/session/app/src/main/java/app/JedisConfig.java b/memorystore/valkey/session/app/src/main/java/app/JedisConfig.java new file mode 100644 index 00000000000..fbfb4e4acc5 --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/JedisConfig.java @@ -0,0 +1,56 @@ +/** + * Configuration for the Jedis client to connect to the Valkey server. + */ + +package app; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.Jedis; + +@Configuration +public class JedisConfig { + + // Redis server configuration properties + @Value("${VALKEY_HOST:localhost}") // Default to localhost if not set + private String redisHost; + + @Value("${VALKEY_PORT:6379}") // Default to 6379 if not set + private int redisPort; + + @Value("${VALKEY_PASSWORD:}") // Empty by default if not set + private String redisPassword; + + @Bean + public Jedis jedis() { + // Validate mandatory properties + if (redisHost == null || redisHost.isEmpty()) { + throw new IllegalArgumentException( + "Redis host (VALKEY_HOST) is not configured" + ); + } + if (redisPort <= 0 || redisPort > 65535) { + throw new IllegalArgumentException("Redis port (VALKEY_PORT) is invalid"); + } + + Jedis jedis = new Jedis(redisHost, redisPort); + + // Authenticate if a password is set + if (!redisPassword.isEmpty()) { + jedis.auth(redisPassword); + } + + // Verify the connection to the Redis server + try { + jedis.ping(); + } catch (Exception e) { + throw new RuntimeException( + "Failed to connect to Redis server at " + redisHost + ":" + redisPort, + e + ); + } + + return jedis; + } +} diff --git a/memorystore/valkey/session/app/src/main/java/app/LoginInfo.java b/memorystore/valkey/session/app/src/main/java/app/LoginInfo.java new file mode 100644 index 00000000000..c3d14213097 --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/LoginInfo.java @@ -0,0 +1,16 @@ +/** + * Data class for holding login information. + */ + +package app; + +public class LoginInfo { + + public String username; + public String password; + + public LoginInfo(String username, String password) { + this.username = username; + this.password = password; + } +} diff --git a/memorystore/valkey/session/app/src/main/java/app/Main.java b/memorystore/valkey/session/app/src/main/java/app/Main.java new file mode 100644 index 00000000000..2a475609beb --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/Main.java @@ -0,0 +1,16 @@ +/** + * Main class for the Spring Boot application. + */ + +package app; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} diff --git a/memorystore/valkey/session/app/src/main/java/app/RegisterInfo.java b/memorystore/valkey/session/app/src/main/java/app/RegisterInfo.java new file mode 100644 index 00000000000..a9ab4b0f332 --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/RegisterInfo.java @@ -0,0 +1,18 @@ +/** + * Data class for holding registration information. + */ + +package app; + +public class RegisterInfo { + + public String email; + public String username; + public String password; + + public RegisterInfo(String email, String username, String password) { + this.email = email; + this.username = username; + this.password = password; + } +} diff --git a/memorystore/valkey/session/app/src/main/java/app/Utils.java b/memorystore/valkey/session/app/src/main/java/app/Utils.java new file mode 100644 index 00000000000..9fddbc851df --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/Utils.java @@ -0,0 +1,49 @@ +/** + * Utility class for generating secure tokens and managing cookies. + */ + +package app; + +import jakarta.servlet.http.Cookie; +import java.security.SecureRandom; +import java.sql.Timestamp; +import java.util.Base64; + +public class Utils { + + public static String generateToken(int tokenByteLength) { + // SecureRandom ensures cryptographic security + SecureRandom secureRandom = new SecureRandom(); + byte[] randomBytes = new byte[tokenByteLength]; + secureRandom.nextBytes(randomBytes); + + // Encode the random bytes into a URL-safe Base64 string + return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); + } + + public static String getTokenFromCookie(Cookie[] cookies) { + if (cookies == null) { + return null; + } + for (Cookie cookie : cookies) { + if (cookie.getName().equals(Global.TOKEN_COOKIE_NAME)) { + return cookie.getValue(); + } + } + return null; + } + + public static Cookie createCookie(String token) { + Cookie cookie = new Cookie(Global.TOKEN_COOKIE_NAME, token); + cookie.setPath("/"); // Available across the app + cookie.setMaxAge(Global.TOKEN_EXPIRATION); // Set expiration + + return cookie; + } + + public static Timestamp getFutureTimestamp(long seconds) { + long currentTime = System.currentTimeMillis(); + long futureTime = currentTime + (seconds * 1000); + return new Timestamp(futureTime); + } +} diff --git a/memorystore/valkey/session/app/src/main/java/app/VerifyResponse.java b/memorystore/valkey/session/app/src/main/java/app/VerifyResponse.java new file mode 100644 index 00000000000..33799b1a490 --- /dev/null +++ b/memorystore/valkey/session/app/src/main/java/app/VerifyResponse.java @@ -0,0 +1,27 @@ +/** + * This class is used to create a response object for the verify endpoint. + * It contains the username and expiration timestamp of the token. + */ + +package app; + +import java.security.Timestamp; +import org.json.JSONObject; + +public class VerifyResponse { + + private String username; + private int expirationSecs; + + public VerifyResponse(String username, int expiration) { + this.username = username; + this.expirationSecs = expiration; + } + + public JSONObject toJson() { + JSONObject json = new JSONObject(); + json.put("username", username); + json.put("expirationSecs", expirationSecs); + return json; + } +} diff --git a/memorystore/valkey/session/app/src/main/resources/public/items.json b/memorystore/valkey/session/app/src/main/resources/public/items.json new file mode 100644 index 00000000000..747fd94b759 --- /dev/null +++ b/memorystore/valkey/session/app/src/main/resources/public/items.json @@ -0,0 +1,202 @@ +[ + { + "id": 1, + "name": "Item 1", + "description": "Description for item 1.", + "price": 268.83, + "category": "Home", + "stock": 20, + "rating": 1.9, + "image_url": "https://picsum.photos/id/101/300/200" + }, + { + "id": 2, + "name": "Item 2", + "description": "Description for item 2.", + "price": 438.3, + "category": "Beauty", + "stock": 54, + "rating": 2.4, + "image_url": "https://picsum.photos/id/102/300/200" + }, + { + "id": 3, + "name": "Item 3", + "description": "Description for item 3.", + "price": 432.23, + "category": "Electronics", + "stock": 5, + "rating": 1.6, + "image_url": "https://picsum.photos/id/103/300/200" + }, + { + "id": 4, + "name": "Item 4", + "description": "Description for item 4.", + "price": 118.8, + "category": "Books", + "stock": 90, + "rating": 1.7, + "image_url": "https://picsum.photos/id/104/300/200" + }, + { + "id": 5, + "name": "Item 5", + "description": "Description for item 5.", + "price": 68.23, + "category": "Clothing", + "stock": 44, + "rating": 4.5, + "image_url": "https://picsum.photos/id/100/300/200" + }, + { + "id": 6, + "name": "Item 6", + "description": "Description for item 6.", + "price": 295.92, + "category": "Clothing", + "stock": 34, + "rating": 2.8, + "image_url": "https://picsum.photos/id/106/300/200" + }, + { + "id": 7, + "name": "Item 7", + "description": "Description for item 7.", + "price": 11.74, + "category": "Electronics", + "stock": 75, + "rating": 3.1, + "image_url": "https://picsum.photos/id/107/300/200" + }, + { + "id": 8, + "name": "Item 8", + "description": "Description for item 8.", + "price": 199.74, + "category": "Electronics", + "stock": 85, + "rating": 1.4, + "image_url": "https://picsum.photos/id/108/300/200" + }, + { + "id": 9, + "name": "Item 9", + "description": "Description for item 9.", + "price": 446.12, + "category": "Books", + "stock": 92, + "rating": 4.3, + "image_url": "https://picsum.photos/id/109/300/200" + }, + { + "id": 10, + "name": "Item 10", + "description": "Description for item 10.", + "price": 158.55, + "category": "Toys", + "stock": 76, + "rating": 1.3, + "image_url": "https://picsum.photos/id/110/300/200" + }, + { + "id": 11, + "name": "Item 11", + "description": "Description for item 11.", + "price": 123.06, + "category": "Clothing", + "stock": 10, + "rating": 1.1, + "image_url": "https://picsum.photos/id/111/300/200" + }, + { + "id": 12, + "name": "Item 12", + "description": "Description for item 12.", + "price": 399.79, + "category": "Home", + "stock": 91, + "rating": 3.5, + "image_url": "https://picsum.photos/id/112/300/200" + }, + { + "id": 13, + "name": "Item 13", + "description": "Description for item 13.", + "price": 22.95, + "category": "Clothing", + "stock": 98, + "rating": 1.5, + "image_url": "https://picsum.photos/id/113/300/200" + }, + { + "id": 14, + "name": "Item 14", + "description": "Description for item 14.", + "price": 178.61, + "category": "Beauty", + "stock": 58, + "rating": 4.3, + "image_url": "https://picsum.photos/id/114/300/200" + }, + { + "id": 15, + "name": "Item 15", + "description": "Description for item 15.", + "price": 447.49, + "category": "Beauty", + "stock": 6, + "rating": 4.6, + "image_url": "https://picsum.photos/id/115/300/200" + }, + { + "id": 16, + "name": "Item 16", + "description": "Description for item 16.", + "price": 54.81, + "category": "Books", + "stock": 9, + "rating": 3.2, + "image_url": "https://picsum.photos/id/116/300/200" + }, + { + "id": 17, + "name": "Item 17", + "description": "Description for item 17.", + "price": 272.11, + "category": "Home", + "stock": 97, + "rating": 4.7, + "image_url": "https://picsum.photos/id/117/300/200" + }, + { + "id": 18, + "name": "Item 18", + "description": "Description for item 18.", + "price": 273.65, + "category": "Toys", + "stock": 13, + "rating": 4.1, + "image_url": "https://picsum.photos/id/118/300/200" + }, + { + "id": 19, + "name": "Item 19", + "description": "Description for item 19.", + "price": 300.57, + "category": "Toys", + "stock": 74, + "rating": 1.8, + "image_url": "https://picsum.photos/id/119/300/200" + }, + { + "id": 20, + "name": "Item 20", + "description": "Description for item 20.", + "price": 251.68, + "category": "Beauty", + "stock": 51, + "rating": 2.4, + "image_url": "https://picsum.photos/id/120/300/200" + } +] diff --git a/memorystore/valkey/session/app/src/main/resources/static/basket.js b/memorystore/valkey/session/app/src/main/resources/static/basket.js new file mode 100644 index 00000000000..51dd3d45641 --- /dev/null +++ b/memorystore/valkey/session/app/src/main/resources/static/basket.js @@ -0,0 +1,226 @@ +let basket = {}; + +function fetchBasket() { + fetch("http://localhost:8080/api/basket", { + method: "GET", + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "localhost:3000", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Credentials": "true", + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error(`Failed to load basket: ${response.statusText}`); + } + return response.json(); + }) + .then((data) => { + if (!data) return; + console.log("Loaded basket:", data); + + // returned data is a map + basket = {}; + Object.keys(data).forEach((key) => { + basket[key] = { id: parseInt(key), quantity: parseInt(data[key]) }; + }); + + updateBasketDisplay(); + }) + .catch((error) => { + console.error("Error loading basket:", error); + alert("Failed to load basket. Please try again later."); + }); +} + +function requestAddItem(itemId, quantity) { + let msTaken = new Date().getTime(); + + fetch( + "http://localhost:8080/api/basket/add?itemId=" + + itemId + + "&quantity=" + + quantity, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "localhost:3000", + "Access-Control-Allow-Methods": "POST", + "Access-Control-Allow-Credentials": "true", + }, + }, + ) + .then((response) => { + msTaken = new Date().getTime() - msTaken; + document.getElementById("response-time").textContent = msTaken; + + if (!response.ok) { + throw new Error(`Failed to add item: ${response.statusText}`); + } + return response.text(); + }) + .then(() => { + if (basket[itemId]) { + basket[itemId].quantity += quantity; + } else { + basket[itemId] = { id: itemId, quantity }; + } + + updateBasketDisplay(); + }) + .catch((error) => { + console.error("Error adding item:", error); + alert("Failed to add item. Please try again later."); + }); +} + +function requestRemoveItem(itemId, quantity) { + let msTaken = new Date().getTime(); + + fetch( + "http://localhost:8080/api/basket/remove?itemId=" + + itemId + + "&quantity=" + + quantity, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "localhost:3000", + "Access-Control-Allow-Methods": "DELETE", + "Access-Control-Allow-Credentials": "true", + }, + }, + ) + .then((response) => { + msTaken = new Date().getTime() - msTaken; + document.getElementById("response-time").textContent = msTaken; + + if (!response.ok) { + throw new Error(`Failed to remove item: ${response.statusText}`); + } + return response.text(); + }) + .then(() => { + if (basket[itemId]) { + basket[itemId].quantity -= quantity; + if (basket[itemId].quantity <= 0) { + delete basket[itemId]; + } + } + + updateBasketDisplay(); + }) + .catch((error) => { + console.error("Error removing item:", error); + alert("Failed to remove item. Please try again later."); + }); +} + +function requestClearBasket() { + let msTaken = new Date().getTime(); + + fetch("http://localhost:8080/api/basket/clear", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "localhost:3000", + "Access-Control-Allow-Methods": "DELETE", + "Access-Control-Allow-Credentials": "true", + }, + }) + .then((response) => { + msTaken = new Date().getTime() - msTaken; + document.getElementById("response-time").textContent = msTaken; + + if (!response.ok) { + throw new Error(`Failed to clear basket: ${response.statusText}`); + } + return response.text(); + }) + .then(() => { + basket = {}; + updateBasketDisplay(); + }) + .catch((error) => { + console.error("Error clearing basket:", error); + alert("Failed to clear basket. Please try again later."); + }); +} + +// Add item to basket +function addToBasket(itemId) { + if (!window.items || window.items.length === 0) { + alert("Items are not loaded yet. Please try again later."); + return; + } + + requestAddItem(itemId, 1); +} + +// Remove item from basket +function removeFromBasket(itemId) { + if (!window.items || window.items.length === 0) { + alert("Items are not loaded yet. Please try again later."); + return; + } + + requestRemoveItem(itemId, 1); +} + +// Clear basket +function clearBasket() { + requestClearBasket(); +} + +// Purchase items +function checkoutItems() { + alert("This operation is not supported in the demo."); +} + +// Update basket display +function updateBasketDisplay() { + const basketItems = document.getElementById("basket-items"); + const basketTotal = document.getElementById("basket-total"); + basketItems.innerHTML = ""; + let total = 0; + + if (Object.keys(basket).length === 0) { + basketItems.innerHTML = ` +

Your basket is empty.

+ `; + basketTotal.textContent = "$0.00"; + return; + } + + Object.values(basket).forEach((item) => { + const itemData = window.items.find((i) => i.id === item.id); + total += itemData.price * item.quantity; + + const itemElement = document.createElement("div"); + itemElement.className = "flex justify-between items-center mb-2"; + itemElement.innerHTML = ` +
+

${itemData.name}

+

$${itemData.price.toFixed(2)} x ${item.quantity}

+
+ + `; + + basketItems.appendChild(itemElement); + }); + + basketTotal.textContent = `$${total.toFixed(2)}`; +} + +// Expose functions globally for onclick handlers +window.addToBasket = addToBasket; +window.removeFromBasket = removeFromBasket; +window.clearBasket = clearBasket; +window.checkoutItems = checkoutItems; diff --git a/memorystore/valkey/session/app/src/main/resources/static/main.js b/memorystore/valkey/session/app/src/main/resources/static/main.js new file mode 100644 index 00000000000..35c970f473a --- /dev/null +++ b/memorystore/valkey/session/app/src/main/resources/static/main.js @@ -0,0 +1,85 @@ +window.items = []; + +let msTaken = new Date().getTime(); + +verifyToken().then((data) => { + msTaken = new Date().getTime() - msTaken; + + if (!data) { + window.location.href = "/login"; + return; + } + + const { username, expirationSecs } = data; + + document.getElementById("response-time").textContent = msTaken; + document.getElementById("session-token").textContent = + document.cookie.split("=")[1]; + document.getElementById("session-expiry").textContent = + formatTime(expirationSecs); + document.querySelector(".username-value").textContent = username; + + document + .querySelectorAll(".loading-required") + .forEach((el) => el.classList.remove("hidden")); + document.querySelector(".loading-required-inverse").classList.add("hidden"); + + // Fetch items from the JSON file + fetch("/items.json") + .then((response) => { + if (!response.ok) { + throw new Error(`Failed to fetch items: ${response.statusText}`); + } + return response.json(); + }) + .then((items) => { + window.items = items; + + // Populate product grid + const productGrid = document.getElementById("product-grid"); + items.forEach((item) => { + const card = document.createElement("div"); + card.className = "bg-white p-4 rounded-lg shadow-md"; + card.innerHTML = ` + ${item.name} +

${item.name}

+

${item.description}

+

$${item.price.toFixed( + 2, + )}

+ + `; + productGrid.appendChild(card); + }); + + fetchBasket(); // Load the basket from the server + updateBasketDisplay(); // Initialize the basket display + + document.getElementById("shopping-interface").classList.remove("hidden"); + }) + .catch((error) => { + console.error("Error loading items:", error); + alert("Failed to load items. Please try again later."); + }); +}); + +function logout() { + fetch("/auth/logout", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + token: document.cookie.split("=")[1], + }), + }).then(() => { + document.cookie = + "session=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; + window.location.href = "/login"; + }); +} + +window.logout = logout; diff --git a/memorystore/valkey/session/app/src/main/resources/static/utils.js b/memorystore/valkey/session/app/src/main/resources/static/utils.js new file mode 100644 index 00000000000..b1a617179ba --- /dev/null +++ b/memorystore/valkey/session/app/src/main/resources/static/utils.js @@ -0,0 +1,22 @@ +function formatTime(seconds) { + if (seconds < 0) { + throw new Error("Seconds cannot be negative"); + } + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + const timeParts = []; + + if (hours > 0) timeParts.push(`${hours} ${hours === 1 ? "hour" : "hours"}`); + if (minutes > 0) + timeParts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`); + if (remainingSeconds > 0 || timeParts.length === 0) { + timeParts.push( + `${remainingSeconds} ${remainingSeconds === 1 ? "second" : "seconds"}`, + ); + } + + return timeParts.join(", "); +} diff --git a/memorystore/valkey/session/app/src/main/resources/static/verify.js b/memorystore/valkey/session/app/src/main/resources/static/verify.js new file mode 100644 index 00000000000..195c8a23a3c --- /dev/null +++ b/memorystore/valkey/session/app/src/main/resources/static/verify.js @@ -0,0 +1,39 @@ +async function verifyToken() { + // Get from cachingMode from localStorage + const cachingModeLocalStorage = localStorage.getItem("cachingMode"); + let cachingMode = + cachingModeLocalStorage === null + ? true + : cachingModeLocalStorage === "true"; + + // Verify session + const data = await fetch( + "http://localhost:8080/auth/verify?useCaching=" + cachingMode, + { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "localhost:3000", + "Access-Control-Allow-Methods": "POST", + "Access-Control-Allow-Credentials": "true", + }, + }, + ) + .then((response) => { + if (!response.ok) { + return null; + } + + return response.json(); + }) + .then((data) => { + if (data === null) { + return null; + } + + return data; + }); + + return data; +} diff --git a/memorystore/valkey/session/app/src/main/resources/templates/index.html b/memorystore/valkey/session/app/src/main/resources/templates/index.html new file mode 100644 index 00000000000..96df5a02d24 --- /dev/null +++ b/memorystore/valkey/session/app/src/main/resources/templates/index.html @@ -0,0 +1,186 @@ + + + + Session Management + + + + + + +
+
+ + + +
+ + +
+ +
+
+

Loading products...

+
+ + + +
+ + +
+
+ Response Time: + Calculating... + ms +
+ +
+
+ Session Token: + None +
+
+ Session Expiry: + None +
+
+
+ + + + + + + diff --git a/memorystore/valkey/session/app/src/main/resources/templates/login.html b/memorystore/valkey/session/app/src/main/resources/templates/login.html new file mode 100644 index 00000000000..3b3e9864ce4 --- /dev/null +++ b/memorystore/valkey/session/app/src/main/resources/templates/login.html @@ -0,0 +1,113 @@ + + + + Session Management + + + + +

Loading...

+

Login

+ + + + + + diff --git a/memorystore/valkey/session/app/src/main/resources/templates/register.html b/memorystore/valkey/session/app/src/main/resources/templates/register.html new file mode 100644 index 00000000000..671d7f8f76a --- /dev/null +++ b/memorystore/valkey/session/app/src/main/resources/templates/register.html @@ -0,0 +1,139 @@ + + + + Session Management + + + + +

Loading...

+

Register

+ + + + + + diff --git a/memorystore/valkey/session/app/src/test/java/app/AuthControllerTest.java b/memorystore/valkey/session/app/src/test/java/app/AuthControllerTest.java new file mode 100644 index 00000000000..46c952329da --- /dev/null +++ b/memorystore/valkey/session/app/src/test/java/app/AuthControllerTest.java @@ -0,0 +1,218 @@ +package app; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.*; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +class AuthControllerTest { + + @Mock + private DataController dataController; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + private AuthController authController; + + @BeforeEach + void setUp() { + dataController = Mockito.mock(DataController.class); + request = Mockito.mock(HttpServletRequest.class); + response = Mockito.mock(HttpServletResponse.class); + authController = new AuthController(dataController); + } + + @Nested + @DisplayName("Testing register() method") + class RegisterTests { + + @Test + @DisplayName("Should return 400 if email is invalid") + void testRegister_InvalidEmail() { + RegisterInfo info = new RegisterInfo( + "invalidEmail", + "username", + "password123" + ); + + ResponseEntity response = authController.register(info); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals(Global.EMAIL_INVALID, response.getBody()); + } + + @Test + @DisplayName("Should return 409 if email is already registered") + void testRegister_EmailAlreadyRegistered() { + given(dataController.checkIfEmailExists("test@example.com")).willReturn( + true + ); + RegisterInfo info = new RegisterInfo( + "test@example.com", + "username", + "password123" + ); + + ResponseEntity response = authController.register(info); + + assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); + assertEquals(Global.EMAIL_ALREADY_REGISTERED, response.getBody()); + } + + @Test + @DisplayName("Should return 200 if registration is successful") + void testRegister_Success() { + RegisterInfo info = new RegisterInfo( + "test@example.com", + "username", + "password123" + ); + + ResponseEntity response = authController.register(info); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(Global.REGISTERED, response.getBody()); + verify(dataController).register(info.email, info.username, info.password); + } + } + + @Nested + @DisplayName("Testing login() method") + class LoginTests { + + @Test + @DisplayName("Should return 401 for invalid credentials") + void testLogin_InvalidCredentials() { + LoginInfo info = new LoginInfo("username", "wrongPassword"); + + given(dataController.login(info.username, info.password)).willReturn( + null + ); + + HttpServletResponse mockResponse = Mockito.mock( + HttpServletResponse.class + ); + ResponseEntity response = authController.login( + info, + mockResponse + ); + + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertEquals(Global.INVALID_CREDENTIALS, response.getBody()); + } + + @Test + @DisplayName("Should return 200 and set cookie for valid credentials") + void testLogin_ValidCredentials() { + LoginInfo info = new LoginInfo("username", "password123"); + String token = "validToken"; + + given(dataController.login(info.username, info.password)).willReturn( + token + ); + + ResponseEntity responseEntity = authController.login( + info, + response + ); + + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + verify(response).addCookie(any(Cookie.class)); + } + } + + @Nested + @DisplayName("Testing logout() method") + class LogoutTests { + + @Test + @DisplayName("Should return 401 if token is missing") + void testLogout_NoToken() { + given(request.getCookies()).willReturn(null); + + ResponseEntity responseEntity = authController.logout(request); + + assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); + assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); + } + + @Test + @DisplayName("Should return 200 and logout user if token is valid") + void testLogout_ValidToken() { + Cookie tokenCookie = new Cookie("token", "validToken"); + given(request.getCookies()).willReturn(new Cookie[] { tokenCookie }); + + ResponseEntity responseEntity = authController.logout(request); + + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertEquals(Global.LOGGED_OUT, responseEntity.getBody()); + verify(dataController).logout("validToken"); + } + } + + @Nested + @DisplayName("Testing verify() method") + class VerifyTests { + + @Test + @DisplayName("Should return 401 if token is missing") + void testVerify_NoToken() { + given(request.getCookies()).willReturn(null); + + ResponseEntity responseEntity = authController.verify( + request, + response + ); + + assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); + assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); + } + + @Test + @DisplayName("Should return 200 and username if token is valid") + void testVerify_ValidToken() { + Cookie tokenCookie = new Cookie("token", "validToken"); + given(request.getCookies()).willReturn(new Cookie[] { tokenCookie }); + given(dataController.verify("validToken")).willReturn("username"); + + ResponseEntity responseEntity = authController.verify( + request, + response + ); + + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + verify(response).addCookie(any(Cookie.class)); + } + + @Test + @DisplayName("Should return 401 if token is invalid") + void testVerify_InvalidToken() { + Cookie tokenCookie = new Cookie("token", "invalidToken"); + given(request.getCookies()).willReturn(new Cookie[] { tokenCookie }); + given(dataController.verify("invalidToken")).willReturn(null); + + ResponseEntity responseEntity = authController.verify( + request, + response + ); + + assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); + assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); + } + } +} diff --git a/memorystore/valkey/session/app/src/test/java/app/DataControllerTest.java b/memorystore/valkey/session/app/src/test/java/app/DataControllerTest.java new file mode 100644 index 00000000000..b474e75dba0 --- /dev/null +++ b/memorystore/valkey/session/app/src/test/java/app/DataControllerTest.java @@ -0,0 +1,246 @@ +package app; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import redis.clients.jedis.Jedis; + +@ExtendWith(MockitoExtension.class) +class DataControllerTest { + + @Mock + private AccountRepository accountRepository; + + @Mock + private Jedis jedis; + + private DataController dataController; + + @BeforeEach + void setUp() { + dataController = new DataController(accountRepository, jedis); + } + + @Nested + @DisplayName("Testing register() method") + class RegisterTests { + + @Test + @DisplayName("Should register a new user") + void testRegister() { + String email = "test@example.com"; + String username = "testUser"; + String password = "securePassword"; + + // Action + dataController.register(email, username, password); + + // Verify + verify(accountRepository).registerUser(email, username, password); + } + } + + @Nested + @DisplayName("Testing login() method") + class LoginTests { + + @Test + @DisplayName("Should return token for valid credentials") + void testLogin_ValidCredentials() { + String username = "testUser"; + String password = "securePassword"; + String token = "generatedToken"; + + // Given + given(accountRepository.authenticateUser(username, password)).willReturn( + Optional.of(1) + ); // pretend userId = 1 + + // Mock static Utils.generateToken(...) + try (MockedStatic mockedUtils = Mockito.mockStatic(Utils.class)) { + mockedUtils + .when(() -> Utils.generateToken(Global.TOKEN_BYTE_LENGTH)) + .thenReturn(token); + + // Action + String result = dataController.login(username, password); + + // Assert & Verify + assertEquals(token, result); + verify(jedis).set(token, username); + verify(jedis).expire(token, Global.TOKEN_EXPIRATION); + } + } + + @Test + @DisplayName("Should return null for invalid credentials") + void testLogin_InvalidCredentials() { + String username = "testUser"; + String password = "wrongPassword"; + + given(accountRepository.authenticateUser(username, password)).willReturn( + Optional.empty() + ); + + String result = dataController.login(username, password); + + assertNull(result); + } + + @Test + @DisplayName("Should throw RuntimeException if Jedis operation fails") + void testLogin_JedisFailure() { + String username = "testUser"; + String password = "securePassword"; + String token = "generatedToken"; + + given(accountRepository.authenticateUser(username, password)).willReturn( + Optional.of(1) + ); + + try (MockedStatic mockedUtils = Mockito.mockStatic(Utils.class)) { + mockedUtils + .when(() -> Utils.generateToken(Global.TOKEN_BYTE_LENGTH)) + .thenReturn(token); + + // Force an error in Jedis.set(...) + doThrow(new RuntimeException("Jedis error")) + .when(jedis) + .set(token, username); + + // Should throw RuntimeException because Jedis fails + assertThrows(RuntimeException.class, () -> + dataController.login(username, password) + ); + } + } + } + + @Nested + @DisplayName("Testing logout() method") + class LogoutTests { + + @Test + @DisplayName("Should delete token from Jedis") + void testLogout() { + String token = "testToken"; + + dataController.logout(token); + + // Verify it deletes from Jedis + verify(jedis).del(token); + } + + @Test + @DisplayName("Should throw RuntimeException if Jedis operation fails") + void testLogout_JedisFailure() { + String token = "testToken"; + + // Force an error in Jedis.del(...) + doThrow(new RuntimeException("Jedis error")).when(jedis).del(token); + + assertThrows(RuntimeException.class, () -> dataController.logout(token)); + } + } + + @Nested + @DisplayName("Testing verify() method") + class VerifyTests { + + @Test + @DisplayName("Should return username and extend token expiration if valid") + void testVerify_ValidToken() { + String token = "testToken"; + String username = "testUser"; + + given(jedis.get(token)).willReturn(username); + + String result = dataController.verify(token); + + assertEquals(username, result); + verify(jedis).expire(token, Global.TOKEN_EXPIRATION); + } + + @Test + @DisplayName("Should return null if token is invalid") + void testVerify_InvalidToken() { + String token = "invalidToken"; + + given(jedis.get(token)).willReturn(null); + + String result = dataController.verify(token); + + assertNull(result); + } + + @Test + @DisplayName("Should throw RuntimeException if Jedis operation fails") + void testVerify_JedisFailure() { + String token = "testToken"; + + doThrow(new RuntimeException("Jedis error")).when(jedis).get(token); + + assertThrows(RuntimeException.class, () -> dataController.verify(token)); + } + } + + @Nested + @DisplayName("Testing checkIfEmailExists() method") + class CheckIfEmailExistsTests { + + @Test + @DisplayName("Should return true if email exists") + void testCheckIfEmailExists_True() { + String email = "test@example.com"; + + given(accountRepository.isEmailRegistered(email)).willReturn(true); + + assertTrue(dataController.checkIfEmailExists(email)); + } + + @Test + @DisplayName("Should return false if email does not exist") + void testCheckIfEmailExists_False() { + String email = "nonexistent@example.com"; + + given(accountRepository.isEmailRegistered(email)).willReturn(false); + + assertFalse(dataController.checkIfEmailExists(email)); + } + } + + @Nested + @DisplayName("Testing checkIfUsernameExists() method") + class CheckIfUsernameExistsTests { + + @Test + @DisplayName("Should return true if username exists") + void testCheckIfUsernameExists_True() { + String username = "testUser"; + + given(accountRepository.isUsernameRegistered(username)).willReturn(true); + + assertTrue(dataController.checkIfUsernameExists(username)); + } + + @Test + @DisplayName("Should return false if username does not exist") + void testCheckIfUsernameExists_False() { + String username = "nonexistentUser"; + + given(accountRepository.isUsernameRegistered(username)).willReturn(false); + + assertFalse(dataController.checkIfUsernameExists(username)); + } + } +} diff --git a/memorystore/valkey/session/sample-data/.gitignore b/memorystore/valkey/session/sample-data/.gitignore new file mode 100644 index 00000000000..e9e224cae7f --- /dev/null +++ b/memorystore/valkey/session/sample-data/.gitignore @@ -0,0 +1,84 @@ +############################## +## Java +############################## +.mtj.tmp/ +*.class +*.jar +*.war +*.ear +*.nar +hs_err_pid* + +############################## +## Maven +############################## +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +pom.xml.bak +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +############################## +## Gradle +############################## +bin/ +build/ +.gradle +.gradletasknamecache +gradle-app.setting +!gradle-wrapper.jar + +############################## +## IntelliJ +############################## +out/ +.idea/ +.idea_modules/ +*.iml +*.ipr +*.iws + +############################## +## Eclipse +############################## +.settings/ +bin/ +tmp/ +.metadata +.classpath +.project +*.tmp +*.bak +*.swp +*~.nib +local.properties +.loadpath +.factorypath + +############################## +## NetBeans +############################## +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +nbactions.xml +nb-configuration.xml + +############################## +## Visual Studio Code +############################## +.vscode/ +.code-workspace + +############################## +## OS X +############################## +.DS_Store \ No newline at end of file diff --git a/memorystore/valkey/session/sample-data/Dockerfile b/memorystore/valkey/session/sample-data/Dockerfile new file mode 100644 index 00000000000..131e16d0504 --- /dev/null +++ b/memorystore/valkey/session/sample-data/Dockerfile @@ -0,0 +1,24 @@ +# Use an OpenJDK base image +FROM openjdk:17-jdk-slim + +# Install Maven for building the project +RUN apt-get update && apt-get install -y maven + +# Set the working directory +WORKDIR /app + +# Copy Maven project files +COPY pom.xml ./ +COPY src ./src + +# Build the project +RUN mvn clean package -DskipTests + +# Copy the built JAR file to the container +RUN cp target/app-1.0-SNAPSHOT.jar app.jar + +# Expose the application port +EXPOSE 8080 + +# Run the application +CMD ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/memorystore/valkey/session/sample-data/docker-compose.yaml b/memorystore/valkey/session/sample-data/docker-compose.yaml new file mode 100644 index 00000000000..0090b2afdb5 --- /dev/null +++ b/memorystore/valkey/session/sample-data/docker-compose.yaml @@ -0,0 +1,52 @@ +name: sessions-app + +services: + valkey: + image: valkey/valkey:latest + ports: + - "6379:6379" + command: ["valkey-server", "--save", "60", "1", "--loglevel", "warning"] + + postgres: + image: postgres:latest + container_name: postgres + ports: + - "5432:5432" + environment: + - POSTGRES_USER=admin + - POSTGRES_PASSWORD=password + - POSTGRES_DB=postgres + volumes: + - ../app/init.sql:/docker-entrypoint-initdb.d/init.sql + + data: + build: + context: . + dockerfile: Dockerfile + container_name: sample-data + ports: + - "8082:8082" + depends_on: + - postgres + environment: + - DB_URL=jdbc:postgresql://postgres:5432/postgres + - DB_USERNAME=admin + - DB_PASSWORD=password + + app: + build: + context: ../app + dockerfile: Dockerfile + container_name: app + ports: + - "8080:8080" + depends_on: + - valkey + - postgres + - data + environment: + - VALKEY_HOST=valkey + - VALKEY_PORT=6379 + - DB_URL=jdbc:postgresql://postgres:5432/postgres + - DB_USERNAME=admin + - DB_PASSWORD=password diff --git a/memorystore/valkey/session/sample-data/pom.xml b/memorystore/valkey/session/sample-data/pom.xml new file mode 100644 index 00000000000..aab7d138ea7 --- /dev/null +++ b/memorystore/valkey/session/sample-data/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + + org.example + app + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + + org.springframework.boot + spring-boot-starter-jdbc + 3.3.6 + + + + + org.postgresql + postgresql + 42.6.0 + + + + + com.github.javafaker + javafaker + 1.0.2 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 17 + 17 + + -parameters + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + true + + + app.Main + + + + + + + + + \ No newline at end of file diff --git a/memorystore/valkey/session/sample-data/src/main/java/app/Main.java b/memorystore/valkey/session/sample-data/src/main/java/app/Main.java new file mode 100644 index 00000000000..df17967787d --- /dev/null +++ b/memorystore/valkey/session/sample-data/src/main/java/app/Main.java @@ -0,0 +1,99 @@ +package app; + +import com.github.javafaker.Faker; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.core.JdbcTemplate; + +public class Main { + + private static final int MAX_GENERATED_ENTRIES = 15000; + + private static final Faker FAKER = new Faker(); + private static final Random RANDOM = new Random(); + + public static void main(String[] args) { + // Connect to PostgreSQL + System.out.println("Connecting to PostgreSQL..."); + JdbcTemplate jdbcTemplate = configureJdbcTemplate(); + + // Populate leaderboard with test data + try { + System.out.println("Populating accounts..."); + populateAccounts(jdbcTemplate); + System.out.println("Populating sessions..."); + populateSessions(jdbcTemplate); + } catch (CannotGetJdbcConnectionException e) { + System.out.println( + "Failed to connect to the database. Retrying in 5 seconds..." + ); + // Sleep for 5 seconds and retry + try { + Thread.sleep(5000); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + main(args); + } + } + + private static void populateAccounts(JdbcTemplate jdbcTemplate) { + String sql = + "INSERT INTO account (email, username, password) VALUES (?, ?, ?)"; + + // Prepare batch arguments + List batchArgs = new ArrayList<>(); + for (int i = 0; i < MAX_GENERATED_ENTRIES; i++) { + String email = FAKER.internet().emailAddress(); + String username = FAKER.name().username(); + username = username.length() > 20 ? username.substring(0, 20) : username; + String password = FAKER.internet().password(); + + batchArgs.add(new Object[] { email, username, password }); + } + + // Execute batch update + jdbcTemplate.batchUpdate(sql, batchArgs); + } + + private static void populateSessions(JdbcTemplate jdbcTemplate) { + String sql = + "INSERT INTO session (token, account_id, expires_at) VALUES (?, ?, ?)"; + + // Prepare batch arguments + List batchArgs = new ArrayList<>(); + for (int i = 0; i < MAX_GENERATED_ENTRIES; i++) { + String token = FAKER.internet().uuid(); + int accountId = RANDOM.nextInt(MAX_GENERATED_ENTRIES) + 1; + long expiresAt = System.currentTimeMillis() + 3600000; + Timestamp expiresAtTimestamp = new Timestamp(expiresAt); + + batchArgs.add(new Object[] { token, accountId, expiresAtTimestamp }); + } + + // Execute batch update + jdbcTemplate.batchUpdate(sql, batchArgs); + } + + private static JdbcTemplate configureJdbcTemplate() { + String jdbcUrl = System.getenv() + .getOrDefault("DB_URL", "jdbc:postgresql://localhost:5432/postgres"); + String jdbcUsername = System.getenv().getOrDefault("DB_USERNAME", "root"); + String jdbcPassword = System.getenv() + .getOrDefault("DB_PASSWORD", "password"); + + JdbcTemplate jdbcTemplate = new JdbcTemplate(); + jdbcTemplate.setDataSource( + DataSourceBuilder.create() + .url(jdbcUrl) + .username(jdbcUsername) + .password(jdbcPassword) + .build() + ); + return jdbcTemplate; + } +} From cc313b0a235c52ba9e286a5f1f5838e24d6da923 Mon Sep 17 00:00:00 2001 From: Darren Ackers Date: Fri, 31 Jan 2025 19:12:47 +0000 Subject: [PATCH 2/7] chore: updated file headers --- .../valkey/session/app/docker-compose.yaml | 14 + .../src/main/java/app/AccountRepository.java | 116 +++--- .../app/src/main/java/app/AuthController.java | 204 +++++----- .../src/main/java/app/BasketController.java | 16 + .../app/src/main/java/app/BasketItem.java | 40 +- .../app/src/main/java/app/DataController.java | 108 +++--- .../session/app/src/main/java/app/Global.java | 52 ++- .../app/src/main/java/app/HomeController.java | 41 +- .../app/src/main/java/app/JdbcConfig.java | 80 ++-- .../app/src/main/java/app/JedisConfig.java | 82 ++-- .../app/src/main/java/app/LoginInfo.java | 29 +- .../session/app/src/main/java/app/Main.java | 23 +- .../app/src/main/java/app/RegisterInfo.java | 33 +- .../session/app/src/main/java/app/Utils.java | 80 ++-- .../app/src/main/java/app/VerifyResponse.java | 46 ++- .../src/main/resources/templates/index.html | 16 +- .../src/main/resources/templates/login.html | 16 +- .../main/resources/templates/register.html | 16 +- .../src/test/java/app/AuthControllerTest.java | 299 +++++++------- .../src/test/java/app/DataControllerTest.java | 366 +++++++++--------- .../sample-data/src/main/java/app/Main.java | 166 ++++---- 21 files changed, 1028 insertions(+), 815 deletions(-) diff --git a/memorystore/valkey/session/app/docker-compose.yaml b/memorystore/valkey/session/app/docker-compose.yaml index 0dce93f9123..b79c2550d8a 100644 --- a/memorystore/valkey/session/app/docker-compose.yaml +++ b/memorystore/valkey/session/app/docker-compose.yaml @@ -1,3 +1,17 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: sessions-app services: diff --git a/memorystore/valkey/session/app/src/main/java/app/AccountRepository.java b/memorystore/valkey/session/app/src/main/java/app/AccountRepository.java index de28ed96c5c..1d43464ca42 100644 --- a/memorystore/valkey/session/app/src/main/java/app/AccountRepository.java +++ b/memorystore/valkey/session/app/src/main/java/app/AccountRepository.java @@ -1,78 +1,84 @@ -/** - * Handles CRUD operations for the account table. +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ +/** Handles CRUD operations for the account table. */ package app; -import java.util.Map; -import java.util.Optional; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.crypto.bcrypt.BCrypt; import org.springframework.stereotype.Repository; +import java.util.Map; +import java.util.Optional; + @Repository public class AccountRepository { - private final JdbcTemplate jdbcTemplate; + private final JdbcTemplate jdbcTemplate; - public AccountRepository(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } + public AccountRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } - public Optional authenticateUser(String username, String password) { - try { - // Fetch hashedPassword and userId in a single query - Map accountData = jdbcTemplate.queryForMap( - "SELECT id, password FROM account WHERE username = ?", - username - ); + public Optional authenticateUser(String username, String password) { + try { + // Fetch hashedPassword and userId in a single query + Map accountData = + jdbcTemplate.queryForMap( + "SELECT id, password FROM account WHERE username = ?", username); - String hashedPassword = (String) accountData.get("password"); - Integer userId = (Integer) accountData.get("id"); + String hashedPassword = (String) accountData.get("password"); + Integer userId = (Integer) accountData.get("id"); - // Check password validity - if (hashedPassword != null && BCrypt.checkpw(password, hashedPassword)) { - return Optional.of(userId); // Authentication successful - } else { - return Optional.empty(); // Authentication failed - } - } catch (EmptyResultDataAccessException e) { - return Optional.empty(); // No user found + // Check password validity + if (hashedPassword != null && BCrypt.checkpw(password, hashedPassword)) { + return Optional.of(userId); // Authentication successful + } else { + return Optional.empty(); // Authentication failed + } + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); // No user found + } } - } - public void registerUser(String email, String username, String password) { - // Validate input - if (email == null || username == null || password == null) { - throw new IllegalArgumentException( - "Email, username, and password must not be null" - ); - } + public void registerUser(String email, String username, String password) { + // Validate input + if (email == null || username == null || password == null) { + throw new IllegalArgumentException("Email, username, and password must not be null"); + } - // Hash the password to securely store it - String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt()); + // Hash the password to securely store it + String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt()); - // Insert user into the database - jdbcTemplate.update( - "INSERT INTO account (email, username, password) VALUES (?, ?, ?)", - email, - username, - hashedPassword - ); - } + // Insert user into the database + jdbcTemplate.update( + "INSERT INTO account (email, username, password) VALUES (?, ?, ?)", + email, + username, + hashedPassword); + } - public boolean isEmailRegistered(String email) { - String sql = "SELECT EXISTS (SELECT 1 FROM account WHERE email = ?)"; - return Boolean.TRUE.equals( - jdbcTemplate.queryForObject(sql, Boolean.class, email) - ); - } + public boolean isEmailRegistered(String email) { + String sql = "SELECT EXISTS (SELECT 1 FROM account WHERE email = ?)"; + return Boolean.TRUE.equals(jdbcTemplate.queryForObject(sql, Boolean.class, email)); + } - public boolean isUsernameRegistered(String username) { - String sql = "SELECT EXISTS (SELECT 1 FROM account WHERE username = ?)"; - return Boolean.TRUE.equals( - jdbcTemplate.queryForObject(sql, Boolean.class, username) - ); - } + public boolean isUsernameRegistered(String username) { + String sql = "SELECT EXISTS (SELECT 1 FROM account WHERE username = ?)"; + return Boolean.TRUE.equals(jdbcTemplate.queryForObject(sql, Boolean.class, username)); + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/AuthController.java b/memorystore/valkey/session/app/src/main/java/app/AuthController.java index 477c44cac1a..fb344d70ffa 100644 --- a/memorystore/valkey/session/app/src/main/java/app/AuthController.java +++ b/memorystore/valkey/session/app/src/main/java/app/AuthController.java @@ -1,19 +1,32 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * The Auth controller for the application. * - * The controller contains the following endpoints: - * - POST /auth/register - Registers a new user - * - POST /auth/login - Logs in a user - * - POST /auth/logout - Logs out a user - * - POST /auth/verify - Verifies a user's token + *

The controller contains the following endpoints: - POST /auth/register - Registers a new user + * - POST /auth/login - Logs in a user - POST /auth/logout - Logs out a user - POST /auth/verify - + * Verifies a user's token */ - package app; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.sql.Timestamp; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -22,115 +35,96 @@ @RequestMapping("/auth") public class AuthController { - private final DataController dataController; - - public AuthController(DataController dataController) { - this.dataController = dataController; - } + private final DataController dataController; - @PostMapping("/register") - public ResponseEntity register(@RequestBody RegisterInfo info) { - String email = info.email; - String username = info.username; - String password = info.password; - - // Validate email - if (!email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) { - return ResponseEntity.badRequest().body(Global.EMAIL_INVALID); + public AuthController(DataController dataController) { + this.dataController = dataController; } - // Validate username - if (!username.matches("^[a-zA-Z0-9._-]+$")) { - return ResponseEntity.badRequest().body(Global.USERNAME_INVALID); - } else if (username.length() < 3 || username.length() > 20) { - return ResponseEntity.badRequest().body(Global.USERNAME_LENGTH); + @PostMapping("/register") + public ResponseEntity register(@RequestBody RegisterInfo info) { + String email = info.email; + String username = info.username; + String password = info.password; + + // Validate email + if (!email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) { + return ResponseEntity.badRequest().body(Global.EMAIL_INVALID); + } + + // Validate username + if (!username.matches("^[a-zA-Z0-9._-]+$")) { + return ResponseEntity.badRequest().body(Global.USERNAME_INVALID); + } else if (username.length() < 3 || username.length() > 20) { + return ResponseEntity.badRequest().body(Global.USERNAME_LENGTH); + } + + // Validate password + if (password.length() < 8 || password.length() > 255) { + return ResponseEntity.badRequest().body(Global.PASSWORD_LENGTH); + } + + // Check if email or username is already taken + if (dataController.checkIfEmailExists(email)) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(Global.EMAIL_ALREADY_REGISTERED); + } + if (dataController.checkIfUsernameExists(username)) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(Global.USERNAME_TAKEN); + } + + // Register user + dataController.register(email, username, password); + return ResponseEntity.ok(Global.REGISTERED); } - // Validate password - if (password.length() < 8 || password.length() > 255) { - return ResponseEntity.badRequest().body(Global.PASSWORD_LENGTH); - } + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginInfo info, HttpServletResponse response) { + String username = info.username; + String password = info.password; - // Check if email or username is already taken - if (dataController.checkIfEmailExists(email)) { - return ResponseEntity.status(HttpStatus.CONFLICT).body( - Global.EMAIL_ALREADY_REGISTERED - ); - } - if (dataController.checkIfUsernameExists(username)) { - return ResponseEntity.status(HttpStatus.CONFLICT).body( - Global.USERNAME_TAKEN - ); - } + // Attempt to log in + String token = dataController.login(username, password); - // Register user - dataController.register(email, username, password); - return ResponseEntity.ok(Global.REGISTERED); - } - - @PostMapping("/login") - public ResponseEntity login( - @RequestBody LoginInfo info, - HttpServletResponse response - ) { - String username = info.username; - String password = info.password; - - // Attempt to log in - String token = dataController.login(username, password); - - // Invalid credentials - if (token == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( - Global.INVALID_CREDENTIALS - ); - } + // Invalid credentials + if (token == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Global.INVALID_CREDENTIALS); + } - // Create and set a cookie - response.addCookie(Utils.createCookie(token)); - return ResponseEntity.ok(Global.LOGGED_IN); - } - - @PostMapping("/logout") - public ResponseEntity logout(HttpServletRequest request) { - String token = Utils.getTokenFromCookie(request.getCookies()); - if (token == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( - Global.INVALID_TOKEN - ); + // Create and set a cookie + response.addCookie(Utils.createCookie(token)); + return ResponseEntity.ok(Global.LOGGED_IN); } - // Logout user - dataController.logout(token); - - return ResponseEntity.ok(Global.LOGGED_OUT); - } - - @PostMapping("/verify") - public ResponseEntity verify( - HttpServletRequest request, - HttpServletResponse response - ) { - String token = Utils.getTokenFromCookie(request.getCookies()); - if (token == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( - Global.INVALID_TOKEN - ); - } + @PostMapping("/logout") + public ResponseEntity logout(HttpServletRequest request) { + String token = Utils.getTokenFromCookie(request.getCookies()); + if (token == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Global.INVALID_TOKEN); + } - // Verify token and extend session - String username = dataController.verify(token); - if (username == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( - Global.INVALID_TOKEN - ); + // Logout user + dataController.logout(token); + + return ResponseEntity.ok(Global.LOGGED_OUT); } - // Refresh cookie expiration - Cookie cookie = Utils.createCookie(token); - response.addCookie(cookie); - return ResponseEntity.ok( - new VerifyResponse(username, cookie.getMaxAge()).toJson().toString() - ); - } + @PostMapping("/verify") + public ResponseEntity verify(HttpServletRequest request, HttpServletResponse response) { + String token = Utils.getTokenFromCookie(request.getCookies()); + if (token == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Global.INVALID_TOKEN); + } + + // Verify token and extend session + String username = dataController.verify(token); + if (username == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Global.INVALID_TOKEN); + } + + // Refresh cookie expiration + Cookie cookie = Utils.createCookie(token); + response.addCookie(cookie); + return ResponseEntity.ok( + new VerifyResponse(username, cookie.getMaxAge()).toJson().toString()); + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/BasketController.java b/memorystore/valkey/session/app/src/main/java/app/BasketController.java index 23efee92a40..345791e7d51 100644 --- a/memorystore/valkey/session/app/src/main/java/app/BasketController.java +++ b/memorystore/valkey/session/app/src/main/java/app/BasketController.java @@ -1,3 +1,19 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * The Auth controller for the application. * diff --git a/memorystore/valkey/session/app/src/main/java/app/BasketItem.java b/memorystore/valkey/session/app/src/main/java/app/BasketItem.java index a8a40edc188..31c4a973f06 100644 --- a/memorystore/valkey/session/app/src/main/java/app/BasketItem.java +++ b/memorystore/valkey/session/app/src/main/java/app/BasketItem.java @@ -1,20 +1,36 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package app; public class BasketItem { - private int id; - private int quantity; + private int id; + private int quantity; - public BasketItem(int id, int quantity) { - this.id = id; - this.quantity = quantity; - } + public BasketItem(int id, int quantity) { + this.id = id; + this.quantity = quantity; + } - public int getId() { - return id; - } + public int getId() { + return id; + } - public int getQuantity() { - return quantity; - } + public int getQuantity() { + return quantity; + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/DataController.java b/memorystore/valkey/session/app/src/main/java/app/DataController.java index 5872f012b15..a5ca0e2c62a 100644 --- a/memorystore/valkey/session/app/src/main/java/app/DataController.java +++ b/memorystore/valkey/session/app/src/main/java/app/DataController.java @@ -1,73 +1,85 @@ -/** - * Responsible for handling the data operations between the API, Valkey, and the database. +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ +/** Responsible for handling the data operations between the API, Valkey, and the database. */ package app; -import java.util.Optional; import org.springframework.stereotype.Controller; + import redis.clients.jedis.Jedis; +import java.util.Optional; + @Controller public class DataController { - private final AccountRepository accountRepository; - private final Jedis jedis; + private final AccountRepository accountRepository; + private final Jedis jedis; - public DataController(AccountRepository accountRepository, Jedis jedis) { - this.accountRepository = accountRepository; - this.jedis = jedis; - } - - public void register(String email, String username, String password) { - accountRepository.registerUser(email, username, password); - } - - public String login(String username, String password) { - // Authenticate user - Optional userId = accountRepository.authenticateUser( - username, - password - ); + public DataController(AccountRepository accountRepository, Jedis jedis) { + this.accountRepository = accountRepository; + this.jedis = jedis; + } - // No user found - if (userId.isEmpty()) { - return null; + public void register(String email, String username, String password) { + accountRepository.registerUser(email, username, password); } - // Generate token for the user - String token = Utils.generateToken(Global.TOKEN_BYTE_LENGTH); + public String login(String username, String password) { + // Authenticate user + Optional userId = accountRepository.authenticateUser(username, password); - // Store token in Valkey - jedis.setex(token, Global.TOKEN_EXPIRATION, username); + // No user found + if (userId.isEmpty()) { + return null; + } - return token; - } + // Generate token for the user + String token = Utils.generateToken(Global.TOKEN_BYTE_LENGTH); - public void logout(String token) { - jedis.del(token); - } + // Store token in Valkey + jedis.setex(token, Global.TOKEN_EXPIRATION, username); - public String verify(String token) { - // Retrieve username from Valkey - String username = jedis.get(token); + return token; + } - // No username found for the token - if (username == null) { - return null; + public void logout(String token) { + jedis.del(token); } - // Extend token expiration - jedis.expire(token, Global.TOKEN_EXPIRATION); + public String verify(String token) { + // Retrieve username from Valkey + String username = jedis.get(token); + + // No username found for the token + if (username == null) { + return null; + } + + // Extend token expiration + jedis.expire(token, Global.TOKEN_EXPIRATION); - return username; - } + return username; + } - public boolean checkIfEmailExists(String email) { - return accountRepository.isEmailRegistered(email); - } + public boolean checkIfEmailExists(String email) { + return accountRepository.isEmailRegistered(email); + } - public boolean checkIfUsernameExists(String username) { - return accountRepository.isUsernameRegistered(username); - } + public boolean checkIfUsernameExists(String username) { + return accountRepository.isUsernameRegistered(username); + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/Global.java b/memorystore/valkey/session/app/src/main/java/app/Global.java index 2474326a1fa..4b83a0af351 100644 --- a/memorystore/valkey/session/app/src/main/java/app/Global.java +++ b/memorystore/valkey/session/app/src/main/java/app/Global.java @@ -1,29 +1,39 @@ -/** - * Global constants for the application. +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ +/** Global constants for the application. */ package app; public class Global { - public static final String INVALID_CREDENTIALS = - "Invalid username or password"; - public static final String INVALID_TOKEN = "Invalid token"; - public static final String REGISTERED = "User registered successfully"; - public static final String EMAIL_INVALID = "Invalid email format"; - public static final String EMAIL_ALREADY_REGISTERED = - "Email is already registered"; - public static final String USERNAME_INVALID = - "Username must only contain letters, numbers, periods, underscores, and hyphens"; - public static final String USERNAME_LENGTH = - "Username must be between 3 and 20 characters"; - public static final String USERNAME_TAKEN = "Username is already taken"; - public static final String PASSWORD_LENGTH = - "Password must be between 8 and 255 characters"; - public static final String LOGGED_IN = "Logged in"; - public static final String LOGGED_OUT = "Logged out"; + public static final String INVALID_CREDENTIALS = "Invalid username or password"; + public static final String INVALID_TOKEN = "Invalid token"; + public static final String REGISTERED = "User registered successfully"; + public static final String EMAIL_INVALID = "Invalid email format"; + public static final String EMAIL_ALREADY_REGISTERED = "Email is already registered"; + public static final String USERNAME_INVALID = + "Username must only contain letters, numbers, periods, underscores, and hyphens"; + public static final String USERNAME_LENGTH = "Username must be between 3 and 20 characters"; + public static final String USERNAME_TAKEN = "Username is already taken"; + public static final String PASSWORD_LENGTH = "Password must be between 8 and 255 characters"; + public static final String LOGGED_IN = "Logged in"; + public static final String LOGGED_OUT = "Logged out"; - public static final Integer TOKEN_BYTE_LENGTH = 128; - public static final Integer TOKEN_EXPIRATION = 1800; // Token expiration time in seconds (30 minutes) - public static final String TOKEN_COOKIE_NAME = "token"; + public static final Integer TOKEN_BYTE_LENGTH = 128; + public static final Integer TOKEN_EXPIRATION = + 1800; // Token expiration time in seconds (30 minutes) + public static final String TOKEN_COOKIE_NAME = "token"; } diff --git a/memorystore/valkey/session/app/src/main/java/app/HomeController.java b/memorystore/valkey/session/app/src/main/java/app/HomeController.java index 5c3b826e931..8d701422fdc 100644 --- a/memorystore/valkey/session/app/src/main/java/app/HomeController.java +++ b/memorystore/valkey/session/app/src/main/java/app/HomeController.java @@ -1,6 +1,21 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package app; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -8,18 +23,18 @@ @Controller public class HomeController { - @GetMapping("/") - public String home(Model model) { - return "index"; // Refers to templates/index.html - } + @GetMapping("/") + public String home(Model model) { + return "index"; // Refers to templates/index.html + } - @GetMapping("/login") - public String login(Model model) { - return "login"; // Refers to templates/login.html - } + @GetMapping("/login") + public String login(Model model) { + return "login"; // Refers to templates/login.html + } - @GetMapping("/register") - public String logout(Model model) { - return "register"; // Refers to templates/register.html - } + @GetMapping("/register") + public String logout(Model model) { + return "register"; // Refers to templates/register.html + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/JdbcConfig.java b/memorystore/valkey/session/app/src/main/java/app/JdbcConfig.java index 9b658dd5982..9b3f72ec22c 100644 --- a/memorystore/valkey/session/app/src/main/java/app/JdbcConfig.java +++ b/memorystore/valkey/session/app/src/main/java/app/JdbcConfig.java @@ -1,49 +1,59 @@ -/** - * Configuration for the JDBC DataSource to connect to the PostgreSQL server. +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ +/** Configuration for the JDBC DataSource to connect to the PostgreSQL server. */ package app; -import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.DriverManagerDataSource; +import javax.sql.DataSource; + @Configuration public class JdbcConfig { - // Database configuration properties with environment variable fallback - @Value("${DB_URL:jdbc:postgresql://localhost:5432/default_db}") - private String url; - - @Value("${DB_USERNAME:postgres}") - private String username; - - @Value("${DB_PASSWORD:}") - private String password; - - @Bean - public DataSource dataSource() { - // Validate mandatory properties - if (url == null || url.isEmpty()) { - throw new IllegalArgumentException( - "Database URL (DB_URL) is not configured" - ); - } - if (username == null || username.isEmpty()) { - throw new IllegalArgumentException( - "Database username (DB_USERNAME) is not configured" - ); + // Database configuration properties with environment variable fallback + @Value("${DB_URL:jdbc:postgresql://localhost:5432/default_db}") + private String url; + + @Value("${DB_USERNAME:postgres}") + private String username; + + @Value("${DB_PASSWORD:}") + private String password; + + @Bean + public DataSource dataSource() { + // Validate mandatory properties + if (url == null || url.isEmpty()) { + throw new IllegalArgumentException("Database URL (DB_URL) is not configured"); + } + if (username == null || username.isEmpty()) { + throw new IllegalArgumentException("Database username (DB_USERNAME) is not configured"); + } + + // Set up the DataSource + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.postgresql.Driver"); + dataSource.setUrl(url); + dataSource.setUsername(username); + dataSource.setPassword(password); + + return dataSource; } - - // Set up the DataSource - DriverManagerDataSource dataSource = new DriverManagerDataSource(); - dataSource.setDriverClassName("org.postgresql.Driver"); - dataSource.setUrl(url); - dataSource.setUsername(username); - dataSource.setPassword(password); - - return dataSource; - } } diff --git a/memorystore/valkey/session/app/src/main/java/app/JedisConfig.java b/memorystore/valkey/session/app/src/main/java/app/JedisConfig.java index fbfb4e4acc5..4eea6b55ef0 100644 --- a/memorystore/valkey/session/app/src/main/java/app/JedisConfig.java +++ b/memorystore/valkey/session/app/src/main/java/app/JedisConfig.java @@ -1,56 +1,66 @@ -/** - * Configuration for the Jedis client to connect to the Valkey server. +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ +/** Configuration for the Jedis client to connect to the Valkey server. */ package app; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; + import redis.clients.jedis.Jedis; @Configuration public class JedisConfig { - // Redis server configuration properties - @Value("${VALKEY_HOST:localhost}") // Default to localhost if not set - private String redisHost; + // Redis server configuration properties + @Value("${VALKEY_HOST:localhost}") // Default to localhost if not set + private String redisHost; - @Value("${VALKEY_PORT:6379}") // Default to 6379 if not set - private int redisPort; + @Value("${VALKEY_PORT:6379}") // Default to 6379 if not set + private int redisPort; - @Value("${VALKEY_PASSWORD:}") // Empty by default if not set - private String redisPassword; + @Value("${VALKEY_PASSWORD:}") // Empty by default if not set + private String redisPassword; - @Bean - public Jedis jedis() { - // Validate mandatory properties - if (redisHost == null || redisHost.isEmpty()) { - throw new IllegalArgumentException( - "Redis host (VALKEY_HOST) is not configured" - ); - } - if (redisPort <= 0 || redisPort > 65535) { - throw new IllegalArgumentException("Redis port (VALKEY_PORT) is invalid"); - } + @Bean + public Jedis jedis() { + // Validate mandatory properties + if (redisHost == null || redisHost.isEmpty()) { + throw new IllegalArgumentException("Redis host (VALKEY_HOST) is not configured"); + } + if (redisPort <= 0 || redisPort > 65535) { + throw new IllegalArgumentException("Redis port (VALKEY_PORT) is invalid"); + } - Jedis jedis = new Jedis(redisHost, redisPort); + Jedis jedis = new Jedis(redisHost, redisPort); - // Authenticate if a password is set - if (!redisPassword.isEmpty()) { - jedis.auth(redisPassword); - } + // Authenticate if a password is set + if (!redisPassword.isEmpty()) { + jedis.auth(redisPassword); + } - // Verify the connection to the Redis server - try { - jedis.ping(); - } catch (Exception e) { - throw new RuntimeException( - "Failed to connect to Redis server at " + redisHost + ":" + redisPort, - e - ); - } + // Verify the connection to the Redis server + try { + jedis.ping(); + } catch (Exception e) { + throw new RuntimeException( + "Failed to connect to Redis server at " + redisHost + ":" + redisPort, e); + } - return jedis; - } + return jedis; + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/LoginInfo.java b/memorystore/valkey/session/app/src/main/java/app/LoginInfo.java index c3d14213097..fb52105aa4b 100644 --- a/memorystore/valkey/session/app/src/main/java/app/LoginInfo.java +++ b/memorystore/valkey/session/app/src/main/java/app/LoginInfo.java @@ -1,16 +1,29 @@ -/** - * Data class for holding login information. +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ +/** Data class for holding login information. */ package app; public class LoginInfo { - public String username; - public String password; + public String username; + public String password; - public LoginInfo(String username, String password) { - this.username = username; - this.password = password; - } + public LoginInfo(String username, String password) { + this.username = username; + this.password = password; + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/Main.java b/memorystore/valkey/session/app/src/main/java/app/Main.java index 2a475609beb..6a12c6c9728 100644 --- a/memorystore/valkey/session/app/src/main/java/app/Main.java +++ b/memorystore/valkey/session/app/src/main/java/app/Main.java @@ -1,7 +1,20 @@ -/** - * Main class for the Spring Boot application. +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ +/** Main class for the Spring Boot application. */ package app; import org.springframework.boot.SpringApplication; @@ -10,7 +23,7 @@ @SpringBootApplication public class Main { - public static void main(String[] args) { - SpringApplication.run(Main.class, args); - } + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/RegisterInfo.java b/memorystore/valkey/session/app/src/main/java/app/RegisterInfo.java index a9ab4b0f332..d0ee46f60d1 100644 --- a/memorystore/valkey/session/app/src/main/java/app/RegisterInfo.java +++ b/memorystore/valkey/session/app/src/main/java/app/RegisterInfo.java @@ -1,18 +1,31 @@ -/** - * Data class for holding registration information. +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ +/** Data class for holding registration information. */ package app; public class RegisterInfo { - public String email; - public String username; - public String password; + public String email; + public String username; + public String password; - public RegisterInfo(String email, String username, String password) { - this.email = email; - this.username = username; - this.password = password; - } + public RegisterInfo(String email, String username, String password) { + this.email = email; + this.username = username; + this.password = password; + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/Utils.java b/memorystore/valkey/session/app/src/main/java/app/Utils.java index 9fddbc851df..112cbf0cb3a 100644 --- a/memorystore/valkey/session/app/src/main/java/app/Utils.java +++ b/memorystore/valkey/session/app/src/main/java/app/Utils.java @@ -1,49 +1,63 @@ -/** - * Utility class for generating secure tokens and managing cookies. +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ +/** Utility class for generating secure tokens and managing cookies. */ package app; import jakarta.servlet.http.Cookie; + import java.security.SecureRandom; import java.sql.Timestamp; import java.util.Base64; public class Utils { - public static String generateToken(int tokenByteLength) { - // SecureRandom ensures cryptographic security - SecureRandom secureRandom = new SecureRandom(); - byte[] randomBytes = new byte[tokenByteLength]; - secureRandom.nextBytes(randomBytes); + public static String generateToken(int tokenByteLength) { + // SecureRandom ensures cryptographic security + SecureRandom secureRandom = new SecureRandom(); + byte[] randomBytes = new byte[tokenByteLength]; + secureRandom.nextBytes(randomBytes); + + // Encode the random bytes into a URL-safe Base64 string + return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); + } + + public static String getTokenFromCookie(Cookie[] cookies) { + if (cookies == null) { + return null; + } + for (Cookie cookie : cookies) { + if (cookie.getName().equals(Global.TOKEN_COOKIE_NAME)) { + return cookie.getValue(); + } + } + return null; + } - // Encode the random bytes into a URL-safe Base64 string - return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); - } + public static Cookie createCookie(String token) { + Cookie cookie = new Cookie(Global.TOKEN_COOKIE_NAME, token); + cookie.setPath("/"); // Available across the app + cookie.setMaxAge(Global.TOKEN_EXPIRATION); // Set expiration - public static String getTokenFromCookie(Cookie[] cookies) { - if (cookies == null) { - return null; + return cookie; } - for (Cookie cookie : cookies) { - if (cookie.getName().equals(Global.TOKEN_COOKIE_NAME)) { - return cookie.getValue(); - } + + public static Timestamp getFutureTimestamp(long seconds) { + long currentTime = System.currentTimeMillis(); + long futureTime = currentTime + (seconds * 1000); + return new Timestamp(futureTime); } - return null; - } - - public static Cookie createCookie(String token) { - Cookie cookie = new Cookie(Global.TOKEN_COOKIE_NAME, token); - cookie.setPath("/"); // Available across the app - cookie.setMaxAge(Global.TOKEN_EXPIRATION); // Set expiration - - return cookie; - } - - public static Timestamp getFutureTimestamp(long seconds) { - long currentTime = System.currentTimeMillis(); - long futureTime = currentTime + (seconds * 1000); - return new Timestamp(futureTime); - } } diff --git a/memorystore/valkey/session/app/src/main/java/app/VerifyResponse.java b/memorystore/valkey/session/app/src/main/java/app/VerifyResponse.java index 33799b1a490..39c23269468 100644 --- a/memorystore/valkey/session/app/src/main/java/app/VerifyResponse.java +++ b/memorystore/valkey/session/app/src/main/java/app/VerifyResponse.java @@ -1,27 +1,41 @@ -/** - * This class is used to create a response object for the verify endpoint. - * It contains the username and expiration timestamp of the token. +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ +/** + * This class is used to create a response object for the verify endpoint. It contains the username + * and expiration timestamp of the token. + */ package app; -import java.security.Timestamp; import org.json.JSONObject; public class VerifyResponse { - private String username; - private int expirationSecs; + private String username; + private int expirationSecs; - public VerifyResponse(String username, int expiration) { - this.username = username; - this.expirationSecs = expiration; - } + public VerifyResponse(String username, int expiration) { + this.username = username; + this.expirationSecs = expiration; + } - public JSONObject toJson() { - JSONObject json = new JSONObject(); - json.put("username", username); - json.put("expirationSecs", expirationSecs); - return json; - } + public JSONObject toJson() { + JSONObject json = new JSONObject(); + json.put("username", username); + json.put("expirationSecs", expirationSecs); + return json; + } } diff --git a/memorystore/valkey/session/app/src/main/resources/templates/index.html b/memorystore/valkey/session/app/src/main/resources/templates/index.html index 96df5a02d24..40ef7c1c6c4 100644 --- a/memorystore/valkey/session/app/src/main/resources/templates/index.html +++ b/memorystore/valkey/session/app/src/main/resources/templates/index.html @@ -1,4 +1,18 @@ - + + + + Session Management diff --git a/memorystore/valkey/session/app/src/main/resources/templates/login.html b/memorystore/valkey/session/app/src/main/resources/templates/login.html index 3b3e9864ce4..2d0e9e3216a 100644 --- a/memorystore/valkey/session/app/src/main/resources/templates/login.html +++ b/memorystore/valkey/session/app/src/main/resources/templates/login.html @@ -1,4 +1,18 @@ - + + + + Session Management diff --git a/memorystore/valkey/session/app/src/main/resources/templates/register.html b/memorystore/valkey/session/app/src/main/resources/templates/register.html index 671d7f8f76a..98c2260fd1e 100644 --- a/memorystore/valkey/session/app/src/main/resources/templates/register.html +++ b/memorystore/valkey/session/app/src/main/resources/templates/register.html @@ -1,4 +1,18 @@ - + + + + Session Management diff --git a/memorystore/valkey/session/app/src/test/java/app/AuthControllerTest.java b/memorystore/valkey/session/app/src/test/java/app/AuthControllerTest.java index 46c952329da..1ac44978a3f 100644 --- a/memorystore/valkey/session/app/src/test/java/app/AuthControllerTest.java +++ b/memorystore/valkey/session/app/src/test/java/app/AuthControllerTest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package app; import static org.junit.jupiter.api.Assertions.*; @@ -7,6 +23,7 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -18,201 +35,163 @@ class AuthControllerTest { - @Mock - private DataController dataController; + @Mock private DataController dataController; - @Mock - private HttpServletRequest request; + @Mock private HttpServletRequest request; - @Mock - private HttpServletResponse response; + @Mock private HttpServletResponse response; - private AuthController authController; + private AuthController authController; - @BeforeEach - void setUp() { - dataController = Mockito.mock(DataController.class); - request = Mockito.mock(HttpServletRequest.class); - response = Mockito.mock(HttpServletResponse.class); - authController = new AuthController(dataController); - } + @BeforeEach + void setUp() { + dataController = Mockito.mock(DataController.class); + request = Mockito.mock(HttpServletRequest.class); + response = Mockito.mock(HttpServletResponse.class); + authController = new AuthController(dataController); + } - @Nested - @DisplayName("Testing register() method") - class RegisterTests { + @Nested + @DisplayName("Testing register() method") + class RegisterTests { - @Test - @DisplayName("Should return 400 if email is invalid") - void testRegister_InvalidEmail() { - RegisterInfo info = new RegisterInfo( - "invalidEmail", - "username", - "password123" - ); + @Test + @DisplayName("Should return 400 if email is invalid") + void testRegister_InvalidEmail() { + RegisterInfo info = new RegisterInfo("invalidEmail", "username", "password123"); - ResponseEntity response = authController.register(info); + ResponseEntity response = authController.register(info); - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertEquals(Global.EMAIL_INVALID, response.getBody()); - } + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals(Global.EMAIL_INVALID, response.getBody()); + } - @Test - @DisplayName("Should return 409 if email is already registered") - void testRegister_EmailAlreadyRegistered() { - given(dataController.checkIfEmailExists("test@example.com")).willReturn( - true - ); - RegisterInfo info = new RegisterInfo( - "test@example.com", - "username", - "password123" - ); - - ResponseEntity response = authController.register(info); - - assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); - assertEquals(Global.EMAIL_ALREADY_REGISTERED, response.getBody()); - } + @Test + @DisplayName("Should return 409 if email is already registered") + void testRegister_EmailAlreadyRegistered() { + given(dataController.checkIfEmailExists("test@example.com")).willReturn(true); + RegisterInfo info = new RegisterInfo("test@example.com", "username", "password123"); - @Test - @DisplayName("Should return 200 if registration is successful") - void testRegister_Success() { - RegisterInfo info = new RegisterInfo( - "test@example.com", - "username", - "password123" - ); + ResponseEntity response = authController.register(info); - ResponseEntity response = authController.register(info); + assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); + assertEquals(Global.EMAIL_ALREADY_REGISTERED, response.getBody()); + } - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(Global.REGISTERED, response.getBody()); - verify(dataController).register(info.email, info.username, info.password); - } - } - - @Nested - @DisplayName("Testing login() method") - class LoginTests { - - @Test - @DisplayName("Should return 401 for invalid credentials") - void testLogin_InvalidCredentials() { - LoginInfo info = new LoginInfo("username", "wrongPassword"); - - given(dataController.login(info.username, info.password)).willReturn( - null - ); - - HttpServletResponse mockResponse = Mockito.mock( - HttpServletResponse.class - ); - ResponseEntity response = authController.login( - info, - mockResponse - ); - - assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); - assertEquals(Global.INVALID_CREDENTIALS, response.getBody()); + @Test + @DisplayName("Should return 200 if registration is successful") + void testRegister_Success() { + RegisterInfo info = new RegisterInfo("test@example.com", "username", "password123"); + + ResponseEntity response = authController.register(info); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(Global.REGISTERED, response.getBody()); + verify(dataController).register(info.email, info.username, info.password); + } } - @Test - @DisplayName("Should return 200 and set cookie for valid credentials") - void testLogin_ValidCredentials() { - LoginInfo info = new LoginInfo("username", "password123"); - String token = "validToken"; + @Nested + @DisplayName("Testing login() method") + class LoginTests { - given(dataController.login(info.username, info.password)).willReturn( - token - ); + @Test + @DisplayName("Should return 401 for invalid credentials") + void testLogin_InvalidCredentials() { + LoginInfo info = new LoginInfo("username", "wrongPassword"); - ResponseEntity responseEntity = authController.login( - info, - response - ); + given(dataController.login(info.username, info.password)).willReturn(null); - assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); - verify(response).addCookie(any(Cookie.class)); - } - } + HttpServletResponse mockResponse = Mockito.mock(HttpServletResponse.class); + ResponseEntity response = authController.login(info, mockResponse); + + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertEquals(Global.INVALID_CREDENTIALS, response.getBody()); + } - @Nested - @DisplayName("Testing logout() method") - class LogoutTests { + @Test + @DisplayName("Should return 200 and set cookie for valid credentials") + void testLogin_ValidCredentials() { + LoginInfo info = new LoginInfo("username", "password123"); + String token = "validToken"; - @Test - @DisplayName("Should return 401 if token is missing") - void testLogout_NoToken() { - given(request.getCookies()).willReturn(null); + given(dataController.login(info.username, info.password)).willReturn(token); - ResponseEntity responseEntity = authController.logout(request); + ResponseEntity responseEntity = authController.login(info, response); - assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); - assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + verify(response).addCookie(any(Cookie.class)); + } } - @Test - @DisplayName("Should return 200 and logout user if token is valid") - void testLogout_ValidToken() { - Cookie tokenCookie = new Cookie("token", "validToken"); - given(request.getCookies()).willReturn(new Cookie[] { tokenCookie }); + @Nested + @DisplayName("Testing logout() method") + class LogoutTests { - ResponseEntity responseEntity = authController.logout(request); + @Test + @DisplayName("Should return 401 if token is missing") + void testLogout_NoToken() { + given(request.getCookies()).willReturn(null); - assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); - assertEquals(Global.LOGGED_OUT, responseEntity.getBody()); - verify(dataController).logout("validToken"); - } - } + ResponseEntity responseEntity = authController.logout(request); - @Nested - @DisplayName("Testing verify() method") - class VerifyTests { + assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); + assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); + } - @Test - @DisplayName("Should return 401 if token is missing") - void testVerify_NoToken() { - given(request.getCookies()).willReturn(null); + @Test + @DisplayName("Should return 200 and logout user if token is valid") + void testLogout_ValidToken() { + Cookie tokenCookie = new Cookie("token", "validToken"); + given(request.getCookies()).willReturn(new Cookie[] {tokenCookie}); - ResponseEntity responseEntity = authController.verify( - request, - response - ); + ResponseEntity responseEntity = authController.logout(request); - assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); - assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertEquals(Global.LOGGED_OUT, responseEntity.getBody()); + verify(dataController).logout("validToken"); + } } - @Test - @DisplayName("Should return 200 and username if token is valid") - void testVerify_ValidToken() { - Cookie tokenCookie = new Cookie("token", "validToken"); - given(request.getCookies()).willReturn(new Cookie[] { tokenCookie }); - given(dataController.verify("validToken")).willReturn("username"); + @Nested + @DisplayName("Testing verify() method") + class VerifyTests { - ResponseEntity responseEntity = authController.verify( - request, - response - ); + @Test + @DisplayName("Should return 401 if token is missing") + void testVerify_NoToken() { + given(request.getCookies()).willReturn(null); - assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); - verify(response).addCookie(any(Cookie.class)); - } + ResponseEntity responseEntity = authController.verify(request, response); + + assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); + assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); + } + + @Test + @DisplayName("Should return 200 and username if token is valid") + void testVerify_ValidToken() { + Cookie tokenCookie = new Cookie("token", "validToken"); + given(request.getCookies()).willReturn(new Cookie[] {tokenCookie}); + given(dataController.verify("validToken")).willReturn("username"); + + ResponseEntity responseEntity = authController.verify(request, response); + + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + verify(response).addCookie(any(Cookie.class)); + } - @Test - @DisplayName("Should return 401 if token is invalid") - void testVerify_InvalidToken() { - Cookie tokenCookie = new Cookie("token", "invalidToken"); - given(request.getCookies()).willReturn(new Cookie[] { tokenCookie }); - given(dataController.verify("invalidToken")).willReturn(null); + @Test + @DisplayName("Should return 401 if token is invalid") + void testVerify_InvalidToken() { + Cookie tokenCookie = new Cookie("token", "invalidToken"); + given(request.getCookies()).willReturn(new Cookie[] {tokenCookie}); + given(dataController.verify("invalidToken")).willReturn(null); - ResponseEntity responseEntity = authController.verify( - request, - response - ); + ResponseEntity responseEntity = authController.verify(request, response); - assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); - assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); + assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); + assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); + } } - } } diff --git a/memorystore/valkey/session/app/src/test/java/app/DataControllerTest.java b/memorystore/valkey/session/app/src/test/java/app/DataControllerTest.java index b474e75dba0..587b33b320b 100644 --- a/memorystore/valkey/session/app/src/test/java/app/DataControllerTest.java +++ b/memorystore/valkey/session/app/src/test/java/app/DataControllerTest.java @@ -1,9 +1,24 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package app; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.*; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -13,234 +28,229 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; + import redis.clients.jedis.Jedis; +import java.util.Optional; + @ExtendWith(MockitoExtension.class) class DataControllerTest { - @Mock - private AccountRepository accountRepository; - - @Mock - private Jedis jedis; - - private DataController dataController; - - @BeforeEach - void setUp() { - dataController = new DataController(accountRepository, jedis); - } + @Mock private AccountRepository accountRepository; - @Nested - @DisplayName("Testing register() method") - class RegisterTests { + @Mock private Jedis jedis; - @Test - @DisplayName("Should register a new user") - void testRegister() { - String email = "test@example.com"; - String username = "testUser"; - String password = "securePassword"; + private DataController dataController; - // Action - dataController.register(email, username, password); - - // Verify - verify(accountRepository).registerUser(email, username, password); - } - } - - @Nested - @DisplayName("Testing login() method") - class LoginTests { - - @Test - @DisplayName("Should return token for valid credentials") - void testLogin_ValidCredentials() { - String username = "testUser"; - String password = "securePassword"; - String token = "generatedToken"; - - // Given - given(accountRepository.authenticateUser(username, password)).willReturn( - Optional.of(1) - ); // pretend userId = 1 - - // Mock static Utils.generateToken(...) - try (MockedStatic mockedUtils = Mockito.mockStatic(Utils.class)) { - mockedUtils - .when(() -> Utils.generateToken(Global.TOKEN_BYTE_LENGTH)) - .thenReturn(token); - - // Action - String result = dataController.login(username, password); - - // Assert & Verify - assertEquals(token, result); - verify(jedis).set(token, username); - verify(jedis).expire(token, Global.TOKEN_EXPIRATION); - } + @BeforeEach + void setUp() { + dataController = new DataController(accountRepository, jedis); } - @Test - @DisplayName("Should return null for invalid credentials") - void testLogin_InvalidCredentials() { - String username = "testUser"; - String password = "wrongPassword"; + @Nested + @DisplayName("Testing register() method") + class RegisterTests { - given(accountRepository.authenticateUser(username, password)).willReturn( - Optional.empty() - ); + @Test + @DisplayName("Should register a new user") + void testRegister() { + String email = "test@example.com"; + String username = "testUser"; + String password = "securePassword"; - String result = dataController.login(username, password); + // Action + dataController.register(email, username, password); - assertNull(result); + // Verify + verify(accountRepository).registerUser(email, username, password); + } } - @Test - @DisplayName("Should throw RuntimeException if Jedis operation fails") - void testLogin_JedisFailure() { - String username = "testUser"; - String password = "securePassword"; - String token = "generatedToken"; - - given(accountRepository.authenticateUser(username, password)).willReturn( - Optional.of(1) - ); - - try (MockedStatic mockedUtils = Mockito.mockStatic(Utils.class)) { - mockedUtils - .when(() -> Utils.generateToken(Global.TOKEN_BYTE_LENGTH)) - .thenReturn(token); - - // Force an error in Jedis.set(...) - doThrow(new RuntimeException("Jedis error")) - .when(jedis) - .set(token, username); - - // Should throw RuntimeException because Jedis fails - assertThrows(RuntimeException.class, () -> - dataController.login(username, password) - ); - } + @Nested + @DisplayName("Testing login() method") + class LoginTests { + + @Test + @DisplayName("Should return token for valid credentials") + void testLogin_ValidCredentials() { + String username = "testUser"; + String password = "securePassword"; + String token = "generatedToken"; + + // Given + given(accountRepository.authenticateUser(username, password)) + .willReturn(Optional.of(1)); // pretend userId = 1 + + // Mock static Utils.generateToken(...) + try (MockedStatic mockedUtils = Mockito.mockStatic(Utils.class)) { + mockedUtils + .when(() -> Utils.generateToken(Global.TOKEN_BYTE_LENGTH)) + .thenReturn(token); + + // Action + String result = dataController.login(username, password); + + // Assert & Verify + assertEquals(token, result); + verify(jedis).set(token, username); + verify(jedis).expire(token, Global.TOKEN_EXPIRATION); + } + } + + @Test + @DisplayName("Should return null for invalid credentials") + void testLogin_InvalidCredentials() { + String username = "testUser"; + String password = "wrongPassword"; + + given(accountRepository.authenticateUser(username, password)) + .willReturn(Optional.empty()); + + String result = dataController.login(username, password); + + assertNull(result); + } + + @Test + @DisplayName("Should throw RuntimeException if Jedis operation fails") + void testLogin_JedisFailure() { + String username = "testUser"; + String password = "securePassword"; + String token = "generatedToken"; + + given(accountRepository.authenticateUser(username, password)) + .willReturn(Optional.of(1)); + + try (MockedStatic mockedUtils = Mockito.mockStatic(Utils.class)) { + mockedUtils + .when(() -> Utils.generateToken(Global.TOKEN_BYTE_LENGTH)) + .thenReturn(token); + + // Force an error in Jedis.set(...) + doThrow(new RuntimeException("Jedis error")).when(jedis).set(token, username); + + // Should throw RuntimeException because Jedis fails + assertThrows( + RuntimeException.class, () -> dataController.login(username, password)); + } + } } - } - @Nested - @DisplayName("Testing logout() method") - class LogoutTests { + @Nested + @DisplayName("Testing logout() method") + class LogoutTests { - @Test - @DisplayName("Should delete token from Jedis") - void testLogout() { - String token = "testToken"; + @Test + @DisplayName("Should delete token from Jedis") + void testLogout() { + String token = "testToken"; - dataController.logout(token); + dataController.logout(token); - // Verify it deletes from Jedis - verify(jedis).del(token); - } + // Verify it deletes from Jedis + verify(jedis).del(token); + } - @Test - @DisplayName("Should throw RuntimeException if Jedis operation fails") - void testLogout_JedisFailure() { - String token = "testToken"; + @Test + @DisplayName("Should throw RuntimeException if Jedis operation fails") + void testLogout_JedisFailure() { + String token = "testToken"; - // Force an error in Jedis.del(...) - doThrow(new RuntimeException("Jedis error")).when(jedis).del(token); + // Force an error in Jedis.del(...) + doThrow(new RuntimeException("Jedis error")).when(jedis).del(token); - assertThrows(RuntimeException.class, () -> dataController.logout(token)); + assertThrows(RuntimeException.class, () -> dataController.logout(token)); + } } - } - @Nested - @DisplayName("Testing verify() method") - class VerifyTests { + @Nested + @DisplayName("Testing verify() method") + class VerifyTests { - @Test - @DisplayName("Should return username and extend token expiration if valid") - void testVerify_ValidToken() { - String token = "testToken"; - String username = "testUser"; + @Test + @DisplayName("Should return username and extend token expiration if valid") + void testVerify_ValidToken() { + String token = "testToken"; + String username = "testUser"; - given(jedis.get(token)).willReturn(username); + given(jedis.get(token)).willReturn(username); - String result = dataController.verify(token); + String result = dataController.verify(token); - assertEquals(username, result); - verify(jedis).expire(token, Global.TOKEN_EXPIRATION); - } + assertEquals(username, result); + verify(jedis).expire(token, Global.TOKEN_EXPIRATION); + } - @Test - @DisplayName("Should return null if token is invalid") - void testVerify_InvalidToken() { - String token = "invalidToken"; + @Test + @DisplayName("Should return null if token is invalid") + void testVerify_InvalidToken() { + String token = "invalidToken"; - given(jedis.get(token)).willReturn(null); + given(jedis.get(token)).willReturn(null); - String result = dataController.verify(token); + String result = dataController.verify(token); - assertNull(result); - } + assertNull(result); + } - @Test - @DisplayName("Should throw RuntimeException if Jedis operation fails") - void testVerify_JedisFailure() { - String token = "testToken"; + @Test + @DisplayName("Should throw RuntimeException if Jedis operation fails") + void testVerify_JedisFailure() { + String token = "testToken"; - doThrow(new RuntimeException("Jedis error")).when(jedis).get(token); + doThrow(new RuntimeException("Jedis error")).when(jedis).get(token); - assertThrows(RuntimeException.class, () -> dataController.verify(token)); + assertThrows(RuntimeException.class, () -> dataController.verify(token)); + } } - } - @Nested - @DisplayName("Testing checkIfEmailExists() method") - class CheckIfEmailExistsTests { + @Nested + @DisplayName("Testing checkIfEmailExists() method") + class CheckIfEmailExistsTests { - @Test - @DisplayName("Should return true if email exists") - void testCheckIfEmailExists_True() { - String email = "test@example.com"; + @Test + @DisplayName("Should return true if email exists") + void testCheckIfEmailExists_True() { + String email = "test@example.com"; - given(accountRepository.isEmailRegistered(email)).willReturn(true); + given(accountRepository.isEmailRegistered(email)).willReturn(true); - assertTrue(dataController.checkIfEmailExists(email)); - } + assertTrue(dataController.checkIfEmailExists(email)); + } - @Test - @DisplayName("Should return false if email does not exist") - void testCheckIfEmailExists_False() { - String email = "nonexistent@example.com"; + @Test + @DisplayName("Should return false if email does not exist") + void testCheckIfEmailExists_False() { + String email = "nonexistent@example.com"; - given(accountRepository.isEmailRegistered(email)).willReturn(false); + given(accountRepository.isEmailRegistered(email)).willReturn(false); - assertFalse(dataController.checkIfEmailExists(email)); + assertFalse(dataController.checkIfEmailExists(email)); + } } - } - @Nested - @DisplayName("Testing checkIfUsernameExists() method") - class CheckIfUsernameExistsTests { + @Nested + @DisplayName("Testing checkIfUsernameExists() method") + class CheckIfUsernameExistsTests { - @Test - @DisplayName("Should return true if username exists") - void testCheckIfUsernameExists_True() { - String username = "testUser"; + @Test + @DisplayName("Should return true if username exists") + void testCheckIfUsernameExists_True() { + String username = "testUser"; - given(accountRepository.isUsernameRegistered(username)).willReturn(true); + given(accountRepository.isUsernameRegistered(username)).willReturn(true); - assertTrue(dataController.checkIfUsernameExists(username)); - } + assertTrue(dataController.checkIfUsernameExists(username)); + } - @Test - @DisplayName("Should return false if username does not exist") - void testCheckIfUsernameExists_False() { - String username = "nonexistentUser"; + @Test + @DisplayName("Should return false if username does not exist") + void testCheckIfUsernameExists_False() { + String username = "nonexistentUser"; - given(accountRepository.isUsernameRegistered(username)).willReturn(false); + given(accountRepository.isUsernameRegistered(username)).willReturn(false); - assertFalse(dataController.checkIfUsernameExists(username)); + assertFalse(dataController.checkIfUsernameExists(username)); + } } - } } diff --git a/memorystore/valkey/session/sample-data/src/main/java/app/Main.java b/memorystore/valkey/session/sample-data/src/main/java/app/Main.java index df17967787d..27675dbf6ad 100644 --- a/memorystore/valkey/session/sample-data/src/main/java/app/Main.java +++ b/memorystore/valkey/session/sample-data/src/main/java/app/Main.java @@ -1,99 +1,111 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package app; import com.github.javafaker.Faker; + +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.core.JdbcTemplate; + import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; import java.util.Random; -import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.jdbc.CannotGetJdbcConnectionException; -import org.springframework.jdbc.core.JdbcTemplate; public class Main { - private static final int MAX_GENERATED_ENTRIES = 15000; - - private static final Faker FAKER = new Faker(); - private static final Random RANDOM = new Random(); - - public static void main(String[] args) { - // Connect to PostgreSQL - System.out.println("Connecting to PostgreSQL..."); - JdbcTemplate jdbcTemplate = configureJdbcTemplate(); - - // Populate leaderboard with test data - try { - System.out.println("Populating accounts..."); - populateAccounts(jdbcTemplate); - System.out.println("Populating sessions..."); - populateSessions(jdbcTemplate); - } catch (CannotGetJdbcConnectionException e) { - System.out.println( - "Failed to connect to the database. Retrying in 5 seconds..." - ); - // Sleep for 5 seconds and retry - try { - Thread.sleep(5000); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - main(args); + private static final int MAX_GENERATED_ENTRIES = 15000; + + private static final Faker FAKER = new Faker(); + private static final Random RANDOM = new Random(); + + public static void main(String[] args) { + // Connect to PostgreSQL + System.out.println("Connecting to PostgreSQL..."); + JdbcTemplate jdbcTemplate = configureJdbcTemplate(); + + // Populate leaderboard with test data + try { + System.out.println("Populating accounts..."); + populateAccounts(jdbcTemplate); + System.out.println("Populating sessions..."); + populateSessions(jdbcTemplate); + } catch (CannotGetJdbcConnectionException e) { + System.out.println("Failed to connect to the database. Retrying in 5 seconds..."); + // Sleep for 5 seconds and retry + try { + Thread.sleep(5000); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + main(args); + } } - } - private static void populateAccounts(JdbcTemplate jdbcTemplate) { - String sql = - "INSERT INTO account (email, username, password) VALUES (?, ?, ?)"; + private static void populateAccounts(JdbcTemplate jdbcTemplate) { + String sql = "INSERT INTO account (email, username, password) VALUES (?, ?, ?)"; - // Prepare batch arguments - List batchArgs = new ArrayList<>(); - for (int i = 0; i < MAX_GENERATED_ENTRIES; i++) { - String email = FAKER.internet().emailAddress(); - String username = FAKER.name().username(); - username = username.length() > 20 ? username.substring(0, 20) : username; - String password = FAKER.internet().password(); + // Prepare batch arguments + List batchArgs = new ArrayList<>(); + for (int i = 0; i < MAX_GENERATED_ENTRIES; i++) { + String email = FAKER.internet().emailAddress(); + String username = FAKER.name().username(); + username = username.length() > 20 ? username.substring(0, 20) : username; + String password = FAKER.internet().password(); - batchArgs.add(new Object[] { email, username, password }); + batchArgs.add(new Object[] {email, username, password}); + } + + // Execute batch update + jdbcTemplate.batchUpdate(sql, batchArgs); } - // Execute batch update - jdbcTemplate.batchUpdate(sql, batchArgs); - } + private static void populateSessions(JdbcTemplate jdbcTemplate) { + String sql = "INSERT INTO session (token, account_id, expires_at) VALUES (?, ?, ?)"; - private static void populateSessions(JdbcTemplate jdbcTemplate) { - String sql = - "INSERT INTO session (token, account_id, expires_at) VALUES (?, ?, ?)"; + // Prepare batch arguments + List batchArgs = new ArrayList<>(); + for (int i = 0; i < MAX_GENERATED_ENTRIES; i++) { + String token = FAKER.internet().uuid(); + int accountId = RANDOM.nextInt(MAX_GENERATED_ENTRIES) + 1; + long expiresAt = System.currentTimeMillis() + 3600000; + Timestamp expiresAtTimestamp = new Timestamp(expiresAt); - // Prepare batch arguments - List batchArgs = new ArrayList<>(); - for (int i = 0; i < MAX_GENERATED_ENTRIES; i++) { - String token = FAKER.internet().uuid(); - int accountId = RANDOM.nextInt(MAX_GENERATED_ENTRIES) + 1; - long expiresAt = System.currentTimeMillis() + 3600000; - Timestamp expiresAtTimestamp = new Timestamp(expiresAt); + batchArgs.add(new Object[] {token, accountId, expiresAtTimestamp}); + } - batchArgs.add(new Object[] { token, accountId, expiresAtTimestamp }); + // Execute batch update + jdbcTemplate.batchUpdate(sql, batchArgs); } - // Execute batch update - jdbcTemplate.batchUpdate(sql, batchArgs); - } - - private static JdbcTemplate configureJdbcTemplate() { - String jdbcUrl = System.getenv() - .getOrDefault("DB_URL", "jdbc:postgresql://localhost:5432/postgres"); - String jdbcUsername = System.getenv().getOrDefault("DB_USERNAME", "root"); - String jdbcPassword = System.getenv() - .getOrDefault("DB_PASSWORD", "password"); - - JdbcTemplate jdbcTemplate = new JdbcTemplate(); - jdbcTemplate.setDataSource( - DataSourceBuilder.create() - .url(jdbcUrl) - .username(jdbcUsername) - .password(jdbcPassword) - .build() - ); - return jdbcTemplate; - } + private static JdbcTemplate configureJdbcTemplate() { + String jdbcUrl = + System.getenv().getOrDefault("DB_URL", "jdbc:postgresql://localhost:5432/postgres"); + String jdbcUsername = System.getenv().getOrDefault("DB_USERNAME", "root"); + String jdbcPassword = System.getenv().getOrDefault("DB_PASSWORD", "password"); + + JdbcTemplate jdbcTemplate = new JdbcTemplate(); + jdbcTemplate.setDataSource( + DataSourceBuilder.create() + .url(jdbcUrl) + .username(jdbcUsername) + .password(jdbcPassword) + .build()); + return jdbcTemplate; + } } From d449bbeed3df2a225611973d7b06f4301752c7d7 Mon Sep 17 00:00:00 2001 From: Darren Ackers Date: Fri, 31 Jan 2025 19:20:21 +0000 Subject: [PATCH 3/7] chore: updated addiitonal file headers --- memorystore/valkey/session/app/main.tf | 16 +++++++++++ .../app/src/main/resources/static/basket.js | 28 ++++++++++++++++--- .../app/src/main/resources/static/main.js | 26 +++++++++++++++-- .../app/src/main/resources/static/utils.js | 18 +++++++++++- .../app/src/main/resources/static/verify.js | 18 +++++++++++- .../session/sample-data/docker-compose.yaml | 14 ++++++++++ 6 files changed, 111 insertions(+), 9 deletions(-) diff --git a/memorystore/valkey/session/app/main.tf b/memorystore/valkey/session/app/main.tf index 79dccfede64..2bb5d171e5a 100644 --- a/memorystore/valkey/session/app/main.tf +++ b/memorystore/valkey/session/app/main.tf @@ -1,3 +1,19 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + provider "google" { project = "cloud-memorystore-demos" region = "us-central1" diff --git a/memorystore/valkey/session/app/src/main/resources/static/basket.js b/memorystore/valkey/session/app/src/main/resources/static/basket.js index 51dd3d45641..2e32d422ac0 100644 --- a/memorystore/valkey/session/app/src/main/resources/static/basket.js +++ b/memorystore/valkey/session/app/src/main/resources/static/basket.js @@ -1,3 +1,19 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + let basket = {}; function fetchBasket() { @@ -50,7 +66,7 @@ function requestAddItem(itemId, quantity) { "Access-Control-Allow-Methods": "POST", "Access-Control-Allow-Credentials": "true", }, - }, + } ) .then((response) => { msTaken = new Date().getTime() - msTaken; @@ -92,7 +108,7 @@ function requestRemoveItem(itemId, quantity) { "Access-Control-Allow-Methods": "DELETE", "Access-Control-Allow-Credentials": "true", }, - }, + } ) .then((response) => { msTaken = new Date().getTime() - msTaken; @@ -204,9 +220,13 @@ function updateBasketDisplay() { itemElement.innerHTML = `

${itemData.name}

-

$${itemData.price.toFixed(2)} x ${item.quantity}

+

$${itemData.price.toFixed(2)} x ${ + item.quantity + }

- `; diff --git a/memorystore/valkey/session/app/src/main/resources/static/utils.js b/memorystore/valkey/session/app/src/main/resources/static/utils.js index b1a617179ba..286c60c7e00 100644 --- a/memorystore/valkey/session/app/src/main/resources/static/utils.js +++ b/memorystore/valkey/session/app/src/main/resources/static/utils.js @@ -1,3 +1,19 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + function formatTime(seconds) { if (seconds < 0) { throw new Error("Seconds cannot be negative"); @@ -14,7 +30,7 @@ function formatTime(seconds) { timeParts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`); if (remainingSeconds > 0 || timeParts.length === 0) { timeParts.push( - `${remainingSeconds} ${remainingSeconds === 1 ? "second" : "seconds"}`, + `${remainingSeconds} ${remainingSeconds === 1 ? "second" : "seconds"}` ); } diff --git a/memorystore/valkey/session/app/src/main/resources/static/verify.js b/memorystore/valkey/session/app/src/main/resources/static/verify.js index 195c8a23a3c..7b531d9b21e 100644 --- a/memorystore/valkey/session/app/src/main/resources/static/verify.js +++ b/memorystore/valkey/session/app/src/main/resources/static/verify.js @@ -1,3 +1,19 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + async function verifyToken() { // Get from cachingMode from localStorage const cachingModeLocalStorage = localStorage.getItem("cachingMode"); @@ -18,7 +34,7 @@ async function verifyToken() { "Access-Control-Allow-Methods": "POST", "Access-Control-Allow-Credentials": "true", }, - }, + } ) .then((response) => { if (!response.ok) { diff --git a/memorystore/valkey/session/sample-data/docker-compose.yaml b/memorystore/valkey/session/sample-data/docker-compose.yaml index 0090b2afdb5..ea78aebd9c5 100644 --- a/memorystore/valkey/session/sample-data/docker-compose.yaml +++ b/memorystore/valkey/session/sample-data/docker-compose.yaml @@ -1,3 +1,17 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: sessions-app services: From b7c4d680b3d505564bcfab59af43e07dc9a25965 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 5 Feb 2025 14:53:35 +0000 Subject: [PATCH 4/7] chore(memorystore-session-demo-application): linting --- memorystore/valkey/session/app/init.sql | 6 + memorystore/valkey/session/app/pom.xml | 10 + .../src/main/java/app/AccountRepository.java | 92 ++--- .../app/src/main/java/app/AuthController.java | 163 ++++---- .../src/main/java/app/BasketController.java | 98 ++--- .../app/src/main/java/app/BasketItem.java | 24 +- .../app/src/main/java/app/DataController.java | 89 +++-- .../session/app/src/main/java/app/Global.java | 33 +- .../app/src/main/java/app/HomeController.java | 24 +- .../app/src/main/java/app/JdbcConfig.java | 59 +-- .../app/src/main/java/app/JedisConfig.java | 62 +-- .../app/src/main/java/app/LoginInfo.java | 13 +- .../session/app/src/main/java/app/Main.java | 20 +- .../app/src/main/java/app/RegisterInfo.java | 17 +- .../session/app/src/main/java/app/Utils.java | 64 ++-- .../app/src/main/java/app/VerifyResponse.java | 25 +- .../src/test/java/app/AuthControllerTest.java | 240 ++++++------ .../src/test/java/app/DataControllerTest.java | 352 +++++++++--------- .../sample-data/src/main/java/app/Main.java | 56 ++- .../src/main/java/app/package-info.java | 19 + 20 files changed, 779 insertions(+), 687 deletions(-) create mode 100644 memorystore/valkey/session/sample-data/src/main/java/app/package-info.java diff --git a/memorystore/valkey/session/app/init.sql b/memorystore/valkey/session/app/init.sql index a1254ea885e..acd694aa43e 100644 --- a/memorystore/valkey/session/app/init.sql +++ b/memorystore/valkey/session/app/init.sql @@ -3,4 +3,10 @@ CREATE TABLE IF NOT EXISTS account ( email VARCHAR(255) NOT NULL, username VARCHAR(20) NOT NULL, password VARCHAR(255) NOT NULL +); + +CREATE TABLE session ( + token VARCHAR(255) PRIMARY KEY, + account_id INTEGER NOT NULL, + expires_at TIMESTAMP NOT NULL ); \ No newline at end of file diff --git a/memorystore/valkey/session/app/pom.xml b/memorystore/valkey/session/app/pom.xml index eac0e0254b8..e75f3254c45 100644 --- a/memorystore/valkey/session/app/pom.xml +++ b/memorystore/valkey/session/app/pom.xml @@ -8,6 +8,16 @@ app 1.0-SNAPSHOT + + + com.google.cloud.samples + shared-configuration + 1.2.0 + + 17 17 diff --git a/memorystore/valkey/session/app/src/main/java/app/AccountRepository.java b/memorystore/valkey/session/app/src/main/java/app/AccountRepository.java index 1d43464ca42..a4fad303499 100644 --- a/memorystore/valkey/session/app/src/main/java/app/AccountRepository.java +++ b/memorystore/valkey/session/app/src/main/java/app/AccountRepository.java @@ -15,70 +15,70 @@ */ /** Handles CRUD operations for the account table. */ + package app; +import java.util.Map; +import java.util.Optional; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.crypto.bcrypt.BCrypt; import org.springframework.stereotype.Repository; -import java.util.Map; -import java.util.Optional; - @Repository public class AccountRepository { - private final JdbcTemplate jdbcTemplate; + private final JdbcTemplate jdbcTemplate; - public AccountRepository(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } + public AccountRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } - public Optional authenticateUser(String username, String password) { - try { - // Fetch hashedPassword and userId in a single query - Map accountData = - jdbcTemplate.queryForMap( - "SELECT id, password FROM account WHERE username = ?", username); + public Optional authenticateUser(String username, String password) { + try { + // Fetch hashedPassword and userId in a single query + Map accountData = + jdbcTemplate.queryForMap( + "SELECT id, password FROM account WHERE username = ?", username); - String hashedPassword = (String) accountData.get("password"); - Integer userId = (Integer) accountData.get("id"); + String hashedPassword = (String) accountData.get("password"); + Integer userId = (Integer) accountData.get("id"); - // Check password validity - if (hashedPassword != null && BCrypt.checkpw(password, hashedPassword)) { - return Optional.of(userId); // Authentication successful - } else { - return Optional.empty(); // Authentication failed - } - } catch (EmptyResultDataAccessException e) { - return Optional.empty(); // No user found - } + // Check password validity + if (hashedPassword != null && BCrypt.checkpw(password, hashedPassword)) { + return Optional.of(userId); // Authentication successful + } else { + return Optional.empty(); // Authentication failed + } + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); // No user found } + } - public void registerUser(String email, String username, String password) { - // Validate input - if (email == null || username == null || password == null) { - throw new IllegalArgumentException("Email, username, and password must not be null"); - } + public void registerUser(String email, String username, String password) { + // Validate input + if (email == null || username == null || password == null) { + throw new IllegalArgumentException("Email, username, and password must not be null"); + } - // Hash the password to securely store it - String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt()); + // Hash the password to securely store it + String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt()); - // Insert user into the database - jdbcTemplate.update( - "INSERT INTO account (email, username, password) VALUES (?, ?, ?)", - email, - username, - hashedPassword); - } + // Insert user into the database + jdbcTemplate.update( + "INSERT INTO account (email, username, password) VALUES (?, ?, ?)", + email, + username, + hashedPassword); + } - public boolean isEmailRegistered(String email) { - String sql = "SELECT EXISTS (SELECT 1 FROM account WHERE email = ?)"; - return Boolean.TRUE.equals(jdbcTemplate.queryForObject(sql, Boolean.class, email)); - } + public boolean isEmailRegistered(String email) { + String sql = "SELECT EXISTS (SELECT 1 FROM account WHERE email = ?)"; + return Boolean.TRUE.equals(jdbcTemplate.queryForObject(sql, Boolean.class, email)); + } - public boolean isUsernameRegistered(String username) { - String sql = "SELECT EXISTS (SELECT 1 FROM account WHERE username = ?)"; - return Boolean.TRUE.equals(jdbcTemplate.queryForObject(sql, Boolean.class, username)); - } + public boolean isUsernameRegistered(String username) { + String sql = "SELECT EXISTS (SELECT 1 FROM account WHERE username = ?)"; + return Boolean.TRUE.equals(jdbcTemplate.queryForObject(sql, Boolean.class, username)); + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/AuthController.java b/memorystore/valkey/session/app/src/main/java/app/AuthController.java index fb344d70ffa..d6e66327d10 100644 --- a/memorystore/valkey/session/app/src/main/java/app/AuthController.java +++ b/memorystore/valkey/session/app/src/main/java/app/AuthController.java @@ -21,110 +21,115 @@ * - POST /auth/login - Logs in a user - POST /auth/logout - Logs out a user - POST /auth/verify - * Verifies a user's token */ + package app; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; - import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/auth") public class AuthController { - private final DataController dataController; + private final DataController dataController; + + public AuthController(DataController dataController) { + this.dataController = dataController; + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody RegisterInfo info) { + String email = info.email; + String username = info.username; + String password = info.password; - public AuthController(DataController dataController) { - this.dataController = dataController; + // Validate email + if (!email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) { + return ResponseEntity.badRequest().body(Global.EMAIL_INVALID); } - @PostMapping("/register") - public ResponseEntity register(@RequestBody RegisterInfo info) { - String email = info.email; - String username = info.username; - String password = info.password; - - // Validate email - if (!email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) { - return ResponseEntity.badRequest().body(Global.EMAIL_INVALID); - } - - // Validate username - if (!username.matches("^[a-zA-Z0-9._-]+$")) { - return ResponseEntity.badRequest().body(Global.USERNAME_INVALID); - } else if (username.length() < 3 || username.length() > 20) { - return ResponseEntity.badRequest().body(Global.USERNAME_LENGTH); - } - - // Validate password - if (password.length() < 8 || password.length() > 255) { - return ResponseEntity.badRequest().body(Global.PASSWORD_LENGTH); - } - - // Check if email or username is already taken - if (dataController.checkIfEmailExists(email)) { - return ResponseEntity.status(HttpStatus.CONFLICT).body(Global.EMAIL_ALREADY_REGISTERED); - } - if (dataController.checkIfUsernameExists(username)) { - return ResponseEntity.status(HttpStatus.CONFLICT).body(Global.USERNAME_TAKEN); - } - - // Register user - dataController.register(email, username, password); - return ResponseEntity.ok(Global.REGISTERED); + // Validate username + if (!username.matches("^[a-zA-Z0-9._-]+$")) { + return ResponseEntity.badRequest().body(Global.USERNAME_INVALID); + } else if (username.length() < 3 || username.length() > 20) { + return ResponseEntity.badRequest().body(Global.USERNAME_LENGTH); } - @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginInfo info, HttpServletResponse response) { - String username = info.username; - String password = info.password; + // Validate password + if (password.length() < 8 || password.length() > 255) { + return ResponseEntity.badRequest().body(Global.PASSWORD_LENGTH); + } - // Attempt to log in - String token = dataController.login(username, password); + // Check if email or username is already taken + if (dataController.checkIfEmailExists(email)) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(Global.EMAIL_ALREADY_REGISTERED); + } + if (dataController.checkIfUsernameExists(username)) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(Global.USERNAME_TAKEN); + } - // Invalid credentials - if (token == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Global.INVALID_CREDENTIALS); - } + // Register user + dataController.register(email, username, password); + return ResponseEntity.ok(Global.REGISTERED); + } - // Create and set a cookie - response.addCookie(Utils.createCookie(token)); - return ResponseEntity.ok(Global.LOGGED_IN); + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginInfo info, HttpServletResponse response) { + String username = info.username; + String password = info.password; + + // Attempt to log in + String token = dataController.login(username, password); + + // Invalid credentials + if (token == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Global.INVALID_CREDENTIALS); + } + + // Create and set a cookie + response.addCookie(Utils.createCookie(token)); + return ResponseEntity.ok(Global.LOGGED_IN); + } + + @PostMapping("/logout") + public ResponseEntity logout(HttpServletRequest request) { + String token = Utils.getTokenFromCookie(request.getCookies()); + if (token == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Global.INVALID_TOKEN); } - @PostMapping("/logout") - public ResponseEntity logout(HttpServletRequest request) { - String token = Utils.getTokenFromCookie(request.getCookies()); - if (token == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Global.INVALID_TOKEN); - } + // Logout user + dataController.logout(token); - // Logout user - dataController.logout(token); + return ResponseEntity.ok(Global.LOGGED_OUT); + } - return ResponseEntity.ok(Global.LOGGED_OUT); + @PostMapping("/verify") + public ResponseEntity verify(HttpServletRequest request, HttpServletResponse response) { + String token = Utils.getTokenFromCookie(request.getCookies()); + if (token == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Global.INVALID_TOKEN); } - @PostMapping("/verify") - public ResponseEntity verify(HttpServletRequest request, HttpServletResponse response) { - String token = Utils.getTokenFromCookie(request.getCookies()); - if (token == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Global.INVALID_TOKEN); - } - - // Verify token and extend session - String username = dataController.verify(token); - if (username == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Global.INVALID_TOKEN); - } - - // Refresh cookie expiration - Cookie cookie = Utils.createCookie(token); - response.addCookie(cookie); - return ResponseEntity.ok( - new VerifyResponse(username, cookie.getMaxAge()).toJson().toString()); + // Verify token and extend session + String username = dataController.verify(token); + if (username == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Global.INVALID_TOKEN); } + + // Refresh cookie expiration + Cookie cookie = Utils.createCookie(token); + response.addCookie(cookie); + return ResponseEntity.ok( + new VerifyResponse(username, cookie.getMaxAge()).toJson().toString()); + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/BasketController.java b/memorystore/valkey/session/app/src/main/java/app/BasketController.java index 345791e7d51..9bcc07bbc2c 100644 --- a/memorystore/valkey/session/app/src/main/java/app/BasketController.java +++ b/memorystore/valkey/session/app/src/main/java/app/BasketController.java @@ -21,69 +21,71 @@ * /api/basket/add - Add item with quantity - POST /api/basket/remove - Remove item quantity - POST * /api/basket/clear - Clear entire basket */ + package app; import jakarta.servlet.http.HttpServletRequest; - +import java.util.Map; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import redis.clients.jedis.Jedis; -import java.util.Map; - @RestController @RequestMapping("/api/basket") public class BasketController { - private final Jedis jedis; + private final Jedis jedis; - public BasketController(Jedis jedis) { - this.jedis = jedis; - } + public BasketController(Jedis jedis) { + this.jedis = jedis; + } - // Get all items - @GetMapping - public ResponseEntity> getBasket(HttpServletRequest request) { - String basketKey = getBasketKey(request); - return ResponseEntity.ok(jedis.hgetAll(basketKey)); - } + // Get all items + @GetMapping + public ResponseEntity> getBasket(HttpServletRequest request) { + String basketKey = getBasketKey(request); + return ResponseEntity.ok(jedis.hgetAll(basketKey)); + } - // Add item with quantity - @PostMapping("/add") - public ResponseEntity addItem( - @RequestParam String itemId, - @RequestParam(defaultValue = "1") int quantity, - HttpServletRequest request) { - String basketKey = getBasketKey(request); - long newQty = jedis.hincrBy(basketKey, itemId, quantity); - return ResponseEntity.ok("Quantity updated: " + newQty); - } + // Add item with quantity + @PostMapping("/add") + public ResponseEntity addItem( + @RequestParam String itemId, + @RequestParam(defaultValue = "1") int quantity, + HttpServletRequest request) { + String basketKey = getBasketKey(request); + long newQty = jedis.hincrBy(basketKey, itemId, quantity); + return ResponseEntity.ok("Quantity updated: " + newQty); + } - // Remove item quantity - @PostMapping("/remove") - public ResponseEntity removeItem( - @RequestParam String itemId, - @RequestParam(defaultValue = "1") int quantity, - HttpServletRequest request) { - String basketKey = getBasketKey(request); - long newQty = jedis.hincrBy(basketKey, itemId, -quantity); - if (newQty <= 0) { - jedis.hdel(basketKey, itemId); - return ResponseEntity.ok("Item removed"); - } - return ResponseEntity.ok("Quantity updated: " + newQty); + // Remove item quantity + @PostMapping("/remove") + public ResponseEntity removeItem( + @RequestParam String itemId, + @RequestParam(defaultValue = "1") int quantity, + HttpServletRequest request) { + String basketKey = getBasketKey(request); + long newQty = jedis.hincrBy(basketKey, itemId, -quantity); + if (newQty <= 0) { + jedis.hdel(basketKey, itemId); + return ResponseEntity.ok("Item removed"); } + return ResponseEntity.ok("Quantity updated: " + newQty); + } - // Clear entire basket - @PostMapping("/clear") - public ResponseEntity clearBasket(HttpServletRequest request) { - jedis.del(getBasketKey(request)); - return ResponseEntity.ok("Basket cleared"); - } + // Clear entire basket + @PostMapping("/clear") + public ResponseEntity clearBasket(HttpServletRequest request) { + jedis.del(getBasketKey(request)); + return ResponseEntity.ok("Basket cleared"); + } - private String getBasketKey(HttpServletRequest request) { - String token = Utils.getTokenFromCookie(request.getCookies()); - return "basket:" + token; - } + private String getBasketKey(HttpServletRequest request) { + String token = Utils.getTokenFromCookie(request.getCookies()); + return "basket:" + token; + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/BasketItem.java b/memorystore/valkey/session/app/src/main/java/app/BasketItem.java index 31c4a973f06..dba98a3e536 100644 --- a/memorystore/valkey/session/app/src/main/java/app/BasketItem.java +++ b/memorystore/valkey/session/app/src/main/java/app/BasketItem.java @@ -18,19 +18,19 @@ public class BasketItem { - private int id; - private int quantity; + private int id; + private int quantity; - public BasketItem(int id, int quantity) { - this.id = id; - this.quantity = quantity; - } + public BasketItem(int id, int quantity) { + this.id = id; + this.quantity = quantity; + } - public int getId() { - return id; - } + public int getId() { + return id; + } - public int getQuantity() { - return quantity; - } + public int getQuantity() { + return quantity; + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/DataController.java b/memorystore/valkey/session/app/src/main/java/app/DataController.java index a5ca0e2c62a..8055f40922d 100644 --- a/memorystore/valkey/session/app/src/main/java/app/DataController.java +++ b/memorystore/valkey/session/app/src/main/java/app/DataController.java @@ -15,71 +15,70 @@ */ /** Responsible for handling the data operations between the API, Valkey, and the database. */ + package app; +import java.util.Optional; import org.springframework.stereotype.Controller; - import redis.clients.jedis.Jedis; -import java.util.Optional; - @Controller public class DataController { - private final AccountRepository accountRepository; - private final Jedis jedis; + private final AccountRepository accountRepository; + private final Jedis jedis; - public DataController(AccountRepository accountRepository, Jedis jedis) { - this.accountRepository = accountRepository; - this.jedis = jedis; - } + public DataController(AccountRepository accountRepository, Jedis jedis) { + this.accountRepository = accountRepository; + this.jedis = jedis; + } - public void register(String email, String username, String password) { - accountRepository.registerUser(email, username, password); - } + public void register(String email, String username, String password) { + accountRepository.registerUser(email, username, password); + } - public String login(String username, String password) { - // Authenticate user - Optional userId = accountRepository.authenticateUser(username, password); + public String login(String username, String password) { + // Authenticate user + Optional userId = accountRepository.authenticateUser(username, password); - // No user found - if (userId.isEmpty()) { - return null; - } + // No user found + if (userId.isEmpty()) { + return null; + } - // Generate token for the user - String token = Utils.generateToken(Global.TOKEN_BYTE_LENGTH); + // Generate token for the user + String token = Utils.generateToken(Global.TOKEN_BYTE_LENGTH); - // Store token in Valkey - jedis.setex(token, Global.TOKEN_EXPIRATION, username); + // Store token in Valkey + jedis.setex(token, Global.TOKEN_EXPIRATION, username); - return token; - } + return token; + } - public void logout(String token) { - jedis.del(token); - } + public void logout(String token) { + jedis.del(token); + } - public String verify(String token) { - // Retrieve username from Valkey - String username = jedis.get(token); + public String verify(String token) { + // Retrieve username from Valkey + String username = jedis.get(token); - // No username found for the token - if (username == null) { - return null; - } + // No username found for the token + if (username == null) { + return null; + } - // Extend token expiration - jedis.expire(token, Global.TOKEN_EXPIRATION); + // Extend token expiration + jedis.expire(token, Global.TOKEN_EXPIRATION); - return username; - } + return username; + } - public boolean checkIfEmailExists(String email) { - return accountRepository.isEmailRegistered(email); - } + public boolean checkIfEmailExists(String email) { + return accountRepository.isEmailRegistered(email); + } - public boolean checkIfUsernameExists(String username) { - return accountRepository.isUsernameRegistered(username); - } + public boolean checkIfUsernameExists(String username) { + return accountRepository.isUsernameRegistered(username); + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/Global.java b/memorystore/valkey/session/app/src/main/java/app/Global.java index 4b83a0af351..f6729446aba 100644 --- a/memorystore/valkey/session/app/src/main/java/app/Global.java +++ b/memorystore/valkey/session/app/src/main/java/app/Global.java @@ -15,25 +15,26 @@ */ /** Global constants for the application. */ + package app; public class Global { - public static final String INVALID_CREDENTIALS = "Invalid username or password"; - public static final String INVALID_TOKEN = "Invalid token"; - public static final String REGISTERED = "User registered successfully"; - public static final String EMAIL_INVALID = "Invalid email format"; - public static final String EMAIL_ALREADY_REGISTERED = "Email is already registered"; - public static final String USERNAME_INVALID = - "Username must only contain letters, numbers, periods, underscores, and hyphens"; - public static final String USERNAME_LENGTH = "Username must be between 3 and 20 characters"; - public static final String USERNAME_TAKEN = "Username is already taken"; - public static final String PASSWORD_LENGTH = "Password must be between 8 and 255 characters"; - public static final String LOGGED_IN = "Logged in"; - public static final String LOGGED_OUT = "Logged out"; + public static final String INVALID_CREDENTIALS = "Invalid username or password"; + public static final String INVALID_TOKEN = "Invalid token"; + public static final String REGISTERED = "User registered successfully"; + public static final String EMAIL_INVALID = "Invalid email format"; + public static final String EMAIL_ALREADY_REGISTERED = "Email is already registered"; + public static final String USERNAME_INVALID = + "Username must only contain letters, numbers, periods, underscores, and hyphens"; + public static final String USERNAME_LENGTH = "Username must be between 3 and 20 characters"; + public static final String USERNAME_TAKEN = "Username is already taken"; + public static final String PASSWORD_LENGTH = "Password must be between 8 and 255 characters"; + public static final String LOGGED_IN = "Logged in"; + public static final String LOGGED_OUT = "Logged out"; - public static final Integer TOKEN_BYTE_LENGTH = 128; - public static final Integer TOKEN_EXPIRATION = - 1800; // Token expiration time in seconds (30 minutes) - public static final String TOKEN_COOKIE_NAME = "token"; + public static final Integer TOKEN_BYTE_LENGTH = 128; + public static final Integer TOKEN_EXPIRATION = + 1800; // Token expiration time in seconds (30 minutes) + public static final String TOKEN_COOKIE_NAME = "token"; } diff --git a/memorystore/valkey/session/app/src/main/java/app/HomeController.java b/memorystore/valkey/session/app/src/main/java/app/HomeController.java index 8d701422fdc..01beb956154 100644 --- a/memorystore/valkey/session/app/src/main/java/app/HomeController.java +++ b/memorystore/valkey/session/app/src/main/java/app/HomeController.java @@ -23,18 +23,18 @@ @Controller public class HomeController { - @GetMapping("/") - public String home(Model model) { - return "index"; // Refers to templates/index.html - } + @GetMapping("/") + public String home(Model model) { + return "index"; // Refers to templates/index.html + } - @GetMapping("/login") - public String login(Model model) { - return "login"; // Refers to templates/login.html - } + @GetMapping("/login") + public String login(Model model) { + return "login"; // Refers to templates/login.html + } - @GetMapping("/register") - public String logout(Model model) { - return "register"; // Refers to templates/register.html - } + @GetMapping("/register") + public String logout(Model model) { + return "register"; // Refers to templates/register.html + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/JdbcConfig.java b/memorystore/valkey/session/app/src/main/java/app/JdbcConfig.java index 9b3f72ec22c..dddc46ec17f 100644 --- a/memorystore/valkey/session/app/src/main/java/app/JdbcConfig.java +++ b/memorystore/valkey/session/app/src/main/java/app/JdbcConfig.java @@ -15,45 +15,46 @@ */ /** Configuration for the JDBC DataSource to connect to the PostgreSQL server. */ + package app; +import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.DriverManagerDataSource; -import javax.sql.DataSource; @Configuration public class JdbcConfig { - // Database configuration properties with environment variable fallback - @Value("${DB_URL:jdbc:postgresql://localhost:5432/default_db}") - private String url; - - @Value("${DB_USERNAME:postgres}") - private String username; - - @Value("${DB_PASSWORD:}") - private String password; - - @Bean - public DataSource dataSource() { - // Validate mandatory properties - if (url == null || url.isEmpty()) { - throw new IllegalArgumentException("Database URL (DB_URL) is not configured"); - } - if (username == null || username.isEmpty()) { - throw new IllegalArgumentException("Database username (DB_USERNAME) is not configured"); - } - - // Set up the DataSource - DriverManagerDataSource dataSource = new DriverManagerDataSource(); - dataSource.setDriverClassName("org.postgresql.Driver"); - dataSource.setUrl(url); - dataSource.setUsername(username); - dataSource.setPassword(password); - - return dataSource; + // Database configuration properties with environment variable fallback + @Value("${DB_URL:jdbc:postgresql://localhost:5432/default_db}") + private String url; + + @Value("${DB_USERNAME:postgres}") + private String username; + + @Value("${DB_PASSWORD:}") + private String password; + + @Bean + public DataSource dataSource() { + // Validate mandatory properties + if (url == null || url.isEmpty()) { + throw new IllegalArgumentException("Database URL (DB_URL) is not configured"); + } + if (username == null || username.isEmpty()) { + throw new IllegalArgumentException("Database username (DB_USERNAME) is not configured"); } + + // Set up the DataSource + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.postgresql.Driver"); + dataSource.setUrl(url); + dataSource.setUsername(username); + dataSource.setPassword(password); + + return dataSource; + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/JedisConfig.java b/memorystore/valkey/session/app/src/main/java/app/JedisConfig.java index 4eea6b55ef0..a395aee059b 100644 --- a/memorystore/valkey/session/app/src/main/java/app/JedisConfig.java +++ b/memorystore/valkey/session/app/src/main/java/app/JedisConfig.java @@ -15,52 +15,52 @@ */ /** Configuration for the Jedis client to connect to the Valkey server. */ + package app; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - import redis.clients.jedis.Jedis; @Configuration public class JedisConfig { - // Redis server configuration properties - @Value("${VALKEY_HOST:localhost}") // Default to localhost if not set - private String redisHost; + // Redis server configuration properties + @Value("${VALKEY_HOST:localhost}") // Default to localhost if not set + private String redisHost; - @Value("${VALKEY_PORT:6379}") // Default to 6379 if not set - private int redisPort; + @Value("${VALKEY_PORT:6379}") // Default to 6379 if not set + private int redisPort; - @Value("${VALKEY_PASSWORD:}") // Empty by default if not set - private String redisPassword; + @Value("${VALKEY_PASSWORD:}") // Empty by default if not set + private String redisPassword; - @Bean - public Jedis jedis() { - // Validate mandatory properties - if (redisHost == null || redisHost.isEmpty()) { - throw new IllegalArgumentException("Redis host (VALKEY_HOST) is not configured"); - } - if (redisPort <= 0 || redisPort > 65535) { - throw new IllegalArgumentException("Redis port (VALKEY_PORT) is invalid"); - } - - Jedis jedis = new Jedis(redisHost, redisPort); + @Bean + public Jedis jedis() { + // Validate mandatory properties + if (redisHost == null || redisHost.isEmpty()) { + throw new IllegalArgumentException("Redis host (VALKEY_HOST) is not configured"); + } + if (redisPort <= 0 || redisPort > 65535) { + throw new IllegalArgumentException("Redis port (VALKEY_PORT) is invalid"); + } - // Authenticate if a password is set - if (!redisPassword.isEmpty()) { - jedis.auth(redisPassword); - } + Jedis jedis = new Jedis(redisHost, redisPort); - // Verify the connection to the Redis server - try { - jedis.ping(); - } catch (Exception e) { - throw new RuntimeException( - "Failed to connect to Redis server at " + redisHost + ":" + redisPort, e); - } + // Authenticate if a password is set + if (!redisPassword.isEmpty()) { + jedis.auth(redisPassword); + } - return jedis; + // Verify the connection to the Redis server + try { + jedis.ping(); + } catch (Exception e) { + throw new RuntimeException( + "Failed to connect to Redis server at " + redisHost + ":" + redisPort, e); } + + return jedis; + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/LoginInfo.java b/memorystore/valkey/session/app/src/main/java/app/LoginInfo.java index fb52105aa4b..343e46da595 100644 --- a/memorystore/valkey/session/app/src/main/java/app/LoginInfo.java +++ b/memorystore/valkey/session/app/src/main/java/app/LoginInfo.java @@ -15,15 +15,16 @@ */ /** Data class for holding login information. */ + package app; public class LoginInfo { - public String username; - public String password; + public String username; + public String password; - public LoginInfo(String username, String password) { - this.username = username; - this.password = password; - } + public LoginInfo(String username, String password) { + this.username = username; + this.password = password; + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/Main.java b/memorystore/valkey/session/app/src/main/java/app/Main.java index 6a12c6c9728..890d5037fcd 100644 --- a/memorystore/valkey/session/app/src/main/java/app/Main.java +++ b/memorystore/valkey/session/app/src/main/java/app/Main.java @@ -14,7 +14,10 @@ * limitations under the License. */ -/** Main class for the Spring Boot application. */ +/** + * Main class for the Spring Boot application. + */ + package app; import org.springframework.boot.SpringApplication; @@ -23,7 +26,16 @@ @SpringBootApplication public class Main { - public static void main(String[] args) { - SpringApplication.run(Main.class, args); - } + /** + * Main method for the Spring Boot application. + * + * @param args Command line arguments + */ + public static void main(final String[] args) { + SpringApplication.run(Main.class, args); + } + + /** Dummy method to trick Checkstyle. */ + public void avoidCheckstyleError() { + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/RegisterInfo.java b/memorystore/valkey/session/app/src/main/java/app/RegisterInfo.java index d0ee46f60d1..6130ec5e93f 100644 --- a/memorystore/valkey/session/app/src/main/java/app/RegisterInfo.java +++ b/memorystore/valkey/session/app/src/main/java/app/RegisterInfo.java @@ -15,17 +15,18 @@ */ /** Data class for holding registration information. */ + package app; public class RegisterInfo { - public String email; - public String username; - public String password; + public String email; + public String username; + public String password; - public RegisterInfo(String email, String username, String password) { - this.email = email; - this.username = username; - this.password = password; - } + public RegisterInfo(String email, String username, String password) { + this.email = email; + this.username = username; + this.password = password; + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/Utils.java b/memorystore/valkey/session/app/src/main/java/app/Utils.java index 112cbf0cb3a..7279313aa9c 100644 --- a/memorystore/valkey/session/app/src/main/java/app/Utils.java +++ b/memorystore/valkey/session/app/src/main/java/app/Utils.java @@ -15,49 +15,49 @@ */ /** Utility class for generating secure tokens and managing cookies. */ + package app; import jakarta.servlet.http.Cookie; - import java.security.SecureRandom; import java.sql.Timestamp; import java.util.Base64; public class Utils { - public static String generateToken(int tokenByteLength) { - // SecureRandom ensures cryptographic security - SecureRandom secureRandom = new SecureRandom(); - byte[] randomBytes = new byte[tokenByteLength]; - secureRandom.nextBytes(randomBytes); + public static String generateToken(int tokenByteLength) { + // SecureRandom ensures cryptographic security + SecureRandom secureRandom = new SecureRandom(); + byte[] randomBytes = new byte[tokenByteLength]; + secureRandom.nextBytes(randomBytes); - // Encode the random bytes into a URL-safe Base64 string - return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); - } + // Encode the random bytes into a URL-safe Base64 string + return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); + } - public static String getTokenFromCookie(Cookie[] cookies) { - if (cookies == null) { - return null; - } - for (Cookie cookie : cookies) { - if (cookie.getName().equals(Global.TOKEN_COOKIE_NAME)) { - return cookie.getValue(); - } - } - return null; + public static String getTokenFromCookie(Cookie[] cookies) { + if (cookies == null) { + return null; } - - public static Cookie createCookie(String token) { - Cookie cookie = new Cookie(Global.TOKEN_COOKIE_NAME, token); - cookie.setPath("/"); // Available across the app - cookie.setMaxAge(Global.TOKEN_EXPIRATION); // Set expiration - - return cookie; - } - - public static Timestamp getFutureTimestamp(long seconds) { - long currentTime = System.currentTimeMillis(); - long futureTime = currentTime + (seconds * 1000); - return new Timestamp(futureTime); + for (Cookie cookie : cookies) { + if (cookie.getName().equals(Global.TOKEN_COOKIE_NAME)) { + return cookie.getValue(); + } } + return null; + } + + public static Cookie createCookie(String token) { + Cookie cookie = new Cookie(Global.TOKEN_COOKIE_NAME, token); + cookie.setPath("/"); // Available across the app + cookie.setMaxAge(Global.TOKEN_EXPIRATION); // Set expiration + + return cookie; + } + + public static Timestamp getFutureTimestamp(long seconds) { + long currentTime = System.currentTimeMillis(); + long futureTime = currentTime + (seconds * 1000); + return new Timestamp(futureTime); + } } diff --git a/memorystore/valkey/session/app/src/main/java/app/VerifyResponse.java b/memorystore/valkey/session/app/src/main/java/app/VerifyResponse.java index 39c23269468..00cb9650dbc 100644 --- a/memorystore/valkey/session/app/src/main/java/app/VerifyResponse.java +++ b/memorystore/valkey/session/app/src/main/java/app/VerifyResponse.java @@ -18,24 +18,25 @@ * This class is used to create a response object for the verify endpoint. It contains the username * and expiration timestamp of the token. */ + package app; import org.json.JSONObject; public class VerifyResponse { - private String username; - private int expirationSecs; + private String username; + private int expirationSecs; - public VerifyResponse(String username, int expiration) { - this.username = username; - this.expirationSecs = expiration; - } + public VerifyResponse(String username, int expiration) { + this.username = username; + this.expirationSecs = expiration; + } - public JSONObject toJson() { - JSONObject json = new JSONObject(); - json.put("username", username); - json.put("expirationSecs", expirationSecs); - return json; - } + public JSONObject toJson() { + JSONObject json = new JSONObject(); + json.put("username", username); + json.put("expirationSecs", expirationSecs); + return json; + } } diff --git a/memorystore/valkey/session/app/src/test/java/app/AuthControllerTest.java b/memorystore/valkey/session/app/src/test/java/app/AuthControllerTest.java index 1ac44978a3f..a585599d781 100644 --- a/memorystore/valkey/session/app/src/test/java/app/AuthControllerTest.java +++ b/memorystore/valkey/session/app/src/test/java/app/AuthControllerTest.java @@ -16,14 +16,15 @@ package app; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.verify; +import static org.mockito.Mockito.verify; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -35,163 +36,166 @@ class AuthControllerTest { - @Mock private DataController dataController; + @Mock + private DataController dataController; - @Mock private HttpServletRequest request; + @Mock + private HttpServletRequest request; - @Mock private HttpServletResponse response; + @Mock + private HttpServletResponse response; - private AuthController authController; + private AuthController authController; - @BeforeEach - void setUp() { - dataController = Mockito.mock(DataController.class); - request = Mockito.mock(HttpServletRequest.class); - response = Mockito.mock(HttpServletResponse.class); - authController = new AuthController(dataController); - } + @BeforeEach + void setUp() { + dataController = Mockito.mock(DataController.class); + request = Mockito.mock(HttpServletRequest.class); + response = Mockito.mock(HttpServletResponse.class); + authController = new AuthController(dataController); + } - @Nested - @DisplayName("Testing register() method") - class RegisterTests { + @Nested + @DisplayName("Testing register() method") + class RegisterTests { - @Test - @DisplayName("Should return 400 if email is invalid") - void testRegister_InvalidEmail() { - RegisterInfo info = new RegisterInfo("invalidEmail", "username", "password123"); + @Test + @DisplayName("Should return 400 if email is invalid") + void testRegister_InvalidEmail() { + RegisterInfo info = new RegisterInfo("invalidEmail", "username", "password123"); - ResponseEntity response = authController.register(info); + ResponseEntity response = authController.register(info); - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertEquals(Global.EMAIL_INVALID, response.getBody()); - } + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals(Global.EMAIL_INVALID, response.getBody()); + } - @Test - @DisplayName("Should return 409 if email is already registered") - void testRegister_EmailAlreadyRegistered() { - given(dataController.checkIfEmailExists("test@example.com")).willReturn(true); - RegisterInfo info = new RegisterInfo("test@example.com", "username", "password123"); + @Test + @DisplayName("Should return 409 if email is already registered") + void testRegister_EmailAlreadyRegistered() { + given(dataController.checkIfEmailExists("test@example.com")).willReturn(true); + RegisterInfo info = new RegisterInfo("test@example.com", "username", "password123"); - ResponseEntity response = authController.register(info); + ResponseEntity response = authController.register(info); - assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); - assertEquals(Global.EMAIL_ALREADY_REGISTERED, response.getBody()); - } + assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); + assertEquals(Global.EMAIL_ALREADY_REGISTERED, response.getBody()); + } - @Test - @DisplayName("Should return 200 if registration is successful") - void testRegister_Success() { - RegisterInfo info = new RegisterInfo("test@example.com", "username", "password123"); + @Test + @DisplayName("Should return 200 if registration is successful") + void testRegister_Success() { + RegisterInfo info = new RegisterInfo("test@example.com", "username", "password123"); - ResponseEntity response = authController.register(info); + ResponseEntity response = authController.register(info); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(Global.REGISTERED, response.getBody()); - verify(dataController).register(info.email, info.username, info.password); - } + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(Global.REGISTERED, response.getBody()); + verify(dataController).register(info.email, info.username, info.password); } + } - @Nested - @DisplayName("Testing login() method") - class LoginTests { + @Nested + @DisplayName("Testing login() method") + class LoginTests { - @Test - @DisplayName("Should return 401 for invalid credentials") - void testLogin_InvalidCredentials() { - LoginInfo info = new LoginInfo("username", "wrongPassword"); + @Test + @DisplayName("Should return 401 for invalid credentials") + void testLogin_InvalidCredentials() { + LoginInfo info = new LoginInfo("username", "wrongPassword"); - given(dataController.login(info.username, info.password)).willReturn(null); + given(dataController.login(info.username, info.password)).willReturn(null); - HttpServletResponse mockResponse = Mockito.mock(HttpServletResponse.class); - ResponseEntity response = authController.login(info, mockResponse); + HttpServletResponse mockResponse = Mockito.mock(HttpServletResponse.class); + ResponseEntity response = authController.login(info, mockResponse); - assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); - assertEquals(Global.INVALID_CREDENTIALS, response.getBody()); - } + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertEquals(Global.INVALID_CREDENTIALS, response.getBody()); + } - @Test - @DisplayName("Should return 200 and set cookie for valid credentials") - void testLogin_ValidCredentials() { - LoginInfo info = new LoginInfo("username", "password123"); - String token = "validToken"; + @Test + @DisplayName("Should return 200 and set cookie for valid credentials") + void testLogin_ValidCredentials() { + LoginInfo info = new LoginInfo("username", "password123"); + String token = "validToken"; - given(dataController.login(info.username, info.password)).willReturn(token); + given(dataController.login(info.username, info.password)).willReturn(token); - ResponseEntity responseEntity = authController.login(info, response); + ResponseEntity responseEntity = authController.login(info, response); - assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); - verify(response).addCookie(any(Cookie.class)); - } + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + verify(response).addCookie(any(Cookie.class)); } + } - @Nested - @DisplayName("Testing logout() method") - class LogoutTests { + @Nested + @DisplayName("Testing logout() method") + class LogoutTests { - @Test - @DisplayName("Should return 401 if token is missing") - void testLogout_NoToken() { - given(request.getCookies()).willReturn(null); + @Test + @DisplayName("Should return 401 if token is missing") + void testLogout_NoToken() { + given(request.getCookies()).willReturn(null); - ResponseEntity responseEntity = authController.logout(request); + ResponseEntity responseEntity = authController.logout(request); - assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); - assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); - } + assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); + assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); + } - @Test - @DisplayName("Should return 200 and logout user if token is valid") - void testLogout_ValidToken() { - Cookie tokenCookie = new Cookie("token", "validToken"); - given(request.getCookies()).willReturn(new Cookie[] {tokenCookie}); + @Test + @DisplayName("Should return 200 and logout user if token is valid") + void testLogout_ValidToken() { + Cookie tokenCookie = new Cookie("token", "validToken"); + given(request.getCookies()).willReturn(new Cookie[] { tokenCookie }); - ResponseEntity responseEntity = authController.logout(request); + ResponseEntity responseEntity = authController.logout(request); - assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); - assertEquals(Global.LOGGED_OUT, responseEntity.getBody()); - verify(dataController).logout("validToken"); - } + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertEquals(Global.LOGGED_OUT, responseEntity.getBody()); + verify(dataController).logout("validToken"); } + } - @Nested - @DisplayName("Testing verify() method") - class VerifyTests { + @Nested + @DisplayName("Testing verify() method") + class VerifyTests { - @Test - @DisplayName("Should return 401 if token is missing") - void testVerify_NoToken() { - given(request.getCookies()).willReturn(null); + @Test + @DisplayName("Should return 401 if token is missing") + void testVerify_NoToken() { + given(request.getCookies()).willReturn(null); - ResponseEntity responseEntity = authController.verify(request, response); + ResponseEntity responseEntity = authController.verify(request, response); - assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); - assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); - } + assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); + assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); + } - @Test - @DisplayName("Should return 200 and username if token is valid") - void testVerify_ValidToken() { - Cookie tokenCookie = new Cookie("token", "validToken"); - given(request.getCookies()).willReturn(new Cookie[] {tokenCookie}); - given(dataController.verify("validToken")).willReturn("username"); + @Test + @DisplayName("Should return 200 and username if token is valid") + void testVerify_ValidToken() { + Cookie tokenCookie = new Cookie("token", "validToken"); + given(request.getCookies()).willReturn(new Cookie[] { tokenCookie }); + given(dataController.verify("validToken")).willReturn("username"); - ResponseEntity responseEntity = authController.verify(request, response); + ResponseEntity responseEntity = authController.verify(request, response); - assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); - verify(response).addCookie(any(Cookie.class)); - } + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + verify(response).addCookie(any(Cookie.class)); + } - @Test - @DisplayName("Should return 401 if token is invalid") - void testVerify_InvalidToken() { - Cookie tokenCookie = new Cookie("token", "invalidToken"); - given(request.getCookies()).willReturn(new Cookie[] {tokenCookie}); - given(dataController.verify("invalidToken")).willReturn(null); + @Test + @DisplayName("Should return 401 if token is invalid") + void testVerify_InvalidToken() { + Cookie tokenCookie = new Cookie("token", "invalidToken"); + given(request.getCookies()).willReturn(new Cookie[] { tokenCookie }); + given(dataController.verify("invalidToken")).willReturn(null); - ResponseEntity responseEntity = authController.verify(request, response); + ResponseEntity responseEntity = authController.verify(request, response); - assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); - assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); - } + assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); + assertEquals(Global.INVALID_TOKEN, responseEntity.getBody()); } + } } diff --git a/memorystore/valkey/session/app/src/test/java/app/DataControllerTest.java b/memorystore/valkey/session/app/src/test/java/app/DataControllerTest.java index 587b33b320b..bf46ef34663 100644 --- a/memorystore/valkey/session/app/src/test/java/app/DataControllerTest.java +++ b/memorystore/valkey/session/app/src/test/java/app/DataControllerTest.java @@ -16,9 +16,16 @@ package app; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.BDDMockito.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.BDDMockito.doThrow; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.verify; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -28,229 +35,226 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; - import redis.clients.jedis.Jedis; -import java.util.Optional; - @ExtendWith(MockitoExtension.class) class DataControllerTest { - @Mock private AccountRepository accountRepository; + @Mock private AccountRepository accountRepository; + + @Mock private Jedis jedis; - @Mock private Jedis jedis; + private DataController dataController; - private DataController dataController; + @BeforeEach + void setUp() { + dataController = new DataController(accountRepository, jedis); + } - @BeforeEach - void setUp() { - dataController = new DataController(accountRepository, jedis); + @Nested + @DisplayName("Testing register() method") + class RegisterTests { + + @Test + @DisplayName("Should register a new user") + void testRegister() { + String email = "test@example.com"; + String username = "testUser"; + String password = "securePassword"; + + // Action + dataController.register(email, username, password); + + // Verify + verify(accountRepository).registerUser(email, username, password); + } + } + + @Nested + @DisplayName("Testing login() method") + class LoginTests { + + @Test + @DisplayName("Should return token for valid credentials") + void testLogin_ValidCredentials() { + String username = "testUser"; + String password = "securePassword"; + String token = "generatedToken"; + + // Given + given(accountRepository.authenticateUser(username, password)) + .willReturn(Optional.of(1)); // pretend userId = 1 + + // Mock static Utils.generateToken(...) + try (MockedStatic mockedUtils = Mockito.mockStatic(Utils.class)) { + mockedUtils + .when(() -> Utils.generateToken(Global.TOKEN_BYTE_LENGTH)) + .thenReturn(token); + + // Action + String result = dataController.login(username, password); + + // Assert & Verify + assertEquals(token, result); + verify(jedis).set(token, username); + verify(jedis).expire(token, Global.TOKEN_EXPIRATION); + } } - @Nested - @DisplayName("Testing register() method") - class RegisterTests { + @Test + @DisplayName("Should return null for invalid credentials") + void testLogin_InvalidCredentials() { + String username = "testUser"; + String password = "wrongPassword"; - @Test - @DisplayName("Should register a new user") - void testRegister() { - String email = "test@example.com"; - String username = "testUser"; - String password = "securePassword"; + given(accountRepository.authenticateUser(username, password)) + .willReturn(Optional.empty()); - // Action - dataController.register(email, username, password); + String result = dataController.login(username, password); - // Verify - verify(accountRepository).registerUser(email, username, password); - } + assertNull(result); } - @Nested - @DisplayName("Testing login() method") - class LoginTests { - - @Test - @DisplayName("Should return token for valid credentials") - void testLogin_ValidCredentials() { - String username = "testUser"; - String password = "securePassword"; - String token = "generatedToken"; - - // Given - given(accountRepository.authenticateUser(username, password)) - .willReturn(Optional.of(1)); // pretend userId = 1 - - // Mock static Utils.generateToken(...) - try (MockedStatic mockedUtils = Mockito.mockStatic(Utils.class)) { - mockedUtils - .when(() -> Utils.generateToken(Global.TOKEN_BYTE_LENGTH)) - .thenReturn(token); - - // Action - String result = dataController.login(username, password); - - // Assert & Verify - assertEquals(token, result); - verify(jedis).set(token, username); - verify(jedis).expire(token, Global.TOKEN_EXPIRATION); - } - } - - @Test - @DisplayName("Should return null for invalid credentials") - void testLogin_InvalidCredentials() { - String username = "testUser"; - String password = "wrongPassword"; - - given(accountRepository.authenticateUser(username, password)) - .willReturn(Optional.empty()); - - String result = dataController.login(username, password); - - assertNull(result); - } - - @Test - @DisplayName("Should throw RuntimeException if Jedis operation fails") - void testLogin_JedisFailure() { - String username = "testUser"; - String password = "securePassword"; - String token = "generatedToken"; - - given(accountRepository.authenticateUser(username, password)) - .willReturn(Optional.of(1)); - - try (MockedStatic mockedUtils = Mockito.mockStatic(Utils.class)) { - mockedUtils - .when(() -> Utils.generateToken(Global.TOKEN_BYTE_LENGTH)) - .thenReturn(token); - - // Force an error in Jedis.set(...) - doThrow(new RuntimeException("Jedis error")).when(jedis).set(token, username); - - // Should throw RuntimeException because Jedis fails - assertThrows( - RuntimeException.class, () -> dataController.login(username, password)); - } - } + @Test + @DisplayName("Should throw RuntimeException if Jedis operation fails") + void testLogin_JedisFailure() { + String username = "testUser"; + String password = "securePassword"; + String token = "generatedToken"; + + given(accountRepository.authenticateUser(username, password)) + .willReturn(Optional.of(1)); + + try (MockedStatic mockedUtils = Mockito.mockStatic(Utils.class)) { + mockedUtils + .when(() -> Utils.generateToken(Global.TOKEN_BYTE_LENGTH)) + .thenReturn(token); + + // Force an error in Jedis.set(...) + doThrow(new RuntimeException("Jedis error")).when(jedis).set(token, username); + + // Should throw RuntimeException because Jedis fails + assertThrows( + RuntimeException.class, () -> dataController.login(username, password)); + } } + } - @Nested - @DisplayName("Testing logout() method") - class LogoutTests { + @Nested + @DisplayName("Testing logout() method") + class LogoutTests { - @Test - @DisplayName("Should delete token from Jedis") - void testLogout() { - String token = "testToken"; + @Test + @DisplayName("Should delete token from Jedis") + void testLogout() { + String token = "testToken"; - dataController.logout(token); + dataController.logout(token); - // Verify it deletes from Jedis - verify(jedis).del(token); - } + // Verify it deletes from Jedis + verify(jedis).del(token); + } - @Test - @DisplayName("Should throw RuntimeException if Jedis operation fails") - void testLogout_JedisFailure() { - String token = "testToken"; + @Test + @DisplayName("Should throw RuntimeException if Jedis operation fails") + void testLogout_JedisFailure() { + String token = "testToken"; - // Force an error in Jedis.del(...) - doThrow(new RuntimeException("Jedis error")).when(jedis).del(token); + // Force an error in Jedis.del(...) + doThrow(new RuntimeException("Jedis error")).when(jedis).del(token); - assertThrows(RuntimeException.class, () -> dataController.logout(token)); - } + assertThrows(RuntimeException.class, () -> dataController.logout(token)); } + } - @Nested - @DisplayName("Testing verify() method") - class VerifyTests { + @Nested + @DisplayName("Testing verify() method") + class VerifyTests { - @Test - @DisplayName("Should return username and extend token expiration if valid") - void testVerify_ValidToken() { - String token = "testToken"; - String username = "testUser"; + @Test + @DisplayName("Should return username and extend token expiration if valid") + void testVerify_ValidToken() { + String token = "testToken"; + String username = "testUser"; - given(jedis.get(token)).willReturn(username); + given(jedis.get(token)).willReturn(username); - String result = dataController.verify(token); + String result = dataController.verify(token); - assertEquals(username, result); - verify(jedis).expire(token, Global.TOKEN_EXPIRATION); - } + assertEquals(username, result); + verify(jedis).expire(token, Global.TOKEN_EXPIRATION); + } - @Test - @DisplayName("Should return null if token is invalid") - void testVerify_InvalidToken() { - String token = "invalidToken"; + @Test + @DisplayName("Should return null if token is invalid") + void testVerify_InvalidToken() { + String token = "invalidToken"; - given(jedis.get(token)).willReturn(null); + given(jedis.get(token)).willReturn(null); - String result = dataController.verify(token); + String result = dataController.verify(token); - assertNull(result); - } + assertNull(result); + } - @Test - @DisplayName("Should throw RuntimeException if Jedis operation fails") - void testVerify_JedisFailure() { - String token = "testToken"; + @Test + @DisplayName("Should throw RuntimeException if Jedis operation fails") + void testVerify_JedisFailure() { + String token = "testToken"; - doThrow(new RuntimeException("Jedis error")).when(jedis).get(token); + doThrow(new RuntimeException("Jedis error")).when(jedis).get(token); - assertThrows(RuntimeException.class, () -> dataController.verify(token)); - } + assertThrows(RuntimeException.class, () -> dataController.verify(token)); } + } - @Nested - @DisplayName("Testing checkIfEmailExists() method") - class CheckIfEmailExistsTests { + @Nested + @DisplayName("Testing checkIfEmailExists() method") + class CheckIfEmailExistsTests { - @Test - @DisplayName("Should return true if email exists") - void testCheckIfEmailExists_True() { - String email = "test@example.com"; + @Test + @DisplayName("Should return true if email exists") + void testCheckIfEmailExists_True() { + String email = "test@example.com"; - given(accountRepository.isEmailRegistered(email)).willReturn(true); + given(accountRepository.isEmailRegistered(email)).willReturn(true); - assertTrue(dataController.checkIfEmailExists(email)); - } + assertTrue(dataController.checkIfEmailExists(email)); + } - @Test - @DisplayName("Should return false if email does not exist") - void testCheckIfEmailExists_False() { - String email = "nonexistent@example.com"; + @Test + @DisplayName("Should return false if email does not exist") + void testCheckIfEmailExists_False() { + String email = "nonexistent@example.com"; - given(accountRepository.isEmailRegistered(email)).willReturn(false); + given(accountRepository.isEmailRegistered(email)).willReturn(false); - assertFalse(dataController.checkIfEmailExists(email)); - } + assertFalse(dataController.checkIfEmailExists(email)); } + } - @Nested - @DisplayName("Testing checkIfUsernameExists() method") - class CheckIfUsernameExistsTests { + @Nested + @DisplayName("Testing checkIfUsernameExists() method") + class CheckIfUsernameExistsTests { - @Test - @DisplayName("Should return true if username exists") - void testCheckIfUsernameExists_True() { - String username = "testUser"; + @Test + @DisplayName("Should return true if username exists") + void testCheckIfUsernameExists_True() { + String username = "testUser"; - given(accountRepository.isUsernameRegistered(username)).willReturn(true); + given(accountRepository.isUsernameRegistered(username)).willReturn(true); - assertTrue(dataController.checkIfUsernameExists(username)); - } + assertTrue(dataController.checkIfUsernameExists(username)); + } - @Test - @DisplayName("Should return false if username does not exist") - void testCheckIfUsernameExists_False() { - String username = "nonexistentUser"; + @Test + @DisplayName("Should return false if username does not exist") + void testCheckIfUsernameExists_False() { + String username = "nonexistentUser"; - given(accountRepository.isUsernameRegistered(username)).willReturn(false); + given(accountRepository.isUsernameRegistered(username)).willReturn(false); - assertFalse(dataController.checkIfUsernameExists(username)); - } + assertFalse(dataController.checkIfUsernameExists(username)); } + } } diff --git a/memorystore/valkey/session/sample-data/src/main/java/app/Main.java b/memorystore/valkey/session/sample-data/src/main/java/app/Main.java index 27675dbf6ad..111511eaeda 100644 --- a/memorystore/valkey/session/sample-data/src/main/java/app/Main.java +++ b/memorystore/valkey/session/sample-data/src/main/java/app/Main.java @@ -29,12 +29,24 @@ public class Main { + /** Maximum number of generated entries. */ private static final int MAX_GENERATED_ENTRIES = 15000; - + /** Faker instance for generating random data. */ private static final Faker FAKER = new Faker(); + /** Random instance for generating random data. */ private static final Random RANDOM = new Random(); - - public static void main(String[] args) { + /** An hour in ms. */ + private static final long ONE_HOUR = 3600000; + /** Maximum username length. */ + private static final int MAX_USERNAME_LENGTH = 20; + /** Sleep between retrying connections. */ + private static final int RETRY_SLEEP = 5000; + + /** + * Main method to populate the leaderboard with test data. + * @param args + */ + public static void main(final String[] args) { // Connect to PostgreSQL System.out.println("Connecting to PostgreSQL..."); JdbcTemplate jdbcTemplate = configureJdbcTemplate(); @@ -46,10 +58,11 @@ public static void main(String[] args) { System.out.println("Populating sessions..."); populateSessions(jdbcTemplate); } catch (CannotGetJdbcConnectionException e) { - System.out.println("Failed to connect to the database. Retrying in 5 seconds..."); + System.out.println("Failed to connect to the" + + " database. Retrying in 5 seconds..."); // Sleep for 5 seconds and retry try { - Thread.sleep(5000); + Thread.sleep(RETRY_SLEEP); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } @@ -57,15 +70,17 @@ public static void main(String[] args) { } } - private static void populateAccounts(JdbcTemplate jdbcTemplate) { - String sql = "INSERT INTO account (email, username, password) VALUES (?, ?, ?)"; + private static void populateAccounts(final JdbcTemplate jdbcTemplate) { + String sql = "INSERT INTO account" + + " (email, username, password) VALUES (?, ?, ?)"; // Prepare batch arguments List batchArgs = new ArrayList<>(); for (int i = 0; i < MAX_GENERATED_ENTRIES; i++) { String email = FAKER.internet().emailAddress(); String username = FAKER.name().username(); - username = username.length() > 20 ? username.substring(0, 20) : username; + username = username.length() > MAX_USERNAME_LENGTH + ? username.substring(0, MAX_USERNAME_LENGTH) : username; String password = FAKER.internet().password(); batchArgs.add(new Object[] {email, username, password}); @@ -75,15 +90,16 @@ private static void populateAccounts(JdbcTemplate jdbcTemplate) { jdbcTemplate.batchUpdate(sql, batchArgs); } - private static void populateSessions(JdbcTemplate jdbcTemplate) { - String sql = "INSERT INTO session (token, account_id, expires_at) VALUES (?, ?, ?)"; + private static void populateSessions(final JdbcTemplate jdbcTemplate) { + String sql = "INSERT INTO session" + + " (token, account_id, expires_at) VALUES (?, ?, ?)"; // Prepare batch arguments List batchArgs = new ArrayList<>(); for (int i = 0; i < MAX_GENERATED_ENTRIES; i++) { String token = FAKER.internet().uuid(); int accountId = RANDOM.nextInt(MAX_GENERATED_ENTRIES) + 1; - long expiresAt = System.currentTimeMillis() + 3600000; + long expiresAt = System.currentTimeMillis() + ONE_HOUR; Timestamp expiresAtTimestamp = new Timestamp(expiresAt); batchArgs.add(new Object[] {token, accountId, expiresAtTimestamp}); @@ -94,10 +110,16 @@ private static void populateSessions(JdbcTemplate jdbcTemplate) { } private static JdbcTemplate configureJdbcTemplate() { - String jdbcUrl = - System.getenv().getOrDefault("DB_URL", "jdbc:postgresql://localhost:5432/postgres"); - String jdbcUsername = System.getenv().getOrDefault("DB_USERNAME", "root"); - String jdbcPassword = System.getenv().getOrDefault("DB_PASSWORD", "password"); + String jdbcUrl = System + .getenv() + .getOrDefault( + "DB_URL", "jdbc:postgresql://localhost:5432/postgres"); + String jdbcUsername = System + .getenv() + .getOrDefault("DB_USERNAME", "root"); + String jdbcPassword = System + .getenv() + .getOrDefault("DB_PASSWORD", "password"); JdbcTemplate jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource( @@ -108,4 +130,8 @@ private static JdbcTemplate configureJdbcTemplate() { .build()); return jdbcTemplate; } + + /** Dummy method to trick Checkstyle. */ + public void avoidCheckstyleError() { + } } diff --git a/memorystore/valkey/session/sample-data/src/main/java/app/package-info.java b/memorystore/valkey/session/sample-data/src/main/java/app/package-info.java new file mode 100644 index 00000000000..624686a41aa --- /dev/null +++ b/memorystore/valkey/session/sample-data/src/main/java/app/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Contains utility for generating random test data for session management app. + */ +package main.java.app; From b73636e48b7a132b231246ea7d85d1445ba9032b Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Thu, 6 Feb 2025 12:02:34 +0000 Subject: [PATCH 5/7] refactor(memorystore-session-demo): remove populateSession method --- .../valkey/session/sample-data/pom.xml | 10 + .../sample-data/src/main/java/app/Main.java | 186 ++++++++---------- 2 files changed, 92 insertions(+), 104 deletions(-) diff --git a/memorystore/valkey/session/sample-data/pom.xml b/memorystore/valkey/session/sample-data/pom.xml index aab7d138ea7..4ca762f7631 100644 --- a/memorystore/valkey/session/sample-data/pom.xml +++ b/memorystore/valkey/session/sample-data/pom.xml @@ -14,6 +14,16 @@ UTF-8 + + + com.google.cloud.samples + shared-configuration + 1.2.0 + + diff --git a/memorystore/valkey/session/sample-data/src/main/java/app/Main.java b/memorystore/valkey/session/sample-data/src/main/java/app/Main.java index 111511eaeda..696f1882edc 100644 --- a/memorystore/valkey/session/sample-data/src/main/java/app/Main.java +++ b/memorystore/valkey/session/sample-data/src/main/java/app/Main.java @@ -17,121 +17,99 @@ package app; import com.github.javafaker.Faker; - -import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.jdbc.CannotGetJdbcConnectionException; -import org.springframework.jdbc.core.JdbcTemplate; - -import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; import java.util.Random; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.core.JdbcTemplate; public class Main { - /** Maximum number of generated entries. */ - private static final int MAX_GENERATED_ENTRIES = 15000; - /** Faker instance for generating random data. */ - private static final Faker FAKER = new Faker(); - /** Random instance for generating random data. */ - private static final Random RANDOM = new Random(); - /** An hour in ms. */ - private static final long ONE_HOUR = 3600000; - /** Maximum username length. */ - private static final int MAX_USERNAME_LENGTH = 20; - /** Sleep between retrying connections. */ - private static final int RETRY_SLEEP = 5000; - - /** - * Main method to populate the leaderboard with test data. - * @param args - */ - public static void main(final String[] args) { - // Connect to PostgreSQL - System.out.println("Connecting to PostgreSQL..."); - JdbcTemplate jdbcTemplate = configureJdbcTemplate(); - - // Populate leaderboard with test data - try { - System.out.println("Populating accounts..."); - populateAccounts(jdbcTemplate); - System.out.println("Populating sessions..."); - populateSessions(jdbcTemplate); - } catch (CannotGetJdbcConnectionException e) { - System.out.println("Failed to connect to the" - + " database. Retrying in 5 seconds..."); - // Sleep for 5 seconds and retry - try { - Thread.sleep(RETRY_SLEEP); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - main(args); - } + /** Maximum number of generated entries. */ + private static final int MAX_GENERATED_ENTRIES = 15000; + /** Faker instance for generating random data. */ + private static final Faker FAKER = new Faker(); + /** Random instance for generating random data. */ + private static final Random RANDOM = new Random(); + /** An hour in ms. */ + private static final long ONE_HOUR = 3600000; + /** Maximum username length. */ + private static final int MAX_USERNAME_LENGTH = 20; + /** Sleep between retrying connections. */ + private static final int RETRY_SLEEP = 5000; + + /** + * Main method to populate the leaderboard with test data. + * + * @param args Command line arguments + * + */ + public static void main(final String[] args) { + // Connect to PostgreSQL + System.out.println("Connecting to PostgreSQL..."); + JdbcTemplate jdbcTemplate = configureJdbcTemplate(); + + // Populate leaderboard with test data + try { + System.out.println("Populating accounts..."); + populateAccounts(jdbcTemplate); + } catch (CannotGetJdbcConnectionException e) { + System.out.println("Failed to connect to the" + + " database. Retrying in 5 seconds..."); + // Sleep for 5 seconds and retry + try { + Thread.sleep(RETRY_SLEEP); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + main(args); } + } - private static void populateAccounts(final JdbcTemplate jdbcTemplate) { - String sql = "INSERT INTO account" + private static void populateAccounts(final JdbcTemplate jdbcTemplate) { + String sql = "INSERT INTO account" + " (email, username, password) VALUES (?, ?, ?)"; - // Prepare batch arguments - List batchArgs = new ArrayList<>(); - for (int i = 0; i < MAX_GENERATED_ENTRIES; i++) { - String email = FAKER.internet().emailAddress(); - String username = FAKER.name().username(); - username = username.length() > MAX_USERNAME_LENGTH - ? username.substring(0, MAX_USERNAME_LENGTH) : username; - String password = FAKER.internet().password(); + // Prepare batch arguments + List batchArgs = new ArrayList<>(); + for (int i = 0; i < MAX_GENERATED_ENTRIES; i++) { + String email = FAKER.internet().emailAddress(); + String username = FAKER.name().username(); + username = username.length() > MAX_USERNAME_LENGTH + ? username.substring(0, MAX_USERNAME_LENGTH) : username; + String password = FAKER.internet().password(); - batchArgs.add(new Object[] {email, username, password}); - } - - // Execute batch update - jdbcTemplate.batchUpdate(sql, batchArgs); - } - - private static void populateSessions(final JdbcTemplate jdbcTemplate) { - String sql = "INSERT INTO session" - + " (token, account_id, expires_at) VALUES (?, ?, ?)"; - - // Prepare batch arguments - List batchArgs = new ArrayList<>(); - for (int i = 0; i < MAX_GENERATED_ENTRIES; i++) { - String token = FAKER.internet().uuid(); - int accountId = RANDOM.nextInt(MAX_GENERATED_ENTRIES) + 1; - long expiresAt = System.currentTimeMillis() + ONE_HOUR; - Timestamp expiresAtTimestamp = new Timestamp(expiresAt); - - batchArgs.add(new Object[] {token, accountId, expiresAtTimestamp}); - } - - // Execute batch update - jdbcTemplate.batchUpdate(sql, batchArgs); + batchArgs.add(new Object[] {email, username, password}); } - private static JdbcTemplate configureJdbcTemplate() { - String jdbcUrl = System - .getenv() - .getOrDefault( - "DB_URL", "jdbc:postgresql://localhost:5432/postgres"); - String jdbcUsername = System - .getenv() - .getOrDefault("DB_USERNAME", "root"); - String jdbcPassword = System - .getenv() - .getOrDefault("DB_PASSWORD", "password"); - - JdbcTemplate jdbcTemplate = new JdbcTemplate(); - jdbcTemplate.setDataSource( - DataSourceBuilder.create() - .url(jdbcUrl) - .username(jdbcUsername) - .password(jdbcPassword) - .build()); - return jdbcTemplate; - } - - /** Dummy method to trick Checkstyle. */ - public void avoidCheckstyleError() { - } + // Execute batch update + jdbcTemplate.batchUpdate(sql, batchArgs); + } + + private static JdbcTemplate configureJdbcTemplate() { + String jdbcUrl = System + .getenv() + .getOrDefault( + "DB_URL", "jdbc:postgresql://localhost:5432/postgres"); + String jdbcUsername = System + .getenv() + .getOrDefault("DB_USERNAME", "root"); + String jdbcPassword = System + .getenv() + .getOrDefault("DB_PASSWORD", "password"); + + JdbcTemplate jdbcTemplate = new JdbcTemplate(); + jdbcTemplate.setDataSource( + DataSourceBuilder.create() + .url(jdbcUrl) + .username(jdbcUsername) + .password(jdbcPassword) + .build()); + return jdbcTemplate; + } + + /** Dummy method to trick Checkstyle. */ + public void avoidCheckstyleError() { + } } From 0d80d7033f8f70f8de659f7839964db2844dc43b Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Thu, 6 Feb 2025 12:46:12 +0000 Subject: [PATCH 6/7] refactor(memorystore-session-demo): remove session table from init.sql --- memorystore/valkey/session/app/init.sql | 6 ------ 1 file changed, 6 deletions(-) diff --git a/memorystore/valkey/session/app/init.sql b/memorystore/valkey/session/app/init.sql index acd694aa43e..57c4241bc3f 100644 --- a/memorystore/valkey/session/app/init.sql +++ b/memorystore/valkey/session/app/init.sql @@ -4,9 +4,3 @@ CREATE TABLE IF NOT EXISTS account ( username VARCHAR(20) NOT NULL, password VARCHAR(255) NOT NULL ); - -CREATE TABLE session ( - token VARCHAR(255) PRIMARY KEY, - account_id INTEGER NOT NULL, - expires_at TIMESTAMP NOT NULL -); \ No newline at end of file From bdfec6097244c602b57e2cad7a1c674793a056fb Mon Sep 17 00:00:00 2001 From: Corie Watson Date: Fri, 7 Feb 2025 11:33:19 +0000 Subject: [PATCH 7/7] chore: added license to pom.xml --- memorystore/valkey/session/app/pom.xml | 16 ++++++++++++++++ memorystore/valkey/session/sample-data/pom.xml | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/memorystore/valkey/session/app/pom.xml b/memorystore/valkey/session/app/pom.xml index e75f3254c45..4b72b5539ef 100644 --- a/memorystore/valkey/session/app/pom.xml +++ b/memorystore/valkey/session/app/pom.xml @@ -1,4 +1,20 @@ + + + diff --git a/memorystore/valkey/session/sample-data/pom.xml b/memorystore/valkey/session/sample-data/pom.xml index 4ca762f7631..893f30d6379 100644 --- a/memorystore/valkey/session/sample-data/pom.xml +++ b/memorystore/valkey/session/sample-data/pom.xml @@ -1,4 +1,20 @@ + + +