對齊 ADR-015:visionA backend 從 OAuth client_credentials 改 pre-shared API key 服務間認證。Phase 0.8 stage e2e 撞 4 個 blocker(MC scope 沒註冊 / converter image 舊版 / converter 缺 env / FAA 不確定)後,使用者拍板 1:1 internal trust 用 OAuth 過度設計,改 API key。
實作(5 個增量 task,T1-T5 全綠 + Reviewer 5 輪 + final cross-task review):
T1 config + env:
- ConversionConfig 新增 ConverterAPIKey / FAAAPIKey 欄位
- Enabled() 改判定 4 欄位齊全(含兩個 API key)
- .env*.example 移除 OIDC service client / OIDC tenant / FAA delegated TTL env、新增 API key env
- TenantID / DelegatedTTLSeconds T1 暫留、T5 整批清
T2 client 改造:
- converter_client / faa_client 移除 MCTokenClient 依賴
- 直接讀 cfg.Conversion.{Converter,FAA}APIKey、set Authorization: Bearer <key>
- NewConverterClient / NewFAAClient APIKey 為空時 panic(fail-fast,對齊 ADR-015 §3.5.3 #1)
- 新增 ErrConverterAuthFailed / ErrFAAAuthFailed sentinel
- 對外 mask 成 converter_unavailable / faa_unavailable(不洩漏 401 細節,對齊 ADR-015 §3.5.3 #3)
T3 砍 mc_token_client:
- mc_token_client.go (624 行) + mc_token_client_test.go (864 行) 整檔砍
- 砍 5 個僅 mc_token_client 用的 sentinel(ErrServiceClientUnauthorized / ErrMCTokenUnavailable / ErrIDPMisconfigured / ErrIDPUnavailable / ErrDownloadTokenFailed)
- helper(truncate / silentLogger)搬到 util.go / testing_helpers_test.go
T4 flow + handler stream proxy:
- Service interface DownloadRedirectURL → DownloadStream(ctx) (io.ReadCloser, *DownloadMetadata, error)
- flow.DownloadStream 用 faa.GetFile 直接 stream NEF binary(取代 MC delegated token + 302)
- handler conversionDownloadHandler 改 io.Copy + Content-Type/Disposition/Cache-Control header
- 新增 sanitizeDownloadFilename helper 防 HTTP header injection
- 跨 package handler test (conversion_test.go) 改測 ErrFAAUnavailable + 補 *_AuthFailed 對稱 test
T5 wire 切換 + cleanup:
- main.go 砍 mcTokenClient wire、改 APIKey 注入、startup log 用 *_api_key_set boolean(不印 key)
- ConverterClient/FAAClient struct Tokens 欄位移除
- mc_token_stub.go (T4 過渡期) 整檔砍
- ConversionConfig TenantID / DelegatedTTLSeconds 欄位移除
- e2e_test.go TestConversionE2E_Download302Redirect 改寫為 TestConversionE2E_DownloadStream
- 補 MaxDownloadStreamBytes = 1 GiB size cap(io.CopyN,T4 reviewer Minor M-1)
- escapeObjectKeyPath Phase 1+ 預留 godoc + //nolint:unused(T4 reviewer Minor M-2)
- conversion.md §4.1 line 502 filename 來源描述歧義修訂(T4 reviewer Minor M-3,由 architect 處理)
- conversion_e2e_test.go 檔頭 docstring 更新(final reviewer Minor #1)
驗證:
- go build ./... exit 0
- go test -race -count=3 ./... 17 packages 全綠
- ADR-015 §6 砍除清單 100% cover(reviewer 獨立 grep 確認 MC chain / TenantID / DelegatedTTLSeconds 全清)
- ADR-015 §3.5.3 部署檢查清單 visionA 範圍 4/4 達成(fail-fast / mask / 不印 token / placeholder env)
不動:
- OIDCConfig.ServiceClientID/Secret 欄位保留(使用者拍板 backward compat)
- user login OIDC 完全不動
下一步:
- 步驟 4 — converter scheduler middleware 改 API key(jimchen 跨 repo,ADR-015 §3.5.1 Go snippet)
- 步驟 5 — FAA middleware 改 API key(warrenchen 跨 repo,ADR-015 §3.5.2 C# snippet)
- 步驟 6 — stage redeploy + e2e 完整測試
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
386 lines
15 KiB
Go
386 lines
15 KiB
Go
package config
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func TestLoad_Defaults(t *testing.T) {
|
||
// Arrange:清掉所有相關 env(t.Setenv 自動還原)
|
||
for _, k := range []string{
|
||
"VISIONA_HOST", "VISIONA_API_PORT", "VISIONA_TUNNEL_PORT", "VISIONA_PROXY_INTERNAL_PORT",
|
||
"VISIONA_SESSION_BACKEND", "VISIONA_PROXY_INTERNAL_URL",
|
||
"VISIONA_AUTH_TYPE", "VISIONA_STATIC_USER_ID", "VISIONA_PAIRING_TOKEN",
|
||
"VISIONA_STORAGE_BACKEND", "VISIONA_STORAGE_LOCALFS_ROOT",
|
||
"VISIONA_MODEL_MAX_SIZE_MB",
|
||
"VISIONA_TUNNEL_HEARTBEAT_INTERVAL", "VISIONA_TUNNEL_IDLE_TIMEOUT",
|
||
"VISIONA_LOG_LEVEL",
|
||
} {
|
||
t.Setenv(k, "")
|
||
}
|
||
|
||
// Act
|
||
cfg := Load()
|
||
|
||
// Assert
|
||
assert.Equal(t, "0.0.0.0", cfg.Server.Host)
|
||
// Port 預設改為 3721 — 對齊 local-tool(B4)。
|
||
assert.Equal(t, 3721, cfg.Server.Port)
|
||
assert.Equal(t, 3800, cfg.Server.TunnelPort)
|
||
assert.Equal(t, 3801, cfg.Server.InternalPort)
|
||
assert.False(t, cfg.Server.SeedDemoData, "預設不 seed demo data")
|
||
assert.Equal(t, "inmemory", cfg.Session.Backend)
|
||
assert.Equal(t, "http://localhost:3801", cfg.Session.ProxyInternalURL)
|
||
assert.Equal(t, "demo-user", cfg.Auth.StaticUserID)
|
||
assert.Equal(t, "localfs", cfg.Storage.Backend)
|
||
assert.Equal(t, "./data/storage", cfg.Storage.RootDir)
|
||
assert.Equal(t, 100, cfg.Model.MaxSizeMB)
|
||
assert.Equal(t, 10*time.Second, cfg.Tunnel.HeartbeatInterval)
|
||
assert.Equal(t, 30*time.Second, cfg.Tunnel.IdleTimeout)
|
||
assert.Equal(t, "info", cfg.Logger.Level)
|
||
}
|
||
|
||
func TestLoad_EnvOverrides(t *testing.T) {
|
||
t.Setenv("VISIONA_API_PORT", "8080")
|
||
t.Setenv("VISIONA_STATIC_USER_ID", "custom-user")
|
||
t.Setenv("VISIONA_MODEL_MAX_SIZE_MB", "500")
|
||
t.Setenv("VISIONA_TUNNEL_HEARTBEAT_INTERVAL", "5s")
|
||
t.Setenv("VISIONA_LOG_LEVEL", "debug")
|
||
|
||
cfg := Load()
|
||
|
||
assert.Equal(t, 8080, cfg.Server.Port)
|
||
assert.Equal(t, "custom-user", cfg.Auth.StaticUserID)
|
||
assert.Equal(t, 500, cfg.Model.MaxSizeMB)
|
||
assert.Equal(t, 5*time.Second, cfg.Tunnel.HeartbeatInterval)
|
||
assert.Equal(t, "debug", cfg.Logger.Level)
|
||
}
|
||
|
||
func TestLoad_InvalidIntFallback(t *testing.T) {
|
||
t.Setenv("VISIONA_API_PORT", "not-a-number")
|
||
cfg := Load()
|
||
assert.Equal(t, 3721, cfg.Server.Port, "無法解析時應 fallback 到預設值(B4 改為 3721)")
|
||
}
|
||
|
||
// TestLoad_SeedDemoData 驗證 VISIONA_SEED_DEMO_DATA env 的解析行為。
|
||
func TestLoad_SeedDemoData(t *testing.T) {
|
||
t.Setenv("VISIONA_SEED_DEMO_DATA", "true")
|
||
cfg := Load()
|
||
assert.True(t, cfg.Server.SeedDemoData)
|
||
|
||
t.Setenv("VISIONA_SEED_DEMO_DATA", "false")
|
||
cfg = Load()
|
||
assert.False(t, cfg.Server.SeedDemoData)
|
||
|
||
// 無法解析時 fallback 到預設 false
|
||
t.Setenv("VISIONA_SEED_DEMO_DATA", "not-a-bool")
|
||
cfg = Load()
|
||
assert.False(t, cfg.Server.SeedDemoData, "無法解析時應 fallback 到預設值")
|
||
}
|
||
|
||
// TestLoad_OIDCDefaults 驗證未設定任何 VISIONA_OIDC_* 時,OIDC 欄位為空字串。
|
||
//
|
||
// OB5 起 OIDC.Enabled 已移除(OIDC 是唯一認證路徑);空字串就是「未設定」,
|
||
// 此時 Validate() 會回 MissingEnvError,main.go 啟動時 fatal log 退出。
|
||
func TestLoad_OIDCDefaults(t *testing.T) {
|
||
for _, k := range []string{
|
||
"VISIONA_OIDC_ISSUER_URL", "VISIONA_OIDC_CLIENT_ID",
|
||
"VISIONA_OIDC_CLIENT_SECRET", "VISIONA_OIDC_REDIRECT_URL", "VISIONA_FRONTEND_URL",
|
||
"VISIONA_OIDC_SERVICE_CLIENT_ID", "VISIONA_OIDC_SERVICE_CLIENT_SECRET",
|
||
"VISIONA_SESSION_SECRET", "VISIONA_SESSION_COOKIE_NAME", "VISIONA_SESSION_COOKIE_DOMAIN",
|
||
"VISIONA_SESSION_COOKIE_SECURE", "VISIONA_SESSION_ABSOLUTE_TTL", "VISIONA_SESSION_IDLE_TTL",
|
||
} {
|
||
t.Setenv(k, "")
|
||
}
|
||
|
||
cfg := Load()
|
||
|
||
assert.Empty(t, cfg.OIDC.IssuerURL)
|
||
assert.Empty(t, cfg.OIDC.ClientID)
|
||
assert.Empty(t, cfg.OIDC.ClientSecret)
|
||
assert.Empty(t, cfg.OIDC.RedirectURL)
|
||
assert.Empty(t, cfg.OIDC.PostLoginURL)
|
||
assert.Empty(t, cfg.OIDC.ServiceClientID, "ServiceClientID 預設留空(A1:未啟用)")
|
||
assert.Empty(t, cfg.OIDC.ServiceClientSecret, "ServiceClientSecret 預設留空(A1:未啟用)")
|
||
|
||
assert.Empty(t, cfg.UserSession.Secret, "雛形 dev 預設不附 secret,由 caller 注入或啟動失敗")
|
||
assert.Equal(t, "visiona_session", cfg.UserSession.CookieName)
|
||
assert.Empty(t, cfg.UserSession.CookieDomain)
|
||
assert.False(t, cfg.UserSession.CookieSecure)
|
||
assert.Equal(t, 168*time.Hour, cfg.UserSession.AbsoluteTTL)
|
||
assert.Equal(t, 24*time.Hour, cfg.UserSession.IdleTTL)
|
||
}
|
||
|
||
// TestLoad_OIDC_ClientSecretOptional:A1(2026-05-01)— 缺 ClientSecret 不再回 MissingEnvError。
|
||
//
|
||
// 模擬 Stage 用的 public PKCE-only client(MC 給的 b8093fea... 沒有 client_secret)。
|
||
func TestLoad_OIDC_ClientSecretOptional(t *testing.T) {
|
||
t.Setenv("VISIONA_OIDC_ISSUER_URL", "https://stage-9527.innovedus.com:7850/")
|
||
t.Setenv("VISIONA_OIDC_CLIENT_ID", "b8093fea1a504a5d8f0e04bee9f78f2e")
|
||
t.Setenv("VISIONA_OIDC_CLIENT_SECRET", "") // 故意留空 — public client
|
||
t.Setenv("VISIONA_OIDC_REDIRECT_URL", "https://stage-9527.innovedus.com:9527/api/auth/callback")
|
||
t.Setenv("VISIONA_FRONTEND_URL", "https://stage-9527.innovedus.com:9527")
|
||
t.Setenv("VISIONA_SESSION_SECRET", "32-byte-or-longer-random-secret-aaaa")
|
||
|
||
cfg := Load()
|
||
assert.Empty(t, cfg.OIDC.ClientSecret, "public client mode:ClientSecret 應為空字串")
|
||
assert.NoError(t, cfg.Validate(), "ClientSecret 為空不應觸發 MissingEnvError")
|
||
}
|
||
|
||
// TestLoad_OIDC_ServiceClientFields:A1 預留 client_credentials grant 兩個欄位能正確讀取。
|
||
// 測試固定值故意用顯而易見的 fake — 不要貼任何環境的真實 client_id / secret 進測試。
|
||
func TestLoad_OIDC_ServiceClientFields(t *testing.T) {
|
||
const fakeServiceID = "fake-service-client-id-for-test"
|
||
const fakeServiceSecret = "fake-service-client-secret-for-test"
|
||
|
||
t.Setenv("VISIONA_OIDC_SERVICE_CLIENT_ID", fakeServiceID)
|
||
t.Setenv("VISIONA_OIDC_SERVICE_CLIENT_SECRET", fakeServiceSecret)
|
||
|
||
cfg := Load()
|
||
assert.Equal(t, fakeServiceID, cfg.OIDC.ServiceClientID)
|
||
assert.Equal(t, fakeServiceSecret, cfg.OIDC.ServiceClientSecret)
|
||
}
|
||
|
||
// TestLoad_OIDCAllSet 驗證 OIDC env vars 設定後能正確讀取。
|
||
func TestLoad_OIDCAllSet(t *testing.T) {
|
||
t.Setenv("VISIONA_OIDC_ISSUER_URL", "http://localhost:5050")
|
||
t.Setenv("VISIONA_OIDC_CLIENT_ID", "visionA")
|
||
t.Setenv("VISIONA_OIDC_CLIENT_SECRET", "secret")
|
||
t.Setenv("VISIONA_OIDC_REDIRECT_URL", "http://localhost:3721/api/auth/callback")
|
||
t.Setenv("VISIONA_FRONTEND_URL", "http://localhost:3000")
|
||
t.Setenv("VISIONA_SESSION_SECRET", "32-byte-or-longer-random-secret-aaaa")
|
||
t.Setenv("VISIONA_SESSION_COOKIE_SECURE", "true")
|
||
t.Setenv("VISIONA_SESSION_ABSOLUTE_TTL", "72h")
|
||
t.Setenv("VISIONA_SESSION_IDLE_TTL", "12h")
|
||
|
||
cfg := Load()
|
||
|
||
assert.Equal(t, "http://localhost:5050", cfg.OIDC.IssuerURL)
|
||
assert.Equal(t, "visionA", cfg.OIDC.ClientID)
|
||
assert.Equal(t, "secret", cfg.OIDC.ClientSecret)
|
||
assert.Equal(t, "http://localhost:3721/api/auth/callback", cfg.OIDC.RedirectURL)
|
||
assert.Equal(t, "http://localhost:3000", cfg.OIDC.PostLoginURL)
|
||
assert.Equal(t, "32-byte-or-longer-random-secret-aaaa", cfg.UserSession.Secret)
|
||
assert.True(t, cfg.UserSession.CookieSecure)
|
||
assert.Equal(t, 72*time.Hour, cfg.UserSession.AbsoluteTTL)
|
||
assert.Equal(t, 12*time.Hour, cfg.UserSession.IdleTTL)
|
||
}
|
||
|
||
// TestConfig_Validate_MissingFields 驗證 OIDC 必填欄位缺失時回 MissingEnvError。
|
||
//
|
||
// A1(2026-05-01):ClientSecret 改為選填,已從必填清單移除;剩 5 項必填。
|
||
func TestConfig_Validate_MissingFields(t *testing.T) {
|
||
cfg := &Config{} // 全部欄位 zero value
|
||
err := cfg.Validate()
|
||
require.Error(t, err)
|
||
|
||
var missErr *MissingEnvError
|
||
require.ErrorAs(t, err, &missErr, "錯誤型別應可被 errors.As 解出")
|
||
// 應列出 5 個必填欄位(不含 ClientSecret)
|
||
assert.ElementsMatch(t, []string{
|
||
"VISIONA_OIDC_ISSUER_URL",
|
||
"VISIONA_OIDC_CLIENT_ID",
|
||
"VISIONA_OIDC_REDIRECT_URL",
|
||
"VISIONA_FRONTEND_URL",
|
||
"VISIONA_SESSION_SECRET",
|
||
}, missErr.Vars)
|
||
assert.NotContains(t, missErr.Vars, "VISIONA_OIDC_CLIENT_SECRET",
|
||
"A1:ClientSecret 為選填,不應出現在必填缺失清單")
|
||
}
|
||
|
||
// TestValidate_ConfidentialClient:完整 confidential client(含 ClientSecret)能通過 Validate。
|
||
func TestValidate_ConfidentialClient(t *testing.T) {
|
||
cfg := &Config{
|
||
OIDC: OIDCConfig{
|
||
IssuerURL: "http://localhost:5050",
|
||
ClientID: "visionA",
|
||
ClientSecret: "secret", // 有值 → confidential mode
|
||
RedirectURL: "http://localhost:3721/api/auth/callback",
|
||
PostLoginURL: "http://localhost:3000",
|
||
},
|
||
UserSession: UserSessionConfig{Secret: "session-secret-32-bytes-aaaaaaaaaaaa"},
|
||
}
|
||
assert.NoError(t, cfg.Validate())
|
||
}
|
||
|
||
// TestValidate_PKCEOnlyPublicClient:A1 — 只給 ClientID 沒給 Secret 也能通過 Validate。
|
||
//
|
||
// 對應 Stage 部署的真實情境:MC 配給 visionA 的 client `b8093fea1a504a5d8f0e04bee9f78f2e`
|
||
// 是 public client,沒有 client_secret,靠 PKCE 防 code interception。
|
||
func TestValidate_PKCEOnlyPublicClient(t *testing.T) {
|
||
cfg := &Config{
|
||
OIDC: OIDCConfig{
|
||
IssuerURL: "https://stage-9527.innovedus.com:7850/",
|
||
ClientID: "b8093fea1a504a5d8f0e04bee9f78f2e",
|
||
// ClientSecret 留空 — public PKCE-only client
|
||
RedirectURL: "https://stage-9527.innovedus.com:9527/api/auth/callback",
|
||
PostLoginURL: "https://stage-9527.innovedus.com:9527",
|
||
},
|
||
UserSession: UserSessionConfig{Secret: "session-secret-32-bytes-aaaaaaaaaaaa"},
|
||
}
|
||
assert.NoError(t, cfg.Validate(),
|
||
"A1:public PKCE-only client(ClientSecret 留空)應通過 Validate")
|
||
}
|
||
|
||
// TestValidate_ServiceClientFieldsNotChecked:A1 — ServiceClientID/Secret 留空不影響 Validate。
|
||
//
|
||
// 兩個欄位是 client_credentials grant 預留鉤子,A1 階段不啟用、不檢查。
|
||
func TestValidate_ServiceClientFieldsNotChecked(t *testing.T) {
|
||
cfg := &Config{
|
||
OIDC: OIDCConfig{
|
||
IssuerURL: "http://localhost:5050",
|
||
ClientID: "visionA",
|
||
RedirectURL: "http://localhost:3721/api/auth/callback",
|
||
PostLoginURL: "http://localhost:3000",
|
||
// 兩個 Service* 都留空 — 預期通過
|
||
},
|
||
UserSession: UserSessionConfig{Secret: "session-secret-32-bytes-aaaaaaaaaaaa"},
|
||
}
|
||
assert.NoError(t, cfg.Validate())
|
||
}
|
||
|
||
// TestLoad_CORSAllowedOrigins 驗證 VISIONA_CORS_ALLOWED_ORIGINS 的逗號分隔解析。
|
||
// 空字串 / 純分隔字元 → fallback 到 nil(交由 api.Deps.validate 塞預設)。
|
||
func TestLoad_CORSAllowedOrigins(t *testing.T) {
|
||
// 未設 → nil
|
||
t.Setenv("VISIONA_CORS_ALLOWED_ORIGINS", "")
|
||
cfg := Load()
|
||
assert.Nil(t, cfg.CORS.AllowedOrigins)
|
||
|
||
// 單一 origin
|
||
t.Setenv("VISIONA_CORS_ALLOWED_ORIGINS", "http://localhost:3000")
|
||
cfg = Load()
|
||
assert.Equal(t, []string{"http://localhost:3000"}, cfg.CORS.AllowedOrigins)
|
||
|
||
// 多個 origin + trim space
|
||
t.Setenv("VISIONA_CORS_ALLOWED_ORIGINS", "http://a.com, http://b.com ,http://c.com")
|
||
cfg = Load()
|
||
assert.Equal(t, []string{"http://a.com", "http://b.com", "http://c.com"}, cfg.CORS.AllowedOrigins)
|
||
|
||
// 只有分隔字元 → fallback(過濾後 len == 0)
|
||
t.Setenv("VISIONA_CORS_ALLOWED_ORIGINS", " , ,")
|
||
cfg = Load()
|
||
assert.Nil(t, cfg.CORS.AllowedOrigins)
|
||
}
|
||
|
||
// TestLoad_ConversionDefaults 驗證 Phase 0.8 / 0.8b conversion 欄位的預設行為。
|
||
//
|
||
// 對齊 .autoflow/04-architecture/conversion.md §3 + ADR-015:留空時 Enabled() 為 false,
|
||
// 5 個 endpoint 不會 wire(main.go 在 wire 階段會跳過)。
|
||
//
|
||
// Phase 0.8b T5:原暫留欄位 TenantID / DelegatedTTLSeconds 已從 ConversionConfig 移除
|
||
// (MC 認證鏈與 delegated download token 機制不存在了);本 test 不再驗這兩欄位。
|
||
func TestLoad_ConversionDefaults(t *testing.T) {
|
||
for _, k := range []string{
|
||
"VISIONA_CONVERTER_BASE_URL", "VISIONA_FAA_BASE_URL",
|
||
"VISIONA_CONVERTER_API_KEY", "VISIONA_FAA_API_KEY",
|
||
"VISIONA_CONVERTER_MAX_MODEL_SIZE_MB",
|
||
} {
|
||
t.Setenv(k, "")
|
||
}
|
||
|
||
cfg := Load()
|
||
assert.Empty(t, cfg.Conversion.ConverterBaseURL)
|
||
assert.Empty(t, cfg.Conversion.FAABaseURL)
|
||
assert.Empty(t, cfg.Conversion.ConverterAPIKey, "Phase 0.8b:API key 預設留空")
|
||
assert.Empty(t, cfg.Conversion.FAAAPIKey, "Phase 0.8b:API key 預設留空")
|
||
assert.Equal(t, 500, cfg.Conversion.MaxModelSizeMB, "預設 500 MB(與 converter 對齊)")
|
||
assert.False(t, cfg.Conversion.Enabled(), "全空 → 不啟用")
|
||
}
|
||
|
||
// TestLoad_ConversionEnabled 驗證 Conversion.Enabled() 的判定邏輯(Phase 0.8b 修訂)。
|
||
//
|
||
// Phase 0.8b 變更:4 個欄位(Converter URL / FAA URL / Converter API key / FAA API key)
|
||
// 全部非空才視為啟用;任一缺即 disable。
|
||
func TestLoad_ConversionEnabled(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
converterURL string
|
||
faaURL string
|
||
converterKey string
|
||
faaKey string
|
||
wantEnabled bool
|
||
}{
|
||
{"all_set_enables",
|
||
"http://converter:9501", "http://faa:5081",
|
||
"converter-key-32-bytes-hex-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||
"faa-key-32-bytes-hex-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||
true},
|
||
{"missing_converter_url_disabled",
|
||
"", "http://faa:5081",
|
||
"converter-key", "faa-key",
|
||
false},
|
||
{"missing_faa_url_disabled",
|
||
"http://converter:9501", "",
|
||
"converter-key", "faa-key",
|
||
false},
|
||
{"missing_converter_key_disabled",
|
||
"http://converter:9501", "http://faa:5081",
|
||
"", "faa-key",
|
||
false},
|
||
{"missing_faa_key_disabled",
|
||
"http://converter:9501", "http://faa:5081",
|
||
"converter-key", "",
|
||
false},
|
||
{"all_empty_disabled",
|
||
"", "", "", "",
|
||
false},
|
||
}
|
||
|
||
for _, tc := range cases {
|
||
tc := tc
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
t.Setenv("VISIONA_CONVERTER_BASE_URL", tc.converterURL)
|
||
t.Setenv("VISIONA_FAA_BASE_URL", tc.faaURL)
|
||
t.Setenv("VISIONA_CONVERTER_API_KEY", tc.converterKey)
|
||
t.Setenv("VISIONA_FAA_API_KEY", tc.faaKey)
|
||
cfg := Load()
|
||
assert.Equal(t, tc.wantEnabled, cfg.Conversion.Enabled())
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestLoad_ConversionAllSet 驗證 Phase 0.8b 所有欄位設定後正確讀取。
|
||
//
|
||
// Phase 0.8b T5:原暫留欄位 TenantID / DelegatedTTLSeconds 已移除,本 test
|
||
// 不再驗這兩欄位(對應 env 也不再讀取)。
|
||
func TestLoad_ConversionAllSet(t *testing.T) {
|
||
const fakeConverterKey = "fake-converter-api-key-for-test-do-not-use-in-prod"
|
||
const fakeFAAKey = "fake-faa-api-key-for-test-do-not-use-in-prod"
|
||
|
||
t.Setenv("VISIONA_CONVERTER_BASE_URL", "http://192.168.0.130:9501")
|
||
t.Setenv("VISIONA_FAA_BASE_URL", "http://192.168.0.130:5081")
|
||
t.Setenv("VISIONA_CONVERTER_API_KEY", fakeConverterKey)
|
||
t.Setenv("VISIONA_FAA_API_KEY", fakeFAAKey)
|
||
t.Setenv("VISIONA_CONVERTER_MAX_MODEL_SIZE_MB", "300")
|
||
|
||
cfg := Load()
|
||
assert.Equal(t, "http://192.168.0.130:9501", cfg.Conversion.ConverterBaseURL)
|
||
assert.Equal(t, "http://192.168.0.130:5081", cfg.Conversion.FAABaseURL)
|
||
assert.Equal(t, fakeConverterKey, cfg.Conversion.ConverterAPIKey)
|
||
assert.Equal(t, fakeFAAKey, cfg.Conversion.FAAAPIKey)
|
||
assert.Equal(t, 300, cfg.Conversion.MaxModelSizeMB)
|
||
assert.True(t, cfg.Conversion.Enabled())
|
||
}
|
||
|
||
// TestLoad_ConversionAPIKeysOnly:Phase 0.8b T5 — 4 個必要欄位齊全即 Enabled。
|
||
//
|
||
// 此 test 在 T1-T4 期間驗證「廢棄 env 不設也能 Enabled」;T5 完成後該邏輯
|
||
// 由本 test 與 TestLoad_ConversionAllSet 共同覆蓋(因為廢棄 env 已徹底移除)。
|
||
func TestLoad_ConversionAPIKeysOnly(t *testing.T) {
|
||
const fakeConverterKey = "fake-converter-api-key-only-test"
|
||
const fakeFAAKey = "fake-faa-api-key-only-test"
|
||
|
||
t.Setenv("VISIONA_CONVERTER_BASE_URL", "http://192.168.0.130:9501")
|
||
t.Setenv("VISIONA_FAA_BASE_URL", "http://192.168.0.130:5081")
|
||
t.Setenv("VISIONA_CONVERTER_API_KEY", fakeConverterKey)
|
||
t.Setenv("VISIONA_FAA_API_KEY", fakeFAAKey)
|
||
|
||
cfg := Load()
|
||
assert.True(t, cfg.Conversion.Enabled(),
|
||
"Phase 0.8b T5:4 個必要欄位齊全即 Enabled")
|
||
}
|