github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/app/installer_webapp_test.go (about)

     1  package app_test
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"os"
     9  	"os/exec"
    10  	"path"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/cozy/cozy-stack/model/app"
    15  	"github.com/cozy/cozy-stack/model/instance"
    16  	"github.com/cozy/cozy-stack/model/instance/lifecycle"
    17  	"github.com/cozy/cozy-stack/model/job"
    18  	"github.com/cozy/cozy-stack/model/permission"
    19  	"github.com/cozy/cozy-stack/model/stack"
    20  	"github.com/cozy/cozy-stack/pkg/appfs"
    21  	"github.com/cozy/cozy-stack/pkg/config/config"
    22  	"github.com/cozy/cozy-stack/pkg/consts"
    23  	"github.com/cozy/cozy-stack/pkg/couchdb"
    24  	"github.com/cozy/cozy-stack/tests/testutils"
    25  	"github.com/spf13/afero"
    26  	"github.com/stretchr/testify/assert"
    27  	"github.com/stretchr/testify/require"
    28  	"golang.org/x/sync/errgroup"
    29  )
    30  
    31  func TestInstallerWebApp(t *testing.T) {
    32  	if testing.Short() {
    33  		t.Skip("an instance is required for this test: test skipped due to the use of --short flag")
    34  	}
    35  
    36  	config.UseTestFile(t)
    37  
    38  	testutils.NeedCouchdb(t)
    39  
    40  	gitURL, done := serveGitRep(t)
    41  	defer done()
    42  
    43  	for i := 0; i < 400; i++ {
    44  		if err := exec.Command("git", "ls-remote", gitURL).Run(); err == nil {
    45  			break
    46  		}
    47  		time.Sleep(10 * time.Millisecond)
    48  	}
    49  
    50  	if !stackStarted {
    51  		_, _, err := stack.Start()
    52  		if err != nil {
    53  			require.NoError(t, err, "Error while starting job system")
    54  		}
    55  		stackStarted = true
    56  	}
    57  
    58  	app.ManifestClient = &http.Client{Transport: &transport{}}
    59  
    60  	ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    61  		_, _ = io.WriteString(w, manGen())
    62  	}))
    63  	t.Cleanup(ts.Close)
    64  
    65  	db := &instance.Instance{
    66  		ContextName: "foo",
    67  		Prefix:      "app-test",
    68  	}
    69  
    70  	require.NoError(t, couchdb.ResetDB(db, consts.Apps))
    71  	require.NoError(t, couchdb.ResetDB(db, consts.Konnectors))
    72  	require.NoError(t, couchdb.ResetDB(db, consts.Files))
    73  
    74  	osFS := afero.NewOsFs()
    75  	tmpDir, err := afero.TempDir(osFS, "", "cozy-installer-test")
    76  	if err != nil {
    77  		require.NoError(t, err)
    78  	}
    79  	t.Cleanup(func() { _ = osFS.RemoveAll(tmpDir) })
    80  
    81  	baseFS := afero.NewBasePathFs(osFS, tmpDir)
    82  	fs := appfs.NewAferoCopier(baseFS)
    83  
    84  	require.NoError(t, couchdb.ResetDB(db, consts.Permissions))
    85  
    86  	g, _ := errgroup.WithContext(context.Background())
    87  	couchdb.DefineIndexes(g, db, couchdb.IndexesByDoctype(consts.Files))
    88  	couchdb.DefineIndexes(g, db, couchdb.IndexesByDoctype(consts.Permissions))
    89  
    90  	require.NoError(t, g.Wait())
    91  
    92  	t.Cleanup(func() {
    93  		assert.NoError(t, couchdb.DeleteDB(db, consts.Apps))
    94  		assert.NoError(t, couchdb.DeleteDB(db, consts.Konnectors))
    95  		assert.NoError(t, couchdb.DeleteDB(db, consts.Files))
    96  		assert.NoError(t, couchdb.DeleteDB(db, consts.Permissions))
    97  	})
    98  
    99  	t.Cleanup(func() { assert.NoError(t, localGitCmd.Process.Signal(os.Interrupt)) })
   100  
   101  	t.Run("WebappInstallBadSlug", func(t *testing.T) {
   102  		manGen = manifestWebapp
   103  		manName = app.WebappManifestName
   104  		_, err := app.NewInstaller(db, fs, &app.InstallerOptions{
   105  			Operation: app.Install,
   106  			Type:      consts.WebappType,
   107  			SourceURL: "git://foo.bar",
   108  		})
   109  		if assert.Error(t, err) {
   110  			assert.Equal(t, app.ErrInvalidSlugName, err)
   111  		}
   112  
   113  		_, err = app.NewInstaller(db, fs, &app.InstallerOptions{
   114  			Operation: app.Install,
   115  			Type:      consts.WebappType,
   116  			Slug:      "coucou/",
   117  			SourceURL: "git://foo.bar",
   118  		})
   119  		if assert.Error(t, err) {
   120  			assert.Equal(t, app.ErrInvalidSlugName, err)
   121  		}
   122  	})
   123  
   124  	t.Run("WebappInstallBadAppsSource", func(t *testing.T) {
   125  		manGen = manifestWebapp
   126  		manName = app.WebappManifestName
   127  		_, err := app.NewInstaller(db, fs, &app.InstallerOptions{
   128  			Operation: app.Install,
   129  			Type:      consts.WebappType,
   130  			Slug:      "app3",
   131  			SourceURL: "foo://bar.baz",
   132  		})
   133  		if assert.Error(t, err) {
   134  			assert.Equal(t, app.ErrNotSupportedSource, err)
   135  		}
   136  
   137  		_, err = app.NewInstaller(db, fs, &app.InstallerOptions{
   138  			Operation: app.Install,
   139  			Type:      consts.WebappType,
   140  			Slug:      "app4",
   141  			SourceURL: "git://bar  .baz",
   142  		})
   143  		if assert.Error(t, err) {
   144  			assert.Contains(t, err.Error(), "invalid character")
   145  		}
   146  
   147  		_, err = app.NewInstaller(db, fs, &app.InstallerOptions{
   148  			Operation: app.Install,
   149  			Type:      consts.WebappType,
   150  			Slug:      "app5",
   151  			SourceURL: "",
   152  		})
   153  		if assert.Error(t, err) {
   154  			assert.Equal(t, app.ErrMissingSource, err)
   155  		}
   156  	})
   157  
   158  	t.Run("WebappInstallSuccessful", func(t *testing.T) {
   159  		manGen = manifestWebapp
   160  		manName = app.WebappManifestName
   161  
   162  		doUpgrade(t, 1)
   163  
   164  		inst, err := app.NewInstaller(db, fs, &app.InstallerOptions{
   165  			Operation: app.Install,
   166  			Type:      consts.WebappType,
   167  			Slug:      "local-cozy-mini",
   168  			SourceURL: gitURL,
   169  		})
   170  		require.NoError(t, err)
   171  
   172  		go inst.Run()
   173  
   174  		var state app.State
   175  		var man app.Manifest
   176  		for {
   177  			var done bool
   178  			var err2 error
   179  			man, done, err2 = inst.Poll()
   180  			require.NoError(t, err2)
   181  
   182  			if state == "" {
   183  				if !assert.EqualValues(t, app.Installing, man.State()) {
   184  					return
   185  				}
   186  			} else if state == app.Installing {
   187  				if !assert.EqualValues(t, app.Ready, man.State()) {
   188  					return
   189  				}
   190  				require.True(t, done)
   191  
   192  				break
   193  			} else {
   194  				t.Fatalf("invalid state")
   195  				return
   196  			}
   197  			state = man.State()
   198  		}
   199  
   200  		ok, err := afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), app.WebappManifestName+".br"))
   201  		assert.NoError(t, err)
   202  		assert.True(t, ok, "The manifest is present")
   203  		ok, err = compressedFileContainsBytes(baseFS, path.Join("/", man.Slug(), man.Version(), app.WebappManifestName+".br"), []byte("1.0.0"))
   204  		assert.NoError(t, err)
   205  		assert.True(t, ok, "The manifest has the right version")
   206  
   207  		inst2, err := app.NewInstaller(db, fs, &app.InstallerOptions{
   208  			Operation: app.Install,
   209  			Type:      consts.WebappType,
   210  			Slug:      "local-cozy-mini",
   211  			SourceURL: gitURL,
   212  		})
   213  		assert.Nil(t, inst2)
   214  		assert.Equal(t, app.ErrAlreadyExists, err)
   215  	})
   216  
   217  	t.Run("WebappInstallSuccessfulWithExtraPerms", func(t *testing.T) {
   218  		manifest1 := func() string {
   219  			return `{
   220  "description": "A mini app to test cozy-stack-v2",
   221  "developer": {
   222    "name": "Cozy",
   223    "url": "cozy.io"
   224  },
   225  "license": "MIT",
   226  "name": "mini-app",
   227  "permissions": {
   228    "rule0": {
   229      "type": "io.cozy.files",
   230      "verbs": ["GET"],
   231      "values": ["foobar"]
   232    },
   233    "rule1": {
   234      "type": "cc.cozycloud.sentry",
   235      "verbs": ["POST"]
   236    }
   237  },
   238  "slug": "mini-test-perms",
   239  "type": "webapp",
   240  "version": "1.0.0"
   241  }`
   242  		}
   243  
   244  		manifest2 := func() string {
   245  			return `{
   246  "description": "A mini app to test cozy-stack-v2",
   247  "developer": {
   248    "name": "Cozy",
   249    "url": "cozy.io"
   250  },
   251  "license": "MIT",
   252  "name": "mini-app",
   253  "permissions": {
   254    "rule0": {
   255      "type": "io.cozy.files",
   256      "verbs": ["GET"],
   257      "values": ["foobar"]
   258    },
   259    "rule1": {
   260      "type": "cc.cozycloud.sentry",
   261      "verbs": ["POST"]
   262    }
   263  },
   264  "slug": "mini-test-perms",
   265  "type": "webapp",
   266  "version": "2.0.0"
   267  }`
   268  		}
   269  
   270  		manifest3 := func() string {
   271  			return `{
   272  "description": "A mini app to test cozy-stack-v2",
   273  "developer": {
   274    "name": "Cozy",
   275    "url": "cozy.io"
   276  },
   277  "license": "MIT",
   278  "name": "mini-app",
   279  "permissions": {
   280    "rule0": {
   281      "type": "io.cozy.files",
   282      "verbs": ["GET"],
   283      "values": ["foobar"]
   284    },
   285    "rule1": {
   286      "type": "cc.cozycloud.errors",
   287      "verbs": ["POST"]
   288    }
   289  },
   290  "slug": "mini-test-perms",
   291  "type": "webapp",
   292  "version": "3.0.0"
   293  }`
   294  		}
   295  
   296  		manGen = manifest1
   297  		manName = app.WebappManifestName
   298  		finished := true
   299  
   300  		instance, err := lifecycle.Create(&lifecycle.Options{
   301  			Domain:             "test-keep-perms",
   302  			OnboardingFinished: &finished,
   303  		})
   304  		assert.NoError(t, err)
   305  
   306  		defer func() { _ = lifecycle.Destroy("test-keep-perms") }()
   307  
   308  		inst, err := app.NewInstaller(instance, fs, &app.InstallerOptions{
   309  			Operation: app.Install,
   310  			Type:      consts.WebappType,
   311  			Slug:      "mini-test-perms",
   312  			SourceURL: gitURL,
   313  		})
   314  		require.NoError(t, err)
   315  
   316  		man, err := inst.RunSync()
   317  		assert.NoError(t, err)
   318  		assert.Contains(t, man.Version(), "1.0.0")
   319  
   320  		// Altering permissions by adding a value and a verb
   321  		newPerms, err := permission.UnmarshalScopeString("io.cozy.files:GET,POST:foobar,foobar2 cc.cozycloud.sentry:POST")
   322  		assert.NoError(t, err)
   323  
   324  		customRule := permission.Rule{
   325  			Title:  "myCustomRule",
   326  			Verbs:  permission.Verbs(permission.PUT),
   327  			Type:   "io.cozy.custom",
   328  			Values: []string{"myCustomValue"},
   329  		}
   330  		newPerms = append(newPerms, customRule)
   331  
   332  		_, err = permission.UpdateWebappSet(instance, "mini-test-perms", newPerms)
   333  		assert.NoError(t, err)
   334  
   335  		p1, err := permission.GetForWebapp(instance, "mini-test-perms")
   336  		assert.NoError(t, err)
   337  		assert.False(t, p1.Permissions.HasSameRules(man.Permissions()))
   338  
   339  		// Update the app
   340  		manGen = manifest2
   341  		inst2, err := app.NewInstaller(instance, fs, &app.InstallerOptions{
   342  			Operation: app.Update,
   343  			Type:      consts.WebappType,
   344  			Slug:      "mini-test-perms",
   345  			SourceURL: gitURL,
   346  		})
   347  		assert.NoError(t, err)
   348  
   349  		man, err = inst2.RunSync()
   350  		assert.NoError(t, err)
   351  
   352  		p2, err := permission.GetForWebapp(instance, "mini-test-perms")
   353  		assert.NoError(t, err)
   354  		assert.Contains(t, man.Version(), "2.0.0")
   355  		// Assert the rules were kept
   356  		assert.False(t, p2.Permissions.HasSameRules(man.Permissions()))
   357  		assert.True(t, p1.Permissions.HasSameRules(p2.Permissions))
   358  
   359  		// Update again the app
   360  		manGen = manifest3
   361  		inst3, err := app.NewInstaller(instance, fs, &app.InstallerOptions{
   362  			Operation:        app.Update,
   363  			Type:             consts.WebappType,
   364  			Slug:             "mini-test-perms",
   365  			SourceURL:        gitURL,
   366  			PermissionsAcked: true,
   367  		})
   368  		assert.NoError(t, err)
   369  
   370  		man, err = inst3.RunSync()
   371  		assert.NoError(t, err)
   372  
   373  		p3, err := permission.GetForWebapp(instance, "mini-test-perms")
   374  		assert.NoError(t, err)
   375  		assert.Contains(t, man.Version(), "3.0.0")
   376  		assert.False(t, p3.Permissions.HasSameRules(man.Permissions()))
   377  		// Assert that rule1 type has been changed
   378  		sentry := permission.Rule{
   379  			Type:  "cc.cozycloud.sentry",
   380  			Title: "rule1",
   381  			Verbs: permission.Verbs(permission.POST),
   382  		}
   383  		assert.False(t, p3.Permissions.RuleInSubset(sentry))
   384  		errors := permission.Rule{
   385  			Type:  "cc.cozycloud.errors",
   386  			Title: "rule1",
   387  			Verbs: permission.Verbs(permission.POST),
   388  		}
   389  		assert.True(t, p3.Permissions.RuleInSubset(errors))
   390  	})
   391  
   392  	t.Run("WebappUpgradeNotExist", func(t *testing.T) {
   393  		manGen = manifestWebapp
   394  		manName = app.WebappManifestName
   395  		inst, err := app.NewInstaller(db, fs, &app.InstallerOptions{
   396  			Operation: app.Update,
   397  			Type:      consts.WebappType,
   398  			Slug:      "cozy-app-not-exist",
   399  		})
   400  		assert.Nil(t, inst)
   401  		assert.Equal(t, app.ErrNotFound, err)
   402  
   403  		inst, err = app.NewInstaller(db, fs, &app.InstallerOptions{
   404  			Operation: app.Delete,
   405  			Type:      consts.WebappType,
   406  			Slug:      "cozy-app-not-exist",
   407  		})
   408  		assert.Nil(t, inst)
   409  		assert.Equal(t, app.ErrNotFound, err)
   410  	})
   411  
   412  	t.Run("WebappInstallWithUpgrade", func(t *testing.T) {
   413  		manGen = manifestWebapp
   414  		manName = app.WebappManifestName
   415  
   416  		defer func() {
   417  			localServices = ""
   418  		}()
   419  
   420  		localServices = `{
   421  		"service1": {
   422  
   423  			"type": "node",
   424  			"file": "/services/service1.js",
   425  			"trigger": "@cron 0 0 0 * * *"
   426  		}
   427  	}`
   428  
   429  		doUpgrade(t, 1)
   430  
   431  		inst, err := app.NewInstaller(db, fs, &app.InstallerOptions{
   432  			Operation: app.Install,
   433  			Type:      consts.WebappType,
   434  			Slug:      "cozy-app-b",
   435  			SourceURL: gitURL,
   436  		})
   437  		require.NoError(t, err)
   438  
   439  		man, err := inst.RunSync()
   440  		assert.NoError(t, err)
   441  
   442  		ok, err := afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), app.WebappManifestName+".br"))
   443  		assert.NoError(t, err)
   444  		assert.True(t, ok, "The manifest is present")
   445  		ok, err = compressedFileContainsBytes(baseFS, path.Join("/", man.Slug(), man.Version(), app.WebappManifestName+".br"), []byte("1.0.0"))
   446  		assert.NoError(t, err)
   447  		assert.True(t, ok, "The manifest has the right version")
   448  		version1 := man.Version()
   449  
   450  		manWebapp := man.(*app.WebappManifest)
   451  		if assert.NotNil(t, manWebapp.Services()["service1"]) {
   452  			service1 := manWebapp.Services()["service1"]
   453  			assert.Equal(t, "/services/service1.js", service1.File)
   454  			assert.Equal(t, "@cron 0 0 0 * * *", service1.TriggerOptions)
   455  			assert.Equal(t, "node", service1.Type)
   456  			assert.NotEmpty(t, service1.TriggerID)
   457  		}
   458  
   459  		doUpgrade(t, 2)
   460  		localServices = ""
   461  
   462  		inst, err = app.NewInstaller(db, fs, &app.InstallerOptions{
   463  			Operation: app.Update,
   464  			Type:      consts.WebappType,
   465  			Slug:      "cozy-app-b",
   466  		})
   467  		require.NoError(t, err)
   468  
   469  		go inst.Run()
   470  
   471  		var state app.State
   472  		for {
   473  			var done bool
   474  			man, done, err = inst.Poll()
   475  			require.NoError(t, err)
   476  
   477  			if state == "" {
   478  				if !assert.EqualValues(t, app.Upgrading, man.State()) {
   479  					return
   480  				}
   481  			} else if state == app.Upgrading {
   482  				if !assert.EqualValues(t, app.Ready, man.State()) {
   483  					return
   484  				}
   485  				require.True(t, done)
   486  
   487  				break
   488  			} else {
   489  				t.Fatalf("invalid state")
   490  				return
   491  			}
   492  			state = man.State()
   493  		}
   494  		version2 := man.Version()
   495  
   496  		t.Log("versions: ", version1, version2)
   497  
   498  		ok, err = afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), app.WebappManifestName+".br"))
   499  		assert.NoError(t, err)
   500  		assert.True(t, ok, "The manifest is present")
   501  		ok, err = compressedFileContainsBytes(baseFS, path.Join("/", man.Slug(), man.Version(), app.WebappManifestName+".br"), []byte("2.0.0"))
   502  		assert.NoError(t, err)
   503  		assert.True(t, ok, "The manifest has the right version")
   504  		manWebapp = man.(*app.WebappManifest)
   505  		assert.Nil(t, manWebapp.Services()["service1"])
   506  	})
   507  
   508  	t.Run("WebappUpdateServices", func(t *testing.T) {
   509  		manifest1 := func() string {
   510  			return `{
   511  "description": "A mini app to test cozy-stack-v2",
   512  "developer": {
   513    "name": "Cozy",
   514    "url": "cozy.io"
   515  },
   516  "license": "MIT",
   517  "name": "mini-app",
   518  "permissions": {
   519    "rule0": {
   520      "type": "io.cozy.files",
   521      "verbs": ["GET"]
   522    }
   523  },
   524  "services": {
   525    "dacc": {
   526      "file": "services/dacc/drive.js",
   527      "trigger": "@every 720h",
   528      "type": "node"
   529    },
   530    "qualificationMigration": {
   531      "debounce": "24h",
   532      "file": "services/qualificationMigration/drive.js",
   533      "trigger": "@event io.cozy.files:CREATED,UPDATED",
   534      "type": "node"
   535    }
   536  },
   537  "slug": "mini-test-services",
   538  "type": "webapp",
   539  "version": "1.0.0"
   540  }`
   541  		}
   542  
   543  		// @every -> @monthly
   544  		manifest2 := func() string {
   545  			return `{
   546  "description": "A mini app to test cozy-stack-v2",
   547  "developer": {
   548    "name": "Cozy",
   549    "url": "cozy.io"
   550  },
   551  "license": "MIT",
   552  "name": "mini-app",
   553  "permissions": {
   554    "rule0": {
   555      "type": "io.cozy.files",
   556      "verbs": ["GET"]
   557    }
   558  },
   559  "services": {
   560    "dacc": {
   561      "file": "services/dacc/drive.js",
   562      "trigger": "@monthly on the 3-5 between 2pm and 7pm",
   563      "type": "node"
   564    },
   565    "qualificationMigration": {
   566      "debounce": "24h",
   567      "file": "services/qualificationMigration/drive.js",
   568      "trigger": "@event io.cozy.files:CREATED,UPDATED",
   569      "type": "node"
   570    }
   571  },
   572  "slug": "mini-test-services",
   573  "type": "webapp",
   574  "version": "2.0.0"
   575  }`
   576  		}
   577  
   578  		// monthly arguments
   579  		manifest3 := func() string {
   580  			return `{
   581  "description": "A mini app to test cozy-stack-v2",
   582  "developer": {
   583    "name": "Cozy",
   584    "url": "cozy.io"
   585  },
   586  "license": "MIT",
   587  "name": "mini-app",
   588  "permissions": {
   589    "rule0": {
   590      "type": "io.cozy.files",
   591      "verbs": ["GET"]
   592    }
   593  },
   594  "services": {
   595    "dacc": {
   596      "file": "services/dacc/drive.js",
   597      "trigger": "@monthly on the 2-4 between 1pm and 6pm",
   598      "type": "node"
   599    },
   600    "qualificationMigration": {
   601      "debounce": "24h",
   602      "file": "services/qualificationMigration/drive.js",
   603      "trigger": "@event io.cozy.files:CREATED,UPDATED",
   604      "type": "node"
   605    }
   606  },
   607  "slug": "mini-test-services",
   608  "type": "webapp",
   609  "version": "3.0.0"
   610  }`
   611  		}
   612  
   613  		manGen = manifest1
   614  		manName = app.WebappManifestName
   615  		finished := true
   616  
   617  		instance, err := lifecycle.Create(&lifecycle.Options{
   618  			Domain:             "test-update-services",
   619  			OnboardingFinished: &finished,
   620  		})
   621  		require.NoError(t, err)
   622  
   623  		defer func() { _ = lifecycle.Destroy("test-update-services") }()
   624  
   625  		inst, err := app.NewInstaller(instance, fs, &app.InstallerOptions{
   626  			Operation: app.Install,
   627  			Type:      consts.WebappType,
   628  			Slug:      "mini-test-services",
   629  			SourceURL: gitURL,
   630  		})
   631  		require.NoError(t, err)
   632  
   633  		man, err := inst.RunSync()
   634  		require.NoError(t, err)
   635  		assert.Contains(t, man.Version(), "1.0.0")
   636  
   637  		jobsSystem := job.System()
   638  		triggers, err := jobsSystem.GetAllTriggers(instance)
   639  		require.NoError(t, err)
   640  		nbTriggers := len(triggers)
   641  
   642  		trigger := findTrigger(triggers, "@event")
   643  		require.NotNil(t, trigger)
   644  		assert.Equal(t, "24h", trigger.Infos().Debounce)
   645  		trigger = findTrigger(triggers, "@every")
   646  		require.NotNil(t, trigger)
   647  		assert.Equal(t, "720h", trigger.Infos().Arguments)
   648  
   649  		// Update the app
   650  		manGen = manifest2
   651  		inst2, err := app.NewInstaller(instance, fs, &app.InstallerOptions{
   652  			Operation: app.Update,
   653  			Type:      consts.WebappType,
   654  			Slug:      "mini-test-services",
   655  			SourceURL: gitURL,
   656  		})
   657  		require.NoError(t, err)
   658  
   659  		man, err = inst2.RunSync()
   660  		require.NoError(t, err)
   661  		assert.Contains(t, man.Version(), "2.0.0")
   662  
   663  		triggers, err = jobsSystem.GetAllTriggers(instance)
   664  		require.NoError(t, err)
   665  		assert.Equal(t, nbTriggers, len(triggers))
   666  
   667  		trigger = findTrigger(triggers, "@event")
   668  		require.NotNil(t, trigger)
   669  		assert.Equal(t, "24h", trigger.Infos().Debounce)
   670  		trigger = findTrigger(triggers, "@every")
   671  		assert.Nil(t, trigger)
   672  		trigger = findTrigger(triggers, "@monthly")
   673  		assert.NotNil(t, trigger)
   674  		assert.Equal(t, "on the 3-5 between 2pm and 7pm", trigger.Infos().Arguments)
   675  
   676  		// Update again the app
   677  		manGen = manifest3
   678  		inst3, err := app.NewInstaller(instance, fs, &app.InstallerOptions{
   679  			Operation: app.Update,
   680  			Type:      consts.WebappType,
   681  			Slug:      "mini-test-services",
   682  			SourceURL: gitURL,
   683  		})
   684  		require.NoError(t, err)
   685  
   686  		man, err = inst3.RunSync()
   687  		require.NoError(t, err)
   688  		assert.Contains(t, man.Version(), "3.0.0")
   689  
   690  		triggers, err = jobsSystem.GetAllTriggers(instance)
   691  		require.NoError(t, err)
   692  		assert.Equal(t, nbTriggers, len(triggers))
   693  		trigger = findTrigger(triggers, "@event")
   694  		require.NotNil(t, trigger)
   695  		assert.Equal(t, "24h", trigger.Infos().Debounce)
   696  		trigger = findTrigger(triggers, "@monthly")
   697  		assert.NotNil(t, trigger)
   698  		assert.Equal(t, "on the 2-4 between 1pm and 6pm", trigger.Infos().Arguments)
   699  	})
   700  
   701  	t.Run("WebappInstallAndUpgradeWithBranch", func(t *testing.T) {
   702  		manGen = manifestWebapp
   703  		manName = app.WebappManifestName
   704  		doUpgrade(t, 3)
   705  
   706  		inst, err := app.NewInstaller(db, fs, &app.InstallerOptions{
   707  			Operation: app.Install,
   708  			Type:      consts.WebappType,
   709  			Slug:      "local-cozy-mini-branch",
   710  			SourceURL: gitURL + "#branch",
   711  		})
   712  		require.NoError(t, err)
   713  
   714  		go inst.Run()
   715  
   716  		var state app.State
   717  		var man app.Manifest
   718  		for {
   719  			var done bool
   720  			var err2 error
   721  			man, done, err2 = inst.Poll()
   722  			require.NoError(t, err2)
   723  
   724  			if state == "" {
   725  				if !assert.EqualValues(t, app.Installing, man.State()) {
   726  					return
   727  				}
   728  			} else if state == app.Installing {
   729  				if !assert.EqualValues(t, app.Ready, man.State()) {
   730  					return
   731  				}
   732  				require.True(t, done)
   733  
   734  				break
   735  			} else {
   736  				t.Fatalf("invalid state")
   737  				return
   738  			}
   739  			state = man.State()
   740  		}
   741  
   742  		ok, err := afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), app.WebappManifestName+".br"))
   743  		assert.NoError(t, err)
   744  		assert.True(t, ok, "The manifest is present")
   745  		ok, err = compressedFileContainsBytes(baseFS, path.Join("/", man.Slug(), man.Version(), app.WebappManifestName+".br"), []byte("3.0.0"))
   746  		assert.NoError(t, err)
   747  		assert.True(t, ok, "The manifest has the right version")
   748  		ok, err = afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), "branch.br"))
   749  		assert.NoError(t, err)
   750  		assert.True(t, ok, "The good branch was checked out")
   751  
   752  		doUpgrade(t, 4)
   753  
   754  		inst, err = app.NewInstaller(db, fs, &app.InstallerOptions{
   755  			Operation: app.Update,
   756  			Type:      consts.WebappType,
   757  			Slug:      "local-cozy-mini-branch",
   758  		})
   759  		require.NoError(t, err)
   760  
   761  		go inst.Run()
   762  
   763  		state = ""
   764  		for {
   765  			var done bool
   766  			var err2 error
   767  			man, done, err2 = inst.Poll()
   768  			require.NoError(t, err2)
   769  
   770  			if state == "" {
   771  				if !assert.EqualValues(t, app.Upgrading, man.State()) {
   772  					return
   773  				}
   774  			} else if state == app.Upgrading {
   775  				if !assert.EqualValues(t, app.Ready, man.State()) {
   776  					return
   777  				}
   778  				require.True(t, done)
   779  
   780  				break
   781  			} else {
   782  				t.Fatalf("invalid state")
   783  				return
   784  			}
   785  			state = man.State()
   786  		}
   787  
   788  		ok, err = afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), app.WebappManifestName+".br"))
   789  		assert.NoError(t, err)
   790  		assert.True(t, ok, "The manifest is present")
   791  		ok, err = compressedFileContainsBytes(baseFS, path.Join("/", man.Slug(), man.Version(), app.WebappManifestName+".br"), []byte("4.0.0"))
   792  		assert.NoError(t, err)
   793  		assert.True(t, ok, "The manifest has the right version")
   794  		ok, err = afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), "branch.br"))
   795  		assert.NoError(t, err)
   796  		assert.True(t, ok, "The good branch was checked out")
   797  
   798  		doUpgrade(t, 5)
   799  
   800  		inst, err = app.NewInstaller(db, fs, &app.InstallerOptions{
   801  			Operation: app.Update,
   802  			Type:      consts.WebappType,
   803  			Slug:      "local-cozy-mini-branch",
   804  			SourceURL: gitURL,
   805  		})
   806  		require.NoError(t, err)
   807  
   808  		man, err = inst.RunSync()
   809  		require.NoError(t, err)
   810  
   811  		assert.Equal(t, gitURL, man.Source())
   812  
   813  		ok, err = afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), app.WebappManifestName+".br"))
   814  		assert.NoError(t, err)
   815  		assert.True(t, ok, "The manifest is present")
   816  		ok, err = compressedFileContainsBytes(baseFS, path.Join("/", man.Slug(), man.Version(), app.WebappManifestName+".br"), []byte("5.0.0"))
   817  		assert.NoError(t, err)
   818  		assert.True(t, ok, "The manifest has the right version")
   819  		ok, err = afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), "branch.br"))
   820  		assert.NoError(t, err)
   821  		assert.False(t, ok, "The good branch was checked out")
   822  	})
   823  
   824  	t.Run("WebappInstallFromGithub", func(t *testing.T) {
   825  		manGen = manifestWebapp
   826  		manName = app.WebappManifestName
   827  		inst, err := app.NewInstaller(db, fs, &app.InstallerOptions{
   828  			Operation: app.Install,
   829  			Type:      consts.WebappType,
   830  			Slug:      "github-cozy-mini",
   831  			SourceURL: "git://github.com/nono/cozy-mini.git",
   832  		})
   833  		require.NoError(t, err)
   834  
   835  		go inst.Run()
   836  
   837  		var state app.State
   838  		for {
   839  			man, done, err := inst.Poll()
   840  			require.NoError(t, err)
   841  
   842  			if state == "" {
   843  				if !assert.EqualValues(t, app.Installing, man.State()) {
   844  					return
   845  				}
   846  			} else if state == app.Installing {
   847  				if !assert.EqualValues(t, app.Ready, man.State()) {
   848  					return
   849  				}
   850  				require.True(t, done)
   851  
   852  				break
   853  			} else {
   854  				t.Fatalf("invalid state")
   855  				return
   856  			}
   857  			state = man.State()
   858  		}
   859  	})
   860  
   861  	t.Run("WebappInstallFromHTTP", func(t *testing.T) {
   862  		manGen = manifestWebapp
   863  		manName = app.WebappManifestName
   864  		inst, err := app.NewInstaller(db, fs, &app.InstallerOptions{
   865  			Operation: app.Install,
   866  			Type:      consts.WebappType,
   867  			Slug:      "http-cozy-drive",
   868  			SourceURL: "https://github.com/cozy/cozy-drive/archive/71e5cde66f754f986afc7111962ed2dd361bd5e4.tar.gz",
   869  		})
   870  		require.NoError(t, err)
   871  
   872  		go inst.Run()
   873  
   874  		var state app.State
   875  		for {
   876  			man, done, err := inst.Poll()
   877  			require.NoError(t, err)
   878  
   879  			if state == "" {
   880  				if !assert.EqualValues(t, app.Installing, man.State()) {
   881  					return
   882  				}
   883  			} else if state == app.Installing {
   884  				if !assert.EqualValues(t, app.Ready, man.State()) {
   885  					return
   886  				}
   887  				require.True(t, done)
   888  
   889  				break
   890  			} else {
   891  				t.Fatalf("invalid state")
   892  				return
   893  			}
   894  			state = man.State()
   895  		}
   896  	})
   897  
   898  	t.Run("WebappUpdateWithService", func(t *testing.T) {
   899  		manifest1 := func() string {
   900  			return ` {
   901  "description": "A mini app to test cozy-stack-v2",
   902  "developer": {
   903  	"name": "Cozy",
   904  	"url": "cozy.io"
   905  },
   906  "license": "MIT",
   907  "name": "mini-app",
   908  "permissions": {
   909    "rule0": {
   910  	"type": "io.cozy.files",
   911  	"verbs": ["GET"],
   912  	"values": ["foobar"]
   913    }
   914  },
   915  "services": {
   916  	"onOperationOrBillCreate": {
   917  		"type": "node",
   918  		"file": "onOperationOrBillCreate.js",
   919  		"trigger": "@event io.cozy.bank.operations:CREATED io.cozy.bills:CREATED",
   920  		"debounce": "3m"
   921  	  }
   922  },
   923  "slug": "mini-test-service",
   924  "type": "webapp",
   925  "version": "1.0.0"
   926  }`
   927  		}
   928  
   929  		manifest2 := func() string {
   930  			return ` {
   931  "description": "A mini app to test cozy-stack-v2",
   932  "developer": {
   933  	"name": "Cozy",
   934  	"url": "cozy.io"
   935  },
   936  "license": "MIT",
   937  "name": "mini-app",
   938  "permissions": {
   939  	"rule0": {
   940  		"type": "io.cozy.files",
   941  		"verbs": ["GET", "POST"],
   942  		"values": ["foobar"]
   943  	}
   944  },
   945  "services": {
   946  	"onOperationOrBillCreate": {
   947  		"type": "node",
   948  		"file": "onOperationOrBillCreate.js",
   949  		"trigger": "@event io.cozy.bank.operations:CREATED io.cozy.bills:CREATED",
   950  		"debounce": "3m"
   951  	  }
   952  },
   953  "slug": "mini-test-service",
   954  "type": "webapp",
   955  "version": "2.0.0"
   956  }`
   957  		}
   958  		conf := config.GetConfig()
   959  		conf.Contexts = map[string]interface{}{
   960  			"default": map[string]interface{}{},
   961  		}
   962  
   963  		manGen = manifest1
   964  		manName = app.WebappManifestName
   965  		finished := true
   966  
   967  		instance, err := lifecycle.Create(&lifecycle.Options{
   968  			Domain:             "test-update-with-service",
   969  			OnboardingFinished: &finished,
   970  		})
   971  		assert.NoError(t, err)
   972  
   973  		defer func() { _ = lifecycle.Destroy("test-update-with-service") }()
   974  
   975  		inst, err := app.NewInstaller(instance, fs, &app.InstallerOptions{
   976  			Operation: app.Install,
   977  			Type:      consts.WebappType,
   978  			Slug:      "mini-test-service",
   979  			SourceURL: gitURL,
   980  		})
   981  		require.NoError(t, err)
   982  
   983  		man, err := inst.RunSync()
   984  		assert.NoError(t, err)
   985  		assert.Contains(t, man.Version(), "1.0.0")
   986  
   987  		t1, err := couchdb.CountAllDocs(instance, consts.Triggers)
   988  		assert.NoError(t, err)
   989  
   990  		// Update the app, but with new perms. The app should stay on the same
   991  		// version
   992  		manGen = manifest2
   993  		inst2, err := app.NewInstaller(instance, fs, &app.InstallerOptions{
   994  			Operation: app.Update,
   995  			Type:      consts.WebappType,
   996  			Slug:      "mini-test-service",
   997  			SourceURL: gitURL,
   998  		})
   999  		assert.NoError(t, err)
  1000  
  1001  		man, err = inst2.RunSync()
  1002  		assert.NoError(t, err)
  1003  		t2, err := couchdb.CountAllDocs(instance, consts.Triggers)
  1004  		assert.NoError(t, err)
  1005  
  1006  		assert.Contains(t, man.Version(), "1.0.0")
  1007  
  1008  		assert.Equal(t, t1, t2)
  1009  	})
  1010  
  1011  	t.Run("WebappUninstall", func(t *testing.T) {
  1012  		manGen = manifestWebapp
  1013  		manName = app.WebappManifestName
  1014  		inst1, err := app.NewInstaller(db, fs, &app.InstallerOptions{
  1015  			Operation: app.Install,
  1016  			Type:      consts.WebappType,
  1017  			Slug:      "github-cozy-delete",
  1018  			SourceURL: gitURL,
  1019  		})
  1020  		require.NoError(t, err)
  1021  
  1022  		go inst1.Run()
  1023  		for {
  1024  			var done bool
  1025  			_, done, err = inst1.Poll()
  1026  			require.NoError(t, err)
  1027  
  1028  			if done {
  1029  				break
  1030  			}
  1031  		}
  1032  		inst2, err := app.NewInstaller(db, fs, &app.InstallerOptions{
  1033  			Operation: app.Delete,
  1034  			Type:      consts.WebappType,
  1035  			Slug:      "github-cozy-delete",
  1036  		})
  1037  		require.NoError(t, err)
  1038  
  1039  		_, err = inst2.RunSync()
  1040  		require.NoError(t, err)
  1041  
  1042  		inst3, err := app.NewInstaller(db, fs, &app.InstallerOptions{
  1043  			Operation: app.Delete,
  1044  			Type:      consts.WebappType,
  1045  			Slug:      "github-cozy-delete",
  1046  		})
  1047  		assert.Nil(t, inst3)
  1048  		assert.Equal(t, app.ErrNotFound, err)
  1049  	})
  1050  
  1051  	t.Run("WebappUninstallErrored", func(t *testing.T) {
  1052  		manGen = manifestWebapp
  1053  		manName = app.WebappManifestName
  1054  
  1055  		inst1, err := app.NewInstaller(db, fs, &app.InstallerOptions{
  1056  			Operation: app.Install,
  1057  			Type:      consts.WebappType,
  1058  			Slug:      "github-cozy-delete-errored",
  1059  			SourceURL: gitURL,
  1060  		})
  1061  		require.NoError(t, err)
  1062  
  1063  		go inst1.Run()
  1064  		for {
  1065  			var done bool
  1066  			_, done, err = inst1.Poll()
  1067  			require.NoError(t, err)
  1068  
  1069  			if done {
  1070  				break
  1071  			}
  1072  		}
  1073  
  1074  		inst2, err := app.NewInstaller(db, fs, &app.InstallerOptions{
  1075  			Operation: app.Update,
  1076  			Type:      consts.WebappType,
  1077  			Slug:      "github-cozy-delete-errored",
  1078  			SourceURL: "git://foobar.does.not.exist/",
  1079  		})
  1080  		require.NoError(t, err)
  1081  
  1082  		go inst2.Run()
  1083  		for {
  1084  			var done bool
  1085  			_, done, err = inst2.Poll()
  1086  			if done || err != nil {
  1087  				break
  1088  			}
  1089  		}
  1090  		require.Error(t, err)
  1091  
  1092  		inst3, err := app.NewInstaller(db, fs, &app.InstallerOptions{
  1093  			Operation: app.Delete,
  1094  			Type:      consts.WebappType,
  1095  			Slug:      "github-cozy-delete-errored",
  1096  		})
  1097  		require.NoError(t, err)
  1098  
  1099  		_, err = inst3.RunSync()
  1100  		require.NoError(t, err)
  1101  
  1102  		inst4, err := app.NewInstaller(db, fs, &app.InstallerOptions{
  1103  			Operation: app.Delete,
  1104  			Type:      consts.WebappType,
  1105  			Slug:      "github-cozy-delete-errored",
  1106  		})
  1107  		assert.Nil(t, inst4)
  1108  		assert.Equal(t, app.ErrNotFound, err)
  1109  	})
  1110  
  1111  	t.Run("WebappInstallBadType", func(t *testing.T) {
  1112  		manGen = manifestKonnector
  1113  		manName = app.KonnectorManifestName
  1114  
  1115  		inst, err := app.NewInstaller(db, fs, &app.InstallerOptions{
  1116  			Operation: app.Install,
  1117  			Type:      consts.WebappType,
  1118  			Slug:      "cozy-bad-type",
  1119  			SourceURL: gitURL,
  1120  		})
  1121  		assert.NoError(t, err)
  1122  		_, err = inst.RunSync()
  1123  		assert.Error(t, err)
  1124  		assert.ErrorIs(t, err, app.ErrInvalidManifestForWebapp)
  1125  	})
  1126  }
  1127  
  1128  func findTrigger(triggers []job.Trigger, typ string) job.Trigger {
  1129  	for _, trigger := range triggers {
  1130  		if trigger.Type() == typ {
  1131  			return trigger
  1132  		}
  1133  	}
  1134  	return nil
  1135  }