From 0aa169536a261063a071851b30939b2046b8e358 Mon Sep 17 00:00:00 2001 From: Mike Drakos Date: Wed, 3 Sep 2025 11:15:22 -0700 Subject: [PATCH 1/2] Update config to override correct URL --- cmd/state-remote-installer/main.go | 2 +- cmd/state/autoupdate.go | 2 +- internal/constants/constants.go | 3 ++ internal/runners/update/update.go | 2 +- internal/updater/checker.go | 4 +- internal/updater/updater.go | 43 ++++++++++++---- internal/updater/updater_test.go | 12 ++++- test/integration/update_int_test.go | 78 +++++++++++++++++++++++++++-- 8 files changed, 125 insertions(+), 21 deletions(-) diff --git a/cmd/state-remote-installer/main.go b/cmd/state-remote-installer/main.go index 8d2c3c6116..0178a0a96d 100644 --- a/cmd/state-remote-installer/main.go +++ b/cmd/state-remote-installer/main.go @@ -208,7 +208,7 @@ func execute(out output.Outputer, prompt prompt.Prompter, cfg *config.Instance, version = fmt.Sprintf("%s (%s)", version, channel) } - update := updater.NewUpdateInstaller(an, availableUpdate) + update := updater.NewUpdateInstaller(cfg, an, availableUpdate) out.Fprint(os.Stdout, locale.Tl("remote_install_downloading", "• Downloading State Tool version [NOTICE]{{.V0}}[/RESET]... ", version)) tmpDir, err := update.DownloadAndUnpack() if err != nil { diff --git a/cmd/state/autoupdate.go b/cmd/state/autoupdate.go index f6902e6427..57362798e4 100644 --- a/cmd/state/autoupdate.go +++ b/cmd/state/autoupdate.go @@ -53,7 +53,7 @@ func autoUpdate(svc *model.SvcModel, args []string, childCmd *captain.Command, c } avUpdate := updater.NewAvailableUpdate(upd.Channel, upd.Version, upd.Platform, upd.Path, upd.Sha256, "") - up := updater.NewUpdateInstaller(an, avUpdate) + up := updater.NewUpdateInstaller(cfg, an, avUpdate) if !up.ShouldInstall() { logging.Debug("Update is not needed") return false, nil diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 0a4b71b785..d9321b8bf5 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -418,6 +418,9 @@ const AnalyticsPixelOverrideConfig = "report.analytics.endpoint" // UpdateEndpointConfig is the config key used to determine the update endpoint to use const UpdateEndpointConfig = "update.endpoint" +// UpdateInfoEndpointConfig is the config key used to determine the update info endpoint to use +const UpdateInfoEndpointConfig = "update.info.endpoint" + // NotificationsURLConfig is the config key used to determine the notifications url to use const NotificationsURLConfig = "notifications.endpoint" diff --git a/internal/runners/update/update.go b/internal/runners/update/update.go index d666727638..3df96a767d 100644 --- a/internal/runners/update/update.go +++ b/internal/runners/update/update.go @@ -65,7 +65,7 @@ func (u *Update) Run(params *Params) error { )) } - update := updater.NewUpdateInstaller(u.an, upd) + update := updater.NewUpdateInstaller(u.cfg, u.an, upd) if !update.ShouldInstall() { logging.Debug("No update found") u.out.Print(output.Prepare( diff --git a/internal/updater/checker.go b/internal/updater/checker.go index f8c71950c4..92df0e1266 100644 --- a/internal/updater/checker.go +++ b/internal/updater/checker.go @@ -35,7 +35,7 @@ var ( ) func init() { - configMediator.RegisterOption(constants.UpdateEndpointConfig, configMediator.String, "") + configMediator.RegisterOption(constants.UpdateInfoEndpointConfig, configMediator.String, "") } type Checker struct { @@ -87,7 +87,7 @@ func (u *Checker) infoURL(tag, desiredVersion, branchName, platform, arch string infoURL string envUrl = os.Getenv("_TEST_UPDATE_INFO_URL") - cfgUrl = u.cfg.GetString(constants.UpdateEndpointConfig) + cfgUrl = u.cfg.GetString(constants.UpdateInfoEndpointConfig) ) switch { case envUrl != "": diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 3560ae1557..921c52e1db 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -23,6 +23,7 @@ import ( "github.com/ActiveState/cli/internal/installation/storage" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" + configMediator "github.com/ActiveState/cli/internal/mediators/config" "github.com/ActiveState/cli/internal/multilog" "github.com/ActiveState/cli/internal/osutils" "github.com/ActiveState/cli/internal/rtutils" @@ -34,6 +35,10 @@ const ( InstallerName = "state-installer" + osutils.ExeExtension ) +func init() { + configMediator.RegisterOption(constants.UpdateEndpointConfig, configMediator.String, "") +} + type ErrorInProgress struct{ *locale.LocalizedError } var errPrivilegeMistmatch = errs.New("Privilege mismatch") @@ -101,22 +106,40 @@ type UpdateInstaller struct { // NewUpdateInstallerByOrigin returns an instance of Update. Allowing origin to // be set is useful for testing. -func NewUpdateInstallerByOrigin(an analytics.Dispatcher, origin *Origin, avUpdate *AvailableUpdate) *UpdateInstaller { - apiUpdateURL := constants.APIUpdateURL - if url, ok := os.LookupEnv("_TEST_UPDATE_URL"); ok { - apiUpdateURL = url - } - - return &UpdateInstaller{ +func NewUpdateInstallerByOrigin(cfg Configurable, an analytics.Dispatcher, origin *Origin, avUpdate *AvailableUpdate) *UpdateInstaller { + updater := &UpdateInstaller{ AvailableUpdate: avUpdate, Origin: origin, - url: apiUpdateURL + "/" + avUpdate.Path, + url: getAPIUpdateURL(cfg, avUpdate.Path), an: an, } + + configMediator.AddListener(constants.UpdateEndpointConfig, func() { + updater.url = getAPIUpdateURL(cfg, avUpdate.Path) + }) + + return updater +} + +func NewUpdateInstaller(cfg Configurable, an analytics.Dispatcher, avUpdate *AvailableUpdate) *UpdateInstaller { + return NewUpdateInstallerByOrigin(cfg, an, NewOriginDefault(), avUpdate) } -func NewUpdateInstaller(an analytics.Dispatcher, avUpdate *AvailableUpdate) *UpdateInstaller { - return NewUpdateInstallerByOrigin(an, NewOriginDefault(), avUpdate) +func getAPIUpdateURL(cfg Configurable, path string) string { + var apiUpdateURL string + + envUrl := os.Getenv("_TEST_UPDATE_URL") + cfgUrl := cfg.GetString(constants.UpdateEndpointConfig) + switch { + case envUrl != "": + apiUpdateURL = envUrl + case cfgUrl != "": + apiUpdateURL = cfgUrl + default: + apiUpdateURL = constants.APIUpdateURL + } + + return apiUpdateURL + "/" + path } func (u *UpdateInstaller) ShouldInstall() bool { diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go index b481be3d8c..788917e5de 100644 --- a/internal/updater/updater_test.go +++ b/internal/updater/updater_test.go @@ -6,6 +6,16 @@ import ( "github.com/stretchr/testify/assert" ) +type mockConfig struct{} + +func (m *mockConfig) GetString(key string) string { + return "" +} + +func (m *mockConfig) Set(key string, value interface{}) error { + return nil +} + func newAvailableUpdate(channel, version string) *AvailableUpdate { return NewAvailableUpdate(channel, version, "platform", "path/to/zipfile.zip", "123456", "") } @@ -45,7 +55,7 @@ func TestUpdateNotNeeded(t *testing.T) { for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { - upd := NewUpdateInstallerByOrigin(nil, tt.Origin, tt.AvailableUpdate) + upd := NewUpdateInstallerByOrigin(&mockConfig{}, nil, tt.Origin, tt.AvailableUpdate) assert.Equal(t, tt.IsUseful, upd.ShouldInstall()) }) } diff --git a/test/integration/update_int_test.go b/test/integration/update_int_test.go index 5f3c0b1965..f3a72f2623 100644 --- a/test/integration/update_int_test.go +++ b/test/integration/update_int_test.go @@ -275,14 +275,14 @@ func (suite *UpdateIntegrationTestSuite) TestUpdateTags() { } } -func (suite *UpdateIntegrationTestSuite) TestUpdateHost_SetBeforeInvocation() { +func (suite *UpdateIntegrationTestSuite) TestUpdateInfoHost_SetBeforeInvocation() { suite.OnlyRunForTags(tagsuite.Update) ts := e2e.New(suite.T(), false) defer ts.Close() - ts.SetConfig(constants.UpdateEndpointConfig, "https://test.example.com/update") - suite.Assert().Equal(ts.GetConfig(constants.UpdateEndpointConfig), "https://test.example.com/update") + ts.SetConfig(constants.UpdateInfoEndpointConfig, "https://test.example.com/update") + suite.Assert().Equal(ts.GetConfig(constants.UpdateInfoEndpointConfig), "https://test.example.com/update") cp := ts.SpawnWithOpts( e2e.OptArgs("--version"), @@ -303,6 +303,63 @@ func (suite *UpdateIntegrationTestSuite) TestUpdateHost_SetBeforeInvocation() { suite.Assert().Greater(correctHostCount, 0, "Log file should contain the configured API host 'test.example.com'") suite.Assert().Equal(incorrectHostCount, 0, "Log file should not contain the default API host 'platform.activestate.com'") + // Clean up - remove the config setting + cp = ts.Spawn("config", "set", constants.UpdateInfoEndpointConfig, "") + cp.Expect("Successfully") + cp.ExpectExitCode(0) +} + +func (suite *UpdateIntegrationTestSuite) TestUpdateInfoHost() { + suite.OnlyRunForTags(tagsuite.Update) + + ts := e2e.New(suite.T(), false) + defer ts.Close() + + cp := ts.Spawn("config", "set", constants.UpdateInfoEndpointConfig, "https://example.com/update-info") + cp.Expect("Successfully set config key") + cp.ExpectExitCode(0) + + cp = ts.SpawnWithOpts( + e2e.OptArgs("update"), + e2e.OptAppendEnv(suite.env(false, false)...), + e2e.OptAppendEnv("VERBOSE=true"), + ) + cp.ExpectExitCode(0) + + output := cp.Snapshot() + suite.Assert().Contains(output, "Getting update info: https://example.com/update-info/") +} + +func (suite *UpdateIntegrationTestSuite) TestUpdateHost_SetBeforeInvocation() { + suite.OnlyRunForTags(tagsuite.Update) + + ts := e2e.New(suite.T(), false) + defer ts.Close() + + ts.SetConfig(constants.UpdateInfoEndpointConfig, "https://test.example.com/update") + suite.Assert().Equal(ts.GetConfig(constants.UpdateInfoEndpointConfig), "https://test.example.com/update") + + cp := ts.SpawnWithOpts( + e2e.OptArgs("update"), + e2e.OptAppendEnv(suite.env(false, false)...), + e2e.OptAppendEnv("VERBOSE=true"), + ) + cp.ExpectExitCode(11) // Expect failure due to DNS resolution of fake host + + correctHostCount := 0 + incorrectHostCount := 0 + for _, path := range ts.LogFiles() { + contents := string(fileutils.ReadFileUnsafe(path)) + if strings.Contains(contents, "https://test.example.com/update") { + correctHostCount++ + } + if strings.Contains(contents, "https://state-tool.activestate.com/update") { + incorrectHostCount++ + } + } + suite.Assert().Greater(correctHostCount, 0, "Log file should contain the configured update endpoint 'test.example.com'") + suite.Assert().Equal(incorrectHostCount, 0, "Log file should not contain the default update endpoint 'state-tool.activestate.com'") + // Clean up - remove the config setting cp = ts.Spawn("config", "set", constants.UpdateEndpointConfig, "") cp.Expect("Successfully") @@ -326,8 +383,19 @@ func (suite *UpdateIntegrationTestSuite) TestUpdateHost() { ) cp.ExpectExitCode(0) - output := cp.Snapshot() - suite.Assert().Contains(output, "Getting update info: https://example.com/update/") + correctHostCount := 0 + incorrectHostCount := 0 + for _, path := range ts.LogFiles() { + contents := string(fileutils.ReadFileUnsafe(path)) + if strings.Contains(contents, "https://example.com/update") { + correctHostCount++ + } + if strings.Contains(contents, "https://state-tool.activestate.com/update") { + incorrectHostCount++ + } + } + suite.Assert().Greater(correctHostCount, 0, "Log file should contain the configured update endpoint 'example.com'") + suite.Assert().Equal(incorrectHostCount, 0, "Log file should not contain the default update endpoint 'state-tool.activestate.com'") } func TestUpdateIntegrationTestSuite(t *testing.T) { From dd35ebf5776dd8053c7af700fcb069a421b7f3af Mon Sep 17 00:00:00 2001 From: Mike Drakos Date: Wed, 3 Sep 2025 13:20:08 -0700 Subject: [PATCH 2/2] Add constant and update tests --- internal/constants/constants.go | 6 ++++++ internal/updater/checker.go | 2 +- internal/updater/updater.go | 2 +- test/integration/update_int_test.go | 14 ++++++-------- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/internal/constants/constants.go b/internal/constants/constants.go index d9321b8bf5..fa44bca0aa 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -133,6 +133,12 @@ const OverwriteDefaultSystemPathEnvVarName = "ACTIVESTATE_TEST_SYSTEM_PATH" // TestAutoUpdateEnvVarName is used to test auto updates, when set to true will always attempt to auto update const TestAutoUpdateEnvVarName = "ACTIVESTATE_TEST_AUTO_UPDATE" +// TestUpdateInfoURLEnvVarName is used to test update info urls, when set to a url will override the default update info url +const TestUpdateInfoURLEnvVarName = "ACTIVESTATE_TEST_UPDATE_INFO_URL" + +// TestUpdateURLEnvVarName is used to test update urls, when set to a url will override the default update url +const TestUpdateURLEnvVarName = "ACTIVESTATE_TEST_UPDATE_URL" + // ForceUpdateEnvVarName is used to force state tool to update, regardless of whether the update is equal to the current version const ForceUpdateEnvVarName = "ACTIVESTATE_FORCE_UPDATE" diff --git a/internal/updater/checker.go b/internal/updater/checker.go index 92df0e1266..b11c2789c0 100644 --- a/internal/updater/checker.go +++ b/internal/updater/checker.go @@ -86,7 +86,7 @@ func (u *Checker) infoURL(tag, desiredVersion, branchName, platform, arch string var ( infoURL string - envUrl = os.Getenv("_TEST_UPDATE_INFO_URL") + envUrl = os.Getenv(constants.TestUpdateInfoURLEnvVarName) cfgUrl = u.cfg.GetString(constants.UpdateInfoEndpointConfig) ) switch { diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 921c52e1db..b3a7f4bed8 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -128,7 +128,7 @@ func NewUpdateInstaller(cfg Configurable, an analytics.Dispatcher, avUpdate *Ava func getAPIUpdateURL(cfg Configurable, path string) string { var apiUpdateURL string - envUrl := os.Getenv("_TEST_UPDATE_URL") + envUrl := os.Getenv(constants.TestUpdateURLEnvVarName) cfgUrl := cfg.GetString(constants.UpdateEndpointConfig) switch { case envUrl != "": diff --git a/test/integration/update_int_test.go b/test/integration/update_int_test.go index f3a72f2623..580f95ce91 100644 --- a/test/integration/update_int_test.go +++ b/test/integration/update_int_test.go @@ -320,9 +320,8 @@ func (suite *UpdateIntegrationTestSuite) TestUpdateInfoHost() { cp.ExpectExitCode(0) cp = ts.SpawnWithOpts( - e2e.OptArgs("update"), + e2e.OptArgs("update", "-v"), e2e.OptAppendEnv(suite.env(false, false)...), - e2e.OptAppendEnv("VERBOSE=true"), ) cp.ExpectExitCode(0) @@ -340,11 +339,11 @@ func (suite *UpdateIntegrationTestSuite) TestUpdateHost_SetBeforeInvocation() { suite.Assert().Equal(ts.GetConfig(constants.UpdateInfoEndpointConfig), "https://test.example.com/update") cp := ts.SpawnWithOpts( - e2e.OptArgs("update"), + e2e.OptArgs("update", "-v"), e2e.OptAppendEnv(suite.env(false, false)...), - e2e.OptAppendEnv("VERBOSE=true"), ) cp.ExpectExitCode(11) // Expect failure due to DNS resolution of fake host + ts.IgnoreLogErrors() correctHostCount := 0 incorrectHostCount := 0 @@ -372,14 +371,13 @@ func (suite *UpdateIntegrationTestSuite) TestUpdateHost() { ts := e2e.New(suite.T(), false) defer ts.Close() - cp := ts.Spawn("config", "set", constants.UpdateEndpointConfig, "https://example.com/update") + cp := ts.Spawn("config", "set", constants.UpdateEndpointConfig, "https://test.example.com/update") cp.Expect("Successfully set config key") cp.ExpectExitCode(0) cp = ts.SpawnWithOpts( - e2e.OptArgs("update"), + e2e.OptArgs("update", "-v"), e2e.OptAppendEnv(suite.env(false, false)...), - e2e.OptAppendEnv("VERBOSE=true"), ) cp.ExpectExitCode(0) @@ -387,7 +385,7 @@ func (suite *UpdateIntegrationTestSuite) TestUpdateHost() { incorrectHostCount := 0 for _, path := range ts.LogFiles() { contents := string(fileutils.ReadFileUnsafe(path)) - if strings.Contains(contents, "https://example.com/update") { + if strings.Contains(contents, "https://test.example.com/update") { correctHostCount++ } if strings.Contains(contents, "https://state-tool.activestate.com/update") {