skillhub/scripts/tests/validate-release-config-test.sh
XiaoSeS 934cfa6ded
Some checks are pending
Deploy Docs / build (push) Waiting to run
Deploy Docs / Deploy (push) Blocked by required conditions
Security / Dependency Review (push) Waiting to run
Security / CodeQL (java-kotlin) (push) Waiting to run
Security / CodeQL (javascript-typescript) (push) Waiting to run
Security / CodeQL (python) (push) Waiting to run
feat(auth): add Feishu as a public login provider (R1-A2) (#877)
* feat(auth): let providers override OAuth userinfo loading

Some providers do not return a flat, standard userinfo payload, so
DefaultOAuth2UserService cannot read them. Add ProviderOAuth2UserService
so a provider can claim its own registration id and supply the loading
step, while everything after it stays shared.

The override runs inside the RemoteIdentityIoExecutor boundary added in
R1-A, so a provider's HTTP call does not hold the surrounding
transaction open. Registrations without an override keep using the
default user service unchanged.

Part of R1-A2 (public Provider adapters) per
openspec/changes/enterprise-identity-platform/rollout-plan.md.

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* feat(auth): add Feishu as a public login provider

Adds Feishu (Lark) as a public sign-in option: it authenticates a
SkillHub platform account and nothing more. No Organization membership,
no directory sync, no Namespace grants.

Feishu deviates from standard OAuth in two ways this handles:
its userinfo response is wrapped in a {code, msg, data} envelope, and it
reports errors with HTTP 200. FeishuOAuth2UserService unwraps that
envelope into flat attributes; FeishuClaimsExtractor maps them to the
shared OAuthClaims, so account decisions still run through the unified
identity core added in R1-A.

Subject and email semantics, which decide whether a login can reach an
existing account:

- open_id is the only subject. union_id stays in extra rather than
  acting as a fallback: a subject that can change between logins would
  split one person across two platform accounts. Promoting union_id
  later needs an explicit alias migration.
- A blank or missing open_id fails the login instead of binding the
  literal string "null".
- emailVerified is always false. Feishu emails are imported by an
  organization admin and never confirmed with the user, so they carry no
  verification signal and cannot be used to join an existing account.

Operational bounds: the userinfo call has connect and read timeouts so an
unresponsive Feishu endpoint cannot hold a login thread, and the
OAuth2Error description carries only the provider error code, because an
upstream message can quote the request URI and with it the access token.
Like the GitHub and GitLab extractors, the claims extractor logs nothing.

The login button follows the existing config-driven catalog: with no
client id configured, /api/v1/auth/methods does not list Feishu and no
button renders. No frontend code change is needed; the icon resolves by
provider name.

Adapted from the implementation in #696 by @yhd4711499, re-extracted onto
current main with the subject, logging and timeout changes above.

Part of R1-A2 (public Provider adapters) per
openspec/changes/enterprise-identity-platform/rollout-plan.md.

Co-authored-by: yhd4711499 <yhd4711499@users.noreply.github.com>
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(auth): bound Feishu userinfo response and stop subject leaking into displayName

Three defects found reviewing this batch against the R1-A2 spec.

Response size limit. The spec's scope line asks for "远程 I/O 超时与响应大小
限制"; only the timeouts were implemented, so a misconfigured or hostile
OAUTH2_FEISHU_BASE_URI could stream an unbounded body into the parser.
Reads at most 64 KB before parsing, mirroring the 10 MB cap the shared
WebClientConfig already applies. Uses InputStream.readNBytes rather than
adding commons-io or guava, neither of which skillhub-auth declares.

Synthesized displayName. Falling back to "feishu-<open_id>" wrote the
external subject into UserAccount.displayName and into
UserActivatedEvent, carrying it somewhere event consumers may log it --
against the R1-A gate that logs must not contain the subject. Now stops
at name -> en_name like the GitHub and GitLab extractors.

Unused mobile attribute. A phone number was extracted into the principal
attributes and read by nothing. It is PII the spec did not ask for and it
widened the redaction surface for free.

Also drops a constructor overload that only passed List.of() through, and
a test that duplicated the blank-subject path.

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* docs(auth): document the provider adapter contract and Feishu operator setup

AGENTS.md and CONTRIBUTING.md both require docs updates when auth flows or
deployment config change; this batch changed both and touched no docs.

03-authentication-design.md described adding a provider as "branch on
registrationId inside CustomOAuth2UserService", which the
ProviderOAuth2UserService strategy supersedes. Rewrites that recipe:
register an OAuthClaimsExtractor bean per provider, add a
ProviderOAuth2UserService only when the userinfo response is non-standard,
and note that the login page needs no code change. Also records the
provider-side obligations that are easy to get wrong -- stable subject with
no fallback, emailVerified only on proven ownership, bounded remote calls,
no subject in logs -- and un-comments the config example, which still
listed GitLab as a future possibility.

faq.md told operators to delete "the github and gitlab blocks" to hide SSO
buttons. That advice was already incomplete and gets worse per provider, so
it now explains the config-driven mechanism: an empty client id keeps the
entry off the login page, no file edit needed.

09-deployment.md listed only the GitHub credentials. Adds GitLab and Feishu,
and flags a deployment trap: Feishu emails are admin-imported so
emailVerified is always false, and skillhub.access-policy.mode=EMAIL_DOMAIN
denies every unverified email, which would reject all Feishu logins.

Squares the Feishu logo viewBox. It was 407.87x324.19 while login-button
renders it in a square w-5 h-5 box, so the mark was distorted.

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* feat(deploy): wire Feishu credentials into the release surfaces

.env.release.example advertised OAUTH2_FEISHU_* knobs that no deployment
path could actually deliver. compose.release.yml has no env_file, so every
variable must be listed explicitly, and the Helm chart and k8s base only
mapped the GitHub secret keys. Setting the documented variables therefore
did nothing.

Adds Feishu to compose.release.yml, the Helm secret template and values,
the k8s deployment and its secret example. GitLab had the identical gap, so
it is wired at the same time rather than leaving the example file half true.

validate-release-config.sh only checked that GitHub's id and secret appear
together. A half-configured provider renders a login button whose exchange
then fails, so the check now loops over all three providers. Its test gained
both-directions cases per provider plus a fully configured pass; reverting
the loop to GitHub-only makes them fail.

Also adds the provider's only failure log. Nothing downstream records a
Feishu userinfo failure -- OAuth2LoginFailureHandler does not log either --
so the previous code was silent on error. Logs the exception class and
Feishu's own error code, never the upstream msg, which can quote the access
token; a test asserts the code is present and the token is not.

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(auth): use JSON token exchange for Feishu OAuth

Made-with: Proma
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(auth): preserve Feishu OAuth browser redirect

Add safe phase-level OAuth diagnostics and redact callback credentials from request logs.

Made-with: Proma
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* docs(deploy): clarify Feishu OAuth configuration and validation

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(deploy): pass Feishu redirect URI through releases

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(deploy): pass S3 chunked encoding setting

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(deploy): preserve default Feishu callback derivation

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

---------

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Co-authored-by: yhd4711499 <yhd4711499@users.noreply.github.com>
2026-09-21 14:39:22 +08:00

329 lines
16 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SCRIPT="$REPO_ROOT/scripts/validate-release-config.sh"
TMP_DIRS=()
cleanup() {
local d
for d in "${TMP_DIRS[@]+"${TMP_DIRS[@]}"}"; do
rm -rf "$d"
done
}
trap cleanup EXIT
new_tmp() {
local d
d="$(mktemp -d)"
TMP_DIRS+=("$d")
echo "$d"
}
fail() {
echo "FAIL: $*" >&2
exit 1
}
write_env() {
local file="$1"
local secret="${2:-}"
local include_secret="${3:-yes}"
cat >"$file" <<EOF
SKILLHUB_PUBLIC_BASE_URL=https://skillhub.example.com
POSTGRES_DB=skillhub
POSTGRES_USER=skillhub
POSTGRES_PASSWORD=strong-postgres-password
SESSION_COOKIE_SECURE=true
BOOTSTRAP_ADMIN_ENABLED=false
SKILLHUB_TRUST_FORWARDED_PROTO=false
SKILLHUB_BUILTIN_SKILLS_ENABLED=true
SKILLHUB_STORAGE_PROVIDER=s3
SKILLHUB_STORAGE_S3_ENDPOINT=https://storage.example.com
SKILLHUB_STORAGE_S3_BUCKET=skillhub
SKILLHUB_STORAGE_S3_ACCESS_KEY=release-access-key
SKILLHUB_STORAGE_S3_SECRET_KEY=release-secret-key
SKILLHUB_STORAGE_S3_REGION=us-east-1
SKILLHUB_STORAGE_S3_FORCE_PATH_STYLE=false
SKILLHUB_STORAGE_S3_AUTO_CREATE_BUCKET=false
EOF
if [[ "$include_secret" == "yes" ]]; then
printf 'SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET=%s\n' "$secret" >>"$file"
fi
}
expect_fail() {
local file="$1"
local expected="$2"
local output
if output="$("$SCRIPT" "$file" 2>&1)"; then
fail "expected validation to fail for $file"
fi
if [[ "$output" != *"$expected"* ]]; then
fail "expected output to contain '$expected', got: $output"
fi
}
tmp="$(new_tmp)"
valid_env="$tmp/valid.env"
write_env "$valid_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SKILLHUB_STORAGE_S3_DISABLE_CHUNKED_ENCODING=true" >>"$valid_env"
"$SCRIPT" "$valid_env" >/dev/null
compose_default_redirect="$tmp/compose-default-redirect.txt"
SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET=release-download-secret-32-bytes-minimum \
SKILLHUB_PUBLIC_BASE_URL=https://skillhub.example.com \
docker compose -f "$REPO_ROOT/compose.release.yml" config \
| grep -A1 'OAUTH2_FEISHU_REDIRECT_URI:' >"$compose_default_redirect"
grep -Fq 'https://skillhub.example.com/login/oauth2/code/feishu' "$compose_default_redirect" \
|| fail "compose must derive the default Feishu redirect URI from SKILLHUB_PUBLIC_BASE_URL"
valid_feishu_env="$tmp/valid-feishu.env"
write_env "$valid_feishu_env" "release-download-secret-32-bytes-minimum"
cat >>"$valid_feishu_env" <<'EOF'
OAUTH2_FEISHU_CLIENT_ID=cli_test
OAUTH2_FEISHU_CLIENT_SECRET=secret_test
OAUTH2_FEISHU_PROTOCOL_VERSION=v2
OAUTH2_FEISHU_AUTHORIZATION_URI=https://accounts.feishu.cn/open-apis/authen/v1/authorize
OAUTH2_FEISHU_TOKEN_URI=https://open.feishu.cn/open-apis/authen/v2/oauth/token
OAUTH2_FEISHU_USER_INFO_URI=https://open.feishu.cn/open-apis/authen/v1/user_info
OAUTH2_FEISHU_REDIRECT_URI=http://127.0.0.1:55041/login/oauth2/code/feishu
EOF
"$SCRIPT" "$valid_feishu_env" >/dev/null
invalid_feishu_protocol_env="$tmp/invalid-feishu-protocol.env"
write_env "$invalid_feishu_protocol_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "OAUTH2_FEISHU_PROTOCOL_VERSION=v1" >>"$invalid_feishu_protocol_env"
expect_fail "$invalid_feishu_protocol_env" "OAUTH2_FEISHU_PROTOCOL_VERSION must be either v2 or v3"
invalid_feishu_endpoint_env="$tmp/invalid-feishu-endpoint.env"
write_env "$invalid_feishu_endpoint_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "OAUTH2_FEISHU_TOKEN_URI=https://open.feishu.cn/oauth/token?tenant=prod" >>"$invalid_feishu_endpoint_env"
expect_fail "$invalid_feishu_endpoint_env" "OAUTH2_FEISHU_TOKEN_URI must not contain a query"
invalid_feishu_redirect_env="$tmp/invalid-feishu-redirect.env"
write_env "$invalid_feishu_redirect_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "OAUTH2_FEISHU_REDIRECT_URI=https://skillhub.example.com/login/oauth2/code/feishu?bad=1" >>"$invalid_feishu_redirect_env"
expect_fail "$invalid_feishu_redirect_env" "OAUTH2_FEISHU_REDIRECT_URI must not contain a query"
disabled_builtin_skills_env="$tmp/disabled-builtin-skills.env"
write_env "$disabled_builtin_skills_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SKILLHUB_BUILTIN_SKILLS_ENABLED=false" >>"$disabled_builtin_skills_env"
"$SCRIPT" "$disabled_builtin_skills_env" >/dev/null
relative_api_base_env="$tmp/relative-api-base.env"
write_env "$relative_api_base_env" "release-download-secret-32-bytes-minimum"
printf 'SKILLHUB_WEB_API_BASE_URL=/skillhub\n' >>"$relative_api_base_env"
"$SCRIPT" "$relative_api_base_env" >/dev/null
sub_path_env="$tmp/sub-path.env"
write_env "$sub_path_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' \
'SKILLHUB_WEB_BASE_PATH=/skillhub/' \
'SKILLHUB_WEB_API_BASE_URL=/skillhub' \
'SKILLHUB_PUBLIC_BASE_URL=https://skillhub.example.com/skillhub' \
>"$tmp/sub-path-overrides"
cat "$tmp/sub-path-overrides" >>"$sub_path_env"
"$SCRIPT" "$sub_path_env" >/dev/null
missing_public_sub_path_env="$tmp/missing-public-sub-path.env"
write_env "$missing_public_sub_path_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' \
'SKILLHUB_WEB_BASE_PATH=/skillhub/' \
'SKILLHUB_WEB_API_BASE_URL=/skillhub' \
>>"$missing_public_sub_path_env"
expect_fail "$missing_public_sub_path_env" "SKILLHUB_PUBLIC_BASE_URL path"
# A public URL that merely ends with the base path but serves it under a different path
# (/other/skillhub) must be rejected: suffix match is not enough, the path must be exact.
wrong_public_path_env="$tmp/wrong-public-path.env"
write_env "$wrong_public_path_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' \
'SKILLHUB_WEB_BASE_PATH=/skillhub/' \
'SKILLHUB_WEB_API_BASE_URL=/skillhub' \
'SKILLHUB_PUBLIC_BASE_URL=https://skillhub.example.com/other/skillhub' \
>>"$wrong_public_path_env"
expect_fail "$wrong_public_path_env" "must equal SKILLHUB_WEB_BASE_PATH without its trailing slash"
# A query or fragment in SKILLHUB_PUBLIC_BASE_URL corrupts concatenated URLs
# (e.g. ${SKILLHUB_PUBLIC_BASE_URL}/cli/auth). Reject it independently of the
# base path, so even a root deployment (no SKILLHUB_WEB_BASE_PATH) is covered.
public_query_env="$tmp/public-query.env"
write_env "$public_query_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' 'SKILLHUB_PUBLIC_BASE_URL=https://skillhub.example.com/skillhub?ref=1' \
>>"$public_query_env"
expect_fail "$public_query_env" "must not contain a query"
public_fragment_env="$tmp/public-fragment.env"
write_env "$public_fragment_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' 'SKILLHUB_PUBLIC_BASE_URL=https://skillhub.example.com#frag' \
>>"$public_fragment_env"
expect_fail "$public_fragment_env" "must not contain a query"
# A scheme with no host (https://) passes a naive prefix check but concatenates
# into an invalid URL like https:///cli/auth. Must be rejected.
public_no_host_env="$tmp/public-no-host.env"
write_env "$public_no_host_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' 'SKILLHUB_PUBLIC_BASE_URL=https://' >>"$public_no_host_env"
expect_fail "$public_no_host_env" "must include a host"
# Base path format must be rejected here, matching the runtime entrypoint check,
# rather than passing config validation and only failing at container start.
dot_segment_base_env="$tmp/dot-segment-base.env"
write_env "$dot_segment_base_env" "release-download-secret-32-bytes-minimum"
printf 'SKILLHUB_WEB_BASE_PATH=/foo/../bar/\n' >>"$dot_segment_base_env"
expect_fail "$dot_segment_base_env" "must not contain '.' or '..' path segments"
double_slash_base_env="$tmp/double-slash-base.env"
write_env "$double_slash_base_env" "release-download-secret-32-bytes-minimum"
printf 'SKILLHUB_WEB_BASE_PATH=/foo//bar/\n' >>"$double_slash_base_env"
expect_fail "$double_slash_base_env" "SKILLHUB_WEB_BASE_PATH contains unsupported characters"
missing_trailing_base_env="$tmp/missing-trailing-base.env"
write_env "$missing_trailing_base_env" "release-download-secret-32-bytes-minimum"
printf 'SKILLHUB_WEB_BASE_PATH=/skillhub\n' >>"$missing_trailing_base_env"
expect_fail "$missing_trailing_base_env" "must be '/' or start and end with '/'"
# A base path whose first segment collides with a server Nginx location (/api/,
# /oauth2/, ...) must be rejected: it would shadow the real route and break the app.
for reserved in /api/ /oauth2/ /login/ /assets/ /registry/ /nginx-health/ /.well-known/ /runtime-config.js/ /api/nested/; do
reserved_base_env="$tmp/reserved-base.env"
write_env "$reserved_base_env" "release-download-secret-32-bytes-minimum"
printf 'SKILLHUB_WEB_BASE_PATH=%s\n' "$reserved" >>"$reserved_base_env"
expect_fail "$reserved_base_env" "reserved by the SkillHub server"
done
# A same-origin API base that disagrees with the web base path routes to the wrong prefix.
mismatch_api_base_env="$tmp/mismatch-api-base.env"
write_env "$mismatch_api_base_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' \
'SKILLHUB_WEB_BASE_PATH=/skillhub/' \
'SKILLHUB_WEB_API_BASE_URL=/other' \
'SKILLHUB_PUBLIC_BASE_URL=https://skillhub.example.com/skillhub' \
>>"$mismatch_api_base_env"
expect_fail "$mismatch_api_base_env" "must equal SKILLHUB_WEB_BASE_PATH without its trailing slash"
missing_env="$tmp/missing.env"
write_env "$missing_env" "" no
expect_fail "$missing_env" "SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET is required"
placeholder_env="$tmp/placeholder.env"
write_env "$placeholder_env" "change-me-in-production"
expect_fail "$placeholder_env" "SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET still uses placeholder/default value"
short_env="$tmp/short.env"
write_env "$short_env" "too-short"
expect_fail "$short_env" "SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET must be at least 32 characters"
invalid_forwarded_proto_env="$tmp/invalid-forwarded-proto.env"
write_env "$invalid_forwarded_proto_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SKILLHUB_TRUST_FORWARDED_PROTO=yes" >>"$invalid_forwarded_proto_env"
expect_fail "$invalid_forwarded_proto_env" "SKILLHUB_TRUST_FORWARDED_PROTO must be true or false"
invalid_builtin_skills_env="$tmp/invalid-builtin-skills.env"
write_env "$invalid_builtin_skills_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SKILLHUB_BUILTIN_SKILLS_ENABLED=yes" >>"$invalid_builtin_skills_env"
expect_fail "$invalid_builtin_skills_env" "SKILLHUB_BUILTIN_SKILLS_ENABLED must be true or false"
valid_redis_cluster_env="$tmp/valid-redis-cluster.env"
write_env "$valid_redis_cluster_env" "release-download-secret-32-bytes-minimum"
cat >>"$valid_redis_cluster_env" <<'EOF'
SPRING_DATA_REDIS_CLUSTER_NODES=redis-a.example.com:6379,redis-b.example.com:6380
SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS=5
SPRING_DATA_REDIS_SSL_ENABLED=true
EOF
"$SCRIPT" "$valid_redis_cluster_env" >/dev/null
invalid_redis_cluster_node_env="$tmp/invalid-redis-cluster-node.env"
write_env "$invalid_redis_cluster_node_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SPRING_DATA_REDIS_CLUSTER_NODES=redis-a.example.com" >>"$invalid_redis_cluster_node_env"
expect_fail "$invalid_redis_cluster_node_env" "SPRING_DATA_REDIS_CLUSTER_NODES entries must use host:port"
invalid_redis_cluster_port_env="$tmp/invalid-redis-cluster-port.env"
write_env "$invalid_redis_cluster_port_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SPRING_DATA_REDIS_CLUSTER_NODES=redis-a.example.com:65536" >>"$invalid_redis_cluster_port_env"
expect_fail "$invalid_redis_cluster_port_env" "SPRING_DATA_REDIS_CLUSTER_NODES port must be between 1 and 65535"
invalid_redis_redirects_env="$tmp/invalid-redis-redirects.env"
write_env "$invalid_redis_redirects_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS=-1" >>"$invalid_redis_redirects_env"
expect_fail "$invalid_redis_redirects_env" "SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS must be a non-negative integer"
invalid_redis_database_env="$tmp/invalid-redis-database.env"
write_env "$invalid_redis_database_env" "release-download-secret-32-bytes-minimum"
cat >>"$invalid_redis_database_env" <<'EOF'
SPRING_DATA_REDIS_CLUSTER_NODES=redis-a.example.com:6379
SPRING_DATA_REDIS_DATABASE=1
EOF
expect_fail "$invalid_redis_database_env" "SPRING_DATA_REDIS_DATABASE must be 0 when SPRING_DATA_REDIS_CLUSTER_NODES is set"
valid_redis_sentinel_env="$tmp/valid-redis-sentinel.env"
write_env "$valid_redis_sentinel_env" "release-download-secret-32-bytes-minimum"
cat >>"$valid_redis_sentinel_env" <<'EOF'
SPRING_DATA_REDIS_SENTINEL_MASTER=mymaster
SPRING_DATA_REDIS_SENTINEL_NODES=sentinel-a.example.com:26379,sentinel-b.example.com:26379
SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST=false
EOF
"$SCRIPT" "$valid_redis_sentinel_env" >/dev/null
missing_redis_sentinel_nodes_env="$tmp/missing-redis-sentinel-nodes.env"
write_env "$missing_redis_sentinel_nodes_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SPRING_DATA_REDIS_SENTINEL_MASTER=mymaster" >>"$missing_redis_sentinel_nodes_env"
expect_fail "$missing_redis_sentinel_nodes_env" "SPRING_DATA_REDIS_SENTINEL_NODES is required when SPRING_DATA_REDIS_SENTINEL_MASTER is set"
missing_redis_sentinel_master_env="$tmp/missing-redis-sentinel-master.env"
write_env "$missing_redis_sentinel_master_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SPRING_DATA_REDIS_SENTINEL_NODES=sentinel-a.example.com:26379" >>"$missing_redis_sentinel_master_env"
expect_fail "$missing_redis_sentinel_master_env" "SPRING_DATA_REDIS_SENTINEL_MASTER is required when SPRING_DATA_REDIS_SENTINEL_NODES is set"
invalid_redis_sentinel_port_env="$tmp/invalid-redis-sentinel-port.env"
write_env "$invalid_redis_sentinel_port_env" "release-download-secret-32-bytes-minimum"
cat >>"$invalid_redis_sentinel_port_env" <<'EOF'
SPRING_DATA_REDIS_SENTINEL_MASTER=mymaster
SPRING_DATA_REDIS_SENTINEL_NODES=sentinel-a.example.com:70000
EOF
expect_fail "$invalid_redis_sentinel_port_env" "SPRING_DATA_REDIS_SENTINEL_NODES port must be between 1 and 65535"
invalid_redis_sentinel_check_env="$tmp/invalid-redis-sentinel-check.env"
write_env "$invalid_redis_sentinel_check_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST=yes" >>"$invalid_redis_sentinel_check_env"
expect_fail "$invalid_redis_sentinel_check_env" "SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST must be true or false"
# An OAuth client id without its secret (or vice versa) leaves the provider half-configured:
# the login button renders but the exchange fails. Checked for every supported provider.
for provider in GITHUB GITLAB FEISHU; do
missing_oauth_secret_env="$tmp/missing-oauth-secret.env"
write_env "$missing_oauth_secret_env" "release-download-secret-32-bytes-minimum"
printf 'OAUTH2_%s_CLIENT_ID=real-client-id\n' "$provider" >>"$missing_oauth_secret_env"
expect_fail "$missing_oauth_secret_env" "OAUTH2_${provider}_CLIENT_SECRET is required"
missing_oauth_id_env="$tmp/missing-oauth-id.env"
write_env "$missing_oauth_id_env" "release-download-secret-32-bytes-minimum"
printf 'OAUTH2_%s_CLIENT_SECRET=real-client-secret\n' "$provider" >>"$missing_oauth_id_env"
expect_fail "$missing_oauth_id_env" "OAUTH2_${provider}_CLIENT_ID is required"
done
# A fully configured provider pair must pass.
valid_oauth_env="$tmp/valid-oauth.env"
write_env "$valid_oauth_env" "release-download-secret-32-bytes-minimum"
cat >>"$valid_oauth_env" <<'EOF'
OAUTH2_FEISHU_CLIENT_ID=cli_release_example
OAUTH2_FEISHU_CLIENT_SECRET=release-feishu-secret
EOF
"$SCRIPT" "$valid_oauth_env" >/dev/null
draft_env="$tmp/draft.env"
while IFS= read -r line || [[ -n "$line" ]]; do
case "$line" in
SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET=*)
printf '%s\n' "SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET=release-download-secret-32-bytes-minimum"
;;
*)
printf '%s\n' "$line"
;;
esac
done <"$REPO_ROOT/.env.release.draft" >"$draft_env"
expect_fail "$draft_env" "POSTGRES_PASSWORD"
echo "validate-release-config-test passed"