github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/bitwarden/bitwarden_test.go (about)

     1  package bitwarden
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/cozy/cozy-stack/model/app"
    12  	"github.com/cozy/cozy-stack/model/bitwarden"
    13  	"github.com/cozy/cozy-stack/model/bitwarden/settings"
    14  	"github.com/cozy/cozy-stack/model/instance/lifecycle"
    15  	"github.com/cozy/cozy-stack/pkg/config/config"
    16  	"github.com/cozy/cozy-stack/pkg/consts"
    17  	"github.com/cozy/cozy-stack/pkg/couchdb"
    18  	"github.com/cozy/cozy-stack/pkg/crypto"
    19  	"github.com/cozy/cozy-stack/tests/testutils"
    20  	"github.com/cozy/cozy-stack/web/errors"
    21  	_ "github.com/cozy/cozy-stack/worker/mails"
    22  	"github.com/gavv/httpexpect/v2"
    23  	"github.com/labstack/echo/v4"
    24  	"github.com/sirupsen/logrus/hooks/test"
    25  	"github.com/stretchr/testify/assert"
    26  	"github.com/stretchr/testify/require"
    27  )
    28  
    29  func TestBitwarden(t *testing.T) {
    30  	if testing.Short() {
    31  		t.Skip("an instance is required for this test: test skipped due to the use of --short flag")
    32  	}
    33  
    34  	var token, collID, orgaID, folderID, cipherID string
    35  
    36  	config.UseTestFile(t)
    37  	testutils.NeedCouchdb(t)
    38  	setup := testutils.NewSetup(t, t.Name())
    39  	inst := setup.GetTestInstance(&lifecycle.Options{
    40  		Domain:     "bitwarden.example.net",
    41  		Passphrase: "cozy",
    42  		PublicName: "Pierre",
    43  		Email:      "pierre@cozy.localhost",
    44  	})
    45  
    46  	ts := setup.GetTestServer("/bitwarden", Routes)
    47  	ts.Config.Handler.(*echo.Echo).HTTPErrorHandler = errors.ErrorHandler
    48  
    49  	// Install cozy-pass webapp (required for OAuth linked clients)
    50  	installer, err := app.NewInstaller(inst, app.Copier(consts.WebappType, inst),
    51  		&app.InstallerOptions{
    52  			Operation:  app.Install,
    53  			Type:       consts.WebappType,
    54  			Slug:       "passwords",
    55  			SourceURL:  "registry://passwords",
    56  			Registries: inst.Registries(),
    57  		},
    58  	)
    59  	require.NoError(t, err)
    60  
    61  	_, err = installer.RunSync()
    62  	require.NoError(t, err)
    63  
    64  	t.Run("Prelogin", func(t *testing.T) {
    65  		e := testutils.CreateTestClient(t, ts.URL)
    66  
    67  		obj := e.POST("/bitwarden/api/accounts/prelogin").
    68  			WithHeader("Content-Type", "application/json").
    69  			WithBytes([]byte(`{ "email": "me@bitwarden.example.net" }`)).
    70  			Expect().Status(http.StatusOK).
    71  			JSON().Object()
    72  
    73  		obj.ValueEqual("Kdf", 0)
    74  		obj.ValueEqual("OIDC", false)
    75  		obj.ValueEqual("KdfIterations", crypto.DefaultPBKDF2Iterations)
    76  		obj.ValueEqual("HasCiphers", false)
    77  	})
    78  
    79  	t.Run("Connect", func(t *testing.T) {
    80  		e := testutils.CreateTestClient(t, ts.URL)
    81  
    82  		testLogger := test.NewGlobal()
    83  		setting, err := settings.Get(inst)
    84  		assert.NoError(t, err)
    85  		setting.EncryptedOrgKey = ""
    86  		err = setting.Save(inst)
    87  		assert.NoError(t, err)
    88  
    89  		email := inst.PassphraseSalt()
    90  		iter := crypto.DefaultPBKDF2Iterations
    91  		pass, _ := crypto.HashPassWithPBKDF2([]byte("cozy"), email, iter)
    92  
    93  		obj := e.POST("/bitwarden/identity/connect/token").
    94  			WithFormField("grant_type", "password").
    95  			WithFormField("username", string(email)).
    96  			WithFormField("password", string(pass)).
    97  			WithFormField("scope", "api offline_access").
    98  			WithFormField("client_id", "browser").
    99  			WithFormField("deviceType", "3").
   100  			Expect().
   101  			Status(http.StatusOK).
   102  			JSON().Object()
   103  
   104  		obj.ValueEqual("token_type", "Bearer")
   105  		obj.ValueEqual("expires_in", consts.AccessTokenValidityDuration.Seconds())
   106  		token = obj.Value("access_token").String().NotEmpty().Raw()
   107  
   108  		obj.Value("refresh_token").String().NotEmpty()
   109  		obj.Value("Key").String().NotEmpty()
   110  		obj.Value("PrivateKey").String().NotEmpty()
   111  		obj.Value("client_id").String().NotEmpty()
   112  		obj.Value("registration_access_token").String().NotEmpty()
   113  		obj.Value("Kdf").Number()
   114  		obj.Value("KdfIterations").Number()
   115  
   116  		entries := testLogger.AllEntries()
   117  		assert.NotZero(t, len(entries))
   118  		orgKeyDoesNotExist := false
   119  		for _, entry := range entries {
   120  			if entry.Message == "Organization key does not exist" {
   121  				orgKeyDoesNotExist = true
   122  			}
   123  		}
   124  		assert.True(t, orgKeyDoesNotExist)
   125  
   126  		setting, err = settings.Get(inst)
   127  		assert.NoError(t, err)
   128  		orgKey, err := setting.OrganizationKey()
   129  		assert.NoError(t, err)
   130  		assert.NotEmpty(t, orgKey)
   131  	})
   132  
   133  	t.Run("GetCozyOrg", func(t *testing.T) {
   134  		e := testutils.CreateTestClient(t, ts.URL)
   135  
   136  		e.GET("/bitwarden/organizations/cozy").
   137  			WithHeader("Authorization", "Bearer invalid-token").
   138  			Expect().Status(401)
   139  
   140  		obj := e.GET("/bitwarden/organizations/cozy").
   141  			WithHeader("Authorization", "Bearer "+token).
   142  			Expect().Status(200).
   143  			JSON().Object()
   144  
   145  		orgaID = obj.Value("organizationId").String().NotEmpty().Raw()
   146  		collID = obj.Value("collectionId").String().NotEmpty().Raw()
   147  		orgKey := obj.Value("organizationKey").String().NotEmpty().Raw()
   148  
   149  		_, err := base64.StdEncoding.DecodeString(orgKey)
   150  		assert.NoError(t, err)
   151  	})
   152  
   153  	t.Run("CreateFolder", func(t *testing.T) {
   154  		e := testutils.CreateTestClient(t, ts.URL)
   155  
   156  		obj := e.POST("/bitwarden/api/folders").
   157  			WithHeader("Content-Type", "application/json").
   158  			WithHeader("Authorization", "Bearer "+token).
   159  			WithBytes([]byte(`{ "name": "2.FQAwIBaDbczEGnEJw4g4hw==|7KreXaC0duAj0ulzZJ8ncA==|nu2sEvotjd4zusvGF8YZJPnS9SiJPDqc1VIfCrfve/o=" }`)).
   160  			Expect().Status(200).
   161  			JSON().Object()
   162  
   163  		obj.ValueEqual("Name", "2.FQAwIBaDbczEGnEJw4g4hw==|7KreXaC0duAj0ulzZJ8ncA==|nu2sEvotjd4zusvGF8YZJPnS9SiJPDqc1VIfCrfve/o=")
   164  		obj.ValueEqual("Object", "folder")
   165  		obj.Value("RevisionDate").String().DateTime(time.RFC3339)
   166  
   167  		folderID = obj.Value("Id").String().NotEmpty().Raw()
   168  	})
   169  
   170  	t.Run("ListFolders", func(t *testing.T) {
   171  		e := testutils.CreateTestClient(t, ts.URL)
   172  
   173  		obj := e.GET("/bitwarden/api/folders").
   174  			WithHeader("Authorization", "Bearer "+token).
   175  			Expect().Status(200).
   176  			JSON().Object()
   177  
   178  		obj.ValueEqual("Object", "list")
   179  		obj.Value("Data").Array().Length().Equal(1)
   180  
   181  		item := obj.Value("Data").Array().First().Object()
   182  		item.ValueEqual("Name", "2.FQAwIBaDbczEGnEJw4g4hw==|7KreXaC0duAj0ulzZJ8ncA==|nu2sEvotjd4zusvGF8YZJPnS9SiJPDqc1VIfCrfve/o=")
   183  		item.ValueEqual("Object", "folder")
   184  		item.ValueEqual("Id", folderID)
   185  		item.Value("RevisionDate").String().DateTime(time.RFC3339)
   186  	})
   187  
   188  	t.Run("GetFolder", func(t *testing.T) {
   189  		e := testutils.CreateTestClient(t, ts.URL)
   190  
   191  		obj := e.GET("/bitwarden/api/folders/"+folderID).
   192  			WithHeader("Authorization", "Bearer "+token).
   193  			Expect().Status(200).
   194  			JSON().Object()
   195  
   196  		obj.ValueEqual("Name", "2.FQAwIBaDbczEGnEJw4g4hw==|7KreXaC0duAj0ulzZJ8ncA==|nu2sEvotjd4zusvGF8YZJPnS9SiJPDqc1VIfCrfve/o=")
   197  		obj.ValueEqual("Object", "folder")
   198  		obj.ValueEqual("Id", folderID)
   199  		obj.Value("RevisionDate").String().DateTime(time.RFC3339)
   200  	})
   201  
   202  	t.Run("RenameFolder", func(t *testing.T) {
   203  		e := testutils.CreateTestClient(t, ts.URL)
   204  
   205  		obj := e.PUT("/bitwarden/api/folders/"+folderID).
   206  			WithHeader("Content-Type", "application/json").
   207  			WithHeader("Authorization", "Bearer "+token).
   208  			WithBytes([]byte(`{ "name": "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=" }`)).
   209  			Expect().Status(200).
   210  			JSON().Object()
   211  
   212  		obj.ValueEqual("Name", "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=")
   213  		obj.ValueEqual("Object", "folder")
   214  		obj.ValueEqual("Id", folderID)
   215  		obj.Value("RevisionDate").String().DateTime(time.RFC3339)
   216  	})
   217  
   218  	t.Run("DeleteFolder", func(t *testing.T) {
   219  		e := testutils.CreateTestClient(t, ts.URL)
   220  
   221  		obj := e.POST("/bitwarden/api/folders").
   222  			WithHeader("Content-Type", "application/json").
   223  			WithHeader("Authorization", "Bearer "+token).
   224  			WithBytes([]byte(`{ "name": "2.FQAwIBaDbczEGnEJw4g4hw==|7KreXaC0duAj0ulzZJ8ncA==|nu2sEvotjd4zusvGF8YZJPnS9SiJPDqc1VIfCrfve/o=" }`)).
   225  			Expect().Status(200).
   226  			JSON().Object()
   227  
   228  		id := obj.Value("Id").String().NotEmpty().Raw()
   229  
   230  		obj = e.POST("/bitwarden/api/ciphers").
   231  			WithHeader("Content-Type", "application/json").
   232  			WithHeader("Authorization", "Bearer "+token).
   233  			WithBytes([]byte(`{
   234        "type": 1,
   235        "favorite": false,
   236        "name": "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=",
   237        "notes": null,
   238        "folderId": "` + id + `",
   239        "organizationId": null,
   240        "login": {
   241          "uri": "2.T57BwAuV8ubIn/sZPbQC+A==|EhUSSpJWSzSYOdJ/AQzfXuUXxwzcs/6C4tOXqhWAqcM=|OWV2VIqLfoWPs9DiouXGUOtTEkVeklbtJQHkQFIXkC8=",
   242          "username": "2.JbFkAEZPnuMm70cdP44wtA==|fsN6nbT+udGmOWv8K4otgw==|JbtwmNQa7/48KszT2hAdxpmJ6DRPZst0EDEZx5GzesI=",
   243          "password": "2.e83hIsk6IRevSr/H1lvZhg==|48KNkSCoTacopXRmIZsbWg==|CIcWgNbaIN2ix2Fx1Gar6rWQeVeboehp4bioAwngr0o=",
   244          "totp": null
   245        }
   246      }`)).
   247  			Expect().Status(200).
   248  			JSON().Object()
   249  
   250  		cID := obj.Value("Id").String().NotEmpty().Raw()
   251  
   252  		e.DELETE("/bitwarden/api/folders/"+id).
   253  			WithHeader("Authorization", "Bearer "+token).
   254  			Expect().Status(200)
   255  
   256  		// Check that the cipher in this folder has been moved out
   257  		obj = e.GET("/bitwarden/api/ciphers/"+cID).
   258  			WithHeader("Authorization", "Bearer "+token).
   259  			Expect().Status(200).
   260  			JSON().Object()
   261  
   262  		obj.ValueEqual("Name", "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=")
   263  		obj.Value("FolderId").Null() // is empty
   264  
   265  		e.DELETE("/bitwarden/api/ciphers/"+cID).
   266  			WithHeader("Authorization", "Bearer "+token).
   267  			Expect().Status(200)
   268  	})
   269  
   270  	t.Run("CreateNoType", func(t *testing.T) {
   271  		e := testutils.CreateTestClient(t, ts.URL)
   272  
   273  		obj := e.POST("/bitwarden/api/ciphers").
   274  			WithHeader("Content-Type", "application/json").
   275  			WithHeader("Authorization", "Bearer "+token).
   276  			WithBytes([]byte(`{
   277        "name": "2.G38TIU3t1pGOfkzjCQE7OQ==|Xa1RupttU7zrWdzIT6oK+w==|J3C6qU1xDrfTgyJD+OrDri1GjgGhU2nmRK75FbZHXoI=",
   278        "organizationId": null
   279      }`)).
   280  			Expect().Status(400).
   281  			JSON().Object()
   282  
   283  		obj.Value("error").String().NotEmpty()
   284  	})
   285  
   286  	t.Run("CreateLogin", func(t *testing.T) {
   287  		e := testutils.CreateTestClient(t, ts.URL)
   288  
   289  		obj := e.POST("/bitwarden/api/ciphers").
   290  			WithHeader("Content-Type", "application/json").
   291  			WithHeader("Authorization", "Bearer "+token).
   292  			WithBytes([]byte(`{
   293        "type": 1,
   294        "favorite": false,
   295        "name": "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=",
   296        "notes": null,
   297        "folderId": null,
   298        "organizationId": null,
   299        "login": {
   300          "uri": "2.T57BwAuV8ubIn/sZPbQC+A==|EhUSSpJWSzSYOdJ/AQzfXuUXxwzcs/6C4tOXqhWAqcM=|OWV2VIqLfoWPs9DiouXGUOtTEkVeklbtJQHkQFIXkC8=",
   301          "username": "2.JbFkAEZPnuMm70cdP44wtA==|fsN6nbT+udGmOWv8K4otgw==|JbtwmNQa7/48KszT2hAdxpmJ6DRPZst0EDEZx5GzesI=",
   302          "password": "2.e83hIsk6IRevSr/H1lvZhg==|48KNkSCoTacopXRmIZsbWg==|CIcWgNbaIN2ix2Fx1Gar6rWQeVeboehp4bioAwngr0o=",
   303          "passwordRevisionDate": "2019-09-13T12:26:42+02:00",
   304          "totp": null
   305        }
   306      }`)).
   307  			Expect().Status(200).
   308  			JSON().Object()
   309  
   310  		assertCipherResponse(t, obj)
   311  
   312  		obj.Value("OrganizationId").Null()
   313  		cipherID = obj.Value("Id").String().NotEmpty().Raw()
   314  	})
   315  
   316  	t.Run("ListCiphers", func(t *testing.T) {
   317  		e := testutils.CreateTestClient(t, ts.URL)
   318  
   319  		obj := e.GET("/bitwarden/api/ciphers").
   320  			WithHeader("Authorization", "Bearer "+token).
   321  			Expect().Status(200).
   322  			JSON().Object()
   323  
   324  		obj.ValueEqual("Object", "list")
   325  
   326  		data := obj.Value("Data").Array()
   327  		data.Length().Equal(1)
   328  
   329  		assertCipherResponse(t, data.First().Object())
   330  
   331  		data.First().Object().Value("OrganizationId").Null()
   332  	})
   333  
   334  	t.Run("GetCipher", func(t *testing.T) {
   335  		e := testutils.CreateTestClient(t, ts.URL)
   336  
   337  		obj := e.GET("/bitwarden/api/ciphers/"+cipherID).
   338  			WithHeader("Authorization", "Bearer "+token).
   339  			Expect().Status(200).
   340  			JSON().Object()
   341  
   342  		assertCipherResponse(t, obj)
   343  
   344  		obj.Value("OrganizationId").Null()
   345  	})
   346  
   347  	t.Run("UpdateCipher", func(t *testing.T) {
   348  		e := testutils.CreateTestClient(t, ts.URL)
   349  
   350  		obj := e.PUT("/bitwarden/api/ciphers/"+cipherID).
   351  			WithHeader("Content-Type", "application/json").
   352  			WithHeader("Authorization", "Bearer "+token).
   353  			WithBytes([]byte(`{
   354        "type": 2,
   355        "favorite": true,
   356        "name": "2.G38TIU3t1pGOfkzjCQE7OQ==|Xa1RupttU7zrWdzIT6oK+w==|J3C6qU1xDrfTgyJD+OrDri1GjgGhU2nmRK75FbZHXoI=",
   357        "folderId": "` + folderID + `",
   358        "organizationId": null,
   359        "notes": "2.rSw0uVQEFgUCEmOQx0JnDg==|MKqHLD25aqaXYHeYJPH/mor7l3EeSQKsI7A/R+0bFTI=|ODcUScISzKaZWHlUe4MRGuTT2S7jpyDmbOHl7d+6HiM=",
   360        "secureNote": {
   361          "type": 0
   362        }
   363      }`)).
   364  			Expect().Status(200).
   365  			JSON().Object()
   366  
   367  		assertUpdatedCipherResponse(t, obj, cipherID, folderID)
   368  
   369  		obj.Value("OrganizationId").Null()
   370  	})
   371  
   372  	t.Run("DeleteCipher", func(t *testing.T) {
   373  		e := testutils.CreateTestClient(t, ts.URL)
   374  
   375  		obj := e.POST("/bitwarden/api/ciphers").
   376  			WithHeader("Content-Type", "application/json").
   377  			WithHeader("Authorization", "Bearer "+token).
   378  			WithBytes([]byte(`{
   379        "type": 1,
   380        "favorite": false,
   381        "name": "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=",
   382        "notes": null,
   383        "folderId": null,
   384        "organizationId": null,
   385        "login": {
   386          "uri": "2.T57BwAuV8ubIn/sZPbQC+A==|EhUSSpJWSzSYOdJ/AQzfXuUXxwzcs/6C4tOXqhWAqcM=|OWV2VIqLfoWPs9DiouXGUOtTEkVeklbtJQHkQFIXkC8=",
   387          "username": "2.JbFkAEZPnuMm70cdP44wtA==|fsN6nbT+udGmOWv8K4otgw==|JbtwmNQa7/48KszT2hAdxpmJ6DRPZst0EDEZx5GzesI=",
   388          "password": "2.e83hIsk6IRevSr/H1lvZhg==|48KNkSCoTacopXRmIZsbWg==|CIcWgNbaIN2ix2Fx1Gar6rWQeVeboehp4bioAwngr0o=",
   389          "totp": null
   390        }
   391      }`)).
   392  			Expect().Status(200).
   393  			JSON().Object()
   394  
   395  		id := obj.Value("Id").String().NotEmpty().Raw()
   396  
   397  		e.DELETE("/bitwarden/api/ciphers/"+id).
   398  			WithHeader("Authorization", "Bearer "+token).
   399  			Expect().Status(200)
   400  	})
   401  
   402  	t.Run("SoftDeleteCipher", func(t *testing.T) {
   403  		e := testutils.CreateTestClient(t, ts.URL)
   404  
   405  		obj := e.POST("/bitwarden/api/ciphers").
   406  			WithHeader("Content-Type", "application/json").
   407  			WithHeader("Authorization", "Bearer "+token).
   408  			WithBytes([]byte(`{
   409        "type": 1,
   410        "favorite": false,
   411        "name": "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=",
   412        "notes": null,
   413        "folderId": null,
   414        "organizationId": null,
   415        "login": {
   416          "uri": "2.T57BwAuV8ubIn/sZPbQC+A==|EhUSSpJWSzSYOdJ/AQzfXuUXxwzcs/6C4tOXqhWAqcM=|OWV2VIqLfoWPs9DiouXGUOtTEkVeklbtJQHkQFIXkC8=",
   417          "username": "2.JbFkAEZPnuMm70cdP44wtA==|fsN6nbT+udGmOWv8K4otgw==|JbtwmNQa7/48KszT2hAdxpmJ6DRPZst0EDEZx5GzesI=",
   418          "password": "2.e83hIsk6IRevSr/H1lvZhg==|48KNkSCoTacopXRmIZsbWg==|CIcWgNbaIN2ix2Fx1Gar6rWQeVeboehp4bioAwngr0o=",
   419          "totp": null
   420        }
   421      }`)).
   422  			Expect().Status(200).
   423  			JSON().Object()
   424  
   425  		id := obj.Value("Id").String().NotEmpty().Raw()
   426  
   427  		e.PUT("/bitwarden/api/ciphers/"+id+"/delete").
   428  			WithHeader("Authorization", "Bearer "+token).
   429  			Expect().Status(200)
   430  
   431  		obj = e.GET("/bitwarden/api/ciphers/"+id).
   432  			WithHeader("Authorization", "Bearer "+token).
   433  			Expect().Status(200).
   434  			JSON().Object()
   435  
   436  		obj.Value("DeletedDate").String().NotEmpty().DateTime(time.RFC3339)
   437  	})
   438  
   439  	t.Run("RestoreCipher", func(t *testing.T) {
   440  		e := testutils.CreateTestClient(t, ts.URL)
   441  
   442  		obj := e.POST(ts.URL+"/bitwarden/api/ciphers").
   443  			WithHeader("Content-Type", "application/json").
   444  			WithHeader("Authorization", "Bearer "+token).
   445  			WithBytes([]byte(`{
   446          "type": 1,
   447          "favorite": false,
   448          "name": "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=",
   449          "notes": null,
   450          "folderId": null,
   451          "organizationId": null,
   452          "login": {
   453            "uri": "2.T57BwAuV8ubIn/sZPbQC+A==|EhUSSpJWSzSYOdJ/AQzfXuUXxwzcs/6C4tOXqhWAqcM=|OWV2VIqLfoWPs9DiouXGUOtTEkVeklbtJQHkQFIXkC8=",
   454            "username": "2.JbFkAEZPnuMm70cdP44wtA==|fsN6nbT+udGmOWv8K4otgw==|JbtwmNQa7/48KszT2hAdxpmJ6DRPZst0EDEZx5GzesI=",
   455            "password": "2.e83hIsk6IRevSr/H1lvZhg==|48KNkSCoTacopXRmIZsbWg==|CIcWgNbaIN2ix2Fx1Gar6rWQeVeboehp4bioAwngr0o=",
   456            "totp": null
   457          }
   458        }`)).
   459  			Expect().Status(200).
   460  			JSON().Object()
   461  
   462  		id := obj.Value("Id").String().NotEmpty().Raw()
   463  
   464  		e.PUT(ts.URL+"/bitwarden/api/ciphers/"+id+"/delete").
   465  			WithHeader("Authorization", "Bearer "+token).
   466  			Expect().Status(200)
   467  
   468  		e.PUT(ts.URL+"/bitwarden/api/ciphers/"+id+"/restore").
   469  			WithHeader("Authorization", "Bearer "+token).
   470  			Expect().Status(200)
   471  
   472  		obj = e.GET(ts.URL+"/bitwarden/api/ciphers/"+id).
   473  			WithHeader("Authorization", "Bearer "+token).
   474  			Expect().Status(200).
   475  			JSON().Object()
   476  
   477  		obj.NotContainsKey("DeletedDate")
   478  	})
   479  
   480  	t.Run("Sync", func(t *testing.T) {
   481  		e := testutils.CreateTestClient(t, ts.URL)
   482  
   483  		obj := e.GET("/bitwarden/api/sync").
   484  			WithHeader("Authorization", "Bearer "+token).
   485  			Expect().Status(200).
   486  			JSON().Object()
   487  
   488  		obj.ValueEqual("Object", "sync")
   489  
   490  		profile := obj.Value("Profile").Object()
   491  		profile.Value("Id").NotNull()
   492  		profile.ValueEqual("Name", "Pierre")
   493  		profile.ValueEqual("Email", "me@bitwarden.example.net")
   494  		profile.ValueEqual("EmailVerified", false)
   495  		profile.ValueEqual("Premium", true)
   496  		profile.ValueEqual("MasterPasswordHint", nil)
   497  		profile.ValueEqual("Culture", "en")
   498  		profile.ValueEqual("TwoFactorEnabled", false)
   499  		profile.Value("Key").NotNull()
   500  		profile.Value("PrivateKey").NotNull()
   501  		profile.Value("SecurityStamp").NotNull()
   502  		profile.ValueEqual("Object", "profile")
   503  
   504  		ciphers := obj.Value("Ciphers").Array()
   505  		ciphers.Length().Equal(3)
   506  		assertUpdatedCipherResponse(t, ciphers.First().Object(), cipherID, folderID)
   507  
   508  		folders := obj.Value("Folders").Array()
   509  		folders.Length().Equal(1)
   510  		folders.First().Object().ValueEqual("Name", "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=")
   511  		folders.First().Object().ValueEqual("Object", "folder")
   512  		folders.First().Object().Value("RevisionDate").String().NotEmpty().DateTime(time.RFC3339)
   513  		folders.First().Object().ValueEqual("Id", folderID)
   514  
   515  		domains := obj.Value("Domains").Object()
   516  		domains.Value("EquivalentDomains").Null()
   517  		domains.Value("GlobalEquivalentDomains").Array().NotEmpty()
   518  		domains.ValueEqual("Object", "domains")
   519  	})
   520  
   521  	t.Run("BulkDeleteCiphers", func(t *testing.T) {
   522  		e := testutils.CreateTestClient(t, ts.URL)
   523  
   524  		// Setup
   525  		nbCiphersToDelete := 5
   526  		nbCiphers, err := couchdb.CountAllDocs(inst, consts.BitwardenCiphers)
   527  		require.NoError(t, err)
   528  
   529  		var ids []string
   530  		for i := 0; i < nbCiphersToDelete; i++ {
   531  			obj := e.POST("/bitwarden/api/ciphers").
   532  				WithHeader("Content-Type", "application/json").
   533  				WithHeader("Authorization", "Bearer "+token).
   534  				WithBytes([]byte(`{
   535          "type": 1,
   536          "favorite": false,
   537          "name": "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=",
   538          "notes": null,
   539          "folderId": null,
   540          "organizationId": null,
   541          "login": {
   542            "uri": "2.T57BwAuV8ubIn/sZPbQC+A==|EhUSSpJWSzSYOdJ/AQzfXuUXxwzcs/6C4tOXqhWAqcM=|OWV2VIqLfoWPs9DiouXGUOtTEkVeklbtJQHkQFIXkC8=",
   543            "username": "2.JbFkAEZPnuMm70cdP44wtA==|fsN6nbT+udGmOWv8K4otgw==|JbtwmNQa7/48KszT2hAdxpmJ6DRPZst0EDEZx5GzesI=",
   544            "password": "2.e83hIsk6IRevSr/H1lvZhg==|48KNkSCoTacopXRmIZsbWg==|CIcWgNbaIN2ix2Fx1Gar6rWQeVeboehp4bioAwngr0o=",
   545            "totp": null
   546          }
   547        }`)).
   548  				Expect().Status(200).
   549  				JSON().Object()
   550  
   551  			ids = append(ids, obj.Value("Id").String().NotEmpty().Raw())
   552  		}
   553  
   554  		nb, err := couchdb.CountAllDocs(inst, consts.BitwardenCiphers)
   555  		assert.NoError(t, err)
   556  		assert.Equal(t, nbCiphers+nbCiphersToDelete, nb)
   557  
   558  		body, _ := json.Marshal(map[string][]string{"ids": ids})
   559  
   560  		// Test soft delete in bulk
   561  		t.Run("Soft delete in bulk", func(t *testing.T) {
   562  			e := testutils.CreateTestClient(t, ts.URL)
   563  
   564  			e.PUT("/bitwarden/api/ciphers/delete").
   565  				WithHeader("Content-Type", "application/json").
   566  				WithHeader("Authorization", "Bearer "+token).
   567  				WithBytes(body).
   568  				Expect().Status(200)
   569  
   570  			for _, id := range ids {
   571  				obj := e.GET("/bitwarden/api/ciphers/"+id).
   572  					WithHeader("Authorization", "Bearer "+token).
   573  					Expect().Status(200).
   574  					JSON().Object()
   575  
   576  				obj.Value("DeletedDate").String().NotEmpty().DateTime(time.RFC3339)
   577  			}
   578  		})
   579  
   580  		t.Run("Restore in bulk", func(t *testing.T) {
   581  			e := testutils.CreateTestClient(t, ts.URL)
   582  
   583  			obj := e.PUT("/bitwarden/api/ciphers/restore").
   584  				WithHeader("Content-Type", "application/json").
   585  				WithHeader("Authorization", "Bearer "+token).
   586  				WithBytes(body).
   587  				Expect().Status(200).
   588  				JSON().Object()
   589  
   590  			obj.ValueEqual("Object", "list")
   591  			data := obj.Value("Data").Array()
   592  			data.Length().Equal(nbCiphersToDelete)
   593  
   594  			for i, item := range data.Iter() {
   595  				item.Object().ValueEqual("Id", ids[i])
   596  				item.Object().NotContainsKey("DeletedDate")
   597  			}
   598  		})
   599  
   600  		t.Run("Delete in bulk", func(t *testing.T) {
   601  			e := testutils.CreateTestClient(t, ts.URL)
   602  
   603  			e.DELETE("/bitwarden/api/ciphers").
   604  				WithHeader("Content-Type", "application/json").
   605  				WithHeader("Authorization", "Bearer "+token).
   606  				WithBytes(body).
   607  				Expect().Status(200)
   608  
   609  			nb, err = couchdb.CountAllDocs(inst, consts.BitwardenCiphers)
   610  			assert.NoError(t, err)
   611  			assert.Equal(t, nbCiphers, nb)
   612  		})
   613  	})
   614  
   615  	t.Run("SharedCipher", func(t *testing.T) {
   616  		e := testutils.CreateTestClient(t, ts.URL)
   617  
   618  		obj := e.POST("/bitwarden/api/ciphers/create").
   619  			WithHeader("Content-Type", "application/json").
   620  			WithHeader("Authorization", "Bearer "+token).
   621  			WithBytes([]byte(`{
   622          "cipher": {
   623            "type": 1,
   624            "favorite": false,
   625            "name": "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=",
   626            "notes": null,
   627            "folderId": null,
   628            "organizationId": "` + orgaID + `",
   629            "login": {
   630              "uri": "2.T57BwAuV8ubIn/sZPbQC+A==|EhUSSpJWSzSYOdJ/AQzfXuUXxwzcs/6C4tOXqhWAqcM=|OWV2VIqLfoWPs9DiouXGUOtTEkVeklbtJQHkQFIXkC8=",
   631              "username": "2.JbFkAEZPnuMm70cdP44wtA==|fsN6nbT+udGmOWv8K4otgw==|JbtwmNQa7/48KszT2hAdxpmJ6DRPZst0EDEZx5GzesI=",
   632              "password": "2.e83hIsk6IRevSr/H1lvZhg==|48KNkSCoTacopXRmIZsbWg==|CIcWgNbaIN2ix2Fx1Gar6rWQeVeboehp4bioAwngr0o=",
   633              "passwordRevisionDate": "2019-09-13T12:26:42+02:00",
   634              "totp": null
   635            }
   636          },
   637          "collectionIds": ["` + collID + `"]
   638        }`)).
   639  			Expect().Status(200).
   640  			JSON().Object()
   641  
   642  		assertCipherResponse(t, obj)
   643  		obj.ValueEqual("OrganizationId", orgaID)
   644  		cipherID := obj.Value("Id").String().NotEmpty().Raw()
   645  
   646  		obj = e.PUT("/bitwarden/api/ciphers/"+cipherID).
   647  			WithHeader("Content-Type", "application/json").
   648  			WithHeader("Authorization", "Bearer "+token).
   649  			WithBytes([]byte(`{
   650          "type": 2,
   651          "favorite": true,
   652          "name": "2.G38TIU3t1pGOfkzjCQE7OQ==|Xa1RupttU7zrWdzIT6oK+w==|J3C6qU1xDrfTgyJD+OrDri1GjgGhU2nmRK75FbZHXoI=",
   653          "folderId": "` + folderID + `",
   654          "organizationId": "` + orgaID + `",
   655          "notes": "2.rSw0uVQEFgUCEmOQx0JnDg==|MKqHLD25aqaXYHeYJPH/mor7l3EeSQKsI7A/R+0bFTI=|ODcUScISzKaZWHlUe4MRGuTT2S7jpyDmbOHl7d+6HiM=",
   656          "secureNote": {
   657            "type": 0
   658          }
   659        }`)).
   660  			Expect().Status(200).
   661  			JSON().Object()
   662  
   663  		assertUpdatedCipherResponse(t, obj, cipherID, folderID)
   664  		obj.ValueEqual("OrganizationId", orgaID)
   665  	})
   666  
   667  	t.Run("SetKeyPair", func(t *testing.T) {
   668  		e := testutils.CreateTestClient(t, ts.URL)
   669  
   670  		// Needs to be marshaled in order to avoid encoding issues
   671  		body, _ := json.Marshal(map[string]string{
   672  			"encryptedPrivateKey": "2.demXNYbv8o47sG+fhYYvhg==|jXpxet7AApeIzrC3Yr752LwmjBdCZn6HJl6SjEOVP3rrOpGu5qV2rN0dBH5yXXWHusfxM7IvXkdC/fzBUAmFFOU5ubTp9kHFBqIn51tiJG6BRs5aTm7kF6TYSHVDIP5kUdX4O7DcmD23dqtq/8211DSAFR/DK1QDm5Da77Clh7NHxQE9Z9RTW1PBGV56DfzrY3N06H6vI+V6fTZ6HJRD2pdPczR2ZNC0ziQP7qCUYNlSjEv70O4VoYMSUsdb4UUE1YetcSdZ+dIAy+V2KHfoHmTFYI4DtMCW6WpDzp0ufPvszFjt1EwaMr78hujMrQr1gFWxgN8kOLJyYCrd1F5aIxWXHghBH/t+QU31gyQOxCdj18f10ssfuY/y7vocSJQ9pTRRPNh4beGAijV1AETaXWLK1L6oMnkbdhr9ZA2I6cZaHNCaHIynHQH7NUqKKQUJL/FyZ8rBv4YNnxCMRi9p88IoTb0oPsUCoNCaIZ2cvzXz+0VpU6zxj4ke7H6Bu7H46MSB1P+YHzGLtFNzZJVsUBEkz7dotUDeTeqlYKnq7oldWJ4HlqODevzCev+FRnYgrYpoXmYC/dxa1R5IlKCu6rEmP05A7Nw4h9cymnTwRMEoZRSppJ2O5FlSx/Go9Jz12g2Tfiaf+RvO7nkIb2qKiz7Jo2aJgakL5lMOlEdBA2+dsYSyX4Tvu8Ua4p0GcYaGOgSjXH27lQ73ZpHSicf4Q1kAooVl+6zTOPAqgMXOnyyVSRqBPse28HuDwGtmD8BAeVDIfkMW+a+PlWa+yoEWKfDHRduoxNod7Pc9xlNFt6eOeGoBQTEIiF7ccBDtNiSU1yfvqBZEgI8QF0QiGUo9eP7+59so5eu9/DuzjdqFMmGPtG3zHifMxuMzO5+E9UxTyHuCwvxuH93F4vmPC8zzXXn8/ErhEeqmYl1lxZbfJDm1qcjTkJibNKJ9+CXUeP0hq8yi07SEN1xJSZpupf90EUjrdFd3impz3gZKummEjTvzr3J1JX4gC/wD0mGkROHQwb0jCTDJNC18cX4usPYtNr3FxLZmxCGgPmZhkzFjF0qppN1aXTxQskdorEejQUwLL5EnWJySd9/W2P6PmjkJTAwKYUNHsmVUAfbMA7y7QBIjVFFWS4xYy0GJcc8NaLKkFMkGv/oluw552prWAJZ4aM2asoNgyv/JARrAF+JbOPSpax+CtUMO+LCFlBITHopbkHz0TwI1UMj/vIOh9wxDiMqe3YBiviymudX+B7awCaUPTLubWW1jwC4kBnXmRGAKyyIvzgOvwkdcKfQRxoTxq7JFTL/hWk7x4HlQqviSWGY166CLIp6SydCT+cqHMf3MHhe8AQZVC+nIDVNQZWfpFbOFb3nNDwlT+laWrtsiuX7hHiL0VLaCU4xzup5m4zvi59/Qxj0+d8n6M/3GP3/Tvp/bKY9m7CHoeimtGF9Ai2QFJFMOEQw3S1SUBL62ZsezKgBap6y1RqmMzdz/h3f5mhHxRMoQ0kgzZwMNWJvi2acGoIttcmBU7Cn6fqxYNi11dg17M7cFJAQCMicvd4pEwl8IBrm7uFrzbLvuLeolyiDx8GX3jfIo//Ceqa6P/RIqN8jKzH3nTSePuVqkXYiIdxhlAeF//EYW0CwOjd3GEoc=|aUt6NKqrLW4HeprkbwjuBzSQbR84imTujhUPxK17eX4=",
   673  			"publicKey":           "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmAYrTtY4FBJL/TeTGqr1uHCoMCzUDgwvgq7gBGiNrk24gPbb3xreM+HxubBvkzTlgoS6m1KKKKtD4tWrLU33Xc+PevbKSZDLvBfUe+golGU1XKFxUcIkgINtB0i8LmCVCShiCrlhn2VorcAbekR/1RXtoJqpqq1urhI+RdGVXy8HBBoULA7BoV7wC8dBdkRtnQMNuvGyHclV7yjgealKGqgxz4aNcgsfybquKvYg6PUj8dAxUy7KlmMR7klPyO8nahYqyhpQ/t0xle0WyCkdx5YuYhRSA67Tok+E8fCW5WXOPfIdPZDXS+6/wW1NhcQEa5j6EW11PF/Xq0awBUFwnwIDAQAB",
   674  		})
   675  
   676  		e.POST("/bitwarden/api/accounts/keys").
   677  			WithHeader("Content-Type", "application/json").
   678  			WithHeader("Authorization", "Bearer "+token).
   679  			WithBytes(body).
   680  			Expect().Status(200)
   681  
   682  		setting, err := settings.Get(inst)
   683  		assert.NoError(t, err)
   684  		orgKey, err := setting.OrganizationKey()
   685  		assert.NoError(t, err)
   686  		assert.NotEmpty(t, orgKey)
   687  	})
   688  
   689  	t.Run("SettingsDomains", func(t *testing.T) {
   690  		e := testutils.CreateTestClient(t, ts.URL)
   691  
   692  		obj := e.POST("/bitwarden/api/settings/domains").
   693  			WithHeader("Content-Type", "application/json").
   694  			WithHeader("Authorization", "Bearer "+token).
   695  			WithBytes([]byte(`{
   696          "equivalentDomains": [ ["stackoverflow.com", "serverfault.com", "superuser.com"] ],
   697          "globalEquivalentDomains": [42, 69]
   698        }`)).
   699  			Expect().Status(200).
   700  			JSON().Object()
   701  
   702  		assertDomainsReponse(t, obj)
   703  
   704  		obj = e.GET("/bitwarden/api/settings/domains").
   705  			WithHeader("Authorization", "Bearer "+token).
   706  			Expect().Status(200).
   707  			JSON().Object()
   708  
   709  		assertDomainsReponse(t, obj)
   710  	})
   711  
   712  	t.Run("ImportCiphers", func(t *testing.T) {
   713  		e := testutils.CreateTestClient(t, ts.URL)
   714  
   715  		nbCiphers, err := couchdb.CountAllDocs(inst, consts.BitwardenCiphers)
   716  		assert.NoError(t, err)
   717  
   718  		nbFolders, err := couchdb.CountAllDocs(inst, consts.BitwardenFolders)
   719  		assert.NoError(t, err)
   720  
   721  		e.POST("/bitwarden/api/ciphers/import").
   722  			WithHeader("Content-Type", "application/json").
   723  			WithHeader("Authorization", "Bearer "+token).
   724  			WithBytes([]byte(`{
   725        "ciphers": [{
   726          "type": 2,
   727          "favorite": true,
   728          "name": "2.G38TIU3t1pGOfkzjCQE7OQ==|Xa1RupttU7zrWdzIT6oK+w==|J3C6qU1xDrfTgyJD+OrDri1GjgGhU2nmRK75FbZHXoI=",
   729          "folderId": null,
   730          "organizationId": null,
   731          "notes": "2.rSw0uVQEFgUCEmOQx0JnDg==|MKqHLD25aqaXYHeYJPH/mor7l3EeSQKsI7A/R+0bFTI=|ODcUScISzKaZWHlUe4MRGuTT2S7jpyDmbOHl7d+6HiM=",
   732          "secureNote": {
   733            "type": 0
   734          }
   735        }, {
   736          "type": 1,
   737          "favorite": false,
   738          "name": "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=",
   739          "folderId": null,
   740          "organizationId": null,
   741          "notes": null,
   742          "login": {
   743            "uri": "2.T57BwAuV8ubIn/sZPbQC+A==|EhUSSpJWSzSYOdJ/AQzfXuUXxwzcs/6C4tOXqhWAqcM=|OWV2VIqLfoWPs9DiouXGUOtTEkVeklbtJQHkQFIXkC8=",
   744            "username": "2.JbFkAEZPnuMm70cdP44wtA==|fsN6nbT+udGmOWv8K4otgw==|JbtwmNQa7/48KszT2hAdxpmJ6DRPZst0EDEZx5GzesI=",
   745            "password": "2.e83hIsk6IRevSr/H1lvZhg==|48KNkSCoTacopXRmIZsbWg==|CIcWgNbaIN2ix2Fx1Gar6rWQeVeboehp4bioAwngr0o=",
   746            "totp": null
   747          }
   748        }],
   749        "folders": [{
   750          "name": "2.FQAwIBaDbczEGnEJw4g4hw==|7KreXaC0duAj0ulzZJ8ncA==|nu2sEvotjd4zusvGF8YZJPnS9SiJPDqc1VIfCrfve/o="
   751        }],
   752        "folderRelationships": [
   753          {"key": 1, "value": 0}
   754        ]
   755      }`)).
   756  			Expect().Status(200)
   757  
   758  		nb, err := couchdb.CountAllDocs(inst, consts.BitwardenCiphers)
   759  		assert.NoError(t, err)
   760  		assert.Equal(t, nbCiphers+2, nb)
   761  
   762  		nb, err = couchdb.CountAllDocs(inst, consts.BitwardenFolders)
   763  		assert.NoError(t, err)
   764  		assert.Equal(t, nbFolders+1, nb)
   765  	})
   766  
   767  	t.Run("Organization", func(t *testing.T) {
   768  		var orgaID string
   769  
   770  		t.Run("Create", func(t *testing.T) {
   771  			e := testutils.CreateTestClient(t, ts.URL)
   772  
   773  			obj := e.POST("/bitwarden/api/organizations").
   774  				WithHeader("Content-Type", "application/json").
   775  				WithHeader("Authorization", "Bearer "+token).
   776  				WithBytes([]byte(`{
   777          "name": "Family Organization",
   778          "key": "bmFjbF53D9mrdGbVqQzMB54uIg678EIpU/uHFYjynSPSA6vIv5/6nUy4Uk22SjIuDB3pZ679wLE3o7R/Imzn47OjfT6IrJ8HaysEhsZA25Dn8zwEtTMtgNepUtH084wAMgNeIcElW24U/MfRscjAk8cDUIm5xnzyi2vtJfe9PcHTmzRXyng=",
   779          "collectionName": "2.rrpSDDODsWZqL7EhLVsu/Q==|OSuh+MmmR89ppdb/A7KxBg==|kofpAocL2G4a3P1C2R1U+i9hWbhfKfsPKM6kfoyCg/M="
   780        }`)).
   781  				Expect().Status(200).
   782  				JSON().Object()
   783  
   784  			obj.ValueEqual("Name", "Family Organization")
   785  			obj.ValueEqual("Object", "profileOrganization")
   786  			obj.ValueEqual("Enabled", true)
   787  			obj.ValueEqual("Status", 2)
   788  			obj.ValueEqual("Type", 0)
   789  
   790  			orgaID = obj.Value("Id").String().NotEmpty().Raw()
   791  
   792  			obj.Value("Key").String().NotEmpty()
   793  		})
   794  
   795  		t.Run("Get", func(t *testing.T) {
   796  			e := testutils.CreateTestClient(t, ts.URL)
   797  
   798  			obj := e.GET("/bitwarden/api/organizations/"+orgaID).
   799  				WithHeader("Authorization", "Bearer "+token).
   800  				Expect().Status(200).
   801  				JSON().Object()
   802  
   803  			obj.ValueEqual("Name", "Family Organization")
   804  			obj.ValueEqual("Object", "profileOrganization")
   805  			obj.ValueEqual("Enabled", true)
   806  			obj.ValueEqual("Status", 2)
   807  			obj.ValueEqual("Type", 0)
   808  			obj.ValueEqual("Id", orgaID)
   809  			obj.Value("Key").String().NotEmpty()
   810  		})
   811  
   812  		t.Run("ListCollections", func(t *testing.T) {
   813  			e := testutils.CreateTestClient(t, ts.URL)
   814  
   815  			obj := e.GET("/bitwarden/api/organizations/"+orgaID+"/collections").
   816  				WithHeader("Authorization", "Bearer "+token).
   817  				Expect().Status(200).
   818  				JSON().Object()
   819  
   820  			obj.ValueEqual("Object", "list")
   821  			data := obj.Value("Data").Array()
   822  			data.Length().Equal(1)
   823  
   824  			coll := data.First().Object()
   825  			coll.Value("Id").String().NotEmpty()
   826  			coll.ValueEqual("Name", "2.rrpSDDODsWZqL7EhLVsu/Q==|OSuh+MmmR89ppdb/A7KxBg==|kofpAocL2G4a3P1C2R1U+i9hWbhfKfsPKM6kfoyCg/M=")
   827  			coll.ValueEqual("Object", "collection")
   828  			coll.ValueEqual("OrganizationId", orgaID)
   829  			coll.ValueEqual("ReadOnly", false)
   830  		})
   831  
   832  		t.Run("SyncOrganizationAndCollection", func(t *testing.T) {
   833  			e := testutils.CreateTestClient(t, ts.URL)
   834  
   835  			obj := e.GET("/bitwarden/api/sync").
   836  				WithHeader("Authorization", "Bearer "+token).
   837  				Expect().Status(200).
   838  				JSON().Object()
   839  
   840  			obj.ValueEqual("Object", "sync")
   841  			profile := obj.Value("Profile").Object()
   842  			orgs := profile.Value("Organizations").Array()
   843  			orgs.Length().Equal(2)
   844  
   845  			for _, item := range orgs.Iter() {
   846  				org := item.Object()
   847  
   848  				if org.Value("Id").String().Raw() == orgaID {
   849  					org.ValueEqual("Name", "Family Organization")
   850  				} else {
   851  					org.ValueEqual("Name", "Cozy")
   852  				}
   853  
   854  				org.Value("Key").String().NotEmpty()
   855  				org.ValueEqual("Object", "profileOrganization")
   856  			}
   857  
   858  			colls := obj.Value("Collections").Array()
   859  			colls.Length().Equal(2)
   860  			for _, item := range colls.Iter() {
   861  				coll := item.Object()
   862  
   863  				if coll.Value("Id").String().Raw() != collID {
   864  					coll.ValueEqual("OrganizationId", orgaID)
   865  					coll.ValueEqual("Name", "2.rrpSDDODsWZqL7EhLVsu/Q==|OSuh+MmmR89ppdb/A7KxBg==|kofpAocL2G4a3P1C2R1U+i9hWbhfKfsPKM6kfoyCg/M=")
   866  				}
   867  
   868  				coll.ValueEqual("Object", "collection")
   869  			}
   870  		})
   871  
   872  		t.Run("DeleteOrganization", func(t *testing.T) {
   873  			e := testutils.CreateTestClient(t, ts.URL)
   874  
   875  			email := inst.PassphraseSalt()
   876  			iter := crypto.DefaultPBKDF2Iterations
   877  			pass, _ := crypto.HashPassWithPBKDF2([]byte("cozy"), email, iter)
   878  
   879  			e.DELETE("/bitwarden/api/organizations/"+orgaID).
   880  				WithHeader("Content-Type", "application/json").
   881  				WithHeader("Authorization", "Bearer "+token).
   882  				WithBytes([]byte(fmt.Sprintf(`{"masterPasswordHash": "%s"}`, pass))).
   883  				Expect().Status(200)
   884  		})
   885  	})
   886  
   887  	t.Run("ChangeSecurityStamp", func(t *testing.T) {
   888  		e := testutils.CreateTestClient(t, ts.URL)
   889  
   890  		email := inst.PassphraseSalt()
   891  		iter := crypto.DefaultPBKDF2Iterations
   892  		pass, _ := crypto.HashPassWithPBKDF2([]byte("cozy"), email, iter)
   893  
   894  		e.POST("/bitwarden/api/accounts/security-stamp").
   895  			WithHeader("Authorization", "Bearer "+token).
   896  			WithBytes([]byte(fmt.Sprintf(`{"masterPasswordHash": %q}`, pass))).Expect().Status(204)
   897  
   898  		// Check that token is no longer valid
   899  		e.GET("/bitwarden/api/folders").
   900  			WithHeader("Authorization", "Bearer "+token).
   901  			Expect().Status(401)
   902  	})
   903  
   904  	t.Run("SendHint", func(t *testing.T) {
   905  		e := testutils.CreateTestClient(t, ts.URL)
   906  
   907  		e.POST("/bitwarden/api/accounts/password-hint").
   908  			WithHeader("Content-Type", "application/json").
   909  			WithBytes([]byte(`{ "email": "me@bitwarden.example.net" }`)).
   910  			Expect().Status(200)
   911  	})
   912  }
   913  
   914  func assertCipherResponse(t *testing.T, obj *httpexpect.Object) {
   915  	t.Helper()
   916  
   917  	obj.ValueEqual("Object", "cipher")
   918  	obj.Value("Id").String().NotEmpty()
   919  	obj.ValueEqual("Type", 1.0)
   920  	obj.ValueEqual("Favorite", false)
   921  	obj.ValueEqual("Name", "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=")
   922  	obj.Value("Notes").Null()
   923  	obj.Value("FolderId").Null()
   924  
   925  	loginObj := obj.Value("Login").Object().NotEmpty()
   926  	loginObj.ValueEqual("PasswordRevisionDate", "2019-09-13T12:26:42+02:00")
   927  	loginObj.Value("Totp").Null()
   928  	loginObj.ValueEqual("Username", "2.JbFkAEZPnuMm70cdP44wtA==|fsN6nbT+udGmOWv8K4otgw==|JbtwmNQa7/48KszT2hAdxpmJ6DRPZst0EDEZx5GzesI=")
   929  	loginObj.ValueEqual("Password", "2.e83hIsk6IRevSr/H1lvZhg==|48KNkSCoTacopXRmIZsbWg==|CIcWgNbaIN2ix2Fx1Gar6rWQeVeboehp4bioAwngr0o=")
   930  
   931  	loginObj.Value("Uris").Array().Length().Equal(1)
   932  	uriObj := loginObj.Value("Uris").Array().First().Object()
   933  	uriObj.ValueEqual("Uri", "2.T57BwAuV8ubIn/sZPbQC+A==|EhUSSpJWSzSYOdJ/AQzfXuUXxwzcs/6C4tOXqhWAqcM=|OWV2VIqLfoWPs9DiouXGUOtTEkVeklbtJQHkQFIXkC8=")
   934  	uriObj.Value("Match").Null()
   935  
   936  	obj.Value("Fields").Null()
   937  	obj.Value("Attachments").Null()
   938  	obj.Value("RevisionDate").String().DateTime(time.RFC3339)
   939  	obj.ValueEqual("Edit", true)
   940  	obj.ValueEqual("OrganizationUseTotp", false)
   941  }
   942  
   943  func assertUpdatedCipherResponse(t *testing.T, obj *httpexpect.Object, cipherID, folderID string) {
   944  	t.Helper()
   945  
   946  	obj.ValueEqual("Object", "cipher")
   947  	obj.ValueEqual("Id", cipherID)
   948  	obj.ValueEqual("Type", 2.0)
   949  	obj.ValueEqual("Favorite", true)
   950  	obj.ValueEqual("Name", "2.G38TIU3t1pGOfkzjCQE7OQ==|Xa1RupttU7zrWdzIT6oK+w==|J3C6qU1xDrfTgyJD+OrDri1GjgGhU2nmRK75FbZHXoI=")
   951  	obj.ValueEqual("FolderId", folderID)
   952  	obj.ValueEqual("Notes", "2.rSw0uVQEFgUCEmOQx0JnDg==|MKqHLD25aqaXYHeYJPH/mor7l3EeSQKsI7A/R+0bFTI=|ODcUScISzKaZWHlUe4MRGuTT2S7jpyDmbOHl7d+6HiM=")
   953  	obj.Value("SecureNote").Object().NotEmpty().ValueEqual("Type", 0.0)
   954  	obj.NotContainsKey("Login")
   955  	obj.Value("Fields").Null()
   956  	obj.Value("Attachments").Null()
   957  	obj.Value("RevisionDate").String().DateTime(time.RFC3339)
   958  	obj.ValueEqual("Edit", true)
   959  	obj.ValueEqual("OrganizationUseTotp", false)
   960  }
   961  
   962  func assertDomainsReponse(t *testing.T, obj *httpexpect.Object) {
   963  	obj.ValueEqual("Object", "domains")
   964  	equivalent := obj.Value("EquivalentDomains").Array()
   965  	equivalent.Length().Equal(1)
   966  	domains := equivalent.First().Array()
   967  	domains.Length().Equal(3)
   968  	domains.Element(0).Equal("stackoverflow.com")
   969  	domains.Element(1).Equal("serverfault.com")
   970  	domains.Element(2).Equal("superuser.com")
   971  
   972  	global := obj.Value("GlobalEquivalentDomains").Array()
   973  	global.Length().Equal(len(bitwarden.GlobalDomains))
   974  
   975  	for _, item := range global.Iter() {
   976  		k := int(item.Object().Value("Type").Number().Raw())
   977  		excluded := (k == 42) || (k == 69)
   978  		item.Object().ValueEqual("Excluded", excluded)
   979  		item.Object().Value("Domains").Array().Length().Gt(0)
   980  	}
   981  }