package handler import ( "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestGetConfigIncludesRuntimeAuthConfig(t *testing.T) { origStorage := testHandler.Storage testHandler.Storage = &mockStorage{} defer func() { testHandler.Storage = origStorage }() t.Setenv("ALLOW_SIGNUP", "false") t.Setenv("GOOGLE_CLIENT_ID", "google-client-id") t.Setenv("POSTHOG_API_KEY", "phc_test") t.Setenv("POSTHOG_HOST", "https://eu.i.posthog.com") t.Setenv("MULTICA_PUBLIC_URL", "https://api.example.com/") t.Setenv("MULTICA_APP_URL", "https://app.example.com/") req := httptest.NewRequest(http.MethodGet, "/api/config", nil) w := httptest.NewRecorder() testHandler.GetConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("GetConfig: expected 200, got %d: %s", w.Code, w.Body.String()) } var cfg AppConfig if err := json.Unmarshal(w.Body.Bytes(), &cfg); err != nil { t.Fatalf("decode config: %v", err) } if cfg.CdnDomain != "cdn.example.com" { t.Fatalf("cdn_domain: want cdn.example.com, got %q", cfg.CdnDomain) } if cfg.AllowSignup { t.Fatalf("allow_signup: want false, got true") } if cfg.GoogleClientID != "google-client-id" { t.Fatalf("google_client_id: want google-client-id, got %q", cfg.GoogleClientID) } if cfg.PosthogKey != "phc_test" { t.Fatalf("posthog_key: want phc_test, got %q", cfg.PosthogKey) } if cfg.PosthogHost != "https://eu.i.posthog.com" { t.Fatalf("posthog_host: want https://eu.i.posthog.com, got %q", cfg.PosthogHost) } if cfg.AnalyticsEnvironment != "dev" { t.Fatalf("analytics_environment: want dev, got %q", cfg.AnalyticsEnvironment) } if cfg.WorkspaceCreationDisabled { t.Fatalf("workspace_creation_disabled: want false by default, got true") } if cfg.DaemonServerURL != "https://api.example.com" { t.Fatalf("daemon_server_url: want https://api.example.com, got %q", cfg.DaemonServerURL) } if cfg.DaemonAppURL != "https://app.example.com" { t.Fatalf("daemon_app_url: want https://app.example.com, got %q", cfg.DaemonAppURL) } } func TestGetConfigUsesAppURLForSameOriginDaemonSetup(t *testing.T) { t.Setenv("MULTICA_APP_URL", "https://multica.internal.example/") req := httptest.NewRequest(http.MethodGet, "/api/config", nil) w := httptest.NewRecorder() testHandler.GetConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("GetConfig: expected 200, got %d: %s", w.Code, w.Body.String()) } var cfg AppConfig if err := json.Unmarshal(w.Body.Bytes(), &cfg); err != nil { t.Fatalf("decode config: %v", err) } if cfg.DaemonServerURL != "https://multica.internal.example" { t.Fatalf("daemon_server_url: want same-origin URL, got %q", cfg.DaemonServerURL) } if cfg.DaemonAppURL != "https://multica.internal.example" { t.Fatalf("daemon_app_url: want app URL, got %q", cfg.DaemonAppURL) } } func TestGetConfigUsesFrontendOriginForSameOriginDaemonSetup(t *testing.T) { t.Setenv("FRONTEND_ORIGIN", "https://multica.internal.example/") req := httptest.NewRequest(http.MethodGet, "/api/config", nil) w := httptest.NewRecorder() testHandler.GetConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("GetConfig: expected 200, got %d: %s", w.Code, w.Body.String()) } var cfg AppConfig if err := json.Unmarshal(w.Body.Bytes(), &cfg); err != nil { t.Fatalf("decode config: %v", err) } if cfg.DaemonServerURL != "https://multica.internal.example" { t.Fatalf("daemon_server_url: want same-origin URL, got %q", cfg.DaemonServerURL) } if cfg.DaemonAppURL != "https://multica.internal.example" { t.Fatalf("daemon_app_url: want frontend origin, got %q", cfg.DaemonAppURL) } } func TestGetConfigOmitsOfficialCloudDaemonSetup(t *testing.T) { t.Setenv("MULTICA_PUBLIC_URL", "https://api.multica.ai") t.Setenv("FRONTEND_ORIGIN", "https://multica.ai") req := httptest.NewRequest(http.MethodGet, "/api/config", nil) w := httptest.NewRecorder() testHandler.GetConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("GetConfig: expected 200, got %d: %s", w.Code, w.Body.String()) } var cfg AppConfig if err := json.Unmarshal(w.Body.Bytes(), &cfg); err != nil { t.Fatalf("decode config: %v", err) } if cfg.DaemonServerURL != "" { t.Fatalf("daemon_server_url: want omitted for cloud, got %q", cfg.DaemonServerURL) } if cfg.DaemonAppURL != "" { t.Fatalf("daemon_app_url: want omitted for cloud, got %q", cfg.DaemonAppURL) } } func TestURLHostEqualsCanonicalizesCommonHostForms(t *testing.T) { tests := []struct { name string raw string want bool }{ {name: "full URL", raw: "https://api.multica.ai", want: true}, {name: "bare host", raw: "api.multica.ai", want: true}, {name: "host port", raw: "api.multica.ai:8080", want: true}, {name: "trailing dot", raw: "https://api.multica.ai.", want: true}, {name: "different host", raw: "https://evil.example", want: false}, {name: "empty", raw: "", want: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := urlHostEquals(tt.raw, "api.multica.ai"); got != tt.want { t.Fatalf("urlHostEquals(%q): want %v, got %v", tt.raw, tt.want, got) } }) } } // TestGetConfigExposesWorkspaceCreationDisabled verifies that the self-host // gate added by #3433 surfaces to the frontend through /api/config so the UI // can hide every "Create workspace" affordance. func TestGetConfigExposesWorkspaceCreationDisabled(t *testing.T) { origStorage := testHandler.Storage testHandler.Storage = &mockStorage{} defer func() { testHandler.Storage = origStorage }() t.Setenv("DISABLE_WORKSPACE_CREATION", "true") req := httptest.NewRequest(http.MethodGet, "/api/config", nil) w := httptest.NewRecorder() testHandler.GetConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("GetConfig: expected 200, got %d: %s", w.Code, w.Body.String()) } var cfg AppConfig if err := json.Unmarshal(w.Body.Bytes(), &cfg); err != nil { t.Fatalf("decode config: %v", err) } if !cfg.WorkspaceCreationDisabled { t.Fatalf("workspace_creation_disabled: want true with env on, got false (body=%s)", w.Body.String()) } }