github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/test/integration/update_int_test.go (about)

     1  package integration
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"runtime"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/ActiveState/cli/internal/testhelpers/suite"
    14  	"github.com/ActiveState/termtest"
    15  
    16  	"github.com/ActiveState/cli/internal/config"
    17  	"github.com/ActiveState/cli/internal/constants"
    18  	"github.com/ActiveState/cli/internal/fileutils"
    19  	"github.com/ActiveState/cli/internal/httputil"
    20  	"github.com/ActiveState/cli/internal/installation"
    21  	"github.com/ActiveState/cli/internal/osutils"
    22  	"github.com/ActiveState/cli/internal/rtutils/singlethread"
    23  	"github.com/ActiveState/cli/internal/testhelpers/e2e"
    24  	"github.com/ActiveState/cli/internal/testhelpers/tagsuite"
    25  )
    26  
    27  type UpdateIntegrationTestSuite struct {
    28  	tagsuite.Suite
    29  }
    30  
    31  type matcherFunc func(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool
    32  
    33  // Todo https://www.pivotaltracker.com/story/show/177863116
    34  // Update to release channel when possible
    35  var (
    36  	targetChannel    = "beta"
    37  	oldUpdateVersion = "beta@0.32.2-SHA3e1d435"
    38  )
    39  
    40  func init() {
    41  	if constants.ChannelName == targetChannel {
    42  		targetChannel = "master"
    43  	}
    44  }
    45  
    46  // env prepares environment variables for the test
    47  // disableUpdates prevents all update code from running
    48  // testUpdate directs to the locally running update directory and requires that a test update bundles has been generated with `state run generate-test-update`
    49  func (suite *UpdateIntegrationTestSuite) env(disableUpdates, forceUpdate bool) []string {
    50  	env := []string{}
    51  
    52  	if disableUpdates {
    53  		env = append(env, constants.DisableUpdates+"=true")
    54  	} else {
    55  		env = append(env, constants.DisableUpdates+"=false")
    56  	}
    57  
    58  	if forceUpdate {
    59  		env = append(env, constants.TestAutoUpdateEnvVarName+"=true")
    60  		env = append(env, constants.ForceUpdateEnvVarName+"=true")
    61  	}
    62  
    63  	dir, err := os.MkdirTemp("", "system*")
    64  	suite.NoError(err)
    65  	env = append(env, fmt.Sprintf("%s=%s", constants.OverwriteDefaultSystemPathEnvVarName, dir))
    66  
    67  	return env
    68  }
    69  
    70  func (suite *UpdateIntegrationTestSuite) versionCompare(ts *e2e.Session, expected string, matcher matcherFunc) {
    71  	type versionData struct {
    72  		Version string `json:"version"`
    73  	}
    74  
    75  	cp := ts.SpawnWithOpts(e2e.OptArgs("--version", "--output=json"), e2e.OptAppendEnv(suite.env(true, false)...))
    76  	cp.ExpectExitCode(0)
    77  
    78  	version := versionData{}
    79  	out := cp.StrippedSnapshot()
    80  	err := json.Unmarshal([]byte(out), &version)
    81  	suite.NoError(err)
    82  
    83  	matcher(expected, version.Version, fmt.Sprintf("Version could not be matched, output:\n\n%s", out))
    84  }
    85  
    86  func (suite *UpdateIntegrationTestSuite) channelCompare(ts *e2e.Session, expected string, matcher matcherFunc) {
    87  	type channelData struct {
    88  		Channel string `json:"channel"`
    89  	}
    90  
    91  	cp := ts.SpawnWithOpts(e2e.OptArgs("--version", "--output=json"), e2e.OptAppendEnv(suite.env(true, false)...))
    92  	cp.ExpectExitCode(0, termtest.OptExpectTimeout(30*time.Second))
    93  
    94  	channel := channelData{}
    95  	out := cp.StrippedSnapshot()
    96  	err := json.Unmarshal([]byte(out), &channel)
    97  	suite.NoError(err)
    98  
    99  	matcher(expected, channel.Channel, fmt.Sprintf("Channel could not be matched, output:\n\n%s", out))
   100  }
   101  
   102  func (suite *UpdateIntegrationTestSuite) TestUpdateAvailable() {
   103  	suite.OnlyRunForTags(tagsuite.Update, tagsuite.Critical)
   104  
   105  	ts := e2e.New(suite.T(), false)
   106  	defer ts.Close()
   107  
   108  	cfg, err := config.NewCustom(ts.Dirs.Config, singlethread.New(), true)
   109  	suite.Require().NoError(err)
   110  	defer cfg.Close()
   111  	err = cfg.Set(constants.AutoUpdateConfigKey, "false")
   112  	suite.Require().NoError(err)
   113  
   114  	search, found := "Update Available", false
   115  	for i := 0; i < 4; i++ {
   116  		if i > 0 {
   117  			time.Sleep(time.Second * 3)
   118  		}
   119  
   120  		cp := ts.SpawnWithOpts(e2e.OptArgs("--version"), e2e.OptAppendEnv(suite.env(false, true)...))
   121  		cp.ExpectExitCode(0)
   122  
   123  		if strings.Contains(cp.Snapshot(), search) {
   124  			found = true
   125  			break
   126  		}
   127  	}
   128  
   129  	suite.Require().True(found, "Expecting to find %q", search)
   130  }
   131  
   132  func (suite *UpdateIntegrationTestSuite) TestUpdate() {
   133  	suite.OnlyRunForTags(tagsuite.Update, tagsuite.Critical)
   134  
   135  	ts := e2e.New(suite.T(), true)
   136  	defer ts.Close()
   137  
   138  	suite.testUpdate(ts, filepath.Dir(ts.Dirs.Bin))
   139  }
   140  
   141  func (suite *UpdateIntegrationTestSuite) testUpdate(ts *e2e.Session, baseDir string, opts ...e2e.SpawnOptSetter) {
   142  	cfg, err := config.NewCustom(ts.Dirs.Config, singlethread.New(), true)
   143  	suite.Require().NoError(err)
   144  	defer cfg.Close()
   145  
   146  	spawnOpts := []e2e.SpawnOptSetter{
   147  		e2e.OptArgs("update"),
   148  		e2e.OptAppendEnv(suite.env(false, true)...),
   149  	}
   150  	if opts != nil {
   151  		spawnOpts = append(spawnOpts, opts...)
   152  	}
   153  
   154  	stateExec, err := installation.StateExecFromDir(baseDir)
   155  	suite.NoError(err)
   156  
   157  	searchA, searchB, found := "Updating State Tool to", "Installing Update", false
   158  	for i := 0; i < 4; i++ {
   159  		if i > 0 {
   160  			time.Sleep(time.Second * 3)
   161  		}
   162  
   163  		cp := ts.SpawnCmdWithOpts(stateExec, spawnOpts...)
   164  		cp.ExpectExitCode(0)
   165  
   166  		snap := cp.Snapshot()
   167  		if strings.Contains(snap, searchA) && strings.Contains(snap, searchB) {
   168  			found = true
   169  			break
   170  		}
   171  	}
   172  
   173  	suite.Require().True(found, "Expecting to find %q and %q", searchA, searchB)
   174  }
   175  
   176  func (suite *UpdateIntegrationTestSuite) TestUpdate_Repair() {
   177  	suite.OnlyRunForTags(tagsuite.Update)
   178  	ts := e2e.New(suite.T(), true)
   179  	defer ts.Close()
   180  
   181  	cfg, err := config.NewCustom(ts.Dirs.Config, singlethread.New(), true)
   182  	suite.Require().NoError(err)
   183  	defer cfg.Close()
   184  
   185  	subBinDir := filepath.Join(ts.Dirs.Bin, "bin")
   186  	files, err := os.ReadDir(ts.Dirs.Bin)
   187  	suite.NoError(err)
   188  	for _, f := range files {
   189  		err = fileutils.CopyFile(filepath.Join(ts.Dirs.Bin, f.Name()), filepath.Join(subBinDir, f.Name()))
   190  		suite.NoError(err)
   191  	}
   192  
   193  	stateExePath := filepath.Join(ts.Dirs.Bin, filepath.Base(ts.Exe))
   194  
   195  	spawnOpts := []e2e.SpawnOptSetter{
   196  		e2e.OptArgs("update"),
   197  		e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.OverwriteDefaultInstallationPathEnvVarName, ts.Dirs.Bin)),
   198  		e2e.OptAppendEnv(suite.env(false, true)...),
   199  	}
   200  
   201  	searchA, searchB, found := "Updating State Tool to version", "Installing Update", false
   202  	for i := 0; i < 4; i++ {
   203  		if i > 0 {
   204  			time.Sleep(time.Second * 3)
   205  		}
   206  
   207  		cp := ts.SpawnCmdWithOpts(stateExePath, spawnOpts...)
   208  		cp.ExpectExitCode(0, termtest.OptExpectTimeout(time.Minute))
   209  
   210  		snap := cp.Snapshot()
   211  		if strings.Contains(snap, searchA) && strings.Contains(snap, searchB) {
   212  			found = true
   213  			break
   214  		}
   215  	}
   216  
   217  	suite.Require().True(found, "Expecting to find %q and %q", searchA, searchB)
   218  
   219  	suite.NoFileExists(filepath.Join(ts.Dirs.Bin, constants.StateCmd+osutils.ExeExtension), "State Tool executable at install dir should no longer exist")
   220  	suite.NoFileExists(filepath.Join(ts.Dirs.Bin, constants.StateSvcCmd+osutils.ExeExtension), "State Service executable at install dir should no longer exist")
   221  }
   222  
   223  func (suite *UpdateIntegrationTestSuite) TestUpdateChannel() {
   224  	suite.OnlyRunForTags(tagsuite.Update, tagsuite.Critical)
   225  
   226  	tests := []struct {
   227  		Name    string
   228  		Channel string
   229  	}{
   230  		{"release-channel", "release"},
   231  		{"specific-update", targetChannel},
   232  	}
   233  
   234  	for _, tt := range tests {
   235  		suite.Run(tt.Name, func() {
   236  			// TODO: Update targetChannel and specificVersion after a v0.34.0 release
   237  			suite.T().Skip("Skipping these tests for now as the update changes need to be available in an older version of the state tool.")
   238  			ts := e2e.New(suite.T(), false)
   239  			defer ts.Close()
   240  
   241  			updateArgs := []string{"update", "--set-channel", tt.Channel}
   242  			env := []string{fmt.Sprintf("%s=%s", constants.OverwriteDefaultInstallationPathEnvVarName, ts.Dirs.Bin)}
   243  			env = append(env, suite.env(false, false)...)
   244  			cp := ts.SpawnWithOpts(
   245  				e2e.OptArgs(updateArgs...),
   246  				e2e.OptAppendEnv(env...),
   247  			)
   248  			cp.Expect("Updating")
   249  			cp.ExpectExitCode(0, termtest.OptExpectTimeout(1*time.Minute))
   250  
   251  			suite.channelCompare(ts, tt.Channel, suite.Equal)
   252  		})
   253  	}
   254  }
   255  
   256  func (suite *UpdateIntegrationTestSuite) TestUpdateTags() {
   257  	// Disabled, waiting for - https://www.pivotaltracker.com/story/show/179646813
   258  	suite.T().Skip("Disabled for now")
   259  	suite.OnlyRunForTags(tagsuite.Update)
   260  
   261  	tests := []struct {
   262  		name          string
   263  		tagged        bool
   264  		expectSuccess bool
   265  	}{
   266  		{"update-to-tag", false, true},
   267  		{"update-with-tag", true, false},
   268  	}
   269  
   270  	for _, tt := range tests {
   271  		suite.Run(tt.name, func() {
   272  			ts := e2e.New(suite.T(), false)
   273  			defer ts.Close()
   274  		})
   275  	}
   276  }
   277  
   278  func TestUpdateIntegrationTestSuite(t *testing.T) {
   279  	if testing.Short() {
   280  		t.Skip("skipping integration test in short mode.")
   281  	}
   282  	suite.Run(t, new(UpdateIntegrationTestSuite))
   283  }
   284  
   285  func lockedProjectURL() string {
   286  	return fmt.Sprintf("https://%s/string/string", constants.PlatformURL)
   287  }
   288  
   289  func (suite *UpdateIntegrationTestSuite) TestAutoUpdate() {
   290  	// suite.T().Skip("Test will not work until v0.34.0")
   291  	suite.OnlyRunForTags(tagsuite.Update, tagsuite.Critical)
   292  
   293  	ts := e2e.New(suite.T(), true)
   294  	defer ts.Close()
   295  
   296  	suite.testAutoUpdate(ts, filepath.Dir(ts.Dirs.Bin))
   297  }
   298  
   299  func (suite *UpdateIntegrationTestSuite) testAutoUpdate(ts *e2e.Session, baseDir string, opts ...e2e.SpawnOptSetter) {
   300  	fakeHome := filepath.Join(ts.Dirs.Work, "home")
   301  	suite.Require().NoError(fileutils.Mkdir(fakeHome))
   302  
   303  	spawnOpts := []e2e.SpawnOptSetter{
   304  		e2e.OptArgs("--version"),
   305  		e2e.OptAppendEnv(suite.env(false, true)...),
   306  		e2e.OptAppendEnv(fmt.Sprintf("HOME=%s", fakeHome)),
   307  	}
   308  	if opts != nil {
   309  		spawnOpts = append(spawnOpts, opts...)
   310  	}
   311  
   312  	stateExec, err := installation.StateExecFromDir(baseDir)
   313  	suite.NoError(err)
   314  
   315  	search, found := "Updating State Tool", false
   316  	for i := 0; i < 4; i++ {
   317  		if i > 0 {
   318  			time.Sleep(time.Second * 4)
   319  		}
   320  
   321  		cp := ts.SpawnCmdWithOpts(stateExec, spawnOpts...)
   322  		cp.ExpectExitCode(0, termtest.OptExpectTimeout(time.Minute))
   323  
   324  		if strings.Contains(cp.Snapshot(), search) {
   325  			found = true
   326  			break
   327  		}
   328  	}
   329  
   330  	suite.Require().True(found, "Expecting to find %q", search)
   331  }
   332  
   333  func (suite *UpdateIntegrationTestSuite) installLatestReleaseVersion(ts *e2e.Session, dir string) {
   334  	var cp *e2e.SpawnedCmd
   335  	if runtime.GOOS != "windows" {
   336  		oneLiner := fmt.Sprintf("sh <(curl -q https://platform.activestate.com/dl/cli/pdli01/install.sh) -f -n -t %s", dir)
   337  		cp = ts.SpawnCmdWithOpts(
   338  			"bash", e2e.OptArgs("-c", oneLiner),
   339  		)
   340  	} else {
   341  		b, err := httputil.GetDirect("https://platform.activestate.com/dl/cli/pdli01/install.ps1")
   342  		suite.Require().NoError(err)
   343  
   344  		ps1File := filepath.Join(ts.Dirs.Work, "install.ps1")
   345  		suite.Require().NoError(fileutils.WriteFile(ps1File, b))
   346  
   347  		cp = ts.SpawnCmdWithOpts("powershell.exe", e2e.OptArgs(ps1File, "-f", "-n", "-t", dir),
   348  			e2e.OptAppendEnv("SHELL="),
   349  		)
   350  	}
   351  	cp.Expect("Installation Complete", termtest.OptExpectTimeout(5*time.Minute))
   352  
   353  	stateExec, err := installation.StateExecFromDir(dir)
   354  	suite.NoError(err)
   355  
   356  	suite.FileExists(stateExec)
   357  }
   358  
   359  func (suite *UpdateIntegrationTestSuite) TestAutoUpdateToCurrent() {
   360  	suite.OnlyRunForTags(tagsuite.Update, tagsuite.Critical)
   361  
   362  	ts := e2e.New(suite.T(), true)
   363  	defer ts.Close()
   364  
   365  	installDir := filepath.Join(ts.Dirs.Work, "install")
   366  	err := fileutils.MkdirUnlessExists(installDir)
   367  	suite.NoError(err)
   368  
   369  	suite.installLatestReleaseVersion(ts, installDir)
   370  
   371  	suite.testAutoUpdate(ts, installDir, e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.UpdateChannelEnvVarName, constants.ChannelName)))
   372  }
   373  
   374  func (suite *UpdateIntegrationTestSuite) TestUpdateToCurrent() {
   375  	if strings.HasPrefix(constants.Version, "0.30") {
   376  		// Feel free to drop this once the release channel is no longer on 0.29
   377  		suite.T().Skip("Updating from release 0.29 to 0.30 is not covered due to how 0.29 did updates (async)")
   378  	}
   379  	suite.OnlyRunForTags(tagsuite.Update, tagsuite.Critical)
   380  
   381  	ts := e2e.New(suite.T(), true)
   382  	defer ts.Close()
   383  
   384  	installDir := filepath.Join(ts.Dirs.Work, "install")
   385  	err := fileutils.MkdirUnlessExists(installDir)
   386  	suite.Require().NoError(err)
   387  
   388  	suite.installLatestReleaseVersion(ts, installDir)
   389  
   390  	suite.testUpdate(ts, installDir, e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.UpdateChannelEnvVarName, constants.ChannelName)))
   391  }