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  }