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 }