github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/sharing/group_test.go (about)

     1  package sharing
     2  
     3  import (
     4  	"strings"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/cozy/cozy-stack/model/contact"
     9  	"github.com/cozy/cozy-stack/model/instance"
    10  	"github.com/cozy/cozy-stack/model/instance/lifecycle"
    11  	"github.com/cozy/cozy-stack/model/job"
    12  	"github.com/cozy/cozy-stack/pkg/config/config"
    13  	"github.com/cozy/cozy-stack/pkg/consts"
    14  	"github.com/cozy/cozy-stack/pkg/couchdb"
    15  	"github.com/cozy/cozy-stack/tests/testutils"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  
    19  	_ "github.com/cozy/cozy-stack/worker/mails"
    20  )
    21  
    22  func TestGroups(t *testing.T) {
    23  	if testing.Short() {
    24  		t.Skip("an instance is required for this test: test skipped due to the use of --short flag")
    25  	}
    26  
    27  	config.UseTestFile(t)
    28  	testutils.NeedCouchdb(t)
    29  	setup := testutils.NewSetup(t, t.Name())
    30  	inst := setup.GetTestInstance(&lifecycle.Options{
    31  		Email:      "alice@example.net",
    32  		PublicName: "Alice",
    33  	})
    34  
    35  	t.Run("RevokeGroup", func(t *testing.T) {
    36  		now := time.Now()
    37  		friends := createGroup(t, inst, "Friends")
    38  		football := createGroup(t, inst, "Football")
    39  		bob := createContactInGroups(t, inst, "Bob", []string{friends.ID()})
    40  		_ = createContactInGroups(t, inst, "Charlie", []string{friends.ID(), football.ID()})
    41  		_ = createContactInGroups(t, inst, "Dave", []string{football.ID()})
    42  
    43  		s := &Sharing{
    44  			Active:      true,
    45  			Owner:       true,
    46  			Description: "Just testing groups",
    47  			Members: []Member{
    48  				{Status: MemberStatusOwner, Name: "Alice", Email: "alice@cozy.tools"},
    49  			},
    50  			Rules: []Rule{
    51  				{
    52  					Title:   "Just testing groups",
    53  					DocType: "io.cozy.tests",
    54  					Values:  []string{uuidv7()},
    55  				},
    56  			},
    57  			CreatedAt: now,
    58  			UpdatedAt: now,
    59  		}
    60  		require.NoError(t, couchdb.CreateDoc(inst, s))
    61  		require.NoError(t, s.AddContact(inst, bob.ID(), false))
    62  		require.NoError(t, s.AddGroup(inst, friends.ID(), false))
    63  		require.NoError(t, s.AddGroup(inst, football.ID(), false))
    64  
    65  		require.Len(t, s.Members, 4)
    66  		require.Equal(t, s.Members[0].Name, "Alice")
    67  		require.Equal(t, s.Members[1].Name, "Bob")
    68  		assert.False(t, s.Members[1].OnlyInGroups)
    69  		assert.Equal(t, s.Members[1].Groups, []int{0})
    70  		require.Equal(t, s.Members[2].Name, "Charlie")
    71  		assert.True(t, s.Members[2].OnlyInGroups)
    72  		assert.Equal(t, s.Members[2].Groups, []int{0, 1})
    73  		require.Equal(t, s.Members[3].Name, "Dave")
    74  		assert.True(t, s.Members[3].OnlyInGroups)
    75  		assert.Equal(t, s.Members[3].Groups, []int{1})
    76  
    77  		require.Len(t, s.Groups, 2)
    78  		require.Equal(t, s.Groups[0].Name, "Friends")
    79  		assert.False(t, s.Groups[0].Revoked)
    80  		require.Equal(t, s.Groups[1].Name, "Football")
    81  		assert.False(t, s.Groups[1].Revoked)
    82  
    83  		require.NoError(t, s.RevokeGroup(inst, 1)) // Revoke the football group
    84  
    85  		require.Len(t, s.Members, 4)
    86  		assert.NotEqual(t, s.Members[1].Status, MemberStatusRevoked)
    87  		assert.Equal(t, s.Members[1].Groups, []int{0})
    88  		assert.NotEqual(t, s.Members[2].Status, MemberStatusRevoked)
    89  		assert.Equal(t, s.Members[2].Groups, []int{0})
    90  		assert.Equal(t, s.Members[3].Status, MemberStatusRevoked)
    91  		assert.Empty(t, s.Members[3].Groups)
    92  
    93  		require.Len(t, s.Groups, 2)
    94  		assert.False(t, s.Groups[0].Revoked)
    95  		assert.True(t, s.Groups[1].Revoked)
    96  
    97  		require.NoError(t, s.RevokeGroup(inst, 0)) // Revoke the fiends group
    98  
    99  		require.Len(t, s.Members, 4)
   100  		assert.NotEqual(t, s.Members[1].Status, MemberStatusRevoked)
   101  		assert.Empty(t, s.Members[1].Groups)
   102  		assert.Equal(t, s.Members[2].Status, MemberStatusRevoked)
   103  		assert.Empty(t, s.Members[2].Groups)
   104  		assert.Equal(t, s.Members[3].Status, MemberStatusRevoked)
   105  		assert.Empty(t, s.Members[3].Groups)
   106  
   107  		require.Len(t, s.Groups, 2)
   108  		assert.True(t, s.Groups[0].Revoked)
   109  		assert.True(t, s.Groups[1].Revoked)
   110  	})
   111  
   112  	t.Run("UpdateGroups", func(t *testing.T) {
   113  		now := time.Now()
   114  		friends := createGroup(t, inst, "Friends")
   115  		football := createGroup(t, inst, "Football")
   116  		_ = createContactInGroups(t, inst, "Bob", []string{friends.ID()})
   117  		charlie := createContactInGroups(t, inst, "Charlie", []string{football.ID()})
   118  		dave := createContactWithoutEmail(t, inst, "Dave", []string{football.ID()})
   119  
   120  		s := &Sharing{
   121  			Active:      true,
   122  			Owner:       true,
   123  			Description: "Just testing groups",
   124  			Members: []Member{
   125  				{Status: MemberStatusOwner, Name: "Alice", Email: "alice@cozy.tools"},
   126  			},
   127  			Rules: []Rule{
   128  				{
   129  					Title:   "Just testing groups",
   130  					DocType: "io.cozy.tests",
   131  					Values:  []string{uuidv7()},
   132  				},
   133  			},
   134  			CreatedAt: now,
   135  			UpdatedAt: now,
   136  		}
   137  		require.NoError(t, s.AddGroup(inst, friends.ID(), false))
   138  		require.NoError(t, s.AddGroup(inst, football.ID(), false))
   139  		perms, err := s.Create(inst)
   140  		require.NoError(t, err)
   141  		require.NoError(t, s.SendInvitations(inst, perms))
   142  		sid := s.ID()
   143  
   144  		require.Len(t, s.Members, 4)
   145  		require.Equal(t, s.Members[0].Name, "Alice")
   146  		require.Equal(t, s.Members[1].Name, "Bob")
   147  		assert.True(t, s.Members[1].OnlyInGroups)
   148  		assert.Equal(t, s.Members[1].Groups, []int{0})
   149  		assert.Equal(t, s.Members[1].Status, MemberStatusPendingInvitation)
   150  		require.Equal(t, s.Members[2].Name, "Charlie")
   151  		assert.True(t, s.Members[2].OnlyInGroups)
   152  		assert.Equal(t, s.Members[2].Groups, []int{1})
   153  		assert.Equal(t, s.Members[2].Status, MemberStatusPendingInvitation)
   154  		require.Equal(t, s.Members[3].Name, "Dave")
   155  		assert.True(t, s.Members[3].OnlyInGroups)
   156  		assert.Equal(t, s.Members[3].Groups, []int{1})
   157  		assert.Equal(t, s.Members[3].Status, MemberStatusMailNotSent)
   158  
   159  		require.Len(t, s.Groups, 2)
   160  		require.Equal(t, s.Groups[0].Name, "Friends")
   161  		assert.False(t, s.Groups[0].Revoked)
   162  		require.Equal(t, s.Groups[1].Name, "Football")
   163  		assert.False(t, s.Groups[1].Revoked)
   164  
   165  		// Charlie is added to the friends group
   166  		msg1 := job.ShareGroupMessage{
   167  			ContactID:   charlie.ID(),
   168  			GroupsAdded: []string{friends.ID()},
   169  		}
   170  		require.NoError(t, UpdateGroups(inst, msg1))
   171  
   172  		s = &Sharing{}
   173  		require.NoError(t, couchdb.GetDoc(inst, consts.Sharings, sid, s))
   174  
   175  		require.Len(t, s.Members, 4)
   176  		require.Equal(t, s.Members[0].Name, "Alice")
   177  		require.Equal(t, s.Members[1].Name, "Bob")
   178  		assert.True(t, s.Members[1].OnlyInGroups)
   179  		assert.Equal(t, s.Members[1].Groups, []int{0})
   180  		require.Equal(t, s.Members[2].Name, "Charlie")
   181  		assert.True(t, s.Members[2].OnlyInGroups)
   182  		assert.Equal(t, s.Members[2].Groups, []int{0, 1})
   183  		require.Equal(t, s.Members[3].Name, "Dave")
   184  		assert.True(t, s.Members[3].OnlyInGroups)
   185  		assert.Equal(t, s.Members[3].Groups, []int{1})
   186  
   187  		// Charlie is removed of the football group
   188  		msg2 := job.ShareGroupMessage{
   189  			ContactID:     charlie.ID(),
   190  			GroupsRemoved: []string{football.ID()},
   191  		}
   192  		require.NoError(t, UpdateGroups(inst, msg2))
   193  
   194  		s = &Sharing{}
   195  		require.NoError(t, couchdb.GetDoc(inst, consts.Sharings, sid, s))
   196  
   197  		require.Len(t, s.Members, 4)
   198  		require.Equal(t, s.Members[0].Name, "Alice")
   199  		require.Equal(t, s.Members[1].Name, "Bob")
   200  		assert.True(t, s.Members[1].OnlyInGroups)
   201  		assert.Equal(t, s.Members[1].Groups, []int{0})
   202  		require.Equal(t, s.Members[2].Name, "Charlie")
   203  		assert.True(t, s.Members[2].OnlyInGroups)
   204  		assert.Equal(t, s.Members[2].Groups, []int{0})
   205  		require.Equal(t, s.Members[3].Name, "Dave")
   206  		assert.True(t, s.Members[3].OnlyInGroups)
   207  		assert.Equal(t, s.Members[3].Groups, []int{1})
   208  
   209  		// Email address is added for Dave, and an invitation can now be sent
   210  		addEmailToContact(t, inst, dave)
   211  		msg3 := job.ShareGroupMessage{
   212  			ContactID:       dave.ID(),
   213  			BecomeInvitable: true,
   214  		}
   215  		require.NoError(t, UpdateGroups(inst, msg3))
   216  
   217  		s = &Sharing{}
   218  		require.NoError(t, couchdb.GetDoc(inst, consts.Sharings, sid, s))
   219  
   220  		require.Len(t, s.Members, 4)
   221  		require.Equal(t, s.Members[0].Name, "Alice")
   222  		require.Equal(t, s.Members[1].Name, "Bob")
   223  		assert.True(t, s.Members[1].OnlyInGroups)
   224  		assert.Equal(t, s.Members[1].Groups, []int{0})
   225  		assert.Equal(t, s.Members[1].Status, MemberStatusPendingInvitation)
   226  		require.Equal(t, s.Members[2].Name, "Charlie")
   227  		assert.True(t, s.Members[2].OnlyInGroups)
   228  		assert.Equal(t, s.Members[2].Groups, []int{0})
   229  		assert.Equal(t, s.Members[1].Status, MemberStatusPendingInvitation)
   230  		require.Equal(t, s.Members[3].Name, "Dave")
   231  		assert.True(t, s.Members[3].OnlyInGroups)
   232  		assert.Equal(t, s.Members[3].Groups, []int{1})
   233  		assert.Equal(t, s.Members[3].Status, MemberStatusPendingInvitation)
   234  	})
   235  }
   236  
   237  func createGroup(t *testing.T, inst *instance.Instance, name string) *contact.Group {
   238  	t.Helper()
   239  	g := contact.NewGroup()
   240  	g.M["name"] = name
   241  	require.NoError(t, couchdb.CreateDoc(inst, g))
   242  	return g
   243  }
   244  
   245  func createContactInGroups(t *testing.T, inst *instance.Instance, contactName string, groupIDs []string) *contact.Contact {
   246  	t.Helper()
   247  	email := strings.ToLower(contactName) + "@cozy.tools"
   248  	mail := map[string]interface{}{"address": email}
   249  
   250  	var groups []interface{}
   251  	for _, id := range groupIDs {
   252  		groups = append(groups, map[string]interface{}{
   253  			"_id":   id,
   254  			"_type": consts.Groups,
   255  		})
   256  	}
   257  
   258  	c := contact.New()
   259  	c.M["fullname"] = contactName
   260  	c.M["email"] = []interface{}{mail}
   261  	c.M["relationships"] = map[string]interface{}{
   262  		"groups": map[string]interface{}{"data": groups},
   263  	}
   264  	require.NoError(t, couchdb.CreateDoc(inst, c))
   265  	return c
   266  }
   267  
   268  func createContactWithoutEmail(t *testing.T, inst *instance.Instance, contactName string, groupIDs []string) *contact.Contact {
   269  	t.Helper()
   270  
   271  	var groups []interface{}
   272  	for _, id := range groupIDs {
   273  		groups = append(groups, map[string]interface{}{
   274  			"_id":   id,
   275  			"_type": consts.Groups,
   276  		})
   277  	}
   278  
   279  	c := contact.New()
   280  	c.M["fullname"] = contactName
   281  	c.M["relationships"] = map[string]interface{}{
   282  		"groups": map[string]interface{}{"data": groups},
   283  	}
   284  	require.NoError(t, couchdb.CreateDoc(inst, c))
   285  	return c
   286  }
   287  
   288  func addEmailToContact(t *testing.T, inst *instance.Instance, c *contact.Contact) {
   289  	t.Helper()
   290  
   291  	email := strings.ToLower(c.PrimaryName()) + "@cozy.tools"
   292  	mail := map[string]interface{}{"address": email}
   293  	c.M["email"] = []interface{}{mail}
   294  	require.NoError(t, couchdb.UpdateDoc(inst, c))
   295  }