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

     1  package sharings_test
     2  
     3  import (
     4  	"encoding/json"
     5  	"net/url"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/cozy/cozy-stack/model/contact"
    11  	"github.com/cozy/cozy-stack/model/instance"
    12  	"github.com/cozy/cozy-stack/model/instance/lifecycle"
    13  	"github.com/cozy/cozy-stack/model/job"
    14  	"github.com/cozy/cozy-stack/model/permission"
    15  	"github.com/cozy/cozy-stack/model/sharing"
    16  	"github.com/cozy/cozy-stack/pkg/assets/dynamic"
    17  	build "github.com/cozy/cozy-stack/pkg/config"
    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/couchdb/mango"
    22  	"github.com/cozy/cozy-stack/tests/testutils"
    23  	"github.com/cozy/cozy-stack/web"
    24  	"github.com/cozy/cozy-stack/web/auth"
    25  	"github.com/cozy/cozy-stack/web/errors"
    26  	"github.com/cozy/cozy-stack/web/middlewares"
    27  	"github.com/cozy/cozy-stack/web/permissions"
    28  	"github.com/cozy/cozy-stack/web/sharings"
    29  	"github.com/cozy/cozy-stack/web/statik"
    30  	"github.com/gavv/httpexpect/v2"
    31  	"github.com/labstack/echo/v4"
    32  	"github.com/stretchr/testify/assert"
    33  	"github.com/stretchr/testify/require"
    34  )
    35  
    36  const iocozytests = "io.cozy.tests"
    37  const iocozytestswildcard = "io.cozy.tests.*"
    38  
    39  // Things that live on Alice's Cozy
    40  var charlieContact, daveContact, edwardContact *contact.Contact
    41  var sharingID, state, aliceAccessToken string
    42  
    43  // Bob's browser
    44  var discoveryLink, authorizeLink string
    45  
    46  func TestSharings(t *testing.T) {
    47  	if testing.Short() {
    48  		t.Skip("an instance is required for this test: test skipped due to the use of --short flag")
    49  	}
    50  
    51  	config.UseTestFile(t)
    52  	build.BuildMode = build.ModeDev
    53  	config.GetConfig().Assets = "../../assets"
    54  	_ = web.LoadSupportedLocales()
    55  	testutils.NeedCouchdb(t)
    56  	render, _ := statik.NewDirRenderer("../../assets")
    57  	middlewares.BuildTemplates()
    58  
    59  	// Prepare Alice's instance
    60  	setup := testutils.NewSetup(t, t.Name()+"_alice")
    61  	aliceInstance := setup.GetTestInstance(&lifecycle.Options{
    62  		Email:      "alice@example.net",
    63  		PublicName: "Alice",
    64  	})
    65  	aliceAppToken := generateAppToken(aliceInstance, "testapp", iocozytests)
    66  	aliceAppTokenWildcard := generateAppToken(aliceInstance, "testapp2", iocozytestswildcard)
    67  	charlieContact = createContact(t, aliceInstance, "Charlie", "charlie@example.net")
    68  	daveContact = createContact(t, aliceInstance, "Dave", "dave@example.net")
    69  	tsA := setup.GetTestServerMultipleRoutes(map[string]func(*echo.Group){
    70  		"/sharings":    sharings.Routes,
    71  		"/permissions": permissions.Routes,
    72  	})
    73  	tsA.Config.Handler.(*echo.Echo).Renderer = render
    74  	tsA.Config.Handler.(*echo.Echo).HTTPErrorHandler = errors.ErrorHandler
    75  	t.Cleanup(tsA.Close)
    76  
    77  	// Prepare Bob's instance
    78  	bobSetup := testutils.NewSetup(t, t.Name()+"_bob")
    79  	bobInstance := bobSetup.GetTestInstance(&lifecycle.Options{
    80  		Email:         "bob@example.net",
    81  		PublicName:    "Bob",
    82  		Passphrase:    "MyPassphrase",
    83  		KdfIterations: 5000,
    84  		Key:           "xxx",
    85  	})
    86  	bobAppToken := generateAppToken(bobInstance, "testapp", iocozytests)
    87  	tsB := bobSetup.GetTestServerMultipleRoutes(map[string]func(*echo.Group){
    88  		"/auth": func(g *echo.Group) {
    89  			g.Use(middlewares.LoadSession)
    90  			auth.Routes(g)
    91  		},
    92  		"/sharings": sharings.Routes,
    93  	})
    94  	tsB.Config.Handler.(*echo.Echo).Renderer = render
    95  	tsB.Config.Handler.(*echo.Echo).HTTPErrorHandler = errors.ErrorHandler
    96  	t.Cleanup(tsB.Close)
    97  
    98  	require.NoError(t, dynamic.InitDynamicAssetFS(config.FsURL().String()), "Could not init dynamic FS")
    99  
   100  	t.Run("CreateSharingSuccess", func(t *testing.T) {
   101  		eA := httpexpect.Default(t, tsA.URL)
   102  
   103  		bobContact := createBobContact(t, aliceInstance)
   104  		assert.NotEmpty(t, aliceAppToken)
   105  		assert.NotNil(t, bobContact)
   106  
   107  		obj := eA.POST("/sharings/").
   108  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   109  			WithHeader("Content-Type", "application/vnd.api+json").
   110  			WithBytes([]byte(`{
   111          "data": {
   112            "type": "` + consts.Sharings + `",
   113            "attributes": {
   114              "description":  "this is a test",
   115              "open_sharing": true,
   116              "rules": [{
   117                  "title": "test one",
   118                  "doctype": "` + iocozytests + `",
   119                  "values": ["000000"],
   120                  "add": "sync"
   121                }]
   122            },
   123            "relationships": {
   124              "recipients": {
   125                "data": [{"id": "` + bobContact.ID() + `", "type": "` + bobContact.DocType() + `"}]
   126              },
   127              "read_only_recipients": {
   128                  "data": [{"id": "` + daveContact.ID() + `", "type": "` + daveContact.DocType() + `"}]
   129              }
   130            }
   131          }
   132        }`)).
   133  			Expect().Status(201).
   134  			JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}).
   135  			Object()
   136  
   137  		sharingID = obj.Value("data").Object().Value("id").String().NotEmpty().Raw()
   138  
   139  		assertSharingIsCorrectOnSharer(t, obj, sharingID, aliceInstance)
   140  		description := assertInvitationMailWasSent(t, aliceInstance)
   141  		assert.Equal(t, description, "this is a test")
   142  		assert.Contains(t, discoveryLink, "/discovery?state=")
   143  	})
   144  
   145  	t.Run("GetSharing", func(t *testing.T) {
   146  		eA := httpexpect.Default(t, tsA.URL)
   147  
   148  		obj := eA.GET("/sharings/"+sharingID).
   149  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   150  			WithHeader("Content-Type", "application/vnd.api+json").
   151  			Expect().Status(200).
   152  			JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}).
   153  			Object()
   154  
   155  		assertSharingIsCorrectOnSharer(t, obj, sharingID, aliceInstance)
   156  	})
   157  
   158  	t.Run("Discovery", func(t *testing.T) {
   159  		u, err := url.Parse(discoveryLink)
   160  		assert.NoError(t, err)
   161  		state = u.Query()["state"][0]
   162  
   163  		// Take only the path and query from the `disoveryLink` and redirect
   164  		// to the tsA host.
   165  		eA := httpexpect.Default(t, tsA.URL)
   166  
   167  		eA.GET(u.Path).
   168  			WithQuery("state", state).
   169  			Expect().Status(200).
   170  			HasContentType("text/html", "utf-8").
   171  			Body().
   172  			Contains("Connect to your Cozy").
   173  			Contains(`<input type="hidden" name="state" value="` + state)
   174  
   175  		redirectHeader := eA.POST(u.Path).
   176  			WithFormField("state", state).
   177  			WithFormField("slug", tsB.URL).
   178  			WithRedirectPolicy(httpexpect.DontFollowRedirects).
   179  			Expect().Status(302).Header("Location")
   180  
   181  		authorizeLink = redirectHeader.NotEmpty().Raw()
   182  		redirectHeader.Contains(tsB.URL)
   183  		redirectHeader.Contains("/auth/authorize/sharing")
   184  
   185  		assertSharingRequestHasBeenCreated(t, aliceInstance, bobInstance, tsB.URL)
   186  	})
   187  
   188  	t.Run("AuthorizeSharing", func(t *testing.T) {
   189  		u, err := url.Parse(authorizeLink)
   190  		assert.NoError(t, err)
   191  		sharingID = u.Query()["sharing_id"][0]
   192  		state := u.Query()["state"][0]
   193  
   194  		eB := httpexpect.Default(t, tsB.URL)
   195  
   196  		// Bob login
   197  		token := eB.GET("/auth/login").
   198  			Expect().Status(200).
   199  			Cookie("_csrf").Value().NotEmpty().Raw()
   200  
   201  		eB.POST("/auth/login").
   202  			WithCookie("_csrf", token).
   203  			WithFormField("passphrase", "MyPassphrase").
   204  			WithFormField("csrf_token", token).
   205  			WithRedirectPolicy(httpexpect.DontFollowRedirects).
   206  			Expect().Status(303).
   207  			Header("Location").Contains("home")
   208  		// End bob login
   209  
   210  		fakeAliceInstance(t, bobInstance, tsA.URL)
   211  
   212  		t.Logf("redirect header: %q\n\n", authorizeLink)
   213  
   214  		body := eB.GET(u.Path).
   215  			WithQuery("sharing_id", sharingID).
   216  			WithQuery("state", state).
   217  			Expect().Status(200).
   218  			HasContentType("text/html", "utf-8").
   219  			Body()
   220  
   221  		body.Contains("and you can collaborate by editing the document")
   222  		body.Contains(`<input type="hidden" name="sharing_id" value="` + sharingID)
   223  		body.Contains(`<input type="hidden" name="state" value="` + state)
   224  		body.Contains(`<span class="filetype-other filetype">`)
   225  
   226  		matches := body.Match(`<input type="hidden" name="csrf_token" value="(\w+)"`)
   227  		matches.Length().IsEqual(2)
   228  
   229  		eB.POST("/auth/authorize/sharing").
   230  			WithFormField("state", state).
   231  			WithFormField("sharing_id", sharingID).
   232  			WithFormField("csrf_token", token).
   233  			WithFormField("synchronize", true).
   234  			WithRedirectPolicy(httpexpect.DontFollowRedirects).
   235  			Expect().Status(303).
   236  			Header("Location").Contains("testapp." + bobInstance.Domain)
   237  
   238  		assertCredentialsHasBeenExchanged(t, aliceInstance, bobInstance, tsA.URL, tsB.URL)
   239  	})
   240  
   241  	t.Run("DelegateAddRecipientByCozyURL", func(t *testing.T) {
   242  		assert.NotEmpty(t, bobAppToken)
   243  		edwardContact = createContact(t, bobInstance, "Edward", "edward@example.net")
   244  
   245  		eB := httpexpect.Default(t, tsB.URL)
   246  
   247  		obj := eB.POST("/sharings/"+sharingID+"/recipients").
   248  			WithHeader("Authorization", "Bearer "+bobAppToken).
   249  			WithHeader("Content-Type", "application/vnd.api+json").
   250  			WithBytes([]byte(`{
   251          "data": {
   252            "type": "` + consts.Sharings + `",
   253            "relationships": {
   254              "recipients": {
   255                "data": [{"id": "` + edwardContact.ID() + `", "type": "` + edwardContact.DocType() + `"}]
   256              }
   257            }
   258          }
   259        }`)).
   260  			Expect().Status(200).
   261  			JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}).
   262  			Object()
   263  
   264  		data := obj.Value("data").Object()
   265  		attrs := data.Value("attributes").Object()
   266  
   267  		members := attrs.Value("members").Array()
   268  		members.Length().IsEqual(4)
   269  
   270  		owner := members.Value(0).Object()
   271  		owner.HasValue("status", "owner")
   272  		owner.HasValue("public_name", "Alice")
   273  		owner.HasValue("email", "alice@example.net")
   274  
   275  		bob := members.Value(1).Object()
   276  		bob.HasValue("status", "ready")
   277  		bob.HasValue("email", "bob@example.net")
   278  
   279  		dave := members.Value(2).Object()
   280  		dave.HasValue("status", "pending")
   281  		dave.HasValue("email", "dave@example.net")
   282  		dave.HasValue("read_only", true)
   283  
   284  		edward := members.Value(3).Object()
   285  		edward.HasValue("name", "Edward")
   286  		edward.HasValue("email", "edward@example.net")
   287  	})
   288  
   289  	t.Run("CreateSharingWithGroup", func(t *testing.T) {
   290  		eA := httpexpect.Default(t, tsA.URL)
   291  		require.NotEmpty(t, aliceAppToken)
   292  
   293  		group := createGroup(t, aliceInstance, "friends")
   294  		require.NotNil(t, group)
   295  		fionaContact := addContactToGroup(t, aliceInstance, group, "Fiona")
   296  		require.NotNil(t, fionaContact)
   297  		gabyContact := addContactToGroup(t, aliceInstance, group, "Gaby")
   298  		require.NotNil(t, gabyContact)
   299  
   300  		obj := eA.POST("/sharings/").
   301  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   302  			WithHeader("Content-Type", "application/vnd.api+json").
   303  			WithBytes([]byte(`{
   304          "data": {
   305            "type": "` + consts.Sharings + `",
   306            "attributes": {
   307              "description":  "this is a test with a group",
   308              "open_sharing": true,
   309              "rules": [{
   310                  "title": "test group",
   311                  "doctype": "` + iocozytests + `",
   312                  "values": ["000001"],
   313                  "add": "sync"
   314                }]
   315            },
   316            "relationships": {
   317              "recipients": {
   318                "data": [{"id": "` + group.ID() + `", "type": "` + consts.Groups + `"}]
   319              }
   320            }
   321          }
   322        }`)).
   323  			Expect().Status(201).
   324  			JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}).
   325  			Object()
   326  
   327  		data := obj.Value("data").Object()
   328  		attrs := data.Value("attributes").Object()
   329  		attrs.HasValue("description", "this is a test with a group")
   330  		members := attrs.Value("members").Array()
   331  		members.Length().IsEqual(3)
   332  
   333  		owner := members.Value(0).Object()
   334  		owner.HasValue("status", "owner")
   335  		owner.HasValue("public_name", "Alice")
   336  		owner.HasValue("email", "alice@example.net")
   337  		owner.HasValue("instance", "http://"+aliceInstance.Domain)
   338  
   339  		recipient := members.Value(1).Object()
   340  		recipient.HasValue("status", "pending")
   341  		recipient.HasValue("name", "Fiona")
   342  		recipient.HasValue("email", "fiona@example.net")
   343  		recipient.HasValue("groups", []int{0})
   344  		recipient.NotContainsKey("read_only")
   345  		recipient.HasValue("only_in_groups", true)
   346  
   347  		recipient = members.Value(2).Object()
   348  		recipient.HasValue("status", "pending")
   349  		recipient.HasValue("name", "Gaby")
   350  		recipient.HasValue("email", "gaby@example.net")
   351  		recipient.HasValue("groups", []int{0})
   352  		recipient.NotContainsKey("read_only")
   353  		recipient.HasValue("only_in_groups", true)
   354  
   355  		groups := attrs.Value("groups").Array()
   356  		groups.Length().IsEqual(1)
   357  		g := groups.Value(0).Object()
   358  		g.HasValue("id", group.ID())
   359  		g.HasValue("name", "friends")
   360  		g.HasValue("addedBy", 0)
   361  	})
   362  
   363  	t.Run("CreateSharingWithPreview", func(t *testing.T) {
   364  		bobContact := createBobContact(t, aliceInstance)
   365  		require.NotEmpty(t, aliceAppToken)
   366  		require.NotNil(t, bobContact)
   367  
   368  		eA := httpexpect.Default(t, tsA.URL)
   369  
   370  		obj := eA.POST("/sharings/").
   371  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   372  			WithHeader("Content-Type", "application/vnd.api+json").
   373  			WithBytes([]byte(`{
   374          "data": {
   375            "type": "` + consts.Sharings + `",
   376            "attributes": {
   377              "description":  "this is a test with preview",
   378              "preview_path": "/preview",
   379              "rules": [{
   380                  "title": "test two",
   381                  "doctype": "` + iocozytests + `",
   382                  "values": ["foobaz"]
   383                }]
   384            },
   385            "relationships": {
   386              "recipients": {
   387                "data": [{"id": "` + bobContact.ID() + `", "type": "` + bobContact.DocType() + `"}]
   388              },
   389              "read_only_recipients": {
   390                  "data": [{"id": "` + daveContact.ID() + `", "type": "` + daveContact.DocType() + `"}]
   391              }
   392            }
   393          }
   394        }`)).
   395  			Expect().Status(201).
   396  			JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}).
   397  			Object()
   398  
   399  		data := obj.Value("data").Object()
   400  		data.HasValue("type", consts.Sharings)
   401  		sharingID = data.Value("id").String().NotEmpty().Raw()
   402  		data.Value("meta").Object().Value("rev").String().NotEmpty()
   403  		data.Value("links").Object().HasValue("self", "/sharings/"+sharingID)
   404  
   405  		attrs := data.Value("attributes").Object()
   406  		attrs.HasValue("description", "this is a test with preview")
   407  		attrs.HasValue("app_slug", "testapp")
   408  		attrs.HasValue("preview_path", "/preview")
   409  		attrs.HasValue("owner", true)
   410  		attrs.Value("created_at").String().AsDateTime(time.RFC3339)
   411  		attrs.Value("updated_at").String().AsDateTime(time.RFC3339)
   412  		attrs.NotContainsKey("credentials")
   413  
   414  		members := attrs.Value("members").Array()
   415  		assertSharingByAliceToBobAndDave(t, members, aliceInstance)
   416  
   417  		rules := attrs.Value("rules").Array()
   418  		rules.Length().IsEqual(1)
   419  		rule := rules.Value(0).Object()
   420  		rule.HasValue("title", "test two")
   421  		rule.HasValue("doctype", iocozytests)
   422  		rule.HasValue("values", []string{"foobaz"})
   423  
   424  		description := assertInvitationMailWasSent(t, aliceInstance)
   425  		assert.Equal(t, description, "this is a test with preview")
   426  		assert.Contains(t, discoveryLink, aliceInstance.Domain)
   427  		assert.Contains(t, discoveryLink, "/preview?sharecode=")
   428  	})
   429  
   430  	t.Run("DiscoveryWithPreview", func(t *testing.T) {
   431  		u, err := url.Parse(discoveryLink)
   432  		assert.NoError(t, err)
   433  		sharecode := u.Query()["sharecode"][0]
   434  
   435  		eA := httpexpect.Default(t, tsA.URL)
   436  
   437  		t.Logf("sharcode: %q\n\n", sharecode)
   438  
   439  		obj := eA.POST("/sharings/"+sharingID+"/discovery").
   440  			WithHeader("Accept", "application/json").
   441  			WithFormField("sharecode", sharecode).
   442  			WithFormField("url", tsB.URL).
   443  			Expect().Status(200).
   444  			JSON().Object()
   445  
   446  		redirectURI := obj.Value("redirect").String().Contains(tsB.URL).Raw()
   447  
   448  		res, err := url.Parse(redirectURI)
   449  		assert.NoError(t, err)
   450  		assert.Equal(t, res.Path, "/auth/authorize/sharing")
   451  		assert.Equal(t, res.Query()["sharing_id"][0], sharingID)
   452  		assert.NotEmpty(t, res.Query()["state"][0])
   453  	})
   454  
   455  	t.Run("AddRecipient", func(t *testing.T) {
   456  		require.NotEmpty(t, aliceAppToken)
   457  		require.NotNil(t, charlieContact)
   458  
   459  		eA := httpexpect.Default(t, tsA.URL)
   460  
   461  		brothers := createGroup(t, aliceInstance, "brothers")
   462  		require.NotNil(t, brothers)
   463  		hugoContact := addContactToGroup(t, aliceInstance, brothers, "Hugo")
   464  		require.NotNil(t, hugoContact)
   465  		idrisContact := addContactToGroup(t, aliceInstance, brothers, "Idris")
   466  		require.NotNil(t, idrisContact)
   467  
   468  		obj := eA.POST("/sharings/"+sharingID+"/recipients").
   469  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   470  			WithHeader("Content-Type", "application/vnd.api+json").
   471  			WithBytes([]byte(`{
   472          "data": {
   473            "type": "` + consts.Sharings + `",
   474            "relationships": {
   475              "recipients": {
   476                "data": [
   477  			    {"id": "` + charlieContact.ID() + `", "type": "` + consts.Contacts + `"},
   478  			    {"id": "` + brothers.ID() + `", "type": "` + consts.Groups + `"}
   479  		      ]
   480              }
   481            }
   482          }
   483        }`)).
   484  			Expect().Status(200).
   485  			JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}).
   486  			Object()
   487  
   488  		data := obj.Value("data").Object()
   489  		attrs := data.Value("attributes").Object()
   490  		members := attrs.Value("members").Array()
   491  
   492  		members.Length().IsEqual(6)
   493  		owner := members.Value(0).Object()
   494  		owner.HasValue("status", "owner")
   495  		owner.HasValue("public_name", "Alice")
   496  		owner.HasValue("email", "alice@example.net")
   497  		owner.HasValue("instance", "http://"+aliceInstance.Domain)
   498  
   499  		bob := members.Value(1).Object()
   500  		bob.HasValue("status", "pending")
   501  		bob.HasValue("name", "Bob")
   502  		bob.HasValue("email", "bob@example.net")
   503  		bob.NotContainsKey("only_in_groups")
   504  
   505  		dave := members.Value(2).Object()
   506  		dave.HasValue("status", "pending")
   507  		dave.HasValue("name", "Dave")
   508  		dave.HasValue("email", "dave@example.net")
   509  		dave.HasValue("read_only", true)
   510  		dave.NotContainsKey("only_in_groups")
   511  
   512  		charlie := members.Value(3).Object()
   513  		charlie.HasValue("status", "pending")
   514  		charlie.HasValue("name", "Charlie")
   515  		charlie.HasValue("email", "charlie@example.net")
   516  		charlie.NotContainsKey("only_in_groups")
   517  
   518  		hugo := members.Value(4).Object()
   519  		hugo.HasValue("status", "pending")
   520  		hugo.HasValue("name", "Hugo")
   521  		hugo.HasValue("email", "hugo@example.net")
   522  		hugo.HasValue("groups", []int{0})
   523  		hugo.HasValue("only_in_groups", true)
   524  
   525  		idris := members.Value(5).Object()
   526  		idris.HasValue("status", "pending")
   527  		idris.HasValue("name", "Idris")
   528  		idris.HasValue("email", "idris@example.net")
   529  		idris.HasValue("groups", []int{0})
   530  		idris.HasValue("only_in_groups", true)
   531  
   532  		groups := attrs.Value("groups").Array()
   533  		groups.Length().IsEqual(1)
   534  		g := groups.Value(0).Object()
   535  		g.HasValue("id", brothers.ID())
   536  		g.HasValue("name", "brothers")
   537  		g.HasValue("addedBy", 0)
   538  	})
   539  
   540  	t.Run("RevokedSharingWithPreview", func(t *testing.T) {
   541  		sharecode := strings.Split(discoveryLink, "=")[1]
   542  
   543  		eA := httpexpect.Default(t, tsA.URL)
   544  
   545  		obj := eA.GET("/permissions/self").
   546  			WithHeader("Authorization", "Bearer "+sharecode).
   547  			WithHeader("Content-Type", "application/vnd.api+json").
   548  			Expect().Status(200).
   549  			JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}).
   550  			Object()
   551  
   552  		sourceID := obj.Value("data").Object().
   553  			Value("attributes").Object().
   554  			Value("source_id").String().NotEmpty().Raw()
   555  		sharingID = strings.Split(sourceID, "/")[1]
   556  
   557  		// Adding a new member to the sharing
   558  		newMemberMail := "foo@bar.com"
   559  		sharingDoc, err := sharing.FindSharing(aliceInstance, sharingID)
   560  		require.NoError(t, err)
   561  
   562  		m := sharing.Member{Email: newMemberMail, ReadOnly: true}
   563  		_, err = sharingDoc.AddDelegatedContact(aliceInstance, m)
   564  		require.NoError(t, err)
   565  		perms, err := permission.GetForSharePreview(aliceInstance, sharingID)
   566  		require.NoError(t, err)
   567  		fooShareCode, err := aliceInstance.CreateShareCode(newMemberMail)
   568  		require.NoError(t, err)
   569  
   570  		// Adding its sharecode
   571  		codes := perms.Codes
   572  		codes[newMemberMail] = fooShareCode
   573  		perms.PatchCodes(codes)
   574  		assert.NoError(t, couchdb.UpdateDoc(aliceInstance, perms))
   575  		assert.NoError(t, couchdb.UpdateDoc(aliceInstance, sharingDoc))
   576  
   577  		// Assert he has access to the sharing preview
   578  		eA.GET("/permissions/self").
   579  			WithHeader("Authorization", "Bearer "+fooShareCode).
   580  			WithHeader("Content-Type", "application/vnd.api+json").
   581  			Expect().Status(200)
   582  
   583  		// Check the member status has been updated to "seen"
   584  		sharingDoc, err = sharing.FindSharing(aliceInstance, sharingID)
   585  		assert.NoError(t, err)
   586  		member, err := sharingDoc.FindMemberBySharecode(aliceInstance, fooShareCode)
   587  		assert.NoError(t, err)
   588  		assert.Equal(t, "seen", member.Status)
   589  
   590  		// Now, revoking the fresh user from the sharing
   591  		member, err = sharingDoc.FindMemberBySharecode(aliceInstance, fooShareCode)
   592  		assert.NoError(t, err)
   593  		index := 0
   594  		for i := range sharingDoc.Members {
   595  			if member == &sharingDoc.Members[i] {
   596  				index = i
   597  				break
   598  			}
   599  		}
   600  		err = sharingDoc.RevokeMember(aliceInstance, index)
   601  		assert.NoError(t, err)
   602  		assert.Equal(t, "revoked", member.Status)
   603  
   604  		// Try to get permissions/self, we should get a 400
   605  		eA.GET("/permissions/self").
   606  			WithHeader("Authorization", "Bearer "+fooShareCode).
   607  			WithHeader("Content-Type", "application/vnd.api+json").
   608  			Expect().Status(400).
   609  			Body().Contains("Invalid JWT")
   610  	})
   611  
   612  	t.Run("CheckPermissions", func(t *testing.T) {
   613  		bobContact := createBobContact(t, aliceInstance)
   614  		assert.NotNil(t, bobContact)
   615  
   616  		eA := httpexpect.Default(t, tsA.URL)
   617  
   618  		eA.POST("/sharings").
   619  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   620  			WithHeader("Content-Type", "application/vnd.api+json").
   621  			WithBytes([]byte(`{
   622          "data": {
   623            "type": "` + consts.Sharings + `",
   624            "attributes": {
   625              "description":  "this is a test",
   626              "preview_path": "/preview",
   627              "rules": [
   628                {
   629                  "title": "test one",
   630                  "doctype": "` + iocozytests + `",
   631                  "values": ["000000"],
   632                  "add": "sync"
   633                },{
   634                  "title": "test two",
   635                  "doctype": "` + consts.Contacts + `",
   636                  "values": ["000000"],
   637                  "add": "sync"
   638                }]
   639            },
   640            "relationships": {
   641              "recipients": {
   642                "data": [{"id": "` + bobContact.ID() + `", "type": "` + bobContact.DocType() + `"}]
   643              }
   644            }
   645          }
   646        }`)).
   647  			Expect().Status(403)
   648  
   649  		other := &sharing.Sharing{
   650  			Description: "Another sharing",
   651  			Rules: []sharing.Rule{
   652  				{
   653  					Title:   "a directory",
   654  					DocType: consts.Files,
   655  					Values:  []string{"6836cc06-33e9-11e8-8157-dfc1aca099b6"},
   656  				},
   657  			},
   658  		}
   659  		assert.NoError(t, other.BeOwner(aliceInstance, "home"))
   660  		assert.NoError(t, other.AddContact(aliceInstance, bobContact.ID(), false))
   661  		_, err := other.Create(aliceInstance)
   662  		assert.NoError(t, err)
   663  
   664  		eA.GET("/sharings/"+other.ID()).
   665  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   666  			WithHeader("Content-Type", "application/vnd.api+json").
   667  			Expect().Status(403)
   668  	})
   669  
   670  	t.Run("CheckSharingInfoByDocType", func(t *testing.T) {
   671  		sharedDocs1 := []string{"fakeid1", "fakeid2", "fakeid3"}
   672  		sharedDocs2 := []string{"fakeid4", "fakeid5"}
   673  		s1 := createSharing(t, aliceInstance, sharedDocs1, tsB.URL)
   674  		s2 := createSharing(t, aliceInstance, sharedDocs2, tsB.URL)
   675  
   676  		for _, id := range sharedDocs1 {
   677  			sid := iocozytests + "/" + id
   678  			sd, errs := createSharedDoc(aliceInstance, sid, s1.ID())
   679  			assert.NoError(t, errs)
   680  			assert.NotNil(t, sd)
   681  		}
   682  		for _, id := range sharedDocs2 {
   683  			sid := iocozytests + "/" + id
   684  			sd, errs := createSharedDoc(aliceInstance, sid, s2.ID())
   685  			assert.NoError(t, errs)
   686  			assert.NotNil(t, sd)
   687  		}
   688  
   689  		eA := httpexpect.Default(t, tsA.URL)
   690  
   691  		obj := eA.GET("/sharings/doctype/"+iocozytests).
   692  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   693  			WithHeader("Content-Type", "application/vnd.api+json").
   694  			Expect().Status(200).
   695  			JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}).
   696  			Object()
   697  
   698  		s1Found := false
   699  		s2Found := false
   700  
   701  		for _, itm := range obj.Value("data").Array().Iter() {
   702  			data := itm.Object()
   703  			data.HasValue("type", consts.Sharings)
   704  			sharingID = data.Value("id").String().NotEmpty().Raw()
   705  			rel := data.Value("relationships").Object()
   706  			sharedDocs := rel.Value("shared_docs").Object()
   707  
   708  			if sharingID == s1.ID() {
   709  				sharedDocsData := sharedDocs.Value("data").Array()
   710  				sharedDocsData.Value(0).Object().Value("id").IsEqual("fakeid1")
   711  				sharedDocsData.Value(1).Object().Value("id").IsEqual("fakeid2")
   712  				sharedDocsData.Value(2).Object().Value("id").IsEqual("fakeid3")
   713  				s1Found = true
   714  			}
   715  
   716  			if sharingID == s2.ID() {
   717  				sharedDocsData := sharedDocs.Value("data").Array()
   718  				sharedDocsData.Value(0).Object().Value("id").IsEqual("fakeid4")
   719  				sharedDocsData.Value(1).Object().Value("id").IsEqual("fakeid5")
   720  				s2Found = true
   721  			}
   722  		}
   723  
   724  		assert.Equal(t, true, s1Found)
   725  		assert.Equal(t, true, s2Found)
   726  
   727  		eA.GET("/sharings/doctype/io.cozy.tests.notyet").
   728  			WithHeader("Authorization", "Bearer "+aliceAppTokenWildcard).
   729  			WithHeader("Content-Type", "application/vnd.api+json").
   730  			Expect().Status(200)
   731  
   732  		eA.GET("/sharings/doctype/"+iocozytests).
   733  			WithHeader("Authorization", "Bearer "+aliceAppTokenWildcard).
   734  			WithHeader("Content-Type", "application/vnd.api+json").
   735  			Expect().Status(200)
   736  
   737  		eA.GET("/sharings/doctype/io.cozy.things").
   738  			WithHeader("Content-Type", "application/vnd.api+json").
   739  			Expect().Status(401)
   740  	})
   741  
   742  	t.Run("RevokeSharing", func(t *testing.T) {
   743  		sharedDocs := []string{"mygreatid1", "mygreatid2"}
   744  		sharedRefs := []*sharing.SharedRef{}
   745  		s := createSharing(t, aliceInstance, sharedDocs, tsB.URL)
   746  
   747  		for _, id := range sharedDocs {
   748  			sid := iocozytests + "/" + id
   749  			sd, errs := createSharedDoc(aliceInstance, sid, s.SID)
   750  			sharedRefs = append(sharedRefs, sd)
   751  			assert.NoError(t, errs)
   752  			assert.NotNil(t, sd)
   753  		}
   754  
   755  		cli, err := sharing.CreateOAuthClient(aliceInstance, &s.Members[1])
   756  		assert.NoError(t, err)
   757  		s.Credentials[0].Client = sharing.ConvertOAuthClient(cli)
   758  		token, err := sharing.CreateAccessToken(aliceInstance, cli, s.SID, permission.ALL)
   759  		assert.NoError(t, err)
   760  		s.Credentials[0].AccessToken = token
   761  		s.Members[1].Status = sharing.MemberStatusReady
   762  
   763  		err = couchdb.UpdateDoc(aliceInstance, s)
   764  		assert.NoError(t, err)
   765  
   766  		err = s.AddTrackTriggers(aliceInstance)
   767  		assert.NoError(t, err)
   768  		err = s.AddReplicateTrigger(aliceInstance)
   769  		assert.NoError(t, err)
   770  
   771  		eA := httpexpect.Default(t, tsA.URL)
   772  
   773  		eA.DELETE("/sharings/"+s.ID()+"/recipients").
   774  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   775  			WithHeader("Content-Type", "application/vnd.api+json").
   776  			Expect().Status(204)
   777  
   778  		var sRevoke sharing.Sharing
   779  		err = couchdb.GetDoc(aliceInstance, s.DocType(), s.SID, &sRevoke)
   780  		assert.NoError(t, err)
   781  
   782  		assert.Equal(t, "", sRevoke.Triggers.TrackID)
   783  		assert.Empty(t, sRevoke.Triggers.TrackIDs)
   784  		assert.Equal(t, "", sRevoke.Triggers.ReplicateID)
   785  		assert.Equal(t, "", sRevoke.Triggers.UploadID)
   786  		assert.Equal(t, false, sRevoke.Active)
   787  
   788  		var sdoc sharing.SharedRef
   789  		err = couchdb.GetDoc(aliceInstance, sharedRefs[0].DocType(), sharedRefs[0].ID(), &sdoc)
   790  		assert.EqualError(t, err, "CouchDB(not_found): deleted")
   791  		err = couchdb.GetDoc(aliceInstance, sharedRefs[1].DocType(), sharedRefs[1].ID(), &sdoc)
   792  		assert.EqualError(t, err, "CouchDB(not_found): deleted")
   793  	})
   794  
   795  	t.Run("RevokeRecipient", func(t *testing.T) {
   796  		sharedDocs := []string{"mygreatid3", "mygreatid4"}
   797  		sharedRefs := []*sharing.SharedRef{}
   798  		s := createSharing(t, aliceInstance, sharedDocs, tsB.URL)
   799  
   800  		for _, id := range sharedDocs {
   801  			sid := iocozytests + "/" + id
   802  			sd, errs := createSharedDoc(aliceInstance, sid, s.SID)
   803  			sharedRefs = append(sharedRefs, sd)
   804  			assert.NoError(t, errs)
   805  			assert.NotNil(t, sd)
   806  		}
   807  
   808  		cli, err := sharing.CreateOAuthClient(aliceInstance, &s.Members[1])
   809  		assert.NoError(t, err)
   810  		s.Credentials[0].Client = sharing.ConvertOAuthClient(cli)
   811  		token, err := sharing.CreateAccessToken(aliceInstance, cli, s.SID, permission.ALL)
   812  		assert.NoError(t, err)
   813  		s.Credentials[0].AccessToken = token
   814  		s.Members[1].Status = sharing.MemberStatusReady
   815  
   816  		s.Members = append(s.Members, sharing.Member{
   817  			Status:   sharing.MemberStatusReady,
   818  			Name:     "Charlie",
   819  			Email:    "charlie@cozy.local",
   820  			Instance: tsB.URL,
   821  		})
   822  
   823  		clientC, err := sharing.CreateOAuthClient(aliceInstance, &s.Members[2])
   824  		assert.NoError(t, err)
   825  		tokenC, err := sharing.CreateAccessToken(aliceInstance, clientC, s.SID, permission.ALL)
   826  		assert.NoError(t, err)
   827  		s.Credentials = append(s.Credentials, sharing.Credentials{
   828  			Client:      sharing.ConvertOAuthClient(clientC),
   829  			AccessToken: tokenC,
   830  		})
   831  
   832  		err = couchdb.UpdateDoc(aliceInstance, s)
   833  		assert.NoError(t, err)
   834  
   835  		err = s.AddTrackTriggers(aliceInstance)
   836  		assert.NoError(t, err)
   837  		err = s.AddReplicateTrigger(aliceInstance)
   838  		assert.NoError(t, err)
   839  
   840  		eA := httpexpect.Default(t, tsA.URL)
   841  
   842  		eA.DELETE("/sharings/"+s.ID()+"/recipients/1").
   843  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   844  			WithHeader("Content-Type", "application/vnd.api+json").
   845  			Expect().Status(204)
   846  
   847  		assertOneRecipientIsRevoked(t, s, aliceInstance)
   848  
   849  		eA.DELETE("/sharings/"+s.ID()+"/recipients/2").
   850  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   851  			WithHeader("Content-Type", "application/vnd.api+json").
   852  			Expect().Status(204)
   853  
   854  		assertLastRecipientIsRevoked(t, s, sharedRefs, aliceInstance)
   855  	})
   856  
   857  	t.Run("RevokeGroup", func(t *testing.T) {
   858  		sharedDocs := []string{"forgroup1"}
   859  		s := createSharing(t, aliceInstance, sharedDocs, tsB.URL)
   860  
   861  		group := createGroup(t, aliceInstance, "friends")
   862  		require.NotNil(t, group)
   863  		fionaContact := addContactToGroup(t, aliceInstance, group, "Fiona")
   864  		require.NotNil(t, fionaContact)
   865  		gabyContact := addContactToGroup(t, aliceInstance, group, "Gaby")
   866  		require.NotNil(t, gabyContact)
   867  
   868  		eA := httpexpect.Default(t, tsA.URL)
   869  
   870  		obj := eA.POST("/sharings/"+s.ID()+"/recipients").
   871  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   872  			WithHeader("Content-Type", "application/vnd.api+json").
   873  			WithBytes([]byte(`{
   874          "data": {
   875            "type": "` + consts.Sharings + `",
   876            "relationships": {
   877              "recipients": {
   878                "data": [
   879  			    {"id": "` + group.ID() + `", "type": "` + consts.Groups + `"}
   880  		      ]
   881              }
   882            }
   883          }
   884        }`)).
   885  			Expect().Status(200).
   886  			JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}).
   887  			Object()
   888  
   889  		data := obj.Value("data").Object()
   890  		attrs := data.Value("attributes").Object()
   891  		members := attrs.Value("members").Array()
   892  		members.Length().IsEqual(4)
   893  
   894  		owner := members.Value(0).Object()
   895  		owner.HasValue("status", "owner")
   896  		owner.HasValue("public_name", "Alice")
   897  
   898  		bob := members.Value(1).Object()
   899  		bob.HasValue("name", "Bob")
   900  
   901  		fiona := members.Value(2).Object()
   902  		fiona.HasValue("status", "pending")
   903  		fiona.HasValue("name", "Fiona")
   904  		fiona.HasValue("groups", []int{0})
   905  		fiona.HasValue("only_in_groups", true)
   906  
   907  		gaby := members.Value(3).Object()
   908  		gaby.HasValue("status", "pending")
   909  		gaby.HasValue("name", "Gaby")
   910  		gaby.HasValue("groups", []int{0})
   911  		gaby.HasValue("only_in_groups", true)
   912  
   913  		eA.DELETE("/sharings/"+s.ID()+"/groups/0").
   914  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   915  			WithHeader("Content-Type", "application/vnd.api+json").
   916  			Expect().Status(204)
   917  
   918  		obj = eA.GET("/sharings/"+s.ID()).
   919  			WithHeader("Authorization", "Bearer "+aliceAppToken).
   920  			WithHeader("Content-Type", "application/vnd.api+json").
   921  			Expect().Status(200).
   922  			JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}).
   923  			Object()
   924  
   925  		data = obj.Value("data").Object()
   926  		attrs = data.Value("attributes").Object()
   927  		members = attrs.Value("members").Array()
   928  		members.Length().IsEqual(4)
   929  
   930  		owner = members.Value(0).Object()
   931  		owner.HasValue("status", "owner")
   932  		owner.HasValue("public_name", "Alice")
   933  
   934  		bob = members.Value(1).Object()
   935  		bob.HasValue("name", "Bob")
   936  
   937  		fiona = members.Value(2).Object()
   938  		fiona.HasValue("status", "revoked")
   939  		fiona.HasValue("name", "Fiona")
   940  
   941  		gaby = members.Value(3).Object()
   942  		gaby.HasValue("status", "revoked")
   943  		gaby.HasValue("name", "Gaby")
   944  	})
   945  
   946  	t.Run("RevocationFromRecipient", func(t *testing.T) {
   947  		sharedDocs := []string{"mygreatid5", "mygreatid6"}
   948  		sharedRefs := []*sharing.SharedRef{}
   949  		s := createSharing(t, aliceInstance, sharedDocs, tsB.URL)
   950  		for _, id := range sharedDocs {
   951  			sid := iocozytests + "/" + id
   952  			sd, errs := createSharedDoc(aliceInstance, sid, s.SID)
   953  			sharedRefs = append(sharedRefs, sd)
   954  			assert.NoError(t, errs)
   955  			assert.NotNil(t, sd)
   956  		}
   957  
   958  		cli, err := sharing.CreateOAuthClient(aliceInstance, &s.Members[1])
   959  		assert.NoError(t, err)
   960  		s.Credentials[0].InboundClientID = cli.ClientID
   961  		s.Credentials[0].Client = sharing.ConvertOAuthClient(cli)
   962  		token, err := sharing.CreateAccessToken(aliceInstance, cli, s.SID, permission.ALL)
   963  		assert.NoError(t, err)
   964  		s.Credentials[0].AccessToken = token
   965  		s.Members[1].Status = sharing.MemberStatusReady
   966  
   967  		s.Members = append(s.Members, sharing.Member{
   968  			Status:   sharing.MemberStatusReady,
   969  			Name:     "Charlie",
   970  			Email:    "charlie@cozy.local",
   971  			Instance: tsB.URL,
   972  		})
   973  		clientC, err := sharing.CreateOAuthClient(aliceInstance, &s.Members[2])
   974  		assert.NoError(t, err)
   975  		tokenC, err := sharing.CreateAccessToken(aliceInstance, clientC, s.SID, permission.ALL)
   976  		assert.NoError(t, err)
   977  		s.Credentials = append(s.Credentials, sharing.Credentials{
   978  			Client:          sharing.ConvertOAuthClient(clientC),
   979  			AccessToken:     tokenC,
   980  			InboundClientID: clientC.ClientID,
   981  		})
   982  
   983  		err = couchdb.UpdateDoc(aliceInstance, s)
   984  		assert.NoError(t, err)
   985  
   986  		err = s.AddTrackTriggers(aliceInstance)
   987  		assert.NoError(t, err)
   988  		err = s.AddReplicateTrigger(aliceInstance)
   989  		assert.NoError(t, err)
   990  
   991  		eA := httpexpect.Default(t, tsA.URL)
   992  
   993  		eA.DELETE("/sharings/"+s.ID()+"/answer").
   994  			WithHeader("Authorization", "Bearer "+s.Credentials[0].AccessToken.AccessToken).
   995  			WithHeader("Content-Type", "application/vnd.api+json").
   996  			Expect().Status(204)
   997  
   998  		assertOneRecipientIsRevoked(t, s, aliceInstance)
   999  
  1000  		eA.DELETE("/sharings/"+s.ID()+"/answer").
  1001  			WithHeader("Authorization", "Bearer "+s.Credentials[1].AccessToken.AccessToken).
  1002  			WithHeader("Content-Type", "application/vnd.api+json").
  1003  			Expect().Status(204)
  1004  
  1005  		assertLastRecipientIsRevoked(t, s, sharedRefs, aliceInstance)
  1006  	})
  1007  
  1008  	t.Run("ClearAppInURL", func(t *testing.T) {
  1009  		host := sharings.ClearAppInURL("https://example.mycozy.cloud/")
  1010  		assert.Equal(t, "https://example.mycozy.cloud/", host)
  1011  		host = sharings.ClearAppInURL("https://example-drive.mycozy.cloud/")
  1012  		assert.Equal(t, "https://example.mycozy.cloud/", host)
  1013  		host = sharings.ClearAppInURL("https://my-cozy.example.net/")
  1014  		assert.Equal(t, "https://my-cozy.example.net/", host)
  1015  	})
  1016  }
  1017  
  1018  func assertSharingByAliceToBobAndDave(t *testing.T, obj *httpexpect.Array, instance *instance.Instance) {
  1019  	t.Helper()
  1020  
  1021  	obj.Length().IsEqual(3)
  1022  
  1023  	owner := obj.Value(0).Object()
  1024  	owner.HasValue("status", "owner")
  1025  	owner.HasValue("public_name", "Alice")
  1026  	owner.HasValue("email", "alice@example.net")
  1027  	owner.HasValue("instance", "http://"+instance.Domain)
  1028  
  1029  	recipient := obj.Value(1).Object()
  1030  	recipient.HasValue("status", "pending")
  1031  	recipient.HasValue("name", "Bob")
  1032  	recipient.HasValue("email", "bob@example.net")
  1033  	recipient.NotContainsKey("read_only")
  1034  
  1035  	recipient2 := obj.Value(2).Object()
  1036  	recipient2.HasValue("status", "pending")
  1037  	recipient2.HasValue("name", "Dave")
  1038  	recipient2.HasValue("email", "dave@example.net")
  1039  	recipient2.HasValue("read_only", true)
  1040  }
  1041  
  1042  func assertSharingIsCorrectOnSharer(t *testing.T, obj *httpexpect.Object, sharingID string, instance *instance.Instance) {
  1043  	t.Helper()
  1044  
  1045  	data := obj.Value("data").Object()
  1046  
  1047  	data.HasValue("type", consts.Sharings)
  1048  	data.Value("meta").Object().Value("rev").String().NotEmpty()
  1049  	data.Value("links").Object().HasValue("self", "/sharings/"+sharingID)
  1050  
  1051  	attrs := data.Value("attributes").Object()
  1052  	attrs.HasValue("description", "this is a test")
  1053  	attrs.HasValue("app_slug", "testapp")
  1054  	attrs.HasValue("owner", true)
  1055  	attrs.Value("created_at").String().AsDateTime(time.RFC3339)
  1056  	attrs.Value("updated_at").String().AsDateTime(time.RFC3339)
  1057  	attrs.NotContainsKey("credentials")
  1058  
  1059  	assertSharingByAliceToBobAndDave(t, attrs.Value("members").Array(), instance)
  1060  
  1061  	rules := attrs.Value("rules").Array()
  1062  	rules.Length().IsEqual(1)
  1063  	rule := rules.Value(0).Object()
  1064  	rule.HasValue("title", "test one")
  1065  	rule.HasValue("doctype", iocozytests)
  1066  	rule.HasValue("values", []interface{}{"000000"})
  1067  }
  1068  
  1069  func assertInvitationMailWasSent(t *testing.T, instance *instance.Instance) string {
  1070  	var jobs []job.Job
  1071  	couchReq := &couchdb.FindRequest{
  1072  		UseIndex: "by-worker-and-state",
  1073  		Selector: mango.And(
  1074  			mango.Equal("worker", "sendmail"),
  1075  			mango.Exists("state"),
  1076  		),
  1077  		Sort: mango.SortBy{
  1078  			mango.SortByField{Field: "worker", Direction: "desc"},
  1079  		},
  1080  		Limit: 2,
  1081  	}
  1082  	err := couchdb.FindDocs(instance, consts.Jobs, couchReq, &jobs)
  1083  	assert.NoError(t, err)
  1084  	assert.Len(t, jobs, 2)
  1085  	var msg map[string]interface{}
  1086  	// Ignore the mail sent to Dave
  1087  	err = json.Unmarshal(jobs[0].Message, &msg)
  1088  	assert.NoError(t, err)
  1089  	if msg["recipient_name"] == "Dave" {
  1090  		err = json.Unmarshal(jobs[1].Message, &msg)
  1091  		assert.NoError(t, err)
  1092  	}
  1093  	assert.Equal(t, msg["mode"], "from")
  1094  	assert.Equal(t, msg["template_name"], "sharing_request")
  1095  	values := msg["template_values"].(map[string]interface{})
  1096  	assert.Equal(t, values["SharerPublicName"], "Alice")
  1097  	discoveryLink = values["SharingLink"].(string)
  1098  	return values["Description"].(string)
  1099  }
  1100  
  1101  func assertSharingRequestHasBeenCreated(t *testing.T, instanceA, instanceB *instance.Instance, serverURL string) {
  1102  	var results []*sharing.Sharing
  1103  	req := couchdb.AllDocsRequest{}
  1104  	err := couchdb.GetAllDocs(instanceB, consts.Sharings, &req, &results)
  1105  	assert.NoError(t, err)
  1106  	assert.Len(t, results, 1)
  1107  	s := results[0]
  1108  	assert.Equal(t, s.SID, sharingID)
  1109  	assert.False(t, s.Active)
  1110  	assert.False(t, s.Owner)
  1111  	assert.Equal(t, s.Description, "this is a test")
  1112  	assert.Equal(t, s.AppSlug, "testapp")
  1113  
  1114  	assert.Len(t, s.Members, 3)
  1115  	owner := s.Members[0]
  1116  	assert.Equal(t, owner.Status, "owner")
  1117  	assert.Equal(t, owner.PublicName, "Alice")
  1118  	assert.Equal(t, owner.Email, "alice@example.net")
  1119  	assert.Equal(t, owner.Instance, "http://"+instanceA.Domain)
  1120  	recipient := s.Members[1]
  1121  	assert.Equal(t, recipient.Status, "pending")
  1122  	assert.Equal(t, recipient.Email, "bob@example.net")
  1123  	assert.Equal(t, recipient.Instance, serverURL)
  1124  	recipient = s.Members[2]
  1125  	assert.Equal(t, recipient.Status, "pending")
  1126  	assert.Equal(t, recipient.Email, "dave@example.net")
  1127  	assert.Equal(t, recipient.ReadOnly, true)
  1128  
  1129  	assert.Len(t, s.Rules, 1)
  1130  	rule := s.Rules[0]
  1131  	assert.Equal(t, rule.Title, "test one")
  1132  	assert.Equal(t, rule.DocType, iocozytests)
  1133  	assert.NotEmpty(t, rule.Values)
  1134  }
  1135  
  1136  func fakeAliceInstance(t *testing.T, instance *instance.Instance, serverURL string) {
  1137  	var results []*sharing.Sharing
  1138  	req := couchdb.AllDocsRequest{}
  1139  	err := couchdb.GetAllDocs(instance, consts.Sharings, &req, &results)
  1140  	assert.NoError(t, err)
  1141  	assert.Len(t, results, 1)
  1142  	s := results[0]
  1143  	assert.Len(t, s.Members, 3)
  1144  	s.Members[0].Instance = serverURL
  1145  	err = couchdb.UpdateDoc(instance, s)
  1146  	assert.NoError(t, err)
  1147  }
  1148  
  1149  func assertCredentialsHasBeenExchanged(t *testing.T, instanceA, instanceB *instance.Instance, urlA, urlB string) {
  1150  	var resultsA []map[string]interface{}
  1151  	req := couchdb.AllDocsRequest{}
  1152  	err := couchdb.GetAllDocs(instanceB, consts.OAuthClients, &req, &resultsA)
  1153  	assert.NoError(t, err)
  1154  	assert.True(t, len(resultsA) > 0)
  1155  	clientA := resultsA[len(resultsA)-1]
  1156  	assert.Equal(t, clientA["client_kind"], "sharing")
  1157  	assert.Equal(t, clientA["client_uri"], urlA+"/")
  1158  	assert.Equal(t, clientA["client_name"], "Sharing Alice")
  1159  
  1160  	var resultsB []map[string]interface{}
  1161  	err = couchdb.GetAllDocs(instanceA, consts.OAuthClients, &req, &resultsB)
  1162  	assert.NoError(t, err)
  1163  	assert.True(t, len(resultsB) > 0)
  1164  	clientB := resultsB[len(resultsB)-1]
  1165  	assert.Equal(t, clientB["client_kind"], "sharing")
  1166  	assert.Equal(t, clientB["client_uri"], urlB+"/")
  1167  	assert.Equal(t, clientB["client_name"], "Sharing Bob")
  1168  
  1169  	var sharingsA []*sharing.Sharing
  1170  	err = couchdb.GetAllDocs(instanceA, consts.Sharings, &req, &sharingsA)
  1171  	assert.NoError(t, err)
  1172  	assert.True(t, len(sharingsA) > 0)
  1173  	assert.Len(t, sharingsA[0].Credentials, 2)
  1174  	credentials := sharingsA[0].Credentials[0]
  1175  	if assert.NotNil(t, credentials.Client) {
  1176  		assert.Equal(t, credentials.Client.ClientID, clientA["_id"])
  1177  	}
  1178  	if assert.NotNil(t, credentials.AccessToken) {
  1179  		assert.NotEmpty(t, credentials.AccessToken.AccessToken)
  1180  		assert.NotEmpty(t, credentials.AccessToken.RefreshToken)
  1181  		aliceAccessToken = credentials.AccessToken.AccessToken
  1182  	}
  1183  	assert.Equal(t, sharingsA[0].Members[1].Status, "ready")
  1184  	assert.Equal(t, sharingsA[0].Members[2].Status, "pending")
  1185  
  1186  	var sharingsB []*sharing.Sharing
  1187  	err = couchdb.GetAllDocs(instanceB, consts.Sharings, &req, &sharingsB)
  1188  	assert.NoError(t, err)
  1189  	assert.True(t, len(sharingsB) > 0)
  1190  	assert.Len(t, sharingsB[0].Credentials, 1)
  1191  	credentials = sharingsB[0].Credentials[0]
  1192  	if assert.NotNil(t, credentials.Client) {
  1193  		assert.Equal(t, credentials.Client.ClientID, clientB["_id"])
  1194  	}
  1195  	if assert.NotNil(t, credentials.AccessToken) {
  1196  		assert.NotEmpty(t, credentials.AccessToken.AccessToken)
  1197  		assert.NotEmpty(t, credentials.AccessToken.RefreshToken)
  1198  	}
  1199  }
  1200  
  1201  func assertOneRecipientIsRevoked(t *testing.T, s *sharing.Sharing, instance *instance.Instance) {
  1202  	var sRevoked sharing.Sharing
  1203  	err := couchdb.GetDoc(instance, s.DocType(), s.SID, &sRevoked)
  1204  	assert.NoError(t, err)
  1205  
  1206  	assert.Equal(t, sharing.MemberStatusRevoked, sRevoked.Members[1].Status)
  1207  	assert.Equal(t, sharing.MemberStatusReady, sRevoked.Members[2].Status)
  1208  	assert.NotEmpty(t, sRevoked.Triggers.TrackIDs)
  1209  	assert.NotEmpty(t, sRevoked.Triggers.ReplicateID)
  1210  	assert.True(t, sRevoked.Active)
  1211  }
  1212  
  1213  func assertLastRecipientIsRevoked(t *testing.T, s *sharing.Sharing, refs []*sharing.SharedRef, instance *instance.Instance) {
  1214  	var sRevoked sharing.Sharing
  1215  	err := couchdb.GetDoc(instance, s.DocType(), s.SID, &sRevoked)
  1216  	assert.NoError(t, err)
  1217  
  1218  	assert.Equal(t, sharing.MemberStatusRevoked, sRevoked.Members[1].Status)
  1219  	assert.Equal(t, sharing.MemberStatusRevoked, sRevoked.Members[2].Status)
  1220  	assert.Empty(t, sRevoked.Triggers.TrackIDs)
  1221  	assert.Empty(t, sRevoked.Triggers.ReplicateID)
  1222  	assert.False(t, sRevoked.Active)
  1223  
  1224  	var sdoc sharing.SharedRef
  1225  	err = couchdb.GetDoc(instance, refs[0].DocType(), refs[0].ID(), &sdoc)
  1226  	assert.EqualError(t, err, "CouchDB(not_found): deleted")
  1227  	err = couchdb.GetDoc(instance, refs[1].DocType(), refs[1].ID(), &sdoc)
  1228  	assert.EqualError(t, err, "CouchDB(not_found): deleted")
  1229  }
  1230  
  1231  func createBobContact(t *testing.T, instance *instance.Instance) *contact.Contact {
  1232  	return createContact(t, instance, "Bob", "bob@example.net")
  1233  }
  1234  
  1235  func createContact(t *testing.T, inst *instance.Instance, name, email string) *contact.Contact {
  1236  	t.Helper()
  1237  	mail := map[string]interface{}{"address": email}
  1238  	c := contact.New()
  1239  	c.M["fullname"] = name
  1240  	c.M["email"] = []interface{}{mail}
  1241  	require.NoError(t, couchdb.CreateDoc(inst, c))
  1242  	return c
  1243  }
  1244  
  1245  func createGroup(t *testing.T, inst *instance.Instance, name string) *contact.Group {
  1246  	t.Helper()
  1247  	g := contact.NewGroup()
  1248  	g.M["name"] = name
  1249  	require.NoError(t, couchdb.CreateDoc(inst, g))
  1250  	return g
  1251  }
  1252  
  1253  func addContactToGroup(t *testing.T, inst *instance.Instance, g *contact.Group, contactName string) *contact.Contact {
  1254  	t.Helper()
  1255  	email := strings.ToLower(contactName) + "@example.net"
  1256  	mail := map[string]interface{}{"address": email}
  1257  	c := contact.New()
  1258  	c.M["fullname"] = contactName
  1259  	c.M["email"] = []interface{}{mail}
  1260  	c.M["relationships"] = map[string]interface{}{
  1261  		"groups": map[string]interface{}{
  1262  			"data": []interface{}{
  1263  				map[string]interface{}{
  1264  					"_id":   g.ID(),
  1265  					"_type": consts.Groups,
  1266  				},
  1267  			},
  1268  		},
  1269  	}
  1270  	require.NoError(t, couchdb.CreateDoc(inst, c))
  1271  	return c
  1272  }
  1273  
  1274  func createSharing(t *testing.T, inst *instance.Instance, values []string, serverURL string) *sharing.Sharing {
  1275  	bobContact := createBobContact(t, inst)
  1276  	assert.NotNil(t, bobContact)
  1277  
  1278  	r := sharing.Rule{
  1279  		Title:   "test",
  1280  		DocType: iocozytests,
  1281  		Values:  values,
  1282  		Add:     sharing.ActionRuleSync,
  1283  	}
  1284  	mail, err := bobContact.ToMailAddress()
  1285  	assert.NoError(t, err)
  1286  	m := sharing.Member{
  1287  		Name:     bobContact.Get("fullname").(string),
  1288  		Email:    mail.Email,
  1289  		Instance: serverURL,
  1290  	}
  1291  	s := &sharing.Sharing{
  1292  		Owner: true,
  1293  		Rules: []sharing.Rule{r},
  1294  	}
  1295  	s.Credentials = append(s.Credentials, sharing.Credentials{})
  1296  	err = s.BeOwner(inst, "")
  1297  	assert.NoError(t, err)
  1298  	s.Members = append(s.Members, m)
  1299  
  1300  	err = couchdb.CreateDoc(inst, s)
  1301  	assert.NoError(t, err)
  1302  	assert.NotNil(t, s)
  1303  	return s
  1304  }
  1305  
  1306  func createSharedDoc(inst *instance.Instance, id, sharingID string) (*sharing.SharedRef, error) {
  1307  	ref := &sharing.SharedRef{
  1308  		SID:       id,
  1309  		Revisions: &sharing.RevsTree{Rev: "1-aaa"},
  1310  		Infos: map[string]sharing.SharedInfo{
  1311  			sharingID: {Rule: 0},
  1312  		},
  1313  	}
  1314  	err := couchdb.CreateNamedDocWithDB(inst, ref)
  1315  	if err != nil {
  1316  		return nil, err
  1317  	}
  1318  	return ref, nil
  1319  }
  1320  
  1321  func generateAppToken(inst *instance.Instance, slug, doctype string) string {
  1322  	rules := permission.Set{
  1323  		permission.Rule{
  1324  			Type:  doctype,
  1325  			Verbs: permission.ALL,
  1326  		},
  1327  	}
  1328  	permReq := permission.Permission{
  1329  		Permissions: rules,
  1330  		Type:        permission.TypeWebapp,
  1331  		SourceID:    consts.Apps + "/" + slug,
  1332  	}
  1333  	err := couchdb.CreateDoc(inst, &permReq)
  1334  	if err != nil {
  1335  		return ""
  1336  	}
  1337  	manifest := &couchdb.JSONDoc{
  1338  		Type: consts.Apps,
  1339  		M: map[string]interface{}{
  1340  			"_id":         consts.Apps + "/" + slug,
  1341  			"slug":        slug,
  1342  			"permissions": rules,
  1343  		},
  1344  	}
  1345  	err = couchdb.CreateNamedDocWithDB(inst, manifest)
  1346  	if err != nil {
  1347  		return ""
  1348  	}
  1349  	return inst.BuildAppToken(slug, "")
  1350  }