From 4d850f9db54fbd00e265f7f79a79ce1225ecd41c Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Thu, 7 May 2026 16:36:34 +0900 Subject: [PATCH] Harden production database configuration --- .github/workflows/deploy.yml | 50 +++++++-- .github/workflows/test.yml | 56 +--------- ontime-back/docs/database-configuration.md | 16 +++ .../resources/application-local.properties | 44 ++++++++ .../resources/application-prod.properties | 22 ++++ .../resources/application-test.properties | 48 +++++++++ .../DatabaseConfigurationPolicyTest.java | 102 ++++++++++++++++++ 7 files changed, 279 insertions(+), 59 deletions(-) create mode 100644 ontime-back/docs/database-configuration.md create mode 100644 ontime-back/src/main/resources/application-local.properties create mode 100644 ontime-back/src/main/resources/application-test.properties create mode 100644 ontime-back/src/test/java/devkor/ontime_back/security/DatabaseConfigurationPolicyTest.java diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5387dbe..7098287 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -93,14 +93,11 @@ jobs: SPRING_DATASOURCE_URL=${{ secrets.SPRING_DATASOURCE_URL }} SPRING_DATASOURCE_USERNAME=${{ secrets.SPRING_DATASOURCE_USERNAME }} SPRING_DATASOURCE_PASSWORD=${{ secrets.SPRING_DATASOURCE_PASSWORD }} - SPRING_DATASOURCE_DRIVER_CLASS_NAME=${{ secrets.SPRING_DATASOURCE_DRIVER_CLASS_NAME }} - SPRING_JPA_HIBERNATE_DDL_AUTO=${{ secrets.SPRING_JPA_HIBERNATE_DDL_AUTO }} + SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver + SPRING_JPA_HIBERNATE_DDL_AUTO=validate SPRING_FLYWAY_ENABLED=true - SPRING_FLYWAY_URL=${{ secrets.SPRING_FLYWAY_URL }} - SPRING_FLYWAY_USER=${{ secrets.SPRING_FLYWAY_USER }} - SPRING_FLYWAY_PASSWORD=${{ secrets.SPRING_FLYWAY_PASSWORD }} - SPRING_FLYWAY_BASELINE_ON_MIGRATE=true + SPRING_FLYWAY_BASELINE_ON_MIGRATE=false JWT_SECRET_KEY=${{ secrets.JWT_SECRETKEY }} JWT_ACCESS_EXPIRATION=${{ secrets.JWT_ACCESS_EXPIRATION }} @@ -139,6 +136,47 @@ jobs: FIREBASE_CREDENTIALS_BASE64=${{ secrets.FIREBASE_CREDENTIALS_BASE64 }} EOF + fail_deploy() { + echo "Unsafe production database configuration: $1" >&2 + exit 1 + } + + get_env_value() { + grep -E "^$1=" .env | tail -n 1 | cut -d= -f2- + } + + DB_URL="$(get_env_value SPRING_DATASOURCE_URL)" + DB_USERNAME="$(get_env_value SPRING_DATASOURCE_USERNAME)" + DB_PASSWORD="$(get_env_value SPRING_DATASOURCE_PASSWORD)" + DDL_AUTO="$(get_env_value SPRING_JPA_HIBERNATE_DDL_AUTO)" + FLYWAY_BASELINE="$(get_env_value SPRING_FLYWAY_BASELINE_ON_MIGRATE)" + NORMALIZED_DB_URL="$(printf '%s' "$DB_URL" | tr '[:upper:]' '[:lower:]')" + NORMALIZED_DB_USERNAME="$(printf '%s' "$DB_USERNAME" | tr '[:upper:]' '[:lower:]')" + + [ -n "$DB_URL" ] || fail_deploy "SPRING_DATASOURCE_URL is required." + [ -n "$DB_USERNAME" ] || fail_deploy "SPRING_DATASOURCE_USERNAME is required." + [ -n "$DB_PASSWORD" ] || fail_deploy "SPRING_DATASOURCE_PASSWORD is required." + [ "$NORMALIZED_DB_USERNAME" != "root" ] || fail_deploy "SPRING_DATASOURCE_USERNAME must not be root." + [ "$DDL_AUTO" = "validate" ] || fail_deploy "SPRING_JPA_HIBERNATE_DDL_AUTO must be validate." + [ "$FLYWAY_BASELINE" = "false" ] || fail_deploy "SPRING_FLYWAY_BASELINE_ON_MIGRATE must be false." + + case "$NORMALIZED_DB_URL" in + *allowpublickeyretrieval=true*) fail_deploy "SPRING_DATASOURCE_URL must not enable allowPublicKeyRetrieval." ;; + esac + + case "$NORMALIZED_DB_URL" in + *createdatabaseifnotexist=true*) fail_deploy "SPRING_DATASOURCE_URL must not create databases at startup." ;; + esac + + case "$NORMALIZED_DB_URL" in + *usessl=false*) fail_deploy "SPRING_DATASOURCE_URL must not disable TLS." ;; + esac + + case "$NORMALIZED_DB_URL" in + *sslmode=required*|*sslmode=verify_ca*|*sslmode=verify_identity*) ;; + *) fail_deploy "SPRING_DATASOURCE_URL must declare sslMode=REQUIRED, VERIFY_CA, or VERIFY_IDENTITY." ;; + esac + echo "${{ secrets.GHCR_READ_TOKEN }}" | sudo docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin if sudo docker compose version >/dev/null 2>&1; then diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cf353db..b2489e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,62 +62,12 @@ jobs: - # 4. 환경 변수 설정 파일 생성 - - name: Create Config Files - run: | - mkdir -p ontime-back/src/main/resources - mkdir -p ontime-back/src/main/resources/key - echo "spring.application.name=${{ secrets.SPRING_APPLICATION_NAME }}" > ontime-back/src/main/resources/application.properties - echo "spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test_db?serverTimezone=UTC&useSSL=false" >> ontime-back/src/main/resources/application.properties - echo "spring.datasource.username=test_user" >> ontime-back/src/main/resources/application.properties - echo "spring.datasource.password=test_password" >> ontime-back/src/main/resources/application.properties - echo "spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver" >> ontime-back/src/main/resources/application.properties - echo "spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect" >> ontime-back/src/main/resources/application.properties - echo "spring.jpa.hibernate.ddl-auto=validate" >> ontime-back/src/main/resources/application.properties - echo "spring.sql.init.mode=always" >> ontime-back/src/main/resources/application.properties - echo "jwt.secret.key=${{ secrets.JWT_SECRETKEY }}" >> ontime-back/src/main/resources/application.properties - echo "jwt.access.expiration=${{ secrets.JWT_ACCESS_EXPIRATION }}" >> ontime-back/src/main/resources/application.properties - echo "jwt.refresh.expiration=${{ secrets.JWT_REFRESH_EXPIRATION }}" >> ontime-back/src/main/resources/application.properties - echo "jwt.access.header=${{ secrets.JWT_ACCESS_HEADER }}" >> ontime-back/src/main/resources/application.properties - echo "jwt.refresh.header=${{ secrets.JWT_REFRESH_HEADER }}" >> ontime-back/src/main/resources/application.properties - echo "google.web.client-id = ${{ secrets.GOOGLE_WEB_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties - echo "google.app.client-id = ${{ secrets.GOOGLE_APP_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.client-secret=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.scope=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_SCOPE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.redirect-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_REDIRECT_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.authorization-grant-type=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_AUTHORIZATION_GRANT_TYPE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.google.client-name=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_NAME }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.google.authorization-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_AUTHORIZATION_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.google.token-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_TOKEN_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.google.user-info-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_INFO_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.google.user-name-attribute=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GOOGLE_USER_NAME_ATTRIBUTE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.client-id=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.scope=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_SCOPE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.redirect-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_REDIRECT_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.authorization-grant-type=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_AUTHORIZATION_GRANT_TYPE }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.registration.kakao.client-name=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_KAKAO_CLIENT_NAME }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.kakao.authorization-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_AUTHORIZATION_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.kakao.token-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_TOKEN_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.kakao.user-info-uri=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_INFO_URI }}" >> ontime-back/src/main/resources/application.properties - echo "spring.security.oauth2.client.provider.kakao.user-name-attribute=${{ secrets.SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_KAKAO_USER_NAME_ATTRIBUTE }}" >> ontime-back/src/main/resources/application.properties - echo "apple.client.id=${{ secrets.APPLE_CLIENT_ID }}" >> ontime-back/src/main/resources/application.properties - echo "apple.client.secret=${{ secrets.APPLE_CLIENT_SECRET }}" >> ontime-back/src/main/resources/application.properties - echo "apple.login.key=${{ secrets.APPLE_LOGIN_KEY }}" >> ontime-back/src/main/resources/application.properties - echo "apple.team.id=${{ secrets.APPLE_TEAM_ID }}" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.enabled=true" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.url=jdbc:mysql://127.0.0.1:3306/test_db?serverTimezone=UTC&useSSL=false" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.user=test_user" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.password=test_password" >> ontime-back/src/main/resources/application.properties - echo "spring.flyway.baseline-on-migrate=true" >> ontime-back/src/main/resources/application.properties - echo "management.endpoints.web.exposure.include=health" >> ontime-back/src/main/resources/application.properties - echo "management.endpoint.health.show-details=always" >> ontime-back/src/main/resources/application.properties - echo "${{ secrets.ONTIME_PUSH_FIREBASE_ADMINSDK }}" > ontime-back/src/main/resources/ontime-c63f1-firebase-adminsdk-fbsvc-a043cdc829.json - echo "${{ secrets.AUTHKEY_743M7R5W3W }}" > ontime-back/src/main/resources/key/AuthKey_743M7R5W3W.p8 - - # 5. Gradle 빌드 & JUnit 테스트 실행 + # 4. Gradle 빌드 & JUnit 테스트 실행 - name: Run Tests with Gradle id: run-tests # 실행 결과를 output으로 저장할 id 추가 continue-on-error: true + env: + SPRING_PROFILES_ACTIVE: test run: | cd ontime-back ./gradlew test diff --git a/ontime-back/docs/database-configuration.md b/ontime-back/docs/database-configuration.md new file mode 100644 index 0000000..b7e6351 --- /dev/null +++ b/ontime-back/docs/database-configuration.md @@ -0,0 +1,16 @@ +# Database Configuration + +Production uses MySQL with environment-provided database credentials. + +## Production + +- Spring profile: `prod` +- Datasource URL: `SPRING_DATASOURCE_URL` +- Datasource username: `SPRING_DATASOURCE_USERNAME` +- Datasource password: `SPRING_DATASOURCE_PASSWORD` +- Datasource driver: `com.mysql.cj.jdbc.Driver` +- Hibernate DDL mode: `validate` +- SQL logging: disabled +- Formatted SQL logging: disabled +- Flyway: enabled +- Flyway baseline on migrate: disabled diff --git a/ontime-back/src/main/resources/application-local.properties b/ontime-back/src/main/resources/application-local.properties new file mode 100644 index 0000000..a8813db --- /dev/null +++ b/ontime-back/src/main/resources/application-local.properties @@ -0,0 +1,44 @@ +# Database Configuration +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/ontime_db?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:root} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:ontime1234} +spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver} + +# JPA / Hibernate +spring.jpa.database=mysql +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +# Flyway +spring.flyway.enabled=true +spring.flyway.baseline-on-migrate=true + +# JWT Configuration +jwt.secret.key=${JWT_SECRET_KEY:my_secret_key_for_ontime_back_application_development_environment_1234567890} +jwt.access.expiration=${JWT_ACCESS_EXPIRATION:3600000} +jwt.refresh.expiration=${JWT_REFRESH_EXPIRATION:1209600000} +jwt.access.header=${JWT_ACCESS_HEADER:Authorization} +jwt.refresh.header=${JWT_REFRESH_HEADER:Authorization-refresh} + +# Google OAuth +google.web.client-id=${GOOGLE_WEB_CLIENT_ID:599377893328-dljp16andl10374bnm9b1nnfp9uj5pvd.apps.googleusercontent.com} +google.app.client-id=${GOOGLE_APP_CLIENT_ID:456571312261-r35ah9qi0qaq7al007e2db0e0jmjcmb4.apps.googleusercontent.com} + +# Apple OAuth +apple.client.id=${APPLE_CLIENT_ID:your_apple_client_id} +apple.team.id=${APPLE_TEAM_ID:your_apple_team_id} +apple.login.key=${APPLE_LOGIN_KEY:your_apple_key_id} +apple.client.secret=${APPLE_CLIENT_SECRET:your_apple_private_key} +apple.private-key.base64=${APPLE_PRIVATE_KEY_BASE64:} + +# Firebase +firebase.credentials.base64=${FIREBASE_CREDENTIALS_BASE64:} + +# Logging +logging.level.root=INFO +logging.level.devkor.ontime_back=DEBUG + +# Feature flags +feature.apple-login.enabled=${FEATURE_APPLE_LOGIN_ENABLED:true} diff --git a/ontime-back/src/main/resources/application-prod.properties b/ontime-back/src/main/resources/application-prod.properties index 851a641..cd67d8b 100644 --- a/ontime-back/src/main/resources/application-prod.properties +++ b/ontime-back/src/main/resources/application-prod.properties @@ -1,3 +1,25 @@ +# Database Configuration +spring.datasource.url=${SPRING_DATASOURCE_URL} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} +spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver} + +# JPA / Hibernate +spring.jpa.database=mysql +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.show-sql=false +spring.jpa.properties.hibernate.format_sql=false + +# Flyway +spring.flyway.enabled=true +spring.flyway.baseline-on-migrate=false + +# Logging +logging.level.root=INFO +logging.level.devkor.ontime_back=INFO + +# Actuator management.endpoint.health.probes.enabled=true management.endpoints.web.exposure.include=health management.health.readinessstate.enabled=true diff --git a/ontime-back/src/main/resources/application-test.properties b/ontime-back/src/main/resources/application-test.properties new file mode 100644 index 0000000..b2e1081 --- /dev/null +++ b/ontime-back/src/main/resources/application-test.properties @@ -0,0 +1,48 @@ +# Database Configuration +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:mysql://127.0.0.1:3306/test_db?serverTimezone=UTC&useSSL=false} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:test_user} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:test_password} +spring.datasource.driver-class-name=${SPRING_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver} + +# JPA / Hibernate +spring.jpa.database=mysql +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.show-sql=false +spring.jpa.properties.hibernate.format_sql=false + +# Flyway +spring.flyway.enabled=true +spring.flyway.baseline-on-migrate=false + +# JWT Configuration +jwt.secret.key=${JWT_SECRET_KEY:test_secret_key_for_ontime_back_application_tests_1234567890} +jwt.access.expiration=${JWT_ACCESS_EXPIRATION:3600000} +jwt.refresh.expiration=${JWT_REFRESH_EXPIRATION:1209600000} +jwt.access.header=${JWT_ACCESS_HEADER:Authorization} +jwt.refresh.header=${JWT_REFRESH_HEADER:Authorization-refresh} + +# Google OAuth +google.web.client-id=${GOOGLE_WEB_CLIENT_ID:test-google-web-client-id} +google.app.client-id=${GOOGLE_APP_CLIENT_ID:test-google-app-client-id} + +# Apple OAuth +apple.client.id=${APPLE_CLIENT_ID:test-apple-client-id} +apple.team.id=${APPLE_TEAM_ID:test-apple-team-id} +apple.login.key=${APPLE_LOGIN_KEY:test-apple-key-id} +apple.client.secret=${APPLE_CLIENT_SECRET:} +apple.private-key.base64=${APPLE_PRIVATE_KEY_BASE64:} + +# Firebase +firebase.credentials.base64=${FIREBASE_CREDENTIALS_BASE64:} + +# Logging +logging.level.root=INFO +logging.level.devkor.ontime_back=INFO + +# Feature flags +feature.apple-login.enabled=${FEATURE_APPLE_LOGIN_ENABLED:false} + +# Actuator +management.endpoints.web.exposure.include=health +management.endpoint.health.show-details=always diff --git a/ontime-back/src/test/java/devkor/ontime_back/security/DatabaseConfigurationPolicyTest.java b/ontime-back/src/test/java/devkor/ontime_back/security/DatabaseConfigurationPolicyTest.java new file mode 100644 index 0000000..25c88de --- /dev/null +++ b/ontime-back/src/test/java/devkor/ontime_back/security/DatabaseConfigurationPolicyTest.java @@ -0,0 +1,102 @@ +package devkor.ontime_back.security; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; + +class DatabaseConfigurationPolicyTest { + + private static final Path RESOURCES_ROOT = Path.of("src/main/resources"); + + @Test + void productionLikeProfilesDoNotMutateSchemaOrLogSql() throws IOException { + Properties properties = loadProperties("application-prod.properties"); + + assertThat(properties) + .as("prod profile database safety") + .containsEntry("spring.datasource.url", "${SPRING_DATASOURCE_URL}") + .containsEntry("spring.datasource.username", "${SPRING_DATASOURCE_USERNAME}") + .containsEntry("spring.datasource.password", "${SPRING_DATASOURCE_PASSWORD}") + .containsEntry("spring.jpa.hibernate.ddl-auto", "validate") + .containsEntry("spring.jpa.show-sql", "false") + .containsEntry("spring.jpa.properties.hibernate.format_sql", "false") + .containsEntry("spring.flyway.enabled", "true") + .containsEntry("spring.flyway.baseline-on-migrate", "false"); + } + + @Test + void productionLikeProfilesDoNotContainFallbackCredentialsOrUnsafeJdbcFlags() throws IOException { + String content = Files.readString(RESOURCES_ROOT.resolve("application-prod.properties")); + String normalizedContent = content.toLowerCase(Locale.ROOT); + + assertThat(normalizedContent) + .as("prod profile must not include unsafe production defaults") + .doesNotContain(":root") + .doesNotContain("ontime1234") + .doesNotContain("allowpublickeyretrieval=true") + .doesNotContain("createdatabaseifnotexist=true") + .doesNotContain("usessl=false"); + } + + @Test + void localProfileKeepsDeveloperOnlySchemaUpdateDefaults() throws IOException { + Properties properties = loadProperties("application-local.properties"); + + assertThat(properties) + .containsEntry("spring.jpa.hibernate.ddl-auto", "update") + .containsEntry("spring.jpa.show-sql", "true") + .containsEntry("spring.jpa.properties.hibernate.format_sql", "true"); + } + + @Test + void deployWorkflowPinsAndValidatesProductionDatabaseSafety() throws IOException { + String workflow = Files.readString(repoRoot().resolve(".github/workflows/deploy.yml")); + + assertThat(workflow) + .contains("SPRING_JPA_HIBERNATE_DDL_AUTO=validate") + .contains("SPRING_FLYWAY_BASELINE_ON_MIGRATE=false") + .contains("SPRING_DATASOURCE_URL is required.") + .contains("SPRING_DATASOURCE_USERNAME is required.") + .contains("SPRING_DATASOURCE_PASSWORD is required.") + .contains("SPRING_DATASOURCE_USERNAME must not be root.") + .contains("allowpublickeyretrieval=true") + .contains("createdatabaseifnotexist=true") + .contains("usessl=false") + .contains("sslmode=required") + .doesNotContain("SPRING_JPA_HIBERNATE_DDL_AUTO=${{ secrets.SPRING_JPA_HIBERNATE_DDL_AUTO }}") + .doesNotContain("SPRING_FLYWAY_BASELINE_ON_MIGRATE=true"); + } + + @Test + void testWorkflowUsesTrackedTestProfileInsteadOfGeneratingIgnoredApplicationProperties() throws IOException { + String workflow = Files.readString(repoRoot().resolve(".github/workflows/test.yml")); + + assertThat(workflow) + .contains("SPRING_PROFILES_ACTIVE: test") + .doesNotContain("Create Config Files") + .doesNotContain("ontime-back/src/main/resources/application.properties"); + } + + private Properties loadProperties(String fileName) throws IOException { + Properties properties = new Properties(); + try (InputStream inputStream = Files.newInputStream(RESOURCES_ROOT.resolve(fileName))) { + properties.load(inputStream); + } + return properties; + } + + private Path repoRoot() { + Path current = Path.of("").toAbsolutePath(); + if (Files.exists(current.resolve(".github/workflows/deploy.yml"))) { + return current; + } + return current.getParent(); + } +}