github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/settings/settings_test.go (about) 1 package settings_test 2 3 import ( 4 "encoding/hex" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "net/http/httptest" 9 "testing" 10 "time" 11 12 "github.com/cozy/cozy-stack/model/bitwarden/settings" 13 "github.com/cozy/cozy-stack/model/instance" 14 "github.com/cozy/cozy-stack/model/instance/lifecycle" 15 "github.com/cozy/cozy-stack/model/oauth" 16 "github.com/cozy/cozy-stack/model/session" 17 csettings "github.com/cozy/cozy-stack/model/settings" 18 "github.com/cozy/cozy-stack/pkg/config/config" 19 "github.com/cozy/cozy-stack/pkg/consts" 20 "github.com/cozy/cozy-stack/pkg/couchdb" 21 "github.com/cozy/cozy-stack/pkg/prefixer" 22 "github.com/cozy/cozy-stack/tests/testutils" 23 "github.com/cozy/cozy-stack/web" 24 "github.com/cozy/cozy-stack/web/errors" 25 "github.com/cozy/cozy-stack/web/middlewares" 26 websettings "github.com/cozy/cozy-stack/web/settings" 27 "github.com/cozy/cozy-stack/web/statik" 28 "github.com/gavv/httpexpect/v2" 29 "github.com/labstack/echo/v4" 30 "github.com/stretchr/testify/assert" 31 "github.com/stretchr/testify/require" 32 33 _ "github.com/cozy/cozy-stack/worker/mails" 34 ) 35 36 func setupRouter(t *testing.T, inst *instance.Instance, svc csettings.Service) *httptest.Server { 37 t.Helper() 38 39 handler := echo.New() 40 handler.HTTPErrorHandler = errors.ErrorHandler 41 group := handler.Group("/settings", func(next echo.HandlerFunc) echo.HandlerFunc { 42 return func(context echo.Context) error { 43 context.Set("instance", inst) 44 45 cookie, err := context.Request().Cookie(session.CookieName(inst)) 46 if err != http.ErrNoCookie { 47 require.NoError(t, err, "Could not get session cookie") 48 if cookie.Value == "connected" { 49 sess, _ := session.New(inst, session.LongRun) 50 context.Set("session", sess) 51 } 52 } 53 54 return next(context) 55 } 56 }) 57 58 websettings.NewHTTPHandler(svc).Register(group) 59 ts := httptest.NewServer(handler) 60 t.Cleanup(ts.Close) 61 62 return ts 63 } 64 65 func TestSettings(t *testing.T) { 66 if testing.Short() { 67 t.Skip("an instance is required for this test: test skipped due to the use of --short flag") 68 } 69 70 var instanceRev string 71 var oauthClientID string 72 73 config.UseTestFile(t) 74 conf := config.GetConfig() 75 conf.Assets = "../../assets" 76 conf.Contexts[config.DefaultInstanceContext] = map[string]interface{}{ 77 "manager_url": "http://manager.example.org", 78 "logos": map[string]interface{}{ 79 "home": map[string]interface{}{ 80 "light": []interface{}{ 81 map[string]interface{}{"src": "/logos/main_cozy.png", "alt": "Cozy Cloud"}, 82 }, 83 }, 84 }, 85 } 86 was := conf.Subdomains 87 conf.Subdomains = config.NestedSubdomains 88 defer func() { conf.Subdomains = was }() 89 90 _ = web.LoadSupportedLocales() 91 testutils.NeedCouchdb(t) 92 setup := testutils.NewSetup(t, t.Name()) 93 render, _ := statik.NewDirRenderer("../../assets") 94 middlewares.BuildTemplates() 95 96 testInstance := setup.GetTestInstance(&lifecycle.Options{ 97 Locale: "en", 98 Timezone: "Europe/Berlin", 99 Email: "alice@example.com", 100 ContextName: "test-context", 101 }) 102 scope := consts.Settings + " " + consts.OAuthClients 103 _, token := setup.GetTestClient(scope) 104 sessCookie := session.CookieName(testInstance) 105 106 svc := csettings.NewServiceMock(t) 107 ts := setupRouter(t, testInstance, svc) 108 ts.Config.Handler.(*echo.Echo).Renderer = render 109 ts.Config.Handler.(*echo.Echo).HTTPErrorHandler = errors.ErrorHandler 110 tsURL := ts.URL 111 112 t.Run("GetContext", func(t *testing.T) { 113 e := testutils.CreateTestClient(t, tsURL) 114 115 obj := e.GET("/settings/context"). 116 WithCookie(sessCookie, "connected"). 117 WithHeader("Accept", "application/vnd.api+json"). 118 WithHeader("Authorization", "Bearer "+token). 119 Expect().Status(200). 120 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 121 Object() 122 123 data := obj.Value("data").Object() 124 data.ValueEqual("type", "io.cozy.settings") 125 data.ValueEqual("id", "io.cozy.settings.context") 126 127 attrs := data.Value("attributes").Object() 128 attrs.ValueEqual("manager_url", "http://manager.example.org") 129 attrs.ValueEqual("logos", map[string]interface{}{ 130 "home": map[string]interface{}{ 131 "light": []interface{}{ 132 map[string]interface{}{"src": "/logos/main_cozy.png", "alt": "Cozy Cloud"}, 133 }, 134 }, 135 }) 136 }) 137 138 t.Run("PatchWithGoodRev", func(t *testing.T) { 139 e := testutils.CreateTestClient(t, tsURL) 140 141 doc1, err := testInstance.SettingsDocument() 142 require.NoError(t, err) 143 144 // We are going to patch an instance with newer values, and give the good rev 145 e.PUT("/settings/instance"). 146 WithCookie(sessCookie, "connected"). 147 WithHeader("Content-Type", "application/vnd.api+json"). 148 WithHeader("Accept", "application/vnd.api+json"). 149 WithHeader("Authorization", "Bearer "+token). 150 WithBytes([]byte(fmt.Sprintf(`{ 151 "data": { 152 "type": "io.cozy.settings", 153 "id": "io.cozy.settings.instance", 154 "meta": { 155 "rev": "%s" 156 }, 157 "attributes": { 158 "tz": "Europe/London", 159 "email": "alice@example.org", 160 "locale": "fr" 161 } 162 } 163 }`, doc1.Rev()))). 164 Expect().Status(200) 165 }) 166 167 t.Run("PatchWithBadRev", func(t *testing.T) { 168 e := testutils.CreateTestClient(t, tsURL) 169 170 // We are going to patch an instance with newer values, but with a totally 171 // random rev 172 rev := "6-2d9b7ef014d10549c2b4e206672d3e44" 173 174 e.PUT("/settings/instance"). 175 WithCookie(sessCookie, "connected"). 176 WithHeader("Content-Type", "application/vnd.api+json"). 177 WithHeader("Accept", "application/vnd.api+json"). 178 WithHeader("Authorization", "Bearer "+token). 179 WithBytes([]byte(fmt.Sprintf(`{ 180 "data": { 181 "type": "io.cozy.settings", 182 "id": "io.cozy.settings.instance", 183 "meta": { 184 "rev": "%s" 185 }, 186 "attributes": { 187 "tz": "Europe/Berlin", 188 "email": "alice@example.com", 189 "locale": "en" 190 } 191 } 192 }`, rev))). 193 Expect().Status(409) 194 }) 195 196 t.Run("PatchWithBadRevNoChanges", func(t *testing.T) { 197 e := testutils.CreateTestClient(t, tsURL) 198 199 // We are defining a random rev, but make no changes in the instance values 200 rev := "6-2d9b7ef014d10549c2b4e206672d3e44" 201 202 e.PUT("/settings/instance"). 203 WithCookie(sessCookie, "connected"). 204 WithHeader("Content-Type", "application/vnd.api+json"). 205 WithHeader("Accept", "application/vnd.api+json"). 206 WithHeader("Authorization", "Bearer "+token). 207 WithBytes([]byte(fmt.Sprintf(`{ 208 "data": { 209 "type": "io.cozy.settings", 210 "id": "io.cozy.settings.instance", 211 "meta": { 212 "rev": "%s" 213 }, 214 "attributes": { 215 "tz": "Europe/London", 216 "email": "alice@example.org", 217 "locale": "fr" 218 } 219 } 220 }`, rev))). 221 Expect().Status(200) 222 }) 223 224 t.Run("PatchWithBadRevAndChanges", func(t *testing.T) { 225 e := testutils.CreateTestClient(t, tsURL) 226 227 // We are defining a random rev, but make changes in the instance values 228 rev := "6-2d9b7ef014d10549c2b4e206672d3e44" 229 230 e.PUT("/settings/instance"). 231 WithCookie(sessCookie, "connected"). 232 WithHeader("Content-Type", "application/vnd.api+json"). 233 WithHeader("Accept", "application/vnd.api+json"). 234 WithHeader("Authorization", "Bearer "+token). 235 WithBytes([]byte(fmt.Sprintf(`{ 236 "data": { 237 "type": "io.cozy.settings", 238 "id": "io.cozy.settings.instance", 239 "meta": { 240 "rev": "%s" 241 }, 242 "attributes": { 243 "tz": "Europe/London", 244 "email": "alice@example.com", 245 "locale": "en" 246 } 247 } 248 }`, rev))). 249 Expect().Status(409) 250 }) 251 252 t.Run("DiskUsage", func(t *testing.T) { 253 e := testutils.CreateTestClient(t, tsURL) 254 255 obj := e.GET("/settings/disk-usage"). 256 WithCookie(sessCookie, "connected"). 257 WithHeader("Authorization", "Bearer "+token). 258 Expect().Status(200). 259 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 260 Object() 261 262 e.GET("/settings/disk-usage"). 263 WithCookie(sessCookie, "connected"). 264 Expect().Status(401) 265 266 data := obj.Value("data").Object() 267 data.HasValue("type", "io.cozy.settings") 268 data.HasValue("id", "io.cozy.settings.disk-usage") 269 270 attrs := data.Value("attributes").Object() 271 attrs.HasValue("used", "0") 272 attrs.HasValue("files", "0") 273 attrs.HasValue("versions", "0") 274 }) 275 276 t.Run("RegisterPassphraseWrongToken", func(t *testing.T) { 277 e := testutils.CreateTestClient(t, tsURL) 278 279 e.POST("/settings/passphrase"). 280 WithCookie(sessCookie, "connected"). 281 WithHeader("Content-Type", "application/json"). 282 WithBytes([]byte(`{ 283 "passphrase": "MyFirstPassphrase", 284 "iterations": 50000, 285 "register_token": "BADBEEF", 286 }`)). 287 Expect().Status(400) 288 289 e.POST("/settings/passphrase"). 290 WithCookie(sessCookie, "connected"). 291 WithHeader("Content-Type", "application/json"). 292 WithBytes([]byte(`{ 293 "passphrase": "MyFirstPassphrase", 294 "iterations": 50000, 295 "register_token": "XYZ", 296 }`)). 297 Expect().Status(400) 298 }) 299 300 t.Run("RegisterPassphraseCorrectToken", func(t *testing.T) { 301 e := testutils.CreateTestClient(t, tsURL) 302 303 res := e.POST("/settings/passphrase"). 304 WithCookie(sessCookie, "connected"). 305 WithJSON(map[string]interface{}{ 306 "passphrase": "MyFirstPassphrase", 307 "iterations": 50000, 308 "register_token": hex.EncodeToString(testInstance.RegisterToken), 309 "key": "xxx", 310 }). 311 Expect().Status(200) 312 313 res.Cookies().Length().IsEqual(1) 314 res.Cookie("cozysessid").Value().NotEmpty() 315 }) 316 317 t.Run("UpdatePassphraseWithWrongPassphrase", func(t *testing.T) { 318 e := testutils.CreateTestClient(t, tsURL) 319 320 e.PUT("/settings/passphrase"). 321 WithCookie(sessCookie, "connected"). 322 WithHeader("Authorization", "Bearer "+token). 323 WithHeader("Content-Type", "application/json"). 324 WithBytes([]byte(`{ 325 "new_passphrase": "MyPassphrase", 326 "current_passphrase": "BADBEEF", 327 "iterations": 50000 328 }`)). 329 Expect().Status(400) 330 }) 331 332 t.Run("UpdatePassphraseSuccess", func(t *testing.T) { 333 e := testutils.CreateTestClient(t, tsURL) 334 335 res := e.PUT("/settings/passphrase"). 336 WithCookie(sessCookie, "connected"). 337 WithHeader("Authorization", "Bearer "+token). 338 WithHeader("Content-Type", "application/json"). 339 WithBytes([]byte(`{ 340 "new_passphrase": "MyUpdatedPassphrase", 341 "current_passphrase": "MyFirstPassphrase", 342 "iterations": 50000 343 }`)). 344 Expect().Status(204) 345 346 res.Cookies().Length().IsEqual(1) 347 res.Cookie("cozysessid").Value().NotEmpty() 348 }) 349 350 t.Run("UpdatePassphraseWithForce", func(t *testing.T) { 351 e := testutils.CreateTestClient(t, tsURL) 352 353 e.PUT("/settings/passphrase"). 354 WithCookie(sessCookie, "connected"). 355 WithHeader("Authorization", "Bearer "+token). 356 WithHeader("Content-Type", "application/json"). 357 WithBytes([]byte(`{ 358 "new_passphrase": "MyPassphrase", 359 "iterations": 50000, 360 "force": true 361 }`)). 362 Expect().Status(400) 363 364 passwordDefined := false 365 testInstance.PasswordDefined = &passwordDefined 366 367 e.PUT("/settings/passphrase"). 368 WithCookie(sessCookie, "connected"). 369 WithQuery("Force", true). 370 WithHeader("Authorization", "Bearer "+token). 371 WithHeader("Content-Type", "application/json"). 372 WithBytes([]byte(`{ 373 "new_passphrase": "MyPassphrase", 374 "iterations": 50000, 375 "force": true 376 }`)). 377 Expect().Status(204) 378 }) 379 380 t.Run("CheckPassphrase", func(t *testing.T) { 381 t.Run("invalid", func(t *testing.T) { 382 e := testutils.CreateTestClient(t, tsURL) 383 384 e.POST("/settings/passphrase/check"). 385 WithCookie(sessCookie, "connected"). 386 WithHeader("Authorization", "Bearer "+token). 387 WithHeader("Content-Type", "application/json"). 388 WithBytes([]byte(`{ 389 "passphrase": "Invalid Passphrase" 390 }`)). 391 Expect().Status(403) 392 }) 393 394 t.Run("valid", func(t *testing.T) { 395 e := testutils.CreateTestClient(t, tsURL) 396 397 e.POST("/settings/passphrase/check"). 398 WithCookie(sessCookie, "connected"). 399 WithHeader("Authorization", "Bearer "+token). 400 WithHeader("Content-Type", "application/json"). 401 WithBytes([]byte(`{ 402 "passphrase": "MyPassphrase" 403 }`)). 404 Expect().Status(204) 405 }) 406 }) 407 408 t.Run("GetHint", func(t *testing.T) { 409 t.Run("WithNoHint", func(t *testing.T) { 410 e := testutils.CreateTestClient(t, tsURL) 411 412 e.GET("/settings/hint"). 413 WithCookie(sessCookie, "connected"). 414 WithHeader("Authorization", "Bearer "+token). 415 Expect().Status(404) 416 }) 417 418 t.Run("WithHint", func(t *testing.T) { 419 e := testutils.CreateTestClient(t, tsURL) 420 421 setting, err := settings.Get(testInstance) 422 assert.NoError(t, err) 423 setting.PassphraseHint = "my hint" 424 err = couchdb.UpdateDoc(testInstance, setting) 425 assert.NoError(t, err) 426 427 e.GET("/settings/hint"). 428 WithCookie(sessCookie, "connected"). 429 WithHeader("Authorization", "Bearer "+token). 430 Expect().Status(204) 431 }) 432 }) 433 434 t.Run("UpdateHint", func(t *testing.T) { 435 e := testutils.CreateTestClient(t, tsURL) 436 437 e.PUT("/settings/hint"). 438 WithCookie(sessCookie, "connected"). 439 WithHeader("Authorization", "Bearer "+token). 440 WithHeader("Content-Type", "application/json"). 441 WithBytes([]byte(`{ 442 "hint": "my updated hint" 443 }`)). 444 Expect().Status(204) 445 446 setting, err := settings.Get(testInstance) 447 assert.NoError(t, err) 448 assert.Equal(t, "my updated hint", setting.PassphraseHint) 449 }) 450 451 t.Run("GetPassphraseParameters", func(t *testing.T) { 452 e := testutils.CreateTestClient(t, tsURL) 453 454 obj := e.GET("/settings/passphrase"). 455 WithCookie(sessCookie, "connected"). 456 WithHeader("Authorization", "Bearer "+token). 457 Expect().Status(200). 458 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 459 Object() 460 461 data := obj.Value("data").Object() 462 data.HasValue("type", "io.cozy.settings") 463 data.HasValue("id", "io.cozy.settings.passphrase") 464 465 attrs := data.Value("attributes").Object() 466 attrs.HasValue("salt", "me@"+testInstance.Domain) 467 attrs.HasValue("kdf", 0.0) 468 attrs.HasValue("iterations", 50000) 469 }) 470 471 t.Run("GetCapabilities", func(t *testing.T) { 472 e := testutils.CreateTestClient(t, tsURL) 473 474 e.GET("/settings/instance"). 475 WithCookie(sessCookie, "connected"). 476 Expect().Status(401) 477 478 obj := e.GET("/settings/capabilities"). 479 WithCookie(sessCookie, "connected"). 480 WithHeader("Authorization", "Bearer "+token). 481 Expect().Status(200). 482 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 483 Object() 484 485 data := obj.Value("data").Object() 486 data.HasValue("type", "io.cozy.settings") 487 data.HasValue("id", "io.cozy.settings.capabilities") 488 489 attrs := data.Value("attributes").Object() 490 attrs.HasValue("file_versioning", true) 491 attrs.HasValue("can_auth_with_password", true) 492 attrs.HasValue("can_auth_with_magic_links", false) 493 attrs.HasValue("can_auth_with_oidc", false) 494 }) 495 496 t.Run("GetInstance", func(t *testing.T) { 497 e := testutils.CreateTestClient(t, tsURL) 498 499 e.GET("/settings/instance"). 500 WithCookie(sessCookie, "connected"). 501 Expect().Status(401) 502 503 testInstance.RegisterToken = []byte("test") 504 505 e.GET("/settings/instance"). 506 WithCookie(sessCookie, "connected"). 507 WithQuery("registerToken", "74657374"). 508 Expect().Status(200) 509 510 testInstance.RegisterToken = []byte{} 511 512 obj := e.GET("/settings/instance"). 513 WithCookie(sessCookie, "connected"). 514 WithHeader("Authorization", "Bearer "+token). 515 Expect().Status(200). 516 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 517 Object() 518 519 data := obj.Value("data").Object() 520 data.HasValue("type", "io.cozy.settings") 521 data.HasValue("id", "io.cozy.settings.instance") 522 523 meta := data.Value("meta").Object() 524 instanceRev = meta.Value("rev").String().NotEmpty().Raw() 525 526 attrs := data.Value("attributes").Object() 527 attrs.HasValue("email", "alice@example.org") 528 attrs.HasValue("tz", "Europe/London") 529 attrs.HasValue("locale", "en") 530 attrs.HasValue("password_defined", true) 531 }) 532 533 t.Run("UpdateInstance", func(t *testing.T) { 534 e := testutils.CreateTestClient(t, tsURL) 535 536 obj := e.PUT("/settings/instance"). 537 WithCookie(sessCookie, "connected"). 538 WithHeader("Content-Type", "application/vnd.api+json"). 539 WithHeader("Accept", "application/vnd.api+json"). 540 WithHeader("Authorization", "Bearer "+token). 541 WithBytes([]byte(fmt.Sprintf(`{ 542 "data": { 543 "type": "io.cozy.settings", 544 "id": "io.cozy.settings.instance", 545 "meta": { 546 "rev": "%s" 547 }, 548 "attributes": { 549 "tz": "Europe/Paris", 550 "email": "alice@example.net", 551 "locale": "fr" 552 } 553 } 554 }`, instanceRev))). 555 Expect().Status(200). 556 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 557 Object() 558 559 data := obj.Value("data").Object() 560 data.HasValue("type", "io.cozy.settings") 561 data.HasValue("id", "io.cozy.settings.instance") 562 563 meta := data.Value("meta").Object() 564 instanceRev = meta.Value("rev").String().NotEmpty().NotEqual(instanceRev).Raw() 565 566 attrs := data.Value("attributes").Object() 567 attrs.HasValue("email", "alice@example.net") 568 attrs.HasValue("tz", "Europe/Paris") 569 attrs.HasValue("locale", "fr") 570 }) 571 572 t.Run("GetUpdatedInstance", func(t *testing.T) { 573 e := testutils.CreateTestClient(t, tsURL) 574 575 obj := e.GET("/settings/instance"). 576 WithCookie(sessCookie, "connected"). 577 WithHeader("Authorization", "Bearer "+token). 578 WithHeader("Accept", "application/vnd.api+json"). 579 WithHeader("Content-Type", "application/vnd.api+json"). 580 Expect().Status(200). 581 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 582 Object() 583 584 data := obj.Value("data").Object() 585 data.HasValue("type", "io.cozy.settings") 586 data.HasValue("id", "io.cozy.settings.instance") 587 588 meta := data.Value("meta").Object() 589 meta.HasValue("rev", instanceRev) 590 591 attrs := data.Value("attributes").Object() 592 attrs.HasValue("email", "alice@example.net") 593 attrs.HasValue("tz", "Europe/Paris") 594 attrs.HasValue("locale", "fr") 595 }) 596 597 t.Run("UpdatePassphraseWithTwoFactorAuth", func(t *testing.T) { 598 e := testutils.CreateTestClient(t, tsURL) 599 600 e.PUT("/settings/instance/auth_mode"). 601 WithCookie(sessCookie, "connected"). 602 WithHeader("Authorization", "Bearer "+token). 603 WithHeader("Accept", "application/json"). 604 WithHeader("Content-Type", "application/json"). 605 WithBytes([]byte(`{ 606 "auth_mode": "two_factor_mail" 607 }`)). 608 Expect().Status(204) 609 610 mailPassCode, err := testInstance.GenerateMailConfirmationCode() 611 require.NoError(t, err) 612 613 e.PUT("/settings/instance/auth_mode"). 614 WithCookie(sessCookie, "connected"). 615 WithHeader("Authorization", "Bearer "+token). 616 WithHeader("Accept", "application/json"). 617 WithHeader("Content-Type", "application/json"). 618 WithBytes([]byte(fmt.Sprintf(`{ 619 "auth_mode": "two_factor_mail", 620 "two_factor_activation_code": "%s" 621 }`, mailPassCode))). 622 Expect().Status(204) 623 624 obj := e.PUT("/settings/passphrase"). 625 WithCookie(sessCookie, "connected"). 626 WithHeader("Authorization", "Bearer "+token). 627 WithHeader("Content-Type", "application/json"). 628 WithBytes([]byte(`{ 629 "current_passphrase": "MyPassphrase" 630 }`)). 631 Expect().Status(200). 632 JSON().Object() 633 634 obj.Value("two_factor_token").String().NotEmpty() 635 636 twoFactorToken, twoFactorPasscode, err := testInstance.GenerateTwoFactorSecrets() 637 require.NoError(t, err) 638 639 e.PUT("/settings/passphrase"). 640 WithCookie(sessCookie, "connected"). 641 WithHeader("Authorization", "Bearer "+token). 642 WithJSON(map[string]interface{}{ 643 "new_passphrase": "MyLastPassphrase", 644 "two_factor_token": twoFactorToken, 645 "two_factor_passcode": twoFactorPasscode, 646 }). 647 Expect().Status(204) 648 }) 649 650 t.Run("ListClients", func(t *testing.T) { 651 e := testutils.CreateTestClient(t, tsURL) 652 653 e.GET("/settings/clients"). 654 WithCookie(sessCookie, "connected"). 655 Expect().Status(401) 656 657 client := &oauth.Client{ 658 RedirectURIs: []string{"http:/localhost:4000/oauth/callback"}, 659 ClientName: "Cozy-desktop on my-new-laptop", 660 ClientKind: "desktop", 661 ClientURI: "https://docs.cozy.io/en/mobile/desktop.html", 662 LogoURI: "https://docs.cozy.io/assets/images/cozy-logo-docs.svg", 663 PolicyURI: "https://cozy.io/policy", 664 SoftwareID: "/github.com/cozy-labs/cozy-desktop", 665 SoftwareVersion: "0.16.0", 666 } 667 regErr := client.Create(testInstance) 668 assert.Nil(t, regErr) 669 oauthClientID = client.ClientID 670 671 obj := e.GET("/settings/clients"). 672 WithCookie(sessCookie, "connected"). 673 WithHeader("Authorization", "Bearer "+token). 674 Expect().Status(200). 675 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 676 Object() 677 678 data := obj.Value("data").Array() 679 data.Length().IsEqual(2) 680 681 el := data.Value(1).Object() 682 el.HasValue("type", "io.cozy.oauth.clients") 683 el.HasValue("id", client.ClientID) 684 685 links := el.Value("links").Object() 686 links.HasValue("self", "/settings/clients/"+client.ClientID) 687 688 attrs := el.Value("attributes").Object() 689 attrs.HasValue("client_name", client.ClientName) 690 attrs.HasValue("client_kind", client.ClientKind) 691 attrs.HasValue("client_uri", client.ClientURI) 692 attrs.HasValue("logo_uri", client.LogoURI) 693 attrs.HasValue("policy_uri", client.PolicyURI) 694 attrs.HasValue("software_id", client.SoftwareID) 695 attrs.HasValue("software_version", client.SoftwareVersion) 696 attrs.NotContainsKey("client_secret") 697 698 redirectURIs := attrs.Value("redirect_uris").Array() 699 redirectURIs.Length().IsEqual(1) 700 redirectURIs.Value(0).String().IsEqual(client.RedirectURIs[0]) 701 }) 702 703 t.Run("RevokeClient", func(t *testing.T) { 704 e := testutils.CreateTestClient(t, tsURL) 705 706 e.DELETE("/settings/clients/"+oauthClientID). 707 WithCookie(sessCookie, "connected"). 708 Expect().Status(401) 709 710 e.DELETE("/settings/clients/"+oauthClientID). 711 WithCookie(sessCookie, "connected"). 712 WithHeader("Authorization", "Bearer "+token). 713 Expect().Status(204) 714 715 obj := e.GET("/settings/clients"). 716 WithCookie(sessCookie, "connected"). 717 WithHeader("Authorization", "Bearer "+token). 718 Expect().Status(200). 719 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 720 Object() 721 722 data := obj.Value("data").Array() 723 data.Length().IsEqual(1) 724 }) 725 726 t.Run("PatchInstanceSameParams", func(t *testing.T) { 727 e := testutils.CreateTestClient(t, tsURL) 728 729 doc1, err := testInstance.SettingsDocument() 730 require.NoError(t, err) 731 732 e.PUT("/settings/instance"). 733 WithCookie(sessCookie, "connected"). 734 WithHeader("Authorization", "Bearer "+token). 735 WithHeader("Accept", "application/vnd.api+json"). 736 WithHeader("Content-Type", "application/vnd.api+json"). 737 WithBytes([]byte(`{ 738 "data": { 739 "type": "io.cozy.settings", 740 "id": "io.cozy.settings.instance", 741 "meta": { 742 "rev": "%s" 743 }, 744 "attributes": { 745 "tz": "Europe/Paris", 746 "email": "alice@example.net", 747 "locale": "fr" 748 } 749 } 750 }`)). 751 Expect().Status(200). 752 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 753 Object().NotEmpty() 754 755 doc2, err := testInstance.SettingsDocument() 756 assert.NoError(t, err) 757 758 // Assert no changes 759 assert.Equal(t, doc1.Rev(), doc2.Rev()) 760 }) 761 762 t.Run("PatchInstanceChangeParams", func(t *testing.T) { 763 e := testutils.CreateTestClient(t, tsURL) 764 765 doc, err := testInstance.SettingsDocument() 766 require.NoError(t, err) 767 768 e.PUT("/settings/instance"). 769 WithCookie(sessCookie, "connected"). 770 WithHeader("Authorization", "Bearer "+token). 771 WithHeader("Accept", "application/vnd.api+json"). 772 WithHeader("Content-Type", "application/vnd.api+json"). 773 WithBytes([]byte(fmt.Sprintf(`{ 774 "data": { 775 "type": "io.cozy.settings", 776 "id": "io.cozy.settings.instance", 777 "meta": { 778 "rev": "%s" 779 }, 780 "attributes": { 781 "tz": "Antarctica/McMurdo", 782 "email": "alice@expat.eu", 783 "locale": "de" 784 } 785 } 786 }`, doc.Rev()))). 787 Expect().Status(200). 788 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 789 Object().NotEmpty() 790 791 doc, err = testInstance.SettingsDocument() 792 assert.NoError(t, err) 793 794 assert.Equal(t, "Antarctica/McMurdo", doc.M["tz"].(string)) 795 assert.Equal(t, "alice@expat.eu", doc.M["email"].(string)) 796 }) 797 798 t.Run("PatchInstanceAddParam", func(t *testing.T) { 799 e := testutils.CreateTestClient(t, tsURL) 800 801 doc1, err := testInstance.SettingsDocument() 802 assert.NoError(t, err) 803 804 e.PUT("/settings/instance"). 805 WithCookie(sessCookie, "connected"). 806 WithHeader("Authorization", "Bearer "+token). 807 WithHeader("Accept", "application/vnd.api+json"). 808 WithHeader("Content-Type", "application/vnd.api+json"). 809 WithBytes([]byte(fmt.Sprintf(`{ 810 "data": { 811 "type": "io.cozy.settings", 812 "id": "io.cozy.settings.instance", 813 "meta": { 814 "rev": "%s" 815 }, 816 "attributes": { 817 "tz": "Europe/Berlin", 818 "email": "alice@example.com", 819 "how_old_are_you": "42" 820 } 821 } 822 }`, doc1.Rev()))). 823 Expect().Status(200). 824 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 825 Object().NotEmpty() 826 827 doc2, err := testInstance.SettingsDocument() 828 assert.NoError(t, err) 829 assert.NotEqual(t, doc1.Rev(), doc2.Rev()) 830 assert.Equal(t, "42", doc2.M["how_old_are_you"].(string)) 831 assert.Equal(t, "Europe/Berlin", doc2.M["tz"].(string)) 832 assert.Equal(t, "alice@example.com", doc2.M["email"].(string)) 833 }) 834 835 t.Run("PatchInstanceRemoveParams", func(t *testing.T) { 836 e := testutils.CreateTestClient(t, tsURL) 837 838 doc1, err := testInstance.SettingsDocument() 839 assert.NoError(t, err) 840 841 e.PUT("/settings/instance"). 842 WithCookie(sessCookie, "connected"). 843 WithHeader("Authorization", "Bearer "+token). 844 WithHeader("Accept", "application/vnd.api+json"). 845 WithHeader("Content-Type", "application/vnd.api+json"). 846 WithBytes([]byte(fmt.Sprintf(`{ 847 "data": { 848 "type": "io.cozy.settings", 849 "id": "io.cozy.settings.instance", 850 "meta": { 851 "rev": "%s" 852 }, 853 "attributes": { 854 "tz": "Europe/Berlin" 855 } 856 } 857 }`, doc1.Rev()))). 858 Expect().Status(200). 859 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 860 Object().NotEmpty() 861 862 doc2, err := testInstance.SettingsDocument() 863 assert.NoError(t, err) 864 assert.NotEqual(t, doc1.Rev(), doc2.Rev()) 865 assert.Equal(t, "Europe/Berlin", doc2.M["tz"].(string)) 866 _, ok := doc2.M["email"] 867 assert.False(t, ok) 868 }) 869 870 t.Run("FeatureFlags", func(t *testing.T) { 871 e := testutils.CreateTestClient(t, tsURL) 872 873 _ = couchdb.DeleteDB(prefixer.GlobalPrefixer, consts.Settings) 874 t.Cleanup(func() { _ = couchdb.DeleteDB(prefixer.GlobalPrefixer, consts.Settings) }) 875 876 obj := e.GET("/settings/flags"). 877 WithCookie(sessCookie, "connected"). 878 WithHeader("Authorization", "Bearer "+token). 879 Expect().Status(200). 880 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 881 Object() 882 883 data := obj.Value("data").Object() 884 data.HasValue("type", "io.cozy.settings") 885 data.HasValue("id", "io.cozy.settings.flags") 886 887 data.Value("attributes").Object().IsEmpty() 888 889 testInstance.FeatureFlags = map[string]interface{}{ 890 "from_instance_flag": true, 891 "from_multiple_source": "instance_flag", 892 "json_object": map[string]interface{}{"foo": "bar"}, 893 } 894 testInstance.FeatureSets = []string{"set1", "set2"} 895 require.NoError(t, instance.Update(testInstance)) 896 897 cache := config.GetConfig().CacheStorage 898 899 cacheKey := fmt.Sprintf("flags:%s:%v", testInstance.ContextName, testInstance.FeatureSets) 900 buf, err := json.Marshal(map[string]interface{}{ 901 "from_feature_sets": true, 902 "from_multiple_source": "manager", 903 }) 904 assert.NoError(t, err) 905 cache.Set(cacheKey, buf, 5*time.Second) 906 ctxFlags := couchdb.JSONDoc{Type: consts.Settings} 907 ctxFlags.M = map[string]interface{}{ 908 "ratio_0": []map[string]interface{}{ 909 {"ratio": 0, "value": "context"}, 910 }, 911 "ratio_1": []map[string]interface{}{ 912 {"ratio": 1, "value": "context"}, 913 }, 914 "ratio_0.000001": []map[string]interface{}{ 915 {"ratio": 0.000001, "value": "context"}, 916 }, 917 "ratio_0.999999": []map[string]interface{}{ 918 {"ratio": 0.999999, "value": "context"}, 919 }, 920 } 921 922 id := fmt.Sprintf("%s.%s", consts.ContextFlagsSettingsID, testInstance.ContextName) 923 ctxFlags.SetID(id) 924 err = couchdb.CreateNamedDocWithDB(prefixer.GlobalPrefixer, &ctxFlags) 925 assert.NoError(t, err) 926 defFlags := couchdb.JSONDoc{Type: consts.Settings} 927 defFlags.M = map[string]interface{}{ 928 "ratio_0": "defaults", 929 "ratio_1": "defaults", 930 "ratio_0.000001": "defaults", 931 "ratio_0.999999": "defaults", 932 "from_multiple_source": "defaults", 933 "from_defaults": true, 934 } 935 defFlags.SetID(consts.DefaultFlagsSettingsID) 936 err = couchdb.CreateNamedDocWithDB(prefixer.GlobalPrefixer, &defFlags) 937 assert.NoError(t, err) 938 939 obj = e.GET("/settings/flags"). 940 WithCookie(sessCookie, "connected"). 941 WithHeader("Authorization", "Bearer "+token). 942 Expect().Status(200). 943 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 944 Object() 945 946 data = obj.Value("data").Object() 947 data.HasValue("type", "io.cozy.settings") 948 data.HasValue("id", "io.cozy.settings.flags") 949 950 attrs := data.Value("attributes").Object() 951 attrs.HasValue("from_instance_flag", true) 952 attrs.HasValue("from_feature_sets", true) 953 attrs.HasValue("from_defaults", true) 954 attrs.HasValue("json_object", testInstance.FeatureFlags["json_object"]) 955 attrs.HasValue("from_multiple_source", "instance_flag") 956 attrs.HasValue("ratio_0", "defaults") 957 attrs.HasValue("ratio_0.000001", "defaults") 958 attrs.HasValue("ratio_0.999999", "context") 959 attrs.HasValue("ratio_1", "context") 960 }) 961 962 t.Run("ClientsLimitExceededWithoutSession", func(t *testing.T) { 963 e := testutils.CreateTestClient(t, tsURL) 964 965 e.GET("/settings/clients/limit-exceeded"). 966 WithRedirectPolicy(httpexpect.DontFollowRedirects). 967 Expect().Status(401) 968 }) 969 970 t.Run("ClientsLimitExceededWithoutLimit", func(t *testing.T) { 971 e := testutils.CreateTestClient(t, tsURL) 972 973 e.GET("/settings/clients/limit-exceeded"). 974 WithCookie(sessCookie, "connected"). 975 WithHeader("Authorization", "Bearer "+token). 976 WithHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"). 977 WithRedirectPolicy(httpexpect.DontFollowRedirects). 978 Expect().Status(302). 979 Header("location").IsEqual(testInstance.DefaultRedirection().String()) 980 981 redirect := "cozy://my-app" 982 e.GET("/settings/clients/limit-exceeded"). 983 WithCookie(sessCookie, "connected"). 984 WithQuery("redirect", redirect). 985 WithHeader("Authorization", "Bearer "+token). 986 WithHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"). 987 WithRedirectPolicy(httpexpect.DontFollowRedirects). 988 Expect().Status(302). 989 Header("location").IsEqual(redirect) 990 }) 991 992 t.Run("ClientsLimitExceededWithLimitExceeded", func(t *testing.T) { 993 e := testutils.CreateTestClient(t, tsURL) 994 995 testutils.WithFlag(t, testInstance, "cozy.oauthclients.max", float64(0)) 996 997 // Create the OAuth client for the flagship app 998 flagship := oauth.Client{ 999 RedirectURIs: []string{"cozy://flagship"}, 1000 ClientName: "flagship-app", 1001 ClientKind: "mobile", 1002 SoftwareID: "github.com/cozy/cozy-stack/testing/flagship", 1003 Flagship: true, 1004 } 1005 require.Nil(t, flagship.Create(testInstance, oauth.NotPending)) 1006 defer flagship.Delete(testInstance) 1007 1008 e.GET("/settings/clients/limit-exceeded"). 1009 WithCookie(sessCookie, "connected"). 1010 WithHeader("Authorization", "Bearer "+token). 1011 WithHost(testInstance.Domain). 1012 WithRedirectPolicy(httpexpect.DontFollowRedirects). 1013 Expect().Status(200). 1014 HasContentType("text/html", "utf-8"). 1015 Body(). 1016 Contains("Disconnect one of your devices or change your Cozy offer to access your Cozy from this device."). 1017 Contains("/#/connectedDevices"). 1018 NotContains("http://manager.example.org") 1019 1020 testutils.WithManager(t, testInstance) 1021 1022 e.GET("/settings/clients/limit-exceeded"). 1023 WithCookie(sessCookie, "connected"). 1024 WithHeader("Authorization", "Bearer "+token). 1025 WithHost(testInstance.Domain). 1026 WithRedirectPolicy(httpexpect.DontFollowRedirects). 1027 Expect().Status(200). 1028 HasContentType("text/html", "utf-8"). 1029 Body(). 1030 Contains("Disconnect one of your devices or change your Cozy offer to access your Cozy from this device."). 1031 Contains("/#/connectedDevices"). 1032 Contains("http://manager.example.org") 1033 1034 testutils.WithFlag(t, testInstance, "flagship.iap.enabled", true) 1035 1036 e.GET("/settings/clients/limit-exceeded"). 1037 WithCookie(sessCookie, "connected"). 1038 WithQuery("isFlagship", true). 1039 WithHeader("Authorization", "Bearer "+token). 1040 WithHost(testInstance.Domain). 1041 WithRedirectPolicy(httpexpect.DontFollowRedirects). 1042 Expect().Status(200). 1043 HasContentType("text/html", "utf-8"). 1044 Body(). 1045 Contains("Disconnect one of your devices or change your Cozy offer to access your Cozy from this device."). 1046 Contains("/#/connectedDevices"). 1047 NotContains("http://manager.example.org") 1048 1049 e.GET("/settings/clients/limit-exceeded"). 1050 WithCookie(sessCookie, "connected"). 1051 WithQuery("isFlagship", true). 1052 WithQuery("isIapAvailable", true). 1053 WithHeader("Authorization", "Bearer "+token). 1054 WithHost(testInstance.Domain). 1055 WithRedirectPolicy(httpexpect.DontFollowRedirects). 1056 Expect().Status(200). 1057 HasContentType("text/html", "utf-8"). 1058 Body(). 1059 Contains("Disconnect one of your devices or change your Cozy offer to access your Cozy from this device."). 1060 Contains("/#/connectedDevices"). 1061 Contains("http://manager.example.org") 1062 }) 1063 1064 t.Run("ClientsLimitExceededWithLimitReached", func(t *testing.T) { 1065 e := testutils.CreateTestClient(t, tsURL) 1066 1067 clients, _, err := oauth.GetConnectedUserClients(testInstance, 100, "") 1068 require.NoError(t, err) 1069 1070 testutils.WithFlag(t, testInstance, "cozy.oauthclients.max", float64(len(clients))) 1071 1072 e.GET("/settings/clients/limit-exceeded"). 1073 WithCookie(sessCookie, "connected"). 1074 WithHeader("Authorization", "Bearer "+token). 1075 WithHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"). 1076 WithRedirectPolicy(httpexpect.DontFollowRedirects). 1077 Expect().Status(302). 1078 Header("location").IsEqual(testInstance.DefaultRedirection().String()) 1079 }) 1080 } 1081 1082 func TestRegisterPassphraseForFlagshipApp(t *testing.T) { 1083 if testing.Short() { 1084 t.Skip("an instance is required for this test: test skipped due to the use of --short flag") 1085 } 1086 1087 config.UseTestFile(t) 1088 testutils.NeedCouchdb(t) 1089 1090 oauthClient := &oauth.Client{ 1091 RedirectURIs: []string{"http:/localhost:4000/oauth/callback"}, 1092 ClientName: "Cozy-desktop on my-new-laptop", 1093 ClientKind: "desktop", 1094 ClientURI: "https://docs.cozy.io/en/mobile/desktop.html", 1095 LogoURI: "https://docs.cozy.io/assets/images/cozy-logo-docs.svg", 1096 PolicyURI: "https://cozy.io/policy", 1097 SoftwareID: "/github.com/cozy-labs/cozy-desktop", 1098 SoftwareVersion: "0.16.0", 1099 } 1100 1101 setupFlagship := testutils.NewSetup(t, t.Name()) 1102 testInstance := setupFlagship.GetTestInstance(&lifecycle.Options{ 1103 Locale: "en", 1104 Timezone: "Europe/Berlin", 1105 Email: "alice2@example.com", 1106 ContextName: "test-context", 1107 }) 1108 1109 svc := csettings.NewServiceMock(t) 1110 tsURL := setupRouter(t, testInstance, svc).URL 1111 1112 require.Nil(t, oauthClient.Create(testInstance)) 1113 client, err := oauth.FindClient(testInstance, oauthClient.ClientID) 1114 require.NoError(t, err) 1115 require.NoError(t, client.SetFlagship(testInstance)) 1116 1117 e := httpexpect.Default(t, tsURL) 1118 obj := e.POST("/settings/passphrase/flagship"). 1119 WithJSON(map[string]interface{}{ 1120 "passphrase": "MyFirstPassphrase", 1121 "iterations": 50000, 1122 "register_token": hex.EncodeToString(testInstance.RegisterToken), 1123 "key": "xxx-key-xxx", 1124 "public_key": "xxx-public-key-xxx", 1125 "private_key": "xxx-private-key-xxx", 1126 "client_id": client.CouchID, 1127 "client_secret": client.ClientSecret, 1128 }). 1129 Expect().Status(200). 1130 JSON().Object() 1131 1132 obj.Value("access_token").String().NotEmpty() 1133 obj.Value("refresh_token").String().NotEmpty() 1134 obj.HasValue("scope", "*") 1135 obj.HasValue("token_type", "bearer") 1136 }