github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/app/installer_konnector_test.go (about) 1 package app_test 2 3 import ( 4 "bytes" 5 "context" 6 "io" 7 "net/http" 8 "net/http/httptest" 9 "os" 10 "os/exec" 11 "path" 12 "testing" 13 "time" 14 15 "github.com/andybalholm/brotli" 16 "github.com/cozy/cozy-stack/model/app" 17 "github.com/cozy/cozy-stack/model/instance" 18 "github.com/cozy/cozy-stack/model/instance/lifecycle" 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 TestInstallerKonnector(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("KonnectorInstallSuccessful", func(t *testing.T) { 102 manGen = manifestKonnector 103 manName = app.KonnectorManifestName 104 105 doUpgrade(t, 1) 106 107 inst, err := app.NewInstaller(db, fs, &app.InstallerOptions{ 108 Operation: app.Install, 109 Type: consts.KonnectorType, 110 Slug: "local-konnector", 111 SourceURL: gitURL, 112 }) 113 require.NoError(t, err) 114 115 go inst.Run() 116 117 var state app.State 118 var man app.Manifest 119 for { 120 var done bool 121 var err2 error 122 man, done, err2 = inst.Poll() 123 require.NoError(t, err2) 124 125 if state == "" { 126 if !assert.EqualValues(t, app.Installing, man.State()) { 127 return 128 } 129 } else if state == app.Installing { 130 if !assert.EqualValues(t, app.Ready, man.State()) { 131 return 132 } 133 require.True(t, done) 134 135 break 136 } else { 137 t.Fatalf("invalid state") 138 return 139 } 140 state = man.State() 141 } 142 143 ok, err := afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), app.KonnectorManifestName+".br")) 144 assert.NoError(t, err) 145 assert.True(t, ok, "The manifest is present") 146 ok, err = compressedFileContainsBytes(baseFS, path.Join("/", man.Slug(), man.Version(), app.KonnectorManifestName+".br"), []byte("1.0.0")) 147 assert.NoError(t, err) 148 assert.True(t, ok, "The manifest has the right version") 149 150 inst2, err := app.NewInstaller(db, fs, &app.InstallerOptions{ 151 Operation: app.Install, 152 Type: consts.KonnectorType, 153 Slug: "local-konnector", 154 SourceURL: gitURL, 155 }) 156 assert.Nil(t, inst2) 157 assert.Equal(t, app.ErrAlreadyExists, err) 158 }) 159 160 t.Run("KonnectorUpgradeNotExist", func(t *testing.T) { 161 manGen = manifestKonnector 162 manName = app.KonnectorManifestName 163 inst, err := app.NewInstaller(db, fs, &app.InstallerOptions{ 164 Operation: app.Update, 165 Type: consts.KonnectorType, 166 Slug: "cozy-konnector-not-exist", 167 }) 168 assert.Nil(t, inst) 169 assert.Equal(t, app.ErrNotFound, err) 170 171 inst, err = app.NewInstaller(db, fs, &app.InstallerOptions{ 172 Operation: app.Delete, 173 Type: consts.KonnectorType, 174 Slug: "cozy-konnector-not-exist", 175 }) 176 assert.Nil(t, inst) 177 assert.Equal(t, app.ErrNotFound, err) 178 }) 179 180 t.Run("KonnectorInstallWithUpgrade", func(t *testing.T) { 181 manGen = manifestKonnector 182 manName = app.KonnectorManifestName 183 184 doUpgrade(t, 1) 185 186 inst, err := app.NewInstaller(db, fs, &app.InstallerOptions{ 187 Operation: app.Install, 188 Type: consts.KonnectorType, 189 Slug: "cozy-konnector-b", 190 SourceURL: gitURL, 191 }) 192 require.NoError(t, err) 193 194 go inst.Run() 195 196 var man app.Manifest 197 for { 198 var done bool 199 man, done, err = inst.Poll() 200 require.NoError(t, err) 201 202 if done { 203 break 204 } 205 } 206 207 ok, err := afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), app.KonnectorManifestName+".br")) 208 assert.NoError(t, err) 209 assert.True(t, ok, "The manifest is present") 210 ok, err = compressedFileContainsBytes(baseFS, path.Join("/", man.Slug(), man.Version(), app.KonnectorManifestName+".br"), []byte("1.0.0")) 211 assert.NoError(t, err) 212 assert.True(t, ok, "The manifest has the right version") 213 214 doUpgrade(t, 2) 215 216 inst, err = app.NewInstaller(db, fs, &app.InstallerOptions{ 217 Operation: app.Update, 218 Type: consts.KonnectorType, 219 Slug: "cozy-konnector-b", 220 }) 221 require.NoError(t, err) 222 223 go inst.Run() 224 225 var state app.State 226 for { 227 var done bool 228 man, done, err = inst.Poll() 229 require.NoError(t, err) 230 231 if state == "" { 232 if !assert.EqualValues(t, app.Upgrading, man.State()) { 233 return 234 } 235 } else if state == app.Upgrading { 236 if !assert.EqualValues(t, app.Ready, man.State()) { 237 return 238 } 239 require.True(t, done) 240 241 break 242 } else { 243 t.Fatalf("invalid state") 244 return 245 } 246 state = man.State() 247 } 248 249 ok, err = afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), app.KonnectorManifestName+".br")) 250 assert.NoError(t, err) 251 assert.True(t, ok, "The manifest is present") 252 ok, err = compressedFileContainsBytes(baseFS, path.Join("/", man.Slug(), man.Version(), app.KonnectorManifestName+".br"), []byte("2.0.0")) 253 assert.NoError(t, err) 254 assert.True(t, ok, "The manifest has the right version") 255 }) 256 257 t.Run("KonnectorUpdateSkipPerms", func(t *testing.T) { 258 // Generating test instance 259 finished := true 260 conf := config.GetConfig() 261 conf.Contexts = map[string]interface{}{ 262 "foocontext": map[string]interface{}{}, 263 } 264 265 instance, err := lifecycle.Create(&lifecycle.Options{ 266 Domain: "test-skip-perms", 267 ContextName: "foocontext", 268 OnboardingFinished: &finished, 269 }) 270 271 defer func() { _ = lifecycle.Destroy("test-skip-perms") }() 272 273 assert.NoError(t, err) 274 275 manGen = manifestKonnector1 276 manName = app.KonnectorManifestName 277 278 inst, err := app.NewInstaller(instance, fs, &app.InstallerOptions{ 279 Operation: app.Install, 280 Type: consts.KonnectorType, 281 Slug: "cozy-konnector-test-skip", 282 SourceURL: gitURL, 283 }) 284 require.NoError(t, err) 285 286 var man app.Manifest 287 288 man, err = inst.RunSync() 289 konnManifest := man.(*app.KonnManifest) 290 assert.NoError(t, err) 291 assert.Empty(t, konnManifest.AvailableVersion()) 292 assert.Contains(t, konnManifest.Version(), "1.0.0") 293 294 // Will now update. New perms will be added, preventing an upgrade 295 manGen = manifestKonnector2 296 297 inst, err = app.NewInstaller(instance, fs, &app.InstallerOptions{ 298 Operation: app.Update, 299 Type: consts.KonnectorType, 300 Slug: "cozy-konnector-test-skip", 301 }) 302 require.NoError(t, err) 303 304 man, err = inst.RunSync() 305 konnManifest = man.(*app.KonnManifest) 306 assert.NoError(t, err) 307 assert.Contains(t, konnManifest.AvailableVersion(), "2.0.0") 308 assert.Contains(t, konnManifest.Version(), "1.0.0") // Assert we stayed on our version 309 310 // Change configuration to tell we skip the verifications 311 conf.Contexts = map[string]interface{}{ 312 "foocontext": map[string]interface{}{ 313 "permissions_skip_verification": true, 314 }, 315 } 316 317 man2, err := inst.RunSync() 318 konnManifest = man2.(*app.KonnManifest) 319 assert.NoError(t, err) 320 // Assert we upgraded version, and the perms have changed 321 assert.False(t, man.Permissions().HasSameRules(man2.Permissions())) 322 assert.Empty(t, konnManifest.AvailableVersion()) 323 assert.Contains(t, konnManifest.Version(), "2.0.0") 324 }) 325 326 t.Run("KonnectorInstallAndUpgradeWithBranch", func(t *testing.T) { 327 manGen = manifestKonnector 328 manName = app.KonnectorManifestName 329 doUpgrade(t, 3) 330 331 inst, err := app.NewInstaller(db, fs, &app.InstallerOptions{ 332 Operation: app.Install, 333 Type: consts.KonnectorType, 334 Slug: "local-konnector-branch", 335 SourceURL: gitURL + "#branch", 336 }) 337 require.NoError(t, err) 338 339 go inst.Run() 340 341 var state app.State 342 var man app.Manifest 343 for { 344 var done bool 345 var err2 error 346 man, done, err2 = inst.Poll() 347 require.NoError(t, err2) 348 349 if state == "" { 350 if !assert.EqualValues(t, app.Installing, man.State()) { 351 return 352 } 353 } else if state == app.Installing { 354 if !assert.EqualValues(t, app.Ready, man.State()) { 355 return 356 } 357 require.True(t, done) 358 359 break 360 } else { 361 t.Fatalf("invalid state") 362 return 363 } 364 state = man.State() 365 } 366 367 ok, err := afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), app.KonnectorManifestName+".br")) 368 assert.NoError(t, err) 369 assert.True(t, ok, "The manifest is present") 370 ok, err = compressedFileContainsBytes(baseFS, path.Join("/", man.Slug(), man.Version(), app.KonnectorManifestName+".br"), []byte("3.0.0")) 371 assert.NoError(t, err) 372 assert.True(t, ok, "The manifest has the right version") 373 374 doUpgrade(t, 4) 375 376 inst, err = app.NewInstaller(db, fs, &app.InstallerOptions{ 377 Operation: app.Update, 378 Type: consts.KonnectorType, 379 Slug: "local-konnector-branch", 380 }) 381 require.NoError(t, err) 382 383 go inst.Run() 384 385 state = "" 386 for { 387 var done bool 388 var err2 error 389 man, done, err2 = inst.Poll() 390 require.NoError(t, err2) 391 392 if state == "" { 393 if !assert.EqualValues(t, app.Upgrading, man.State()) { 394 return 395 } 396 } else if state == app.Upgrading { 397 if !assert.EqualValues(t, app.Ready, man.State()) { 398 return 399 } 400 require.True(t, done) 401 402 break 403 } else { 404 t.Fatalf("invalid state") 405 return 406 } 407 state = man.State() 408 } 409 410 ok, err = afero.Exists(baseFS, path.Join("/", man.Slug(), man.Version(), app.KonnectorManifestName+".br")) 411 assert.NoError(t, err) 412 assert.True(t, ok, "The manifest is present") 413 ok, err = compressedFileContainsBytes(baseFS, path.Join("/", man.Slug(), man.Version(), app.KonnectorManifestName+".br"), []byte("4.0.0")) 414 assert.NoError(t, err) 415 assert.True(t, ok, "The manifest has the right version") 416 }) 417 418 t.Run("KonnectorUninstall", func(t *testing.T) { 419 manGen = manifestKonnector 420 manName = app.KonnectorManifestName 421 inst1, err := app.NewInstaller(db, fs, &app.InstallerOptions{ 422 Operation: app.Install, 423 Type: consts.KonnectorType, 424 Slug: "konnector-delete", 425 SourceURL: gitURL, 426 }) 427 require.NoError(t, err) 428 429 go inst1.Run() 430 for { 431 var done bool 432 _, done, err = inst1.Poll() 433 require.NoError(t, err) 434 435 if done { 436 break 437 } 438 } 439 inst2, err := app.NewInstaller(db, fs, &app.InstallerOptions{ 440 Operation: app.Delete, 441 Type: consts.KonnectorType, 442 Slug: "konnector-delete", 443 }) 444 require.NoError(t, err) 445 446 _, err = inst2.RunSync() 447 require.NoError(t, err) 448 449 inst3, err := app.NewInstaller(db, fs, &app.InstallerOptions{ 450 Operation: app.Delete, 451 Type: consts.KonnectorType, 452 Slug: "konnector-delete", 453 }) 454 assert.Nil(t, inst3) 455 assert.Equal(t, app.ErrNotFound, err) 456 }) 457 458 t.Run("KonnectorInstallBadType", func(t *testing.T) { 459 manGen = manifestWebapp 460 manName = app.WebappManifestName 461 462 inst, err := app.NewInstaller(db, fs, &app.InstallerOptions{ 463 Operation: app.Install, 464 Type: consts.KonnectorType, 465 Slug: "cozy-bad-type", 466 SourceURL: gitURL, 467 }) 468 assert.NoError(t, err) 469 _, err = inst.RunSync() 470 assert.Error(t, err) 471 assert.ErrorIs(t, err, app.ErrInvalidManifestForKonnector) 472 }) 473 } 474 475 func compressedFileContainsBytes(fs afero.Fs, filename string, content []byte) (ok bool, err error) { 476 f, err := fs.Open(filename) 477 if err != nil { 478 return 479 } 480 defer f.Close() 481 br := brotli.NewReader(f) 482 b, err := io.ReadAll(br) 483 if err != nil { 484 return 485 } 486 ok = bytes.Contains(b, content) 487 return 488 } 489 490 func manifestKonnector1() string { 491 return `{ 492 "description": "A mini konnector to test cozy-stack-v2", 493 "type": "node", 494 "developer": { 495 "name": "Bruno", 496 "url": "cozy.io" 497 }, 498 "license": "MIT", 499 "name": "mini-app", 500 "permissions": { 501 "bills": { 502 "type": "io.cozy.bills" 503 } 504 }, 505 "slug": "mini", 506 "type": "konnector", 507 "version": "1.0.0" 508 }` 509 } 510 511 func manifestKonnector2() string { 512 return `{ 513 "description": "A mini konnector to test cozy-stack-v2", 514 "type": "node", 515 "developer": { 516 "name": "Bruno", 517 "url": "cozy.io" 518 }, 519 "license": "MIT", 520 "name": "mini-app", 521 "permissions": { 522 "bills": { 523 "type": "io.cozy.bills" 524 }, 525 "files": { 526 "type": "io.cozy.files" 527 } 528 }, 529 "slug": "mini", 530 "type": "konnector", 531 "version": "2.0.0" 532 }` 533 }