From 32e780a18b072fe670221571c86ee0b92fa222f0 Mon Sep 17 00:00:00 2001 From: Mihai Mitrea Date: Fri, 10 Apr 2026 13:13:59 +0000 Subject: [PATCH 1/5] Fix auth profiles misclassifying SPOG hosts as workspace configs SPOG hosts (e.g. db-deco-test.gcp.databricks.com) don't match the accounts.* URL prefix, so ConfigType() classifies them as WorkspaceConfig. This causes `auth profiles` to validate with CurrentUser.Me instead of Workspaces.List, which fails for account-scoped SPOG profiles. Use the resolved DiscoveryURL from .well-known/databricks-config to detect SPOG hosts with account-scoped OIDC, matching the routing logic in auth.AuthArguments.ToOAuthArgument(). Also add a fallback for legacy profiles with Experimental_IsUnifiedHost where .well-known is unreachable. --- cmd/auth/profiles.go | 27 +++++- cmd/auth/profiles_test.go | 191 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 1 deletion(-) diff --git a/cmd/auth/profiles.go b/cmd/auth/profiles.go index 38ba3599fc..da76eceecb 100644 --- a/cmd/auth/profiles.go +++ b/cmd/auth/profiles.go @@ -5,9 +5,11 @@ import ( "errors" "fmt" "io/fs" + "strings" "sync" "time" + "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/databrickscfg" "github.com/databricks/cli/libs/databrickscfg/profile" @@ -56,7 +58,30 @@ func (c *profileMetadata) Load(ctx context.Context, configFilePath string, skipV return } - switch cfg.ConfigType() { + // ConfigType() classifies based on the host URL prefix (accounts.* → + // AccountConfig, everything else → WorkspaceConfig). SPOG hosts don't + // match the accounts.* prefix so they're misclassified as WorkspaceConfig. + // Use the resolved DiscoveryURL (from .well-known/databricks-config) to + // detect SPOG hosts with account-scoped OIDC, matching the routing logic + // in auth.AuthArguments.ToOAuthArgument(). + configType := cfg.ConfigType() + isAccountScopedOIDC := cfg.DiscoveryURL != "" && strings.Contains(cfg.DiscoveryURL, "/oidc/accounts/") + if configType != config.AccountConfig && cfg.AccountID != "" && isAccountScopedOIDC { + if cfg.WorkspaceID != "" && cfg.WorkspaceID != auth.WorkspaceIDNone { + configType = config.WorkspaceConfig + } else { + configType = config.AccountConfig + } + } + + // Legacy backward compat: profiles with Experimental_IsUnifiedHost where + // .well-known is unreachable (so DiscoveryURL is empty). Matches the + // fallback in auth.AuthArguments.ToOAuthArgument(). + if configType == config.InvalidConfig && cfg.Experimental_IsUnifiedHost && cfg.AccountID != "" { + configType = config.AccountConfig + } + + switch configType { case config.AccountConfig: a, err := databricks.NewAccountClient((*databricks.Config)(cfg)) if err != nil { diff --git a/cmd/auth/profiles_test.go b/cmd/auth/profiles_test.go index afd7b0b548..63185c73fd 100644 --- a/cmd/auth/profiles_test.go +++ b/cmd/auth/profiles_test.go @@ -1,6 +1,10 @@ package auth import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" "path/filepath" "runtime" "testing" @@ -74,3 +78,190 @@ func TestProfilesDefaultMarker(t *testing.T) { require.NoError(t, err) assert.Equal(t, "profile-a", defaultProfile) } + +// newProfileTestServer creates a mock server for profile validation tests. +// It serves /.well-known/databricks-config with the given OIDC shape and +// responds to the workspace/account validation API endpoints. +func newProfileTestServer(t *testing.T, accountScoped bool, accountID string) *httptest.Server { + t.Helper() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch { + case r.URL.Path == "/.well-known/databricks-config": + oidcEndpoint := r.Host + "/oidc" + if accountScoped { + oidcEndpoint = r.Host + "/oidc/accounts/" + accountID + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "account_id": accountID, + "oidc_endpoint": oidcEndpoint, + }) + case r.URL.Path == "/api/2.0/preview/scim/v2/Me": + _ = json.NewEncoder(w).Encode(map[string]any{ + "userName": "test-user", + }) + case r.URL.Path == "/api/2.0/accounts/"+accountID+"/workspaces": + _ = json.NewEncoder(w).Encode([]map[string]any{}) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + t.Cleanup(server.Close) + return server +} + +func TestProfileLoadSPOGConfigType(t *testing.T) { + spogServer := newProfileTestServer(t, true, "spog-acct") + wsServer := newProfileTestServer(t, false, "ws-acct") + + cases := []struct { + name string + host string + accountID string + workspaceID string + wantValid bool + wantConfigCloud string + }{ + { + name: "SPOG account profile validated as account", + host: spogServer.URL, + accountID: "spog-acct", + wantValid: true, + }, + { + name: "SPOG workspace profile validated as workspace", + host: spogServer.URL, + accountID: "spog-acct", + workspaceID: "ws-123", + wantValid: true, + }, + { + name: "SPOG profile with workspace_id=none validated as account", + host: spogServer.URL, + accountID: "spog-acct", + workspaceID: "none", + wantValid: true, + }, + { + name: "classic workspace with account_id from discovery stays workspace", + host: wsServer.URL, + accountID: "ws-acct", + wantValid: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + configFile := filepath.Join(dir, ".databrickscfg") + t.Setenv("HOME", dir) + if runtime.GOOS == "windows" { + t.Setenv("USERPROFILE", dir) + } + + content := "[test-profile]\nhost = " + tc.host + "\ntoken = test-token\n" + if tc.accountID != "" { + content += "account_id = " + tc.accountID + "\n" + } + if tc.workspaceID != "" { + content += "workspace_id = " + tc.workspaceID + "\n" + } + require.NoError(t, os.WriteFile(configFile, []byte(content), 0o600)) + + p := &profileMetadata{ + Name: "test-profile", + Host: tc.host, + AccountID: tc.accountID, + } + p.Load(t.Context(), configFile, false) + + assert.Equal(t, tc.wantValid, p.Valid, "Valid mismatch") + assert.NotEmpty(t, p.Host, "Host should be set") + assert.NotEmpty(t, p.AuthType, "AuthType should be set") + }) + } +} + +func TestProfileLoadUnifiedHostFallback(t *testing.T) { + // When Experimental_IsUnifiedHost is set but .well-known is unreachable, + // ConfigType() returns InvalidConfig. The fallback should reclassify as + // AccountConfig so the profile is still validated. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch { + case r.URL.Path == "/.well-known/databricks-config": + // Simulate unreachable/missing .well-known endpoint + w.WriteHeader(http.StatusNotFound) + case r.URL.Path == "/api/2.0/accounts/unified-acct/workspaces": + _ = json.NewEncoder(w).Encode([]map[string]any{}) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + t.Cleanup(server.Close) + + dir := t.TempDir() + configFile := filepath.Join(dir, ".databrickscfg") + t.Setenv("HOME", dir) + if runtime.GOOS == "windows" { + t.Setenv("USERPROFILE", dir) + } + + content := "[unified-profile]\nhost = " + server.URL + "\ntoken = test-token\naccount_id = unified-acct\nexperimental_is_unified_host = true\n" + require.NoError(t, os.WriteFile(configFile, []byte(content), 0o600)) + + p := &profileMetadata{ + Name: "unified-profile", + Host: server.URL, + AccountID: "unified-acct", + } + p.Load(t.Context(), configFile, false) + + assert.True(t, p.Valid, "unified host profile should be valid via fallback") + assert.NotEmpty(t, p.Host) + assert.NotEmpty(t, p.AuthType) +} + +func TestProfileLoadClassicAccountHost(t *testing.T) { + // Classic accounts.* hosts are already correctly classified as AccountConfig + // by ConfigType(). Verify that behavior is preserved. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch { + case r.URL.Path == "/.well-known/databricks-config": + _ = json.NewEncoder(w).Encode(map[string]any{ + "account_id": "classic-acct", + "oidc_endpoint": r.Host + "/oidc/accounts/classic-acct", + }) + case r.URL.Path == "/api/2.0/accounts/classic-acct/workspaces": + _ = json.NewEncoder(w).Encode([]map[string]any{}) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + t.Cleanup(server.Close) + + dir := t.TempDir() + configFile := filepath.Join(dir, ".databrickscfg") + t.Setenv("HOME", dir) + if runtime.GOOS == "windows" { + t.Setenv("USERPROFILE", dir) + } + + // Use the test server URL but override the host to look like an accounts host. + // Since we can't actually use accounts.cloud.databricks.com in tests, we verify + // indirectly: a SPOG profile without workspace_id should be validated as account. + content := "[acct-profile]\nhost = " + server.URL + "\ntoken = test-token\naccount_id = classic-acct\n" + require.NoError(t, os.WriteFile(configFile, []byte(content), 0o600)) + + p := &profileMetadata{ + Name: "acct-profile", + Host: server.URL, + AccountID: "classic-acct", + } + p.Load(t.Context(), configFile, false) + + assert.True(t, p.Valid, "classic account profile should be valid") + assert.NotEmpty(t, p.Host) + assert.NotEmpty(t, p.AuthType) +} From 7b8d12242993d267ec42474dae2ccab04e3531a9 Mon Sep 17 00:00:00 2001 From: Mihai Mitrea Date: Fri, 10 Apr 2026 13:19:22 +0000 Subject: [PATCH 2/5] Add acceptance test for SPOG account profile validation --- .../auth/profiles/spog-account/out.test.toml | 5 +++++ .../cmd/auth/profiles/spog-account/output.txt | 16 +++++++++++++++ .../cmd/auth/profiles/spog-account/script | 15 ++++++++++++++ .../cmd/auth/profiles/spog-account/test.toml | 20 +++++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 acceptance/cmd/auth/profiles/spog-account/out.test.toml create mode 100644 acceptance/cmd/auth/profiles/spog-account/output.txt create mode 100644 acceptance/cmd/auth/profiles/spog-account/script create mode 100644 acceptance/cmd/auth/profiles/spog-account/test.toml diff --git a/acceptance/cmd/auth/profiles/spog-account/out.test.toml b/acceptance/cmd/auth/profiles/spog-account/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/cmd/auth/profiles/spog-account/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/auth/profiles/spog-account/output.txt b/acceptance/cmd/auth/profiles/spog-account/output.txt new file mode 100644 index 0000000000..f5ce0ac53c --- /dev/null +++ b/acceptance/cmd/auth/profiles/spog-account/output.txt @@ -0,0 +1,16 @@ + +=== SPOG account profile should be valid +>>> [CLI] auth profiles --output json +{ + "profiles": [ + { + "name":"spog-account", + "host":"[DATABRICKS_URL]", + "account_id":"spog-acct-123", + "workspace_id":"none", + "cloud":"aws", + "auth_type":"pat", + "valid":true + } + ] +} diff --git a/acceptance/cmd/auth/profiles/spog-account/script b/acceptance/cmd/auth/profiles/spog-account/script new file mode 100644 index 0000000000..64285ad0ec --- /dev/null +++ b/acceptance/cmd/auth/profiles/spog-account/script @@ -0,0 +1,15 @@ +sethome "./home" + +# Create a SPOG account profile: non-accounts.* host with account_id, no workspace_id. +# Before the fix, this was misclassified as WorkspaceConfig and validated with +# CurrentUser.Me, which fails on account-scoped SPOG hosts. +cat > "./home/.databrickscfg" < Date: Fri, 10 Apr 2026 13:26:44 +0000 Subject: [PATCH 3/5] Use tagged switch to fix staticcheck QF1002 lint errors --- cmd/auth/profiles_test.go | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/cmd/auth/profiles_test.go b/cmd/auth/profiles_test.go index 63185c73fd..edbdd25fb5 100644 --- a/cmd/auth/profiles_test.go +++ b/cmd/auth/profiles_test.go @@ -86,8 +86,8 @@ func newProfileTestServer(t *testing.T, accountScoped bool, accountID string) *h t.Helper() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - switch { - case r.URL.Path == "/.well-known/databricks-config": + switch r.URL.Path { + case "/.well-known/databricks-config": oidcEndpoint := r.Host + "/oidc" if accountScoped { oidcEndpoint = r.Host + "/oidc/accounts/" + accountID @@ -96,11 +96,11 @@ func newProfileTestServer(t *testing.T, accountScoped bool, accountID string) *h "account_id": accountID, "oidc_endpoint": oidcEndpoint, }) - case r.URL.Path == "/api/2.0/preview/scim/v2/Me": + case "/api/2.0/preview/scim/v2/Me": _ = json.NewEncoder(w).Encode(map[string]any{ "userName": "test-user", }) - case r.URL.Path == "/api/2.0/accounts/"+accountID+"/workspaces": + case "/api/2.0/accounts/" + accountID + "/workspaces": _ = json.NewEncoder(w).Encode([]map[string]any{}) default: w.WriteHeader(http.StatusNotFound) @@ -188,11 +188,10 @@ func TestProfileLoadUnifiedHostFallback(t *testing.T) { // AccountConfig so the profile is still validated. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - switch { - case r.URL.Path == "/.well-known/databricks-config": - // Simulate unreachable/missing .well-known endpoint + switch r.URL.Path { + case "/.well-known/databricks-config": w.WriteHeader(http.StatusNotFound) - case r.URL.Path == "/api/2.0/accounts/unified-acct/workspaces": + case "/api/2.0/accounts/unified-acct/workspaces": _ = json.NewEncoder(w).Encode([]map[string]any{}) default: w.WriteHeader(http.StatusNotFound) @@ -227,13 +226,13 @@ func TestProfileLoadClassicAccountHost(t *testing.T) { // by ConfigType(). Verify that behavior is preserved. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - switch { - case r.URL.Path == "/.well-known/databricks-config": + switch r.URL.Path { + case "/.well-known/databricks-config": _ = json.NewEncoder(w).Encode(map[string]any{ "account_id": "classic-acct", "oidc_endpoint": r.Host + "/oidc/accounts/classic-acct", }) - case r.URL.Path == "/api/2.0/accounts/classic-acct/workspaces": + case "/api/2.0/accounts/classic-acct/workspaces": _ = json.NewEncoder(w).Encode([]map[string]any{}) default: w.WriteHeader(http.StatusNotFound) From a7198ac502491ace7f697017d87a0f1e5980f618 Mon Sep 17 00:00:00 2001 From: Mihai Mitrea Date: Mon, 13 Apr 2026 09:41:42 +0000 Subject: [PATCH 4/5] Clean up test: remove unused field, fix misleading comment --- cmd/auth/profiles_test.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/cmd/auth/profiles_test.go b/cmd/auth/profiles_test.go index edbdd25fb5..8dad3a2679 100644 --- a/cmd/auth/profiles_test.go +++ b/cmd/auth/profiles_test.go @@ -115,12 +115,11 @@ func TestProfileLoadSPOGConfigType(t *testing.T) { wsServer := newProfileTestServer(t, false, "ws-acct") cases := []struct { - name string - host string - accountID string - workspaceID string - wantValid bool - wantConfigCloud string + name string + host string + accountID string + workspaceID string + wantValid bool }{ { name: "SPOG account profile validated as account", @@ -222,8 +221,8 @@ func TestProfileLoadUnifiedHostFallback(t *testing.T) { } func TestProfileLoadClassicAccountHost(t *testing.T) { - // Classic accounts.* hosts are already correctly classified as AccountConfig - // by ConfigType(). Verify that behavior is preserved. + // Verify that a host with account-scoped OIDC from discovery is validated + // as an account config (via Workspaces.List, not CurrentUser.Me). server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.URL.Path { @@ -247,9 +246,6 @@ func TestProfileLoadClassicAccountHost(t *testing.T) { t.Setenv("USERPROFILE", dir) } - // Use the test server URL but override the host to look like an accounts host. - // Since we can't actually use accounts.cloud.databricks.com in tests, we verify - // indirectly: a SPOG profile without workspace_id should be validated as account. content := "[acct-profile]\nhost = " + server.URL + "\ntoken = test-token\naccount_id = classic-acct\n" require.NoError(t, os.WriteFile(configFile, []byte(content), 0o600)) From c680516df717d10c279447718a7cf7ad67d6a759 Mon Sep 17 00:00:00 2001 From: Mihai Mitrea Date: Mon, 13 Apr 2026 10:51:59 +0000 Subject: [PATCH 5/5] Address PR review comments - Add workspace_id branching to legacy IsUnifiedHost fallback - Rename TestProfileLoadClassicAccountHost to TestProfileLoadSPOGAccountWithDiscovery - Extract hasWorkspace variable to deduplicate condition - Add negative test: no discovery + account_id stays WorkspaceConfig - Fix misleading mock server comments --- cmd/auth/profiles.go | 19 +++++--- cmd/auth/profiles_test.go | 93 ++++++++++++++++++++++++++++++++------- 2 files changed, 90 insertions(+), 22 deletions(-) diff --git a/cmd/auth/profiles.go b/cmd/auth/profiles.go index da76eceecb..c7e5a7d04c 100644 --- a/cmd/auth/profiles.go +++ b/cmd/auth/profiles.go @@ -65,20 +65,29 @@ func (c *profileMetadata) Load(ctx context.Context, configFilePath string, skipV // detect SPOG hosts with account-scoped OIDC, matching the routing logic // in auth.AuthArguments.ToOAuthArgument(). configType := cfg.ConfigType() + hasWorkspace := cfg.WorkspaceID != "" && cfg.WorkspaceID != auth.WorkspaceIDNone + isAccountScopedOIDC := cfg.DiscoveryURL != "" && strings.Contains(cfg.DiscoveryURL, "/oidc/accounts/") if configType != config.AccountConfig && cfg.AccountID != "" && isAccountScopedOIDC { - if cfg.WorkspaceID != "" && cfg.WorkspaceID != auth.WorkspaceIDNone { + if hasWorkspace { configType = config.WorkspaceConfig } else { configType = config.AccountConfig } } - // Legacy backward compat: profiles with Experimental_IsUnifiedHost where - // .well-known is unreachable (so DiscoveryURL is empty). Matches the - // fallback in auth.AuthArguments.ToOAuthArgument(). + // Legacy backward compat: SDK v0.126.0 removed the UnifiedHost case from + // ConfigType(), so profiles with Experimental_IsUnifiedHost now get + // InvalidConfig instead of being routed to account/workspace validation. + // When .well-known is also unreachable (DiscoveryURL empty), the override + // above can't help. Fall back to workspace_id to choose the validation + // strategy, matching the IsUnifiedHost fallback in ToOAuthArgument(). if configType == config.InvalidConfig && cfg.Experimental_IsUnifiedHost && cfg.AccountID != "" { - configType = config.AccountConfig + if hasWorkspace { + configType = config.WorkspaceConfig + } else { + configType = config.AccountConfig + } } switch configType { diff --git a/cmd/auth/profiles_test.go b/cmd/auth/profiles_test.go index 8dad3a2679..42cd5fd3b2 100644 --- a/cmd/auth/profiles_test.go +++ b/cmd/auth/profiles_test.go @@ -79,29 +79,49 @@ func TestProfilesDefaultMarker(t *testing.T) { assert.Equal(t, "profile-a", defaultProfile) } -// newProfileTestServer creates a mock server for profile validation tests. -// It serves /.well-known/databricks-config with the given OIDC shape and -// responds to the workspace/account validation API endpoints. -func newProfileTestServer(t *testing.T, accountScoped bool, accountID string) *httptest.Server { +// newSPOGServer creates a mock SPOG server that returns account-scoped OIDC. +// It serves both validation endpoints since SPOG workspace profiles (with a +// real workspace_id) need CurrentUser.Me, while account profiles need +// Workspaces.List. The workspace-only newWorkspaceServer omits the account +// endpoint to prove routing correctness for non-SPOG hosts. +func newSPOGServer(t *testing.T, accountID string) *httptest.Server { t.Helper() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.URL.Path { case "/.well-known/databricks-config": - oidcEndpoint := r.Host + "/oidc" - if accountScoped { - oidcEndpoint = r.Host + "/oidc/accounts/" + accountID - } _ = json.NewEncoder(w).Encode(map[string]any{ "account_id": accountID, - "oidc_endpoint": oidcEndpoint, + "oidc_endpoint": r.Host + "/oidc/accounts/" + accountID, }) + case "/api/2.0/accounts/" + accountID + "/workspaces": + _ = json.NewEncoder(w).Encode([]map[string]any{}) case "/api/2.0/preview/scim/v2/Me": + // SPOG workspace profiles also need CurrentUser.Me to succeed. + _ = json.NewEncoder(w).Encode(map[string]any{"userName": "test-user"}) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + t.Cleanup(server.Close) + return server +} + +// newWorkspaceServer creates a mock workspace server that returns workspace-scoped +// OIDC and only serves the workspace validation endpoint. The account validation +// endpoint returns 404 to prove the workspace path was taken. +func newWorkspaceServer(t *testing.T, accountID string) *httptest.Server { + t.Helper() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/.well-known/databricks-config": _ = json.NewEncoder(w).Encode(map[string]any{ - "userName": "test-user", + "account_id": accountID, + "oidc_endpoint": r.Host + "/oidc", }) - case "/api/2.0/accounts/" + accountID + "/workspaces": - _ = json.NewEncoder(w).Encode([]map[string]any{}) + case "/api/2.0/preview/scim/v2/Me": + _ = json.NewEncoder(w).Encode(map[string]any{"userName": "test-user"}) default: w.WriteHeader(http.StatusNotFound) } @@ -111,8 +131,8 @@ func newProfileTestServer(t *testing.T, accountScoped bool, accountID string) *h } func TestProfileLoadSPOGConfigType(t *testing.T) { - spogServer := newProfileTestServer(t, true, "spog-acct") - wsServer := newProfileTestServer(t, false, "ws-acct") + spogServer := newSPOGServer(t, "spog-acct") + wsServer := newWorkspaceServer(t, "ws-acct") cases := []struct { name string @@ -220,9 +240,9 @@ func TestProfileLoadUnifiedHostFallback(t *testing.T) { assert.NotEmpty(t, p.AuthType) } -func TestProfileLoadClassicAccountHost(t *testing.T) { - // Verify that a host with account-scoped OIDC from discovery is validated - // as an account config (via Workspaces.List, not CurrentUser.Me). +func TestProfileLoadSPOGAccountWithDiscovery(t *testing.T) { + // Supplementary SPOG case: a host with account-scoped OIDC from discovery + // is validated as account config (via Workspaces.List, not CurrentUser.Me). server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.URL.Path { @@ -260,3 +280,42 @@ func TestProfileLoadClassicAccountHost(t *testing.T) { assert.NotEmpty(t, p.Host) assert.NotEmpty(t, p.AuthType) } + +func TestProfileLoadNoDiscoveryStaysWorkspace(t *testing.T) { + // When .well-known returns 404 and Experimental_IsUnifiedHost is false, + // the SPOG override should NOT trigger even if account_id is set. The + // profile should stay WorkspaceConfig and validate via CurrentUser.Me. + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/.well-known/databricks-config": + w.WriteHeader(http.StatusNotFound) + case "/api/2.0/preview/scim/v2/Me": + _ = json.NewEncoder(w).Encode(map[string]any{"userName": "test-user"}) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + t.Cleanup(server.Close) + + dir := t.TempDir() + configFile := filepath.Join(dir, ".databrickscfg") + t.Setenv("HOME", dir) + if runtime.GOOS == "windows" { + t.Setenv("USERPROFILE", dir) + } + + content := "[ws-profile]\nhost = " + server.URL + "\ntoken = test-token\naccount_id = some-acct\n" + require.NoError(t, os.WriteFile(configFile, []byte(content), 0o600)) + + p := &profileMetadata{ + Name: "ws-profile", + Host: server.URL, + AccountID: "some-acct", + } + p.Load(t.Context(), configFile, false) + + assert.True(t, p.Valid, "should validate as workspace when discovery is unavailable") + assert.NotEmpty(t, p.Host) + assert.Equal(t, "pat", p.AuthType) +}