• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vocdoni / saas-backend / 15452033010

04 Jun 2025 08:25PM UTC coverage: 55.908% (+1.3%) from 54.574%
15452033010

Pull #115

github

emmdim
feat: add organization member groups functionality

    - Introduced endpoints for managing organization member groups, including creation, retrieval, updating, and deletion.
    - Implemented MongoDB storage methods for organization member groups, including validation of member IDs.
    - Added types and structures for organization member groups in the database schema.
    - Created tests for organization member groups to ensure functionality and data integrity.
Pull Request #115: Adds orgParticipant Groups Entity and API

411 of 547 new or added lines in 7 files covered. (75.14%)

38 existing lines in 1 file now uncovered.

4736 of 8471 relevant lines covered (55.91%)

24.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.52
/db/org_members_groups.go
1
package db
2

3
import (
4
        "context"
5
        "fmt"
6
        "time"
7

8
        "go.mongodb.org/mongo-driver/bson"
9
        "go.mongodb.org/mongo-driver/bson/primitive"
10
        "go.mongodb.org/mongo-driver/mongo"
11
        "go.vocdoni.io/dvote/log"
12
)
13

14
// OrgMembersGroup returns an organization members group
15
func (ms *MongoStorage) OrganizationMemberGroup(groupID, orgAddress string) (*OrganizationMemberGroup, error) {
39✔
16
        objID, err := primitive.ObjectIDFromHex(groupID)
39✔
17
        if err != nil {
42✔
18
                return nil, ErrInvalidData
3✔
19
        }
3✔
20

21
        filter := bson.M{"_id": objID, "orgAddress": orgAddress}
36✔
22
        // create a context with a timeout
36✔
23
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
36✔
24
        defer cancel()
36✔
25
        // find the organization in the database
36✔
26
        result := ms.orgMemberGroups.FindOne(ctx, filter)
36✔
27
        var group *OrganizationMemberGroup
36✔
28
        if err := result.Decode(&group); err != nil {
43✔
29
                // if the organization doesn't exist return a specific error
7✔
30
                if err == mongo.ErrNoDocuments {
14✔
31
                        return nil, ErrNotFound
7✔
32
                }
7✔
NEW
33
                return nil, err
×
34
        }
35
        return group, nil
29✔
36
}
37

38
// OrgMembersGroupsByOrg returns the list of an organization's members groups
39
func (ms *MongoStorage) OrganizationMemberGroups(orgAddress string) ([]*OrganizationMemberGroup, error) {
7✔
40
        filter := bson.M{"orgAddress": orgAddress}
7✔
41
        // create a context with a timeout
7✔
42
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
7✔
43
        defer cancel()
7✔
44
        // find the organization in the database
7✔
45
        cursor, err := ms.orgMemberGroups.Find(ctx, filter)
7✔
46
        if err != nil {
7✔
NEW
47
                return nil, err
×
NEW
48
        }
×
49
        defer func() {
14✔
50
                if err := cursor.Close(ctx); err != nil {
7✔
NEW
51
                        log.Warnw("error closing cursor", "error", err)
×
NEW
52
                }
×
53
        }()
54

55
        var groups []*OrganizationMemberGroup
7✔
56
        for cursor.Next(ctx) {
21✔
57
                var group OrganizationMemberGroup
14✔
58
                if err := cursor.Decode(&group); err != nil {
14✔
NEW
59
                        return nil, err
×
NEW
60
                }
×
61
                groups = append(groups, &group)
14✔
62
        }
63

64
        if err := cursor.Err(); err != nil {
7✔
NEW
65
                return nil, err
×
NEW
66
        }
×
67

68
        return groups, nil
7✔
69
}
70

71
// CreateOrganizationGroup Creates an organization member group
72
func (ms *MongoStorage) CreateOrganizationMembersGroup(group *OrganizationMemberGroup) (string, error) {
26✔
73
        // create a context with a timeout
26✔
74
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
26✔
75
        defer cancel()
26✔
76

26✔
77
        if group == nil || group.OrgAddress == "" || len(group.MemberIDs) == 0 {
26✔
NEW
78
                return "", ErrInvalidData
×
NEW
79
        }
×
80

81
        // check that the organization exists
82
        if _, err := ms.fetchOrganizationFromDB(ctx, group.OrgAddress); err != nil {
27✔
83
                if err == ErrNotFound {
2✔
84
                        return "", ErrInvalidData
1✔
85
                }
1✔
NEW
86
                return "", fmt.Errorf("organization not found: %w", err)
×
87
        }
88
        // check that the members are valid
89
        err := ms.validateOrgMembers(ctx, group.OrgAddress, group.MemberIDs)
25✔
90
        if err != nil {
27✔
91
                return "", err
2✔
92
        }
2✔
93
        // create the group id
94
        group.ID = primitive.NewObjectID()
23✔
95
        group.CreatedAt = time.Now()
23✔
96
        group.UpdatedAt = time.Now()
23✔
97
        group.CensusIDs = make([]string, 0)
23✔
98

23✔
99
        // Only lock the mutex during the actual database operations
23✔
100
        ms.keysLock.Lock()
23✔
101
        defer ms.keysLock.Unlock()
23✔
102

23✔
103
        // insert the group into the database
23✔
104
        if _, err := ms.orgMemberGroups.InsertOne(ctx, *group); err != nil {
23✔
NEW
105
                return "", fmt.Errorf("could not create organization members group: %w", err)
×
NEW
106
        }
×
107
        return group.ID.Hex(), nil
23✔
108
}
109

110
// UpdateOrganizationMembersGroup updates an organization members group by adding
111
// and/or removing members. If a member exists in both lists, it will be removed
112
// TODO allow to update the rest of the fields as well. Maybe a different function?
113
func (ms *MongoStorage) UpdateOrganizationMembersGroup(
114
        groupID, orgAddress string,
115
        title, description string, addedMembers, removedMembers []string,
116
) error {
9✔
117
        group, err := ms.OrganizationMemberGroup(groupID, orgAddress)
9✔
118
        if err != nil {
11✔
119
                if err == ErrNotFound {
3✔
120
                        return ErrInvalidData
1✔
121
                }
1✔
122
                return fmt.Errorf("could not retrieve organization members group: %w", err)
1✔
123
        }
124
        // create a context with a timeout
125
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
7✔
126
        defer cancel()
7✔
127
        objID, err := primitive.ObjectIDFromHex(groupID)
7✔
128
        if err != nil {
7✔
NEW
129
                return err
×
NEW
130
        }
×
131

132
        // check that the addedMembers contains valid IDs from the orgMembers collection
133
        if len(addedMembers) > 0 {
10✔
134
                err = ms.validateOrgMembers(ctx, group.OrgAddress, addedMembers)
3✔
135
                if err != nil {
4✔
136
                        return err
1✔
137
                }
1✔
138
        }
139

140
        filter := bson.M{"_id": objID, "orgAddress": orgAddress}
6✔
141

6✔
142
        // Only lock the mutex during the actual database operations
6✔
143
        ms.keysLock.Lock()
6✔
144
        defer ms.keysLock.Unlock()
6✔
145

6✔
146
        // First, update metadata if needed
6✔
147
        if title != "" || description != "" {
10✔
148
                updateFields := bson.M{}
4✔
149
                if title != "" {
8✔
150
                        updateFields["title"] = title
4✔
151
                }
4✔
152
                if description != "" {
8✔
153
                        updateFields["description"] = description
4✔
154
                }
4✔
155
                updateFields["updatedAt"] = time.Now()
4✔
156

4✔
157
                metadataUpdate := bson.D{{Key: "$set", Value: updateFields}}
4✔
158
                _, err = ms.orgMemberGroups.UpdateOne(ctx, filter, metadataUpdate)
4✔
159
                if err != nil {
4✔
NEW
160
                        return err
×
NEW
161
                }
×
162
        }
163

164
        // Get the updated group to ensure we have the latest state
165
        updatedGroup, err := ms.OrganizationMemberGroup(groupID, orgAddress)
6✔
166
        if err != nil {
6✔
NEW
167
                return err
×
NEW
168
        }
×
169

170
        // Now handle member updates if needed
171
        if len(addedMembers) > 0 || len(removedMembers) > 0 {
10✔
172
                // Calculate the final list of members
4✔
173
                finalMembers := make([]string, 0, len(updatedGroup.MemberIDs)+len(addedMembers))
4✔
174

4✔
175
                // Add existing members that aren't in the removedMembers list
4✔
176
                for _, id := range updatedGroup.MemberIDs {
13✔
177
                        if !contains(removedMembers, id) {
15✔
178
                                finalMembers = append(finalMembers, id)
6✔
179
                        }
6✔
180
                }
181

182
                // Add new members that aren't already in the list
183
                for _, id := range addedMembers {
7✔
184
                        if !contains(finalMembers, id) {
6✔
185
                                finalMembers = append(finalMembers, id)
3✔
186
                        }
3✔
187
                }
188

189
                // Update the member list
190
                memberUpdate := bson.D{{Key: "$set", Value: bson.M{
4✔
191
                        "memberIds": finalMembers,
4✔
192
                        "updatedAt": time.Now(),
4✔
193
                }}}
4✔
194

4✔
195
                _, err = ms.orgMemberGroups.UpdateOne(ctx, filter, memberUpdate)
4✔
196
                if err != nil {
4✔
NEW
197
                        return err
×
NEW
198
                }
×
199
        }
200

201
        return nil
6✔
202
}
203

204
// DeleteOrganizationMemberGroup deletes an organization member group by its ID
205
func (ms *MongoStorage) DeleteOrganizationMemberGroup(groupID, orgAddress string) error {
6✔
206
        // create a context with a timeout
6✔
207
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
6✔
208
        defer cancel()
6✔
209

6✔
210
        objID, err := primitive.ObjectIDFromHex(groupID)
6✔
211
        if err != nil {
7✔
212
                return fmt.Errorf("invalid group ID: %w", err)
1✔
213
        }
1✔
214

215
        // Only lock the mutex during the actual database operations
216
        ms.keysLock.Lock()
5✔
217
        defer ms.keysLock.Unlock()
5✔
218

5✔
219
        // delete the group from the database
5✔
220
        filter := bson.M{"_id": objID, "orgAddress": orgAddress}
5✔
221
        if _, err := ms.orgMemberGroups.DeleteOne(ctx, filter); err != nil {
5✔
NEW
222
                return fmt.Errorf("could not delete organization members group: %w", err)
×
NEW
223
        }
×
224
        return nil
5✔
225
}
226

227
// ListOrganizationMemberGroup lists all the members of an organization member group and the total number of members
228
func (ms *MongoStorage) ListOrganizationMemberGroup(
229
        groupID, orgAddress string,
230
        page, pageSize int64,
231
) (int, []*OrgParticipant, error) {
9✔
232
        // get the group
9✔
233
        group, err := ms.OrganizationMemberGroup(groupID, orgAddress)
9✔
234
        if err != nil {
12✔
235
                return 0, nil, fmt.Errorf("could not retrieve organization members group: %w", err)
3✔
236
        }
3✔
237

238
        return ms.orgMembersByIDs(
6✔
239
                orgAddress,
6✔
240
                group.MemberIDs,
6✔
241
                page,
6✔
242
                pageSize,
6✔
243
        )
6✔
244
}
245

246
// Helper function to check if a string is in a slice
247
func contains(slice []string, item string) bool {
12✔
248
        for _, s := range slice {
26✔
249
                if s == item {
17✔
250
                        return true
3✔
251
                }
3✔
252
        }
253
        return false
9✔
254
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc