diff --git a/.github/workflows/android-test.yml b/.github/workflows/android-test.yml index a8515775f..95ff37f1b 100644 --- a/.github/workflows/android-test.yml +++ b/.github/workflows/android-test.yml @@ -34,7 +34,7 @@ jobs: - name: Setup sample app environment id: sample_env if: ${{ !cancelled() }} - run: ${{ github.workspace }}/scripts/setup_storefront_env --skip-optional-prompts + run: ${{ github.workspace }}/scripts/setup_storefront_env env: STOREFRONT_DOMAIN: example.myshopify.com STOREFRONT_ACCESS_TOKEN: test-token @@ -113,7 +113,7 @@ jobs: cache: 'gradle' - name: Setup sample app environment - run: ${{ github.workspace }}/scripts/setup_storefront_env --skip-optional-prompts + run: ${{ github.workspace }}/scripts/setup_storefront_env env: STOREFRONT_DOMAIN: example.myshopify.com STOREFRONT_ACCESS_TOKEN: test-token @@ -146,7 +146,7 @@ jobs: run: ./gradlew detekt - name: Setup sample app environment - run: ${{ github.workspace }}/scripts/setup_storefront_env --skip-optional-prompts + run: ${{ github.workspace }}/scripts/setup_storefront_env env: STOREFRONT_DOMAIN: example.myshopify.com STOREFRONT_ACCESS_TOKEN: test-token diff --git a/.github/workflows/rn-test-android.yml b/.github/workflows/rn-test-android.yml index 219607b4b..9d14f165d 100644 --- a/.github/workflows/rn-test-android.yml +++ b/.github/workflows/rn-test-android.yml @@ -69,6 +69,6 @@ jobs: echo "JAVA_HOME: $JAVA_HOME" java -version javac -version - ${{ github.workspace }}/scripts/setup_storefront_env --skip-optional-prompts + ${{ github.workspace }}/scripts/setup_storefront_env pnpm module build pnpm sample test:android --no-daemon diff --git a/.github/workflows/swift-test-workflow.yml b/.github/workflows/swift-test-workflow.yml index 115199108..9f04f602a 100644 --- a/.github/workflows/swift-test-workflow.yml +++ b/.github/workflows/swift-test-workflow.yml @@ -84,7 +84,7 @@ jobs: env: STOREFRONT_DOMAIN: example.myshopify.com STOREFRONT_ACCESS_TOKEN: test-token - run: ${{ github.workspace }}/scripts/setup_storefront_env --skip-optional-prompts + run: ${{ github.workspace }}/scripts/setup_storefront_env - if: ${{ inputs.setup-storefront-env || inputs.summarize-tests }} uses: ./.github/actions/setup-mint diff --git a/config/secrets/demo.ejson b/config/secrets/demo.ejson index 1c9bc5375..c024e55ad 100644 --- a/config/secrets/demo.ejson +++ b/config/secrets/demo.ejson @@ -2,8 +2,8 @@ "_public_key": "58b34b9a2be67c206423293ba2c0317e6fbe8f727f7ac124ff62349d36fa0136", "_description": "Storefront config for the sample apps. Generates .env, run `dev up` to propagate these values to the sample apps.", "environment": { - "STOREFRONT_DOMAIN": "EJ[1:Vsi9n4WYYvGd935C37cvRzygtUXcpBHu1CkCTFFUqzQ=:MDf5DnXyI+E2j9n1zeMMNprubZ0QpUSS:Ratyqcyc9xd5Ha14St4V0YFjKbt8DhbLK5cOC5GSbGdFyBnHuvtm5mdomVFHqCetPuulRsk=]", - "STOREFRONT_ACCESS_TOKEN": "EJ[1:Vsi9n4WYYvGd935C37cvRzygtUXcpBHu1CkCTFFUqzQ=:31/QaDjKTxu+CgWP1+Pps3/TSphwS16+:oWNA0Zg7iehiUz/kzXPI2sgBV4us6UNEtj2akQTeb3+vF0XRVYSjJfUIa6wqL2Sf]", + "STOREFRONT_DOMAIN": "EJ[1:l7BO8tiknXaR3V2OCydmWpvD6Nsd2/6ZKXWEdXagawo=:afgDjqCHl4uwBnLq0ghL3BAuVs4D2gd+:oyP/5BKL3/TM64sPlmdo5q78zCgKcxzYCqBZWNbCuDjGYPA4vdVfNKVe95dgGhyNYt8=]", + "STOREFRONT_ACCESS_TOKEN": "EJ[1:l7BO8tiknXaR3V2OCydmWpvD6Nsd2/6ZKXWEdXagawo=:AC4QzxArJXQcTI09aHjNqZfiLF2kkZb2:S5NFZID+ysX9l2/22mgCuxv9GxAbAoEr8VcAGTdHcyEVfeVk5u0awb1/Fl3hkPzP]", "API_VERSION": "EJ[1:COcqDoNiIoRpNVpn5RtY+/98SiWXneivqVxsvjS2j1M=:tYFS86UJtujPHsIZHB/u4wKb2isFXQFC:/a2Zafq6GTzkympf135XmS+ZHh7L3Zg=]", "CUSTOMER_ACCOUNT_API_CLIENT_ID": "EJ[1:COcqDoNiIoRpNVpn5RtY+/98SiWXneivqVxsvjS2j1M=:YbjANfMN0CrNMGQ8mHuX7LOSZunMDGl+:KjleSaYcWwtnMkZ3U8WSGNsXq23sOpyD4YYv16WhXhLdyoy3bfe8ShQXE/SOLPdswlo3WA==]", "CUSTOMER_ACCOUNT_API_SHOP_ID": "EJ[1:COcqDoNiIoRpNVpn5RtY+/98SiWXneivqVxsvjS2j1M=:KjkLuMIgiMHdO1kBj24PTWjmumLq0trf:PFTqWETJCU6Rubg8Zc0+eQT1S5fEBVNDfYOG]", diff --git a/dev.yml b/dev.yml index 08378e016..cde55c85c 100644 --- a/dev.yml +++ b/dev.yml @@ -32,17 +32,25 @@ up: met?: ./scripts/secrets_setup --check meet: "true" - custom: - name: Copy root env into worktree + name: Copy .env.local, .dev.env, local.properties into worktree met?: ./scripts/copy_worktree_env --check meet: ./scripts/copy_worktree_env + - custom: + name: Keep a hand-written .env as .env.local + met?: ./scripts/migrate_env_to_local --check + meet: ./scripts/migrate_env_to_local + - custom: + name: Generate env files from config/secrets + met?: ./scripts/generate_env_files --check + meet: ./scripts/generate_env_files - custom: name: Install the pinned Maestro version met?: ./scripts/install_maestro --check meet: ./scripts/install_maestro - custom: name: Run Checkout Kit workspace setup - met?: ./scripts/setup_dev_workspace --check --skip-optional-prompts - meet: ./scripts/setup_dev_workspace --skip-optional-prompts + met?: ./scripts/setup_dev_workspace --check + meet: ./scripts/setup_dev_workspace - tophat_mobile - custom: name: Configure Tophat Quick Launch items @@ -59,6 +67,7 @@ open: check: ejson-plaintext: ./scripts/ejson_lint + generate-env-tests: ./scripts/test_generate_env_files storefront-env-tests: ./scripts/test_setup_storefront_env ruby-script-tests: ./scripts/test_ruby maestro-hide-keyboard-lint: ./e2e/scripts/check_hide_keyboard_usage @@ -98,7 +107,7 @@ commands: esac copy-env: - desc: Copy the root .env into the current worktree so `dev up` can regenerate sample config + desc: Copy .dev.env, .env.local, local.properties into worktree run: ./scripts/copy_worktree_env secrets: diff --git a/e2e/scripts/bitrise_ci_helpers b/e2e/scripts/bitrise_ci_helpers index a05c14be8..a9d3fc06f 100755 --- a/e2e/scripts/bitrise_ci_helpers +++ b/e2e/scripts/bitrise_ci_helpers @@ -22,7 +22,7 @@ e2e_configure_storefront() { : "${STOREFRONT_DOMAIN:?STOREFRONT_DOMAIN is required. Check https://app.bitrise.io/app/f51f9054-053e-40f1-81e9-ae727567ae76/workflow_editor#!/secrets and enable Expose for pull requests.}" : "${STOREFRONT_ACCESS_TOKEN:?STOREFRONT_ACCESS_TOKEN is required. Check https://app.bitrise.io/app/f51f9054-053e-40f1-81e9-ae727567ae76/workflow_editor#!/secrets and enable Expose for pull requests.}" e2e_log "Configuring storefront environment" - ./scripts/setup_storefront_env --skip-optional-prompts + ./scripts/setup_storefront_env } e2e_nightly_commit_window() { diff --git a/scripts/copy_worktree_env b/scripts/copy_worktree_env index 573e0df12..a10ed8235 100755 --- a/scripts/copy_worktree_env +++ b/scripts/copy_worktree_env @@ -2,17 +2,8 @@ set -euo pipefail -# Copies the repo-root .env (and other root-level machine-local config, if -# present) from the main checkout into the current git worktree, so that a -# subsequent `dev up` can regenerate all nested sample app configuration from it. -# -# Runs automatically as a `dev up` step. With --check it reports (without -# copying) whether the current worktree is missing any root env files: exit 0 -# when nothing is needed (main checkout, or already seeded) and non-zero when a -# copy is required, so it can serve as a `dev up` met? probe. -# -# This command prints file paths and status only; it never prints configured -# values. +# Copies gitignored machine-local configuration from the main checkout into the +# current worktree. Generated env files are excluded because `dev up` rebuilds them. mode="copy" if [[ "${1:-}" == "--check" ]]; then @@ -27,17 +18,15 @@ COMMON_GIT_DIR="$(git rev-parse --path-format=absolute --git-common-dir)" MAIN_ROOT="$(dirname "${COMMON_GIT_DIR}")" if [[ "${CURRENT_ROOT}" == "${MAIN_ROOT}" ]]; then - # In the main checkout there is never anything to copy. [[ "${mode}" == "check" ]] && exit 0 echo "Already in the main checkout (${CURRENT_ROOT}); nothing to copy." exit 0 fi -# Root-level, gitignored source-of-truth files. Nested sample config -# (Android/Swift/RN) is intentionally omitted: `dev up` regenerates it from the -# root .env via scripts/setup_storefront_env. +# Copying .env would let migrate_env_to_local preserve stale generated values as +# local overrides before this worktree regenerates them. ROOT_FILES=( - ".env" + ".env.local" ".dev.env" "local.properties" ) @@ -58,7 +47,6 @@ for rel in "${ROOT_FILES[@]}"; do continue fi - # A source file exists in the main checkout but is missing here. if [[ "${mode}" == "check" ]]; then echo "missing: ${rel}" exit 1 @@ -69,11 +57,10 @@ for rel in "${ROOT_FILES[@]}"; do copied_any="true" done -# Check mode reached here with nothing missing: the worktree is already seeded. [[ "${mode}" == "check" ]] && exit 0 if [[ "${copied_any}" != "true" ]]; then - echo "No new root env files to copy." + echo "No new machine-local files to copy." fi echo "Next: run \`dev up\` in this worktree to regenerate sample app config." diff --git a/scripts/generate_env_files b/scripts/generate_env_files new file mode 100755 index 000000000..65cac7f72 --- /dev/null +++ b/scripts/generate_env_files @@ -0,0 +1,128 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Generates .env and e2e/.env from committed EJSON secrets. --check reports stale +# output without writing. A missing private key is supported for contributors who +# use their own config; other decrypt failures preserve existing files and fail. + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +KEYDIR="${EJSON_KEYDIR:-/opt/ejson/keys}" + +# name:source:destination +PAIRS=( + "demo:config/secrets/demo.ejson:.env" + "e2e:config/secrets/e2e.ejson:e2e/.env" +) + +mode="generate" +if [[ "${1:-}" == "--check" ]]; then + mode="check" +elif [[ $# -gt 0 ]]; then + echo "Usage: scripts/generate_env_files [--check]" >&2 + exit 1 +fi + +public_key_of() { + sed -n 's/.*"_public_key"[[:space:]]*:[[:space:]]*"\([0-9a-f]\{64\}\)".*/\1/p' "$1" | head -n 1 +} + +generated_header() { + local name="$1" + local source_rel="$2" + local dest_rel="$3" + + cat </dev/null 2>&1; then + echo "generate_env_files: ejson2env is not installed, so no env file can be generated." >&2 + echo "generate_env_files: run \`dev up\` to install it." >&2 + exit 1 +fi + +stale=() +generated=() +skipped=() + +for pair in "${PAIRS[@]}"; do + name="${pair%%:*}" + rest="${pair#*:}" + source_rel="${rest%%:*}" + dest_rel="${rest#*:}" + + source_path="${ROOT_DIR}/${source_rel}" + dest_path="${ROOT_DIR}/${dest_rel}" + + if [[ ! -f "${source_path}" ]]; then + echo "generate_env_files: ${source_rel} does not exist, so ${dest_rel} is left alone." + skipped+=("${dest_rel}") + continue + fi + + public_key="$(public_key_of "${source_path}")" + if [[ -z "${public_key}" ]]; then + echo "generate_env_files: ${source_rel} has no _public_key, so ${dest_rel} cannot be generated." >&2 + exit 1 + fi + + if [[ ! -f "${KEYDIR}/${public_key}" ]]; then + echo "generate_env_files: no private key for ${public_key} in ${KEYDIR}, so ${dest_rel} is left alone." + skipped+=("${dest_rel}") + continue + fi + + rendered="$(mktemp)" + + if ! { + generated_header "${name}" "${source_rel}" "${dest_rel}" + ejson2env --quiet --keydir "${KEYDIR}" "${source_path}" + } >"${rendered}" 2>/dev/null; then + rm -f "${rendered}" + echo "generate_env_files: could not decrypt ${source_rel}, so ${dest_rel} is unchanged." >&2 + echo "generate_env_files: check that the private key for ${public_key} is the current one." >&2 + exit 1 + fi + + if [[ "${mode}" == "check" ]]; then + if [[ ! -f "${dest_path}" ]] || ! cmp -s "${dest_path}" "${rendered}"; then + stale+=("${dest_rel}") + fi + + rm -f "${rendered}" + continue + fi + + mkdir -p "$(dirname "${dest_path}")" + mv "${rendered}" "${dest_path}" + generated+=("${dest_rel}") +done + +if [[ "${mode}" == "check" ]]; then + if [[ "${#stale[@]}" -gt 0 ]]; then + echo "generate_env_files: missing or out of date: ${stale[*]}" >&2 + exit 1 + fi + + echo "generate_env_files: generated env files are up to date." + exit 0 +fi + +if [[ "${#generated[@]}" -gt 0 ]]; then + echo "generate_env_files: wrote ${generated[*]}" +fi + +if [[ "${#skipped[@]}" -gt 0 ]]; then + echo "generate_env_files: your own values in ${skipped[*]} stay under your control." +fi diff --git a/scripts/migrate_env_to_local b/scripts/migrate_env_to_local new file mode 100755 index 000000000..6d442dfa2 --- /dev/null +++ b/scripts/migrate_env_to_local @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Preserves a hand-written .env as .env.local before .env becomes generated. +# Existing .env.local files are never overwritten. + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +ROOT_ENV="${ROOT_DIR}/.env" +ROOT_ENV_LOCAL="${ROOT_DIR}/.env.local" + +mode="migrate" +if [[ "${1:-}" == "--check" ]]; then + mode="check" +elif [[ $# -gt 0 ]]; then + echo "Usage: scripts/migrate_env_to_local [--check]" >&2 + exit 1 +fi + +nothing_to_do() { + [[ "${mode}" == "check" ]] && exit 0 + + echo "migrate_env_to_local: $1" + exit 0 +} + +if [[ -e "${ROOT_ENV_LOCAL}" ]]; then + nothing_to_do ".env.local already exists, so nothing is copied." +fi + +if [[ ! -f "${ROOT_ENV}" ]]; then + nothing_to_do "no .env to migrate." +fi + +if [[ "${mode}" == "check" ]]; then + echo "migrate_env_to_local: .env has no .env.local yet" + exit 1 +fi + +cp "${ROOT_ENV}" "${ROOT_ENV_LOCAL}" + +echo "migrate_env_to_local: copied .env to .env.local, because .env is now generated." +echo "migrate_env_to_local: .env.local wins over .env, so your values keep working." +echo "migrate_env_to_local: delete .env.local to follow the shared config instead." diff --git a/scripts/secrets_edit b/scripts/secrets_edit index 04df86649..489c44768 100755 --- a/scripts/secrets_edit +++ b/scripts/secrets_edit @@ -124,6 +124,20 @@ def relative(path) path.delete_prefix("#{REPO_ROOT}/") end +# Regenerate after edits under config/secrets. Failure does not discard the +# encrypted edit. +def regenerate_env_files(path) + return unless File.realpath(path).start_with?("#{File.realpath(SECRETS_DIR)}/") + + script = File.join(REPO_ROOT, "scripts", "generate_env_files") + out, status = Open3.capture2e(script) + print out + + return if status.success? + + warn CliOutput.suggestion("the edit is saved; rerun `dev up` once that is fixed") +end + subcommand, target, *extra = ARGV if ["--help", "-h", "help"].include?(subcommand) @@ -184,4 +198,6 @@ Dir.mktmpdir("secrets-edit") do |workspace| puts "Updated #{relative(path)}. #{changed.length} value(s) changed:" changed.each { |key| puts " #{key}" } puts "Commit the file to share the change." + + regenerate_env_files(path) end diff --git a/scripts/setup_dev_workspace b/scripts/setup_dev_workspace index 3adb8ca98..f0ab653e7 100755 --- a/scripts/setup_dev_workspace +++ b/scripts/setup_dev_workspace @@ -5,7 +5,6 @@ set -o pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" mode="sync" -storefront_prompt_arg="--skip-optional-prompts" step_names=() step_statuses=() @@ -18,7 +17,7 @@ gemfile_lock_aligned=1 usage() { cat <&2 - exit 1 - fi - - while [[ -z "$value" ]]; do - read -r -s -p "${label}: " value - echo - value="$(printf '%s' "$value" | trim)" - done - - printf '%s' "$value" -} - -prompt_optional() { - local label="$1" - local value="" - - if [[ ! -t 0 ]]; then - return 0 - fi - - read -r -s -p "${label} (optional, press Enter to skip): " value - echo - printf '%s' "$value" | trim -} - load_values() { STOREFRONT_DOMAIN_VALUE="$(first_config_value \ "$(required_config_value STOREFRONT_DOMAIN)" \ @@ -418,8 +351,8 @@ load_values() { "$(read_env_value STOREFRONT_ACCESS_TOKEN "$SWIFT_ACCELERATED_XCCONFIG")")" API_VERSION_VALUE="$(first_config_value \ - "$(read_env_value API_VERSION "$ROOT_ENV")" \ - "$(read_env_value STOREFRONT_VERSION "$ROOT_ENV")" \ + "$(read_root_value API_VERSION)" \ + "$(read_root_value STOREFRONT_VERSION)" \ "$(env_fallback API_VERSION)" \ "$(env_fallback STOREFRONT_VERSION)" \ "$(read_env_value API_VERSION "$ANDROID_ENV")" \ @@ -431,7 +364,7 @@ load_values() { REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER_VALUE="$(root_or_source_value REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER \ "$(env_fallback REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER)" \ - "$(read_env_value STOREFRONT_MERCHANT_IDENTIFIER "$ROOT_ENV")" \ + "$(read_root_value STOREFRONT_MERCHANT_IDENTIFIER)" \ "$(env_fallback STOREFRONT_MERCHANT_IDENTIFIER)" \ "$(read_env_value APPLE_PAY_MERCHANT_IDENTIFIER "$REACT_NATIVE_ENV")" \ "$(read_env_value STOREFRONT_MERCHANT_IDENTIFIER "$REACT_NATIVE_ENV")" \ @@ -439,18 +372,21 @@ load_values() { SWIFT_APPLE_PAY_MERCHANT_IDENTIFIER_VALUE="$(root_or_source_value SWIFT_APPLE_PAY_MERCHANT_IDENTIFIER \ "$(env_fallback SWIFT_APPLE_PAY_MERCHANT_IDENTIFIER)" \ - "$(read_env_value STOREFRONT_MERCHANT_IDENTIFIER "$ROOT_ENV")" \ + "$(read_root_value STOREFRONT_MERCHANT_IDENTIFIER)" \ "$(env_fallback STOREFRONT_MERCHANT_IDENTIFIER)" \ "$(read_env_value APPLE_PAY_MERCHANT_IDENTIFIER "$SWIFT_DEMO_XCCONFIG")" \ "$(read_env_value STOREFRONT_MERCHANT_IDENTIFIER "$SWIFT_DEMO_XCCONFIG")")" - CUSTOMER_ACCOUNT_API_CLIENT_ID_VALUE="$(root_or_source_value CUSTOMER_ACCOUNT_API_CLIENT_ID \ + # Nonempty, not plain: a blank line in .env means ejson has no value yet, so the + # process environment must still be able to supply one. Neither chain ends in a + # default, so a key that is genuinely unset still resolves to blank. + CUSTOMER_ACCOUNT_API_CLIENT_ID_VALUE="$(root_or_source_nonempty_value CUSTOMER_ACCOUNT_API_CLIENT_ID \ "$(env_fallback CUSTOMER_ACCOUNT_API_CLIENT_ID)" \ "$(read_env_value CUSTOMER_ACCOUNT_API_CLIENT_ID "$ANDROID_ENV")" \ "$(read_env_value CUSTOMER_ACCOUNT_API_CLIENT_ID "$REACT_NATIVE_ENV")" \ "$(read_env_value CUSTOMER_ACCOUNT_API_CLIENT_ID "$SWIFT_DEMO_XCCONFIG")")" - CUSTOMER_ACCOUNT_API_SHOP_ID_VALUE="$(root_or_source_value CUSTOMER_ACCOUNT_API_SHOP_ID \ + CUSTOMER_ACCOUNT_API_SHOP_ID_VALUE="$(root_or_source_nonempty_value CUSTOMER_ACCOUNT_API_SHOP_ID \ "$(env_fallback CUSTOMER_ACCOUNT_API_SHOP_ID)" \ "$(derive_customer_account_api_shop_id)" \ "$(read_env_value CUSTOMER_ACCOUNT_API_SHOP_ID "$REACT_NATIVE_ENV")" \ @@ -482,31 +418,15 @@ load_values() { "$(env_fallback DEVELOPMENT_TEAM)")" } -collect_missing_values() { - fill_optional_values_from_environment || true - +# Not fatal. The generated files are still written, so a partly configured +# checkout builds; the sample app then fails at its first network call instead. +report_missing_required_values() { if is_missing_required_value "$STOREFRONT_DOMAIN_VALUE"; then - STOREFRONT_DOMAIN_VALUE="$(prompt_required "Storefront domain")" + echo "No usable STOREFRONT_DOMAIN. Set it in .env.local, or run \`dev up\` to generate .env." >&2 fi if is_missing_required_value "$STOREFRONT_ACCESS_TOKEN_VALUE"; then - STOREFRONT_ACCESS_TOKEN_VALUE="$(prompt_required "Storefront access token")" - fi - - if [[ "$prompt_optional_values" == "true" && -z "$REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER_VALUE" ]]; then - REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER_VALUE="$(prompt_optional "React Native Apple Pay merchant identifier")" - fi - - if [[ "$prompt_optional_values" == "true" && -z "$SWIFT_APPLE_PAY_MERCHANT_IDENTIFIER_VALUE" ]]; then - SWIFT_APPLE_PAY_MERCHANT_IDENTIFIER_VALUE="$(prompt_optional "Swift Apple Pay merchant identifier")" - fi - - if [[ "$prompt_optional_values" == "true" && -z "$CUSTOMER_ACCOUNT_API_CLIENT_ID_VALUE" ]]; then - CUSTOMER_ACCOUNT_API_CLIENT_ID_VALUE="$(prompt_optional "Customer Account API client ID")" - fi - - if [[ "$prompt_optional_values" == "true" && -z "$CUSTOMER_ACCOUNT_API_SHOP_ID_VALUE" ]]; then - CUSTOMER_ACCOUNT_API_SHOP_ID_VALUE="$(prompt_optional "Customer Account API shop ID")" + echo "No usable STOREFRONT_ACCESS_TOKEN. Set it in .env.local, or run \`dev up\` to generate .env." >&2 fi } @@ -523,66 +443,18 @@ write_xcconfig_assignment() { printf '%s = %s\n' "$1" "$2" } -generate_root_env() { - cat <&2 - exit 1 - fi - - if is_missing_required_value "$(read_env_value STOREFRONT_DOMAIN "$ROOT_ENV")" || - is_missing_required_value "$(read_env_value STOREFRONT_ACCESS_TOKEN "$ROOT_ENV")"; then - echo "Root .env is missing required storefront configuration." >&2 - exit 1 - fi - - if ! root_has_canonical_keys; then - echo "Root .env is missing canonical storefront configuration keys." >&2 - exit 1 - fi - - if customer_account_api_version_needs_default; then - echo "Root .env has a blank Customer Account API version." >&2 - exit 1 - fi - - return 0 - fi - - local root_needs_write="false" - if [[ ! -f "$ROOT_ENV" ]]; then - root_needs_write="true" - echo "Creating root storefront configuration at .env." - elif is_missing_required_value "$(read_env_value STOREFRONT_DOMAIN "$ROOT_ENV")" || - is_missing_required_value "$(read_env_value STOREFRONT_ACCESS_TOKEN "$ROOT_ENV")"; then - root_needs_write="true" - echo "Updating root storefront configuration at .env." - elif ! root_has_canonical_keys; then - root_needs_write="true" - echo "Normalizing root storefront configuration at .env." - elif customer_account_api_version_needs_default; then - root_needs_write="true" - echo "Normalizing root storefront configuration at .env." - fi - - if [[ "$root_needs_write" == "true" ]]; then - collect_missing_values - generate_root_env >"$ROOT_ENV" - elif fill_optional_values_from_environment; then - echo "Updating optional storefront configuration at .env." - generate_root_env >"$ROOT_ENV" - elif optional_values_need_prompt; then - echo "Updating optional storefront configuration at .env." - collect_missing_values - generate_root_env >"$ROOT_ENV" - fi - - load_values -} - write_generated_files() { generate_android_env >"$ANDROID_ENV" generate_swift_demo_xcconfig >"$SWIFT_DEMO_XCCONFIG" @@ -794,10 +607,12 @@ check_generated_files() { echo "Sample app storefront configuration is up to date." } -ensure_root_env +report_local_overrides +load_values if [[ "$mode" == "check" ]]; then check_generated_files else + report_missing_required_values write_generated_files fi diff --git a/scripts/test/secrets_edit_test.rb b/scripts/test/secrets_edit_test.rb index b60b077f1..3348443dd 100644 --- a/scripts/test/secrets_edit_test.rb +++ b/scripts/test/secrets_edit_test.rb @@ -149,6 +149,23 @@ def test_an_unknown_subcommand_names_itself_and_shows_help assert_includes out, "dev secrets edit" end + def test_editing_a_secrets_file_regenerates_the_env_files + skip "ejson2env is not installed" unless system("command -v ejson2env >/dev/null 2>&1") + + root = fake_repo_root + out, status = run_in(root, %w[API_VERSION=2026-10]) + + assert_equal 0, status, "secrets_edit failed:\n#{out}" + assert_includes File.read(File.join(root, ".env")), "API_VERSION=2026-10" + end + + def test_editing_a_file_outside_config_secrets_leaves_the_env_files_alone + out, = edit(%w[API_VERSION=2026-10]) + + refute_includes out, "generate_env_files" + refute_path_exists File.join(@dir, ".env") + end + def test_an_unknown_name_is_rejected out, status = run_script(editor: setting_editor([]), target: "nope") @@ -199,6 +216,26 @@ def run_script(editor:, target: @path, keydir: @keydir) [out, status.exitstatus] end + def fake_repo_root + root = File.join(@dir, "repo") + FileUtils.mkdir_p(File.join(root, "scripts")) + FileUtils.mkdir_p(File.join(root, "config", "secrets")) + + FileUtils.cp(SCRIPT, File.join(root, "scripts")) + FileUtils.cp(File.join(REPO_ROOT, "scripts", "generate_env_files"), File.join(root, "scripts")) + FileUtils.cp_r(File.join(REPO_ROOT, "scripts", "lib"), File.join(root, "scripts")) + FileUtils.cp(@path, File.join(root, "config", "secrets", "demo.ejson")) + + root + end + + def run_in(root, assignments) + env = {"EJSON_KEYDIR" => @keydir, "EDITOR" => setting_editor(assignments), "NO_COLOR" => "1"} + target = File.join(root, "config", "secrets", "demo.ejson") + out, status = Open3.capture2e(env, File.join(root, "scripts", "secrets_edit"), "edit", target, chdir: root) + [out, status.exitstatus] + end + def setting_editor(assignments) fake_editor(<<~RUBY) values = #{assignments.inspect}.to_h { |pair| pair.split("=", 2) } diff --git a/scripts/test_generate_env_files b/scripts/test_generate_env_files new file mode 100755 index 000000000..d093d8fc8 --- /dev/null +++ b/scripts/test_generate_env_files @@ -0,0 +1,286 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Synthetic tests for scripts/generate_env_files and scripts/migrate_env_to_local. +# +# Every fixture gets its own ejson keypair in its own keydir, so these tests +# exercise the real ejson and ejson2env binaries without ever touching the +# repository keys or the committed ciphertext. +# +# Values here are synthetic. assert_output_is_sanitized fails the suite if any of +# them reaches the command output, because these scripts must report key names +# and paths only. + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Holds enough of the platform to run the scripts, but not enough to find the +# real ejson2env, which is how the missing-binary path gets tested. +MINIMAL_PATH="/usr/bin:/bin" + +fixtures=() + +cleanup() { + local fixture + if [[ "${#fixtures[@]}" -eq 0 ]]; then + return + fi + + for fixture in "${fixtures[@]}"; do + rm -rf "$fixture" + done +} + +trap cleanup EXIT + +fail() { + echo "test_generate_env_files: $1" >&2 + exit 1 +} + +require_binary() { + command -v "$1" >/dev/null 2>&1 || + fail "$1 is not installed, so these tests cannot run. Run \`dev up\`." +} + +make_fixture() { + local fixture public_key + fixture="$(mktemp -d "${TMPDIR:-/tmp}/checkout-kit-generate-env.XXXXXX")" + fixtures+=("$fixture") + + mkdir -p "$fixture/scripts" "$fixture/config/secrets" "$fixture/e2e" "$fixture/keys" + + cp "$REPO_ROOT/scripts/generate_env_files" "$fixture/scripts/generate_env_files" + cp "$REPO_ROOT/scripts/migrate_env_to_local" "$fixture/scripts/migrate_env_to_local" + chmod +x "$fixture/scripts/generate_env_files" "$fixture/scripts/migrate_env_to_local" + + public_key="$(EJSON_KEYDIR="$fixture/keys" ejson keygen -w)" + + write_secrets_file "$fixture/config/secrets/demo.ejson" "$public_key" \ + STOREFRONT_DOMAIN synthetic-store.example.myshopify.com \ + STOREFRONT_ACCESS_TOKEN synthetic-token + write_secrets_file "$fixture/config/secrets/e2e.ejson" "$public_key" \ + STOREFRONT_DOMAIN synthetic-e2e-store.example.myshopify.com \ + E2E_CUSTOMER_ACCOUNT_EMAIL synthetic-e2e@example.com + + printf '%s\n' "$fixture" +} + +write_secrets_file() { + local path="$1" + local public_key="$2" + shift 2 + + { + printf '{\n' + printf ' "_public_key": "%s",\n' "$public_key" + printf ' "_description": "Synthetic fixture.",\n' + printf ' "environment": {\n' + while [[ $# -gt 0 ]]; do + printf ' "%s": "%s"' "$1" "$2" + shift 2 + [[ $# -eq 0 ]] || printf ',' + printf '\n' + done + printf ' }\n' + printf '}\n' + } >"$path" + + ejson encrypt "$path" >/dev/null +} + +run_generate() { + local fixture="$1" + shift + + EJSON_KEYDIR="$fixture/keys" "$fixture/scripts/generate_env_files" "$@" +} + +assert_file_exists() { + [[ -f "$1" ]] || fail "expected file is missing: $1" +} + +assert_file_absent() { + [[ ! -e "$1" ]] || fail "file must not exist: $1" +} + +assert_contains() { + local path="$1" + local pattern="$2" + + grep -Fq "$pattern" "$path" || fail "expected content was not found: $path: $pattern" +} + +assert_files_match() { + cmp -s "$1" "$2" || fail "files differ but must be byte-identical: $1 $2" +} + +assert_output_is_sanitized() { + local output_path="$1" + local value + + for value in \ + synthetic-store.example.myshopify.com \ + synthetic-token \ + synthetic-e2e-store.example.myshopify.com \ + synthetic-e2e@example.com \ + hand-written-store.example.myshopify.com; do + if grep -Fq "$value" "$output_path"; then + fail "command output included a configured value" + fi + done +} + +test_generates_both_env_files() { + local fixture output + fixture="$(make_fixture)" + output="$fixture/output.log" + + run_generate "$fixture" >"$output" 2>&1 + + assert_output_is_sanitized "$output" + assert_file_exists "$fixture/.env" + assert_file_exists "$fixture/e2e/.env" + + assert_contains "$fixture/.env" "STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com" + assert_contains "$fixture/.env" "STOREFRONT_ACCESS_TOKEN=synthetic-token" + assert_contains "$fixture/e2e/.env" "E2E_CUSTOMER_ACCOUNT_EMAIL=synthetic-e2e@example.com" + + # Each file must say it is generated, or someone edits it and loses the change. + assert_contains "$fixture/.env" "dev secrets edit demo" + assert_contains "$fixture/.env" "Gitignored overrides: .env.local" + assert_contains "$fixture/e2e/.env" "dev secrets edit e2e" + if grep -Fq ".env.local" "$fixture/e2e/.env"; then + fail "e2e/.env advertises an unsupported local override" + fi +} + +test_check_passes_when_current_and_fails_when_stale() { + local fixture output + fixture="$(make_fixture)" + output="$fixture/output.log" + + run_generate "$fixture" >"$output" 2>&1 + + run_generate "$fixture" --check >"$output" 2>&1 || + fail "--check failed straight after a successful generate" + assert_output_is_sanitized "$output" + + printf '%s\n' "STOREFRONT_DOMAIN=stale" >"$fixture/.env" + + if run_generate "$fixture" --check >"$output" 2>&1; then + fail "--check passed with a stale .env" + fi + assert_output_is_sanitized "$output" +} + +test_missing_ejson2env_fails_and_names_dev_up() { + local fixture output + fixture="$(make_fixture)" + output="$fixture/output.log" + + if PATH="$MINIMAL_PATH" run_generate "$fixture" >"$output" 2>&1; then + fail "generate_env_files succeeded without ejson2env installed" + fi + + assert_output_is_sanitized "$output" + assert_contains "$output" "dev up" + assert_file_absent "$fixture/.env" +} + +test_missing_private_key_skips_and_preserves_existing_env() { + local fixture output before + fixture="$(make_fixture)" + output="$fixture/output.log" + before="$fixture/before" + + printf 'STOREFRONT_DOMAIN=%s\n' hand-written-store.example.myshopify.com >"$fixture/.env" + cp "$fixture/.env" "$before" + rm -f "$fixture"/keys/* + + run_generate "$fixture" >"$output" 2>&1 || + fail "a missing private key must not fail; it is a supported state" + + assert_output_is_sanitized "$output" + assert_contains "$output" "no private key" + assert_files_match "$fixture/.env" "$before" + + run_generate "$fixture" --check >"$output" 2>&1 || + fail "--check must pass when no key is installed" + assert_output_is_sanitized "$output" +} + +test_failed_decrypt_preserves_existing_env() { + local fixture output before key_file + fixture="$(make_fixture)" + output="$fixture/output.log" + before="$fixture/before" + + printf 'STOREFRONT_DOMAIN=%s\n' hand-written-store.example.myshopify.com >"$fixture/.env" + cp "$fixture/.env" "$before" + + key_file="$(find "$fixture/keys" -type f -print -quit)" + chmod u+w "$key_file" + printf 'invalid-private-key\n' >"$key_file" + + if run_generate "$fixture" >"$output" 2>&1; then + fail "generate_env_files succeeded with an invalid private key" + fi + + assert_output_is_sanitized "$output" + assert_files_match "$fixture/.env" "$before" +} + +test_migrate_lifecycle() { + local fixture output after_first + fixture="$(make_fixture)" + output="$fixture/output.log" + after_first="$fixture/after_first" + + "$fixture/scripts/migrate_env_to_local" >"$output" 2>&1 + assert_file_absent "$fixture/.env.local" + + printf 'STOREFRONT_DOMAIN=%s\n' hand-written-store.example.myshopify.com >"$fixture/.env" + if "$fixture/scripts/migrate_env_to_local" --check >"$output" 2>&1; then + fail "--check passed while a migration was pending" + fi + + "$fixture/scripts/migrate_env_to_local" >"$output" 2>&1 + assert_output_is_sanitized "$output" + assert_files_match "$fixture/.env.local" "$fixture/.env" + cp "$fixture/.env.local" "$after_first" + + "$fixture/scripts/migrate_env_to_local" >"$output" 2>&1 + "$fixture/scripts/migrate_env_to_local" --check >"$output" 2>&1 || + fail "--check failed after migration" + assert_files_match "$fixture/.env.local" "$after_first" +} + +test_migrate_never_overwrites_an_existing_env_local() { + local fixture output before + fixture="$(make_fixture)" + output="$fixture/output.log" + before="$fixture/before" + + printf 'STOREFRONT_DOMAIN=%s\n' hand-written-store.example.myshopify.com >"$fixture/.env" + printf 'STOREFRONT_DOMAIN=%s\n' synthetic-store.example.myshopify.com >"$fixture/.env.local" + cp "$fixture/.env.local" "$before" + + "$fixture/scripts/migrate_env_to_local" >"$output" 2>&1 + + assert_output_is_sanitized "$output" + assert_files_match "$fixture/.env.local" "$before" +} + +require_binary ejson +require_binary ejson2env + +test_generates_both_env_files +test_check_passes_when_current_and_fails_when_stale +test_missing_ejson2env_fails_and_names_dev_up +test_missing_private_key_skips_and_preserves_existing_env +test_failed_decrypt_preserves_existing_env +test_migrate_lifecycle +test_migrate_never_overwrites_an_existing_env_local + +echo "generate_env_files synthetic tests passed." diff --git a/scripts/test_setup_storefront_env b/scripts/test_setup_storefront_env index c7c16a2e7..9850d8840 100755 --- a/scripts/test_setup_storefront_env +++ b/scripts/test_setup_storefront_env @@ -2,6 +2,16 @@ set -euo pipefail +# Synthetic tests for scripts/setup_storefront_env. +# +# The central guarantee is that .env is only ever read. Shopify employees get it +# from scripts/generate_env_files; everyone else writes it by hand. Either way a +# run here must leave it byte-identical, so most tests below assert that as well +# as the generated output. +# +# Values here are synthetic. assert_output_is_sanitized fails the suite if any of +# them reaches the command output, because this script reports paths only. + REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" fixtures=() @@ -42,8 +52,32 @@ make_fixture() { printf '%s\n' "$fixture" } +android_env_of() { + printf '%s\n' "$1/platforms/android/samples/CheckoutKitAndroidDemo/.env" +} + +react_native_env_of() { + printf '%s\n' "$1/platforms/react-native/sample/.env" +} + +react_native_xcconfig_of() { + printf '%s\n' "$1/platforms/react-native/sample/ios/Storefront.xcconfig" +} + +swift_demo_xcconfig_of() { + printf '%s\n' "$1/platforms/swift/Samples/CheckoutKitSwiftDemo/Storefront.xcconfig" +} + +swift_accelerated_xcconfig_of() { + printf '%s\n' "$1/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" +} + assert_file_exists() { - [[ -f "$1" ]] || fail "expected generated file is missing" + [[ -f "$1" ]] || fail "expected generated file is missing: $1" +} + +assert_file_absent() { + [[ ! -e "$1" ]] || fail "file must not be created: $1" } assert_contains() { @@ -58,10 +92,44 @@ assert_not_contains() { local pattern="$2" if grep -Fq -- "$pattern" "$path"; then - fail "unexpected generated file content was found" + fail "unexpected generated file content was found: $path: $pattern" fi } +write_canonical_root_env() { + local path="$1" + local domain="$2" + + cat >"$path" <"$output" 2>&1 + write_canonical_root_env "$fixture/.env" synthetic-store.example.myshopify.com + cp "$fixture/.env" "$root_before" + + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 assert_output_is_sanitized "$output" - assert_contains "$fixture/.env" \ - "REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER=synthetic-react-native-merchant" - assert_contains "$fixture/.env" \ - "SWIFT_APPLE_PAY_MERCHANT_IDENTIFIER=synthetic-swift-merchant" - assert_contains "$fixture/platforms/react-native/sample/.env" \ + assert_file_is_unchanged "$fixture/.env" "$root_before" + assert_contains "$(react_native_env_of "$fixture")" \ 'APPLE_PAY_MERCHANT_IDENTIFIER="synthetic-react-native-merchant"' - assert_contains "$react_native_xcconfig" \ + assert_contains "$(react_native_xcconfig_of "$fixture")" \ "APPLE_PAY_MERCHANT_IDENTIFIER = synthetic-react-native-merchant" - assert_contains "$swift_xcconfig" \ + assert_contains "$(swift_demo_xcconfig_of "$fixture")" \ "APPLE_PAY_MERCHANT_IDENTIFIER = synthetic-swift-merchant" } @@ -142,276 +210,258 @@ test_sample_projects_read_generated_merchant_identifier() { } test_sync_and_check() { - local fixture output + local fixture output root_before fixture="$(make_fixture)" output="$fixture/output.log" + root_before="$fixture/root_before" - STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com \ - STOREFRONT_ACCESS_TOKEN=synthetic-token \ - REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER=synthetic-react-native-merchant \ - SWIFT_APPLE_PAY_MERCHANT_IDENTIFIER=synthetic-swift-merchant \ - CUSTOMER_ACCOUNT_API_CLIENT_ID=synthetic-client \ - CUSTOMER_ACCOUNT_API_SHOP_ID=synthetic-shop \ - "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 + write_canonical_root_env "$fixture/.env" synthetic-store.example.myshopify.com + cp "$fixture/.env" "$root_before" + + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 assert_output_is_sanitized "$output" - assert_file_exists "$fixture/.env" - assert_file_exists "$fixture/platforms/android/samples/CheckoutKitAndroidDemo/.env" - assert_file_exists "$fixture/platforms/react-native/sample/.env" - assert_file_exists "$fixture/platforms/swift/Samples/CheckoutKitSwiftDemo/Storefront.xcconfig" - assert_file_exists "$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" - assert_not_contains "$fixture/platforms/android/samples/CheckoutKitAndroidDemo/.env" "APPLE_PAY_MERCHANT_IDENTIFIER" - assert_not_contains "$fixture/platforms/react-native/sample/.env" "CUSTOMER_ACCOUNT_API_VERSION" - assert_not_contains "$fixture/platforms/swift/Samples/CheckoutKitSwiftDemo/Storefront.xcconfig" "CUSTOMER_ACCOUNT_API_VERSION" - assert_not_contains "$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" "APPLE_PAY_MERCHANT_IDENTIFIER" - assert_not_contains "$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" "CUSTOMER_ACCOUNT_API_CLIENT_ID" - assert_not_contains "$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" "EMAIL" + assert_file_is_unchanged "$fixture/.env" "$root_before" + assert_file_exists "$(android_env_of "$fixture")" + assert_file_exists "$(react_native_env_of "$fixture")" + assert_file_exists "$(react_native_xcconfig_of "$fixture")" + assert_file_exists "$(swift_demo_xcconfig_of "$fixture")" + assert_file_exists "$(swift_accelerated_xcconfig_of "$fixture")" + + assert_not_contains "$(react_native_env_of "$fixture")" "CUSTOMER_ACCOUNT_API_VERSION" + assert_not_contains "$(swift_demo_xcconfig_of "$fixture")" "CUSTOMER_ACCOUNT_API_VERSION" + assert_not_contains "$(swift_accelerated_xcconfig_of "$fixture")" "CUSTOMER_ACCOUNT_API_VERSION" + assert_not_contains "$(swift_accelerated_xcconfig_of "$fixture")" "CUSTOMER_ACCOUNT_API_CLIENT_ID" + assert_not_contains "$(swift_accelerated_xcconfig_of "$fixture")" "EMAIL" "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1 assert_output_is_sanitized "$output" + # A changed .env must reach every generated file on the next run. awk ' /^API_VERSION=/ { print "API_VERSION=2026-01"; next } { print } ' "$fixture/.env" >"$fixture/.env.next" mv "$fixture/.env.next" "$fixture/.env" + cp "$fixture/.env" "$root_before" "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 assert_output_is_sanitized "$output" - assert_contains "$fixture/platforms/android/samples/CheckoutKitAndroidDemo/.env" "API_VERSION=2026-01" - assert_contains "$fixture/platforms/react-native/sample/.env" "API_VERSION=\"2026-01\"" - assert_contains "$fixture/platforms/swift/Samples/CheckoutKitSwiftDemo/Storefront.xcconfig" "API_VERSION = 2026-01" - assert_contains "$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" "API_VERSION = 2026-01" + assert_file_is_unchanged "$fixture/.env" "$root_before" + assert_contains "$(android_env_of "$fixture")" "API_VERSION=2026-01" + assert_contains "$(react_native_env_of "$fixture")" "API_VERSION=\"2026-01\"" + assert_contains "$(swift_demo_xcconfig_of "$fixture")" "API_VERSION = 2026-01" + assert_contains "$(swift_accelerated_xcconfig_of "$fixture")" "API_VERSION = 2026-01" - printf '%s\n' "stale" >"$fixture/platforms/android/samples/CheckoutKitAndroidDemo/.env" + printf '%s\n' "stale" >"$(android_env_of "$fixture")" if "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1; then fail "stale generated config passed --check" fi assert_output_is_sanitized "$output" } -test_required_values_only() { - local fixture output +# The core guarantee of this phase. scripts/generate_env_files owns .env now, so a +# sync that rewrote it would either destroy a hand-written file or bake in a +# .env.local override permanently. +test_root_env_is_never_written() { + local fixture output root_before fixture="$(make_fixture)" output="$fixture/output.log" + root_before="$fixture/root_before" - STOREFRONT_DOMAIN=https://synthetic-store.example.myshopify.com/ \ - STOREFRONT_ACCESS_TOKEN=synthetic-token \ - "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 + # Deliberately non-canonical: two keys only, no trailing buyer identity block. + # The old script rewrote .env in exactly this case, so this fixture is what + # makes the assertion bite. + cat >"$fixture/.env" <<'EOF' +STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=synthetic-token +EOF + cp "$fixture/.env" "$root_before" - assert_output_is_sanitized "$output" - assert_file_exists "$fixture/.env" - assert_contains "$fixture/.env" "STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com" - assert_contains "$fixture/platforms/android/samples/CheckoutKitAndroidDemo/.env" "STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com" - assert_contains "$fixture/platforms/react-native/sample/.env" "STOREFRONT_DOMAIN=\"synthetic-store.example.myshopify.com\"" - assert_contains "$fixture/platforms/swift/Samples/CheckoutKitSwiftDemo/Storefront.xcconfig" "STOREFRONT_DOMAIN = synthetic-store.example.myshopify.com" - assert_contains "$fixture/.env" "REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER=" - assert_contains "$fixture/.env" "SWIFT_APPLE_PAY_MERCHANT_IDENTIFIER=" - assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_CLIENT_ID=" - assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_SHOP_ID=" - assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" - assert_contains "$fixture/platforms/android/samples/CheckoutKitAndroidDemo/.env" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" - assert_not_contains "$fixture/platforms/react-native/sample/.env" "CUSTOMER_ACCOUNT_API_VERSION" - assert_not_contains "$fixture/platforms/swift/Samples/CheckoutKitSwiftDemo/Storefront.xcconfig" "CUSTOMER_ACCOUNT_API_VERSION" - assert_not_contains "$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" "CUSTOMER_ACCOUNT_API_VERSION" + CUSTOMER_ACCOUNT_API_CLIENT_ID=synthetic-client \ + CUSTOMER_ACCOUNT_API_SHOP_ID=synthetic-shop \ + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 - "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1 assert_output_is_sanitized "$output" + assert_file_is_unchanged "$fixture/.env" "$root_before" + + # The generated files are still complete, from defaults and the process env. + assert_contains "$(android_env_of "$fixture")" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" + assert_contains "$(android_env_of "$fixture")" "CUSTOMER_ACCOUNT_API_CLIENT_ID=synthetic-client" + + "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1 || + fail "--check failed for a sparse .env that had just been synced" } -test_optional_sync_updates_blank_optional_values_after_required_setup() { +test_absent_root_env_is_not_created() { local fixture output fixture="$(make_fixture)" output="$fixture/output.log" STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com \ STOREFRONT_ACCESS_TOKEN=synthetic-token \ - "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 - - assert_output_is_sanitized "$output" - assert_contains "$fixture/.env" "REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER=" - assert_contains "$fixture/.env" "SWIFT_APPLE_PAY_MERCHANT_IDENTIFIER=" - assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_CLIENT_ID=" - assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_SHOP_ID=" - - REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER=synthetic-react-native-merchant \ - SWIFT_APPLE_PAY_MERCHANT_IDENTIFIER=synthetic-swift-merchant \ - CUSTOMER_ACCOUNT_API_CLIENT_ID=synthetic-client \ - CUSTOMER_ACCOUNT_API_SHOP_ID=synthetic-shop \ "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 assert_output_is_sanitized "$output" - assert_contains "$fixture/.env" "REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER=synthetic-react-native-merchant" - assert_contains "$fixture/.env" "SWIFT_APPLE_PAY_MERCHANT_IDENTIFIER=synthetic-swift-merchant" - assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_CLIENT_ID=synthetic-client" - assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_SHOP_ID=synthetic-shop" - assert_contains "$fixture/platforms/react-native/sample/.env" "APPLE_PAY_MERCHANT_IDENTIFIER=\"synthetic-react-native-merchant\"" - assert_contains "$fixture/platforms/swift/Samples/CheckoutKitSwiftDemo/Storefront.xcconfig" "CUSTOMER_ACCOUNT_API_CLIENT_ID = synthetic-client" - assert_contains "$fixture/platforms/android/samples/CheckoutKitAndroidDemo/.env" "CUSTOMER_ACCOUNT_API_REDIRECT_URI=shop.synthetic-shop.app://callback" + assert_file_absent "$fixture/.env" + assert_contains "$(android_env_of "$fixture")" "STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com" } -test_shared_merchant_identifier_migrates_to_app_specific_keys() { - local fixture output +# The external contributor: no dev, no ejson, no key, a .env written by hand from +# .env.example. Nothing may overwrite it, and the sample apps must still build. +test_hand_written_root_env_survives_and_still_configures_the_samples() { + local fixture output root_before fixture="$(make_fixture)" output="$fixture/output.log" + root_before="$fixture/root_before" cat >"$fixture/.env" <<'EOF' -STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com -STOREFRONT_ACCESS_TOKEN=synthetic-token -STOREFRONT_MERCHANT_IDENTIFIER=synthetic-merchant +# My own store. Written by hand from .env.example. +STOREFRONT_DOMAIN=hand-written-store.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=hand-written-token EOF + cp "$fixture/.env" "$root_before" - "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 + PATH="/usr/bin:/bin" "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 assert_output_is_sanitized "$output" + assert_file_is_unchanged "$fixture/.env" "$root_before" - assert_not_contains "$fixture/.env" "STOREFRONT_MERCHANT_IDENTIFIER=" - assert_contains "$fixture/.env" \ - "REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER=synthetic-merchant" - assert_contains "$fixture/.env" \ - "SWIFT_APPLE_PAY_MERCHANT_IDENTIFIER=synthetic-merchant" - assert_contains "$fixture/platforms/react-native/sample/.env" \ - 'APPLE_PAY_MERCHANT_IDENTIFIER="synthetic-merchant"' - assert_contains "$fixture/platforms/swift/Samples/CheckoutKitSwiftDemo/Storefront.xcconfig" \ - "APPLE_PAY_MERCHANT_IDENTIFIER = synthetic-merchant" + assert_contains "$(android_env_of "$fixture")" "STOREFRONT_DOMAIN=hand-written-store.example.myshopify.com" + assert_contains "$(react_native_env_of "$fixture")" "STOREFRONT_ACCESS_TOKEN=\"hand-written-token\"" + assert_contains "$(swift_demo_xcconfig_of "$fixture")" "STOREFRONT_DOMAIN = hand-written-store.example.myshopify.com" + + PATH="/usr/bin:/bin" "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1 || + fail "--check failed for a hand-written .env that had just been synced" + assert_file_is_unchanged "$fixture/.env" "$root_before" } -test_normalizes_existing_root_env() { +# Reporting a missing value must not abort. The generated files still get written, +# so the sample app builds and fails at its first network call instead. +test_missing_required_values_are_reported_without_writing_root_env() { local fixture output fixture="$(make_fixture)" output="$fixture/output.log" - cat >"$fixture/.env" <<'EOF' -STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com -STOREFRONT_ACCESS_TOKEN=synthetic-token -EOF - - if "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1; then - fail "non-canonical root .env passed --check" - fi - assert_output_is_sanitized "$output" - - "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 - assert_output_is_sanitized "$output" - - assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" - assert_contains "$fixture/.env" "EMAIL=checkout-kit@example.com" + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 - "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1 assert_output_is_sanitized "$output" + assert_file_absent "$fixture/.env" + assert_contains "$output" "No usable STOREFRONT_DOMAIN" + assert_contains "$output" "No usable STOREFRONT_ACCESS_TOKEN" + assert_contains "$output" ".env.local" + assert_file_exists "$(android_env_of "$fixture")" } -test_blank_customer_account_api_version_defaults() { +test_normalizes_the_storefront_domain() { local fixture output fixture="$(make_fixture)" output="$fixture/output.log" - cat >"$fixture/.env" <<'EOF' -STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com -STOREFRONT_ACCESS_TOKEN=synthetic-token -REACT_NATIVE_APPLE_PAY_MERCHANT_IDENTIFIER= -SWIFT_APPLE_PAY_MERCHANT_IDENTIFIER= -API_VERSION=2026-04 -CUSTOMER_ACCOUNT_API_CLIENT_ID=synthetic-client -CUSTOMER_ACCOUNT_API_SHOP_ID=synthetic-shop -CUSTOMER_ACCOUNT_API_VERSION= -EMAIL=checkout-kit@example.com -ADDRESS_1=650 King Street -ADDRESS_2=Shopify HQ -CITY=Toronto -COMPANY=Shopify -COUNTRY=CA -FIRST_NAME=Evelyn -LAST_NAME=Hartley -PROVINCE=ON -ZIP=M5V 1M7 -PHONE=+14165550100 -EOF + STOREFRONT_DOMAIN=https://synthetic-store.example.myshopify.com/ \ + STOREFRONT_ACCESS_TOKEN=synthetic-token \ + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 - "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 assert_output_is_sanitized "$output" + assert_contains "$(android_env_of "$fixture")" "STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com" + assert_contains "$(react_native_env_of "$fixture")" "STOREFRONT_DOMAIN=\"synthetic-store.example.myshopify.com\"" + assert_contains "$(swift_demo_xcconfig_of "$fixture")" "STOREFRONT_DOMAIN = synthetic-store.example.myshopify.com" + assert_contains "$(android_env_of "$fixture")" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" + assert_not_contains "$(react_native_env_of "$fixture")" "CUSTOMER_ACCOUNT_API_VERSION" - assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" - assert_contains "$fixture/platforms/android/samples/CheckoutKitAndroidDemo/.env" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" - assert_contains "$fixture/platforms/android/samples/CheckoutKitAndroidDemo/.env" "/2026-04/graphql" + "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1 + assert_output_is_sanitized "$output" } -# project.yml reads $(DEVELOPMENT_TEAM) from the generated xcconfig. Setup applies no -# default team, so a clone with no DEVELOPMENT_TEAM in .env gets a blank one and fails at -# signing. A value in .env has to win, and clearing that value has to clear the xcconfig -# again rather than recover the previous team from the generated file. -test_development_team_follows_env_and_clears() { - local fixture output demo_xcconfig accelerated_xcconfig +test_process_environment_fills_blank_optional_values() { + local fixture output root_before fixture="$(make_fixture)" output="$fixture/output.log" - demo_xcconfig="$fixture/platforms/swift/Samples/CheckoutKitSwiftDemo/Storefront.xcconfig" - accelerated_xcconfig="$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" - - STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com \ - STOREFRONT_ACCESS_TOKEN=synthetic-token \ - "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 - - assert_output_is_sanitized "$output" - assert_contains "$demo_xcconfig" "DEVELOPMENT_TEAM =" - assert_not_contains "$demo_xcconfig" "DEVELOPMENT_TEAM = A7XGC83MZE" - # .env has to carry the key as well, or the next normalizing rewrite drops the override. - assert_contains "$fixture/.env" "DEVELOPMENT_TEAM=" + root_before="$fixture/root_before" + write_canonical_root_env "$fixture/.env" synthetic-store.example.myshopify.com awk ' - /^DEVELOPMENT_TEAM=/ { print "DEVELOPMENT_TEAM=SYNTHETIC9"; next } + /^CUSTOMER_ACCOUNT_API_CLIENT_ID=/ { print "CUSTOMER_ACCOUNT_API_CLIENT_ID="; next } + /^CUSTOMER_ACCOUNT_API_SHOP_ID=/ { print "CUSTOMER_ACCOUNT_API_SHOP_ID="; next } { print } ' "$fixture/.env" >"$fixture/.env.next" mv "$fixture/.env.next" "$fixture/.env" + cp "$fixture/.env" "$root_before" + + CUSTOMER_ACCOUNT_API_CLIENT_ID=synthetic-client \ + CUSTOMER_ACCOUNT_API_SHOP_ID=synthetic-shop \ + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 - "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 assert_output_is_sanitized "$output" - assert_contains "$demo_xcconfig" "DEVELOPMENT_TEAM = SYNTHETIC9" - assert_contains "$accelerated_xcconfig" "DEVELOPMENT_TEAM = SYNTHETIC9" - assert_contains "$fixture/.env" "DEVELOPMENT_TEAM=SYNTHETIC9" + assert_file_is_unchanged "$fixture/.env" "$root_before" + assert_contains "$(swift_demo_xcconfig_of "$fixture")" "CUSTOMER_ACCOUNT_API_CLIENT_ID = synthetic-client" + assert_contains "$(android_env_of "$fixture")" "CUSTOMER_ACCOUNT_API_REDIRECT_URI=shop.synthetic-shop.app://callback" +} + +test_blank_customer_account_api_version_defaults() { + local fixture output android_env root_before + fixture="$(make_fixture)" + output="$fixture/output.log" + android_env="$(android_env_of "$fixture")" + root_before="$fixture/root_before" + write_canonical_root_env "$fixture/.env" synthetic-store.example.myshopify.com awk ' - /^DEVELOPMENT_TEAM=/ { print "DEVELOPMENT_TEAM="; next } + /^CUSTOMER_ACCOUNT_API_VERSION=/ { print "CUSTOMER_ACCOUNT_API_VERSION="; next } { print } ' "$fixture/.env" >"$fixture/.env.next" mv "$fixture/.env.next" "$fixture/.env" + cp "$fixture/.env" "$root_before" - "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 assert_output_is_sanitized "$output" - assert_not_contains "$demo_xcconfig" "SYNTHETIC9" - assert_not_contains "$accelerated_xcconfig" "SYNTHETIC9" + + assert_file_is_unchanged "$fixture/.env" "$root_before" + assert_contains "$android_env" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" + assert_contains "$android_env" "/2026-04/graphql" } -# A clone whose .env predates DEVELOPMENT_TEAM holds every other canonical key, so the -# normalizing rewrite has to notice the one absent key. Otherwise dev up leaves .env alone -# and the developer never sees the key they are meant to override. -test_development_team_is_added_to_an_existing_env() { - local fixture output accelerated_xcconfig +# project.yml reads $(DEVELOPMENT_TEAM) from the generated xcconfig. Setup applies no +# default team, so a clone whose .env omits the key gets a blank one and fails at signing. +# .env.local has to win so a second Apple team can build the sample, and dropping that +# override has to blank the xcconfig again rather than recover the team from it. +test_development_team_follows_env_and_clears() { + local fixture output demo_xcconfig accelerated_xcconfig fixture="$(make_fixture)" output="$fixture/output.log" - accelerated_xcconfig="$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" + demo_xcconfig="$(swift_demo_xcconfig_of "$fixture")" + accelerated_xcconfig="$(swift_accelerated_xcconfig_of "$fixture")" - STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com \ - STOREFRONT_ACCESS_TOKEN=synthetic-token \ - "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 + write_canonical_root_env "$fixture/.env" synthetic-store.example.myshopify.com - grep -v '^DEVELOPMENT_TEAM=' "$fixture/.env" >"$fixture/.env.next" - mv "$fixture/.env.next" "$fixture/.env" + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 + assert_output_is_sanitized "$output" + assert_contains "$demo_xcconfig" "DEVELOPMENT_TEAM =" + assert_contains "$accelerated_xcconfig" "DEVELOPMENT_TEAM =" + assert_not_contains "$demo_xcconfig" "DEVELOPMENT_TEAM = A7XGC83MZE" + assert_not_contains "$accelerated_xcconfig" "DEVELOPMENT_TEAM = A7XGC83MZE" - if "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1; then - fail "root .env without DEVELOPMENT_TEAM passed --check" - fi + printf 'DEVELOPMENT_TEAM=SYNTHETIC9\n' >"$fixture/.env.local" + + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 assert_output_is_sanitized "$output" + assert_contains "$demo_xcconfig" "DEVELOPMENT_TEAM = SYNTHETIC9" + assert_contains "$accelerated_xcconfig" "DEVELOPMENT_TEAM = SYNTHETIC9" + + rm "$fixture/.env.local" - "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 assert_output_is_sanitized "$output" - assert_contains "$fixture/.env" "DEVELOPMENT_TEAM=" - assert_contains "$accelerated_xcconfig" "DEVELOPMENT_TEAM =" + assert_not_contains "$demo_xcconfig" "SYNTHETIC9" + assert_not_contains "$accelerated_xcconfig" "SYNTHETIC9" } -test_migration_from_platform_config() { +# Generated platform config is the last fallback, so a checkout that still has it +# but has lost .env rebuilds the same values instead of silently blanking them. +test_existing_platform_config_seeds_the_missing_values() { local fixture output android_env fixture="$(make_fixture)" output="$fixture/output.log" - android_env="$fixture/platforms/android/samples/CheckoutKitAndroidDemo/.env" + android_env="$(android_env_of "$fixture")" cat >"$android_env" <<'EOF' STOREFRONT_DOMAIN=migrated-store.example.myshopify.com @@ -426,24 +476,145 @@ EOF "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 assert_output_is_sanitized "$output" - assert_file_exists "$fixture/.env" - assert_contains "$fixture/.env" "API_VERSION=2026-04" - assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" + assert_file_absent "$fixture/.env" + assert_contains "$android_env" "STOREFRONT_DOMAIN=migrated-store.example.myshopify.com" + assert_contains "$android_env" "API_VERSION=2026-04" + assert_contains "$android_env" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" + assert_contains "$(swift_demo_xcconfig_of "$fixture")" "CUSTOMER_ACCOUNT_API_CLIENT_ID = migrated-client" "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1 assert_output_is_sanitized "$output" } -test_merchant_identifier_propagates_to_sample_apps +test_duplicate_keys_resolve_last_wins() { + local fixture output android_env + fixture="$(make_fixture)" + output="$fixture/output.log" + android_env="$(android_env_of "$fixture")" + + write_canonical_root_env "$fixture/.env" synthetic-store.example.myshopify.com + printf 'STOREFRONT_DOMAIN=%s\n' later-store.example.myshopify.com >>"$fixture/.env" + + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 + assert_output_is_sanitized "$output" + + assert_contains "$android_env" "STOREFRONT_DOMAIN=later-store.example.myshopify.com" + assert_not_contains "$android_env" "synthetic-store.example.myshopify.com" +} + +test_env_local_overrides_root_env() { + local fixture output android_env root_before local_before + fixture="$(make_fixture)" + output="$fixture/output.log" + android_env="$(android_env_of "$fixture")" + root_before="$fixture/root_before" + local_before="$fixture/local_before" + + write_canonical_root_env "$fixture/.env" synthetic-store.example.myshopify.com + cat >"$fixture/.env.local" <<'EOF' +STOREFRONT_DOMAIN=overridden-store.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=overridden-token +EOF + cp "$fixture/.env" "$root_before" + cp "$fixture/.env.local" "$local_before" + + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 + assert_output_is_sanitized "$output" + + assert_contains "$android_env" "STOREFRONT_DOMAIN=overridden-store.example.myshopify.com" + assert_contains "$android_env" "STOREFRONT_ACCESS_TOKEN=overridden-token" + assert_contains "$android_env" "EMAIL=checkout-kit@example.com" + assert_file_is_unchanged "$fixture/.env" "$root_before" + assert_file_is_unchanged "$fixture/.env.local" "$local_before" +} + +test_env_local_warning_names_only_keys() { + local fixture output + fixture="$(make_fixture)" + output="$fixture/output.log" + + write_canonical_root_env "$fixture/.env" synthetic-store.example.myshopify.com + cat >"$fixture/.env.local" <<'EOF' +STOREFRONT_DOMAIN=overridden-store.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=overridden-token +EOF + + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 + assert_output_is_sanitized "$output" + + assert_contains "$output" ".env.local" + assert_contains "$output" "STOREFRONT_DOMAIN" + assert_contains "$output" "STOREFRONT_ACCESS_TOKEN" + assert_not_contains "$output" "EMAIL" +} + +# An override must never be copied into .env, or it would outlive .env.local and +# then keep applying after that file is deleted. +test_env_local_is_never_baked_into_root_env() { + local fixture output root_before + fixture="$(make_fixture)" + output="$fixture/output.log" + root_before="$fixture/root_before" + + cat >"$fixture/.env" <<'EOF' +STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=synthetic-token +EOF + printf 'STOREFRONT_DOMAIN=%s\n' overridden-store.example.myshopify.com >"$fixture/.env.local" + cp "$fixture/.env" "$root_before" + + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 + assert_output_is_sanitized "$output" + + assert_file_is_unchanged "$fixture/.env" "$root_before" + assert_not_contains "$fixture/.env" "overridden-store.example.myshopify.com" + assert_contains "$(android_env_of "$fixture")" \ + "STOREFRONT_DOMAIN=overridden-store.example.myshopify.com" +} + +test_legacy_merchant_identifier_configures_both_samples_without_rewriting_root_env() { + local fixture output root_before + fixture="$(make_fixture)" + output="$fixture/output.log" + root_before="$fixture/root_before" + + cat >"$fixture/.env" <<'EOF' +STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=synthetic-token +STOREFRONT_MERCHANT_IDENTIFIER=synthetic-merchant +EOF + cp "$fixture/.env" "$root_before" + + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 + + assert_output_is_sanitized "$output" + assert_file_is_unchanged "$fixture/.env" "$root_before" + assert_contains "$(react_native_env_of "$fixture")" \ + 'APPLE_PAY_MERCHANT_IDENTIFIER="synthetic-merchant"' + assert_contains "$(react_native_xcconfig_of "$fixture")" \ + "APPLE_PAY_MERCHANT_IDENTIFIER = synthetic-merchant" + assert_contains "$(swift_demo_xcconfig_of "$fixture")" \ + "APPLE_PAY_MERCHANT_IDENTIFIER = synthetic-merchant" + assert_not_contains "$(android_env_of "$fixture")" "synthetic-merchant" + assert_not_contains "$(swift_accelerated_xcconfig_of "$fixture")" "synthetic-merchant" +} + +test_merchant_identifiers_propagate_without_rewriting_root_env test_sample_projects_read_generated_merchant_identifier test_sync_and_check -test_required_values_only -test_optional_sync_updates_blank_optional_values_after_required_setup -test_shared_merchant_identifier_migrates_to_app_specific_keys -test_normalizes_existing_root_env +test_root_env_is_never_written +test_absent_root_env_is_not_created +test_hand_written_root_env_survives_and_still_configures_the_samples +test_missing_required_values_are_reported_without_writing_root_env +test_normalizes_the_storefront_domain +test_process_environment_fills_blank_optional_values test_blank_customer_account_api_version_defaults test_development_team_follows_env_and_clears -test_development_team_is_added_to_an_existing_env -test_migration_from_platform_config +test_existing_platform_config_seeds_the_missing_values +test_duplicate_keys_resolve_last_wins +test_env_local_overrides_root_env +test_env_local_warning_names_only_keys +test_env_local_is_never_baked_into_root_env +test_legacy_merchant_identifier_configures_both_samples_without_rewriting_root_env echo "setup_storefront_env synthetic tests passed."