diff --git a/AGENTS.md b/AGENTS.md index 948d802..5a97f72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -160,6 +160,11 @@ void methodName_condition_expectation() { - `@Transactional` behavior → requires Spring context - Hibernate queries → requires DB +### Verification Gate +- After every code, SQL, configuration, or test change, run `./scripts/test.sh` before reporting the work as verified. +- Before recommending a commit or push, report the test result. Do not recommend either when the test gate fails. +- If the change affects Docker runtime configuration, also verify the relevant Docker Compose startup path. + --- ## Security Rules — NEVER Violate diff --git a/scripts/dev-start.sh b/scripts/dev-start.sh index 67f5f56..0ec8392 100755 --- a/scripts/dev-start.sh +++ b/scripts/dev-start.sh @@ -1,42 +1,18 @@ -#!/bin/bash -# ================================================================ -# dev-start.sh — Start local development environment -# Usage: ./script/dev-start.sh -# ================================================================ +#!/usr/bin/env bash +# Start the complete local environment declared in docker-compose.yml. +# Usage: ./scripts/dev-start.sh +set -euo pipefail -# Move to project root regardless of where script is called from -cd "$(dirname "$0")/.." || exit 1 +cd "$(dirname "$0")/.." -# 1. Load .env FIRST — Docker Compose needs these variables to start -# set -a exports ALL variables defined after it -# source .env reads the file -# set +a stops auto-exporting -if [ ! -f .env ]; then - echo "ERROR: .env file not found. Copy .env.example and fill in your values." +if [[ ! -f .env ]]; then + echo "ERROR: .env not found. Copy .env.example to .env and set private values." exit 1 fi -set -a && source .env && set +a -echo "Environment variables loaded." -# 2. Start PostgreSQL container -echo "Starting PostgreSQL..." -docker compose up -d postgres +# Docker Compose reads .env itself. Do not source it in Bash: passwords may +# contain characters such as $, / or ! that should remain literal values. +docker compose up -d --build +docker compose ps -# 3. Wait for PostgreSQL to be healthy (uses healthcheck defined in docker-compose.yml) -echo "Waiting for PostgreSQL to be ready..." -until docker compose exec -T postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" 2>/dev/null; do - sleep 1 -done -echo "PostgreSQL is ready." - -# 4. Start Spring Boot with dev profile (background + PID) -# WHY background + PID file: allows dev-stop.sh to kill only THIS process, -# not any other Java project running on the same machine. -echo "Starting Spring Boot on port ${SERVER_PORT:-8080}..." -./mvnw spring-boot:run -Dspring-boot.run.profiles=dev & -SPRING_PID=$! -echo "$SPRING_PID" > .spring-boot.pid -echo "Spring Boot PID: $SPRING_PID (saved to .spring-boot.pid)" - -# Wait keeps the script alive so you can see logs — Ctrl+C to stop -wait +echo "Environment started. Inspect API logs with: docker compose logs -f wallet-api" diff --git a/scripts/dev-stop.sh b/scripts/dev-stop.sh index beb7135..bb6bad1 100755 --- a/scripts/dev-stop.sh +++ b/scripts/dev-stop.sh @@ -1,28 +1,11 @@ -#!/bin/bash -# ================================================================ -# dev-stop.sh — Stop local development environment -# Usage: ./script/dev-stop.sh -# ================================================================ +#!/usr/bin/env bash +# Stop the complete Docker Compose environment without deleting database data. +# Usage: ./scripts/dev-stop.sh +set -euo pipefail -cd "$(dirname "$0")/.." || exit 1 +cd "$(dirname "$0")/.." -# 1. Stop Spring Boot using PID file -# WHY PID file instead of pkill: isolates THIS project — won't kill other Java apps. -echo "Stopping Spring Boot..." -if [ -f .spring-boot.pid ]; then - PID=$(cat .spring-boot.pid) - if kill "$PID" 2>/dev/null; then - echo "Spring Boot stopped (PID $PID)." - else - echo "Spring Boot process (PID $PID) was not running." - fi - rm -f .spring-boot.pid -else - echo "No .spring-boot.pid found. Spring Boot may not be running." -fi - -echo "Stopping PostgreSQL container..." -docker compose stop postgres -echo "PostgreSQL stopped." - -echo "Done. All services stopped." +# `stop` preserves the PostgreSQL bind-mounted data directory. A destructive +# reset must always be an explicit, separate action. +docker compose stop +echo "Environment stopped. Database data was preserved." diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..c7737bc --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Run the same Maven verification gate used by the CI build-and-test job. +# Usage: ./scripts/test.sh +set -euo pipefail + +cd "$(dirname "$0")/.." + +# `clean` prevents stale compiled classes from hiding errors. Tests use the +# `test` Spring profile and H2; Docker/PostgreSQL are not required. +SPRING_PROFILES_ACTIVE=test ./mvnw clean test --no-transfer-progress diff --git a/src/main/java/com/wallet/secure/audit/entity/AuditLog.java b/src/main/java/com/wallet/secure/audit/entity/AuditLog.java index 0547ad8..445cb63 100644 --- a/src/main/java/com/wallet/secure/audit/entity/AuditLog.java +++ b/src/main/java/com/wallet/secure/audit/entity/AuditLog.java @@ -66,6 +66,7 @@ public class AuditLog { * The specific action that occurred. * Maps to PostgreSQL ENUM audit_action (02-types.sql). */ + @JdbcTypeCode(SqlTypes.NAMED_ENUM) @Enumerated(EnumType.STRING) @Column(name = "action", nullable = false, length = 50) private AuditAction action; @@ -75,6 +76,7 @@ public class AuditLog { * Maps to PostgreSQL ENUM log_severity (02-types.sql). * INFO / WARNING / ERROR / CRITICAL */ + @JdbcTypeCode(SqlTypes.NAMED_ENUM) @Enumerated(EnumType.STRING) @Column(name = "severity_level", nullable = false, length = 20) @Builder.Default diff --git a/src/main/java/com/wallet/secure/transaction/entity/Transaction.java b/src/main/java/com/wallet/secure/transaction/entity/Transaction.java index 0e20f90..c5fd637 100644 --- a/src/main/java/com/wallet/secure/transaction/entity/Transaction.java +++ b/src/main/java/com/wallet/secure/transaction/entity/Transaction.java @@ -103,6 +103,7 @@ public class Transaction { * Must match the currency of both wallets involved * Cross-currency transactions are NOT supported - validated in service */ + @JdbcTypeCode(SqlTypes.NAMED_ENUM) @Enumerated(EnumType.STRING) @Column(name = "currency", nullable = false, length = 10) private CurrencyCode currency; @@ -113,6 +114,7 @@ public class Transaction { * TRANSFER, DEPOSIT, or WITHDRAWAL * Determines which wallet fields are required (null rules above) */ + @JdbcTypeCode(SqlTypes.NAMED_ENUM) @Enumerated(EnumType.STRING) @Column(name = "transaction_type", nullable = false, length = 20) private TransactionType transactionType; @@ -127,6 +129,7 @@ public class Transaction { * A transaction stuck in PENDING or PROCESSING -> needs manual review. * Immediate COMPLETED would hide failed partial operations */ + @JdbcTypeCode(SqlTypes.NAMED_ENUM) @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false, length = 20) @Builder.Default diff --git a/src/main/java/com/wallet/secure/transaction/entity/TransactionHistory.java b/src/main/java/com/wallet/secure/transaction/entity/TransactionHistory.java index 4209ea1..84bcb44 100644 --- a/src/main/java/com/wallet/secure/transaction/entity/TransactionHistory.java +++ b/src/main/java/com/wallet/secure/transaction/entity/TransactionHistory.java @@ -4,6 +4,8 @@ import com.wallet.secure.user.entity.User; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -74,6 +76,7 @@ public class TransactionHistory { * Example: null → PENDING (initial creation) * PENDING → PROCESSING (started execution) */ + @JdbcTypeCode(SqlTypes.NAMED_ENUM) @Enumerated(EnumType.STRING) @Column(name = "old_status") private TransactionStatus oldStatus; @@ -83,6 +86,7 @@ public class TransactionHistory { * DB constraint: NOT NULL * DB CHECK: old_status IS DISTINCT FROM new_status (no no-op entries) */ + @JdbcTypeCode(SqlTypes.NAMED_ENUM) @Enumerated(EnumType.STRING) @Column(name = "new_status", nullable = false) private TransactionStatus newStatus; diff --git a/src/main/java/com/wallet/secure/user/entity/User.java b/src/main/java/com/wallet/secure/user/entity/User.java index e3fa7d5..7d7dafa 100644 --- a/src/main/java/com/wallet/secure/user/entity/User.java +++ b/src/main/java/com/wallet/secure/user/entity/User.java @@ -7,6 +7,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -82,6 +84,7 @@ public class User { * Maps to PostgreSQL ENUM user_role (02-types.sql). * OWASP A01: principle of least privilege. */ + @JdbcTypeCode(SqlTypes.NAMED_ENUM) @Enumerated(EnumType.STRING) @Column(name = "role", nullable = false, length = 20) @Builder.Default diff --git a/src/main/java/com/wallet/secure/wallet/entity/Wallet.java b/src/main/java/com/wallet/secure/wallet/entity/Wallet.java index 5bb5d77..f4ff7a3 100644 --- a/src/main/java/com/wallet/secure/wallet/entity/Wallet.java +++ b/src/main/java/com/wallet/secure/wallet/entity/Wallet.java @@ -5,6 +5,8 @@ import com.wallet.secure.user.entity.User; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -76,6 +78,7 @@ public class Wallet { * USD, EUR, COP, MXN, ARS * A user can have one wallet per currency - enforced by DB UNIQUE constraint */ + @JdbcTypeCode(SqlTypes.NAMED_ENUM) @Enumerated(EnumType.STRING) @Column(name = "currency", nullable = false, length = 10) private CurrencyCode currency; @@ -92,6 +95,7 @@ public class Wallet { * SUSPENDED -> ACTIVE (admin restores) * ACTIVE/SUSPENDED -> CLOSED (user request or admin - irreversible) */ + @JdbcTypeCode(SqlTypes.NAMED_ENUM) @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false, length = 20) @Builder.Default diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 90e50b2..dce7edb 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -9,7 +9,10 @@ spring: jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: - ddl-auto: create-drop # Create/drop schema on each test + # SecurityConfigTest only exercises the web-security filter chain. + # H2 cannot generate PostgreSQL NAMED_ENUM columns; production schema is + # validated against PostgreSQL, so no schema generation is needed here. + ddl-auto: none show-sql: true properties: hibernate: @@ -37,4 +40,4 @@ management: include: health endpoint: health: - show-details: never \ No newline at end of file + show-details: never