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

vocdoni / saas-backend / 16301338771

15 Jul 2025 06:29PM UTC coverage: 56.793% (+0.7%) from 56.082%
16301338771

Pull #165

github

emmdim
refactor:  census creation

- Removed the PublishedCensus type and added it as Census parameter.
- Introduced new OrgMemberAuthFields defining the data options for member authentication.
- Added the `CheckOrgMemberAuthFields` function that checks a set of members and given auth fields empties an
d duplicates
- Add the option to create a census through the api based on a given group
Pull Request #165: Implements group based census creation

250 of 399 new or added lines in 9 files covered. (62.66%)

4 existing lines in 3 files now uncovered.

5104 of 8987 relevant lines covered (56.79%)

25.32 hits per line

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

87.94
/db/org_members_groups.go
1
package db
2

3
import (
4
        "context"
5
        "fmt"
6
        "math"
7
        "strings"
8
        "time"
9

10
        "go.mongodb.org/mongo-driver/bson"
11
        "go.mongodb.org/mongo-driver/bson/primitive"
12
        "go.mongodb.org/mongo-driver/mongo"
13
        "go.mongodb.org/mongo-driver/mongo/options"
14
        "go.vocdoni.io/dvote/log"
15
)
16

17
// OrgMembersGroup returns an organization members group
18
func (ms *MongoStorage) OrganizationMemberGroup(groupID, orgAddress string) (*OrganizationMemberGroup, error) {
62✔
19
        objID, err := primitive.ObjectIDFromHex(groupID)
62✔
20
        if err != nil {
67✔
21
                return nil, ErrInvalidData
5✔
22
        }
5✔
23

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

41
// OrganizationMemberGroups returns the list of an organization's members groups
42
func (ms *MongoStorage) OrganizationMemberGroups(
43
        orgAddress string,
44
        page, pageSize int,
45
) (int, []*OrganizationMemberGroup, error) {
7✔
46
        // create a context with a timeout
7✔
47
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
7✔
48
        defer cancel()
7✔
49

7✔
50
        filter := bson.M{"orgAddress": orgAddress}
7✔
51

7✔
52
        // Count total documents
7✔
53
        totalCount, err := ms.orgMemberGroups.CountDocuments(ctx, filter)
7✔
54
        if err != nil {
7✔
55
                return 0, nil, err
×
56
        }
×
57
        totalPages := int(math.Ceil(float64(totalCount) / float64(pageSize)))
7✔
58

7✔
59
        // Calculate skip value based on page and pageSize
7✔
60
        skip := (page - 1) * pageSize
7✔
61

7✔
62
        // Set up options for pagination
7✔
63
        findOptions := options.Find().
7✔
64
                SetSkip(int64(skip)).
7✔
65
                SetLimit(int64(pageSize))
7✔
66

7✔
67
        // find the organization in the database
7✔
68
        cursor, err := ms.orgMemberGroups.Find(ctx, filter, findOptions)
7✔
69
        if err != nil {
7✔
70
                return 0, nil, err
×
71
        }
×
72
        defer func() {
14✔
73
                if err := cursor.Close(ctx); err != nil {
7✔
74
                        log.Warnw("error closing cursor", "error", err)
×
75
                }
×
76
        }()
77

78
        var groups []*OrganizationMemberGroup
7✔
79
        for cursor.Next(ctx) {
21✔
80
                var group OrganizationMemberGroup
14✔
81
                if err := cursor.Decode(&group); err != nil {
14✔
82
                        return 0, nil, err
×
83
                }
×
84
                groups = append(groups, &group)
14✔
85
        }
86

87
        if err := cursor.Err(); err != nil {
7✔
88
                return 0, nil, err
×
89
        }
×
90

91
        return totalPages, groups, nil
7✔
92
}
93

94
// CreateOrganizationMemberGroup Creates an organization member group
95
func (ms *MongoStorage) CreateOrganizationMemberGroup(group *OrganizationMemberGroup) (string, error) {
38✔
96
        // create a context with a timeout
38✔
97
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
38✔
98
        defer cancel()
38✔
99

38✔
100
        if group == nil || group.OrgAddress == "" || len(group.MemberIDs) == 0 {
38✔
101
                return "", ErrInvalidData
×
102
        }
×
103

104
        // check that the organization exists
105
        if _, err := ms.fetchOrganizationFromDB(ctx, group.OrgAddress); err != nil {
39✔
106
                if err == ErrNotFound {
2✔
107
                        return "", ErrInvalidData
1✔
108
                }
1✔
109
                return "", fmt.Errorf("organization not found: %w", err)
×
110
        }
111
        // check that the members are valid
112
        err := ms.validateOrgMembers(ctx, group.OrgAddress, group.MemberIDs)
37✔
113
        if err != nil {
39✔
114
                return "", err
2✔
115
        }
2✔
116
        // create the group id
117
        group.ID = primitive.NewObjectID()
35✔
118
        group.CreatedAt = time.Now()
35✔
119
        group.UpdatedAt = time.Now()
35✔
120
        group.CensusIDs = make([]string, 0)
35✔
121

35✔
122
        // Only lock the mutex during the actual database operations
35✔
123
        ms.keysLock.Lock()
35✔
124
        defer ms.keysLock.Unlock()
35✔
125

35✔
126
        // insert the group into the database
35✔
127
        if _, err := ms.orgMemberGroups.InsertOne(ctx, *group); err != nil {
35✔
128
                return "", fmt.Errorf("could not create organization members group: %w", err)
×
129
        }
×
130
        return group.ID.Hex(), nil
35✔
131
}
132

133
// UpdateOrganizationMemberGroup updates an organization members group by adding
134
// and/or removing members. If a member exists in both lists, it will be removed
135
// TODO allow to update the rest of the fields as well. Maybe a different function?
136
func (ms *MongoStorage) UpdateOrganizationMemberGroup(
137
        groupID, orgAddress string,
138
        title, description string, addedMembers, removedMembers []string,
139
) error {
9✔
140
        group, err := ms.OrganizationMemberGroup(groupID, orgAddress)
9✔
141
        if err != nil {
11✔
142
                if err == ErrNotFound {
3✔
143
                        return ErrInvalidData
1✔
144
                }
1✔
145
                return fmt.Errorf("could not retrieve organization members group: %w", err)
1✔
146
        }
147
        // create a context with a timeout
148
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
7✔
149
        defer cancel()
7✔
150
        objID, err := primitive.ObjectIDFromHex(groupID)
7✔
151
        if err != nil {
7✔
152
                return err
×
153
        }
×
154

155
        // check that the addedMembers contains valid IDs from the orgMembers collection
156
        if len(addedMembers) > 0 {
10✔
157
                err = ms.validateOrgMembers(ctx, group.OrgAddress, addedMembers)
3✔
158
                if err != nil {
4✔
159
                        return err
1✔
160
                }
1✔
161
        }
162

163
        filter := bson.M{"_id": objID, "orgAddress": orgAddress}
6✔
164

6✔
165
        // Only lock the mutex during the actual database operations
6✔
166
        ms.keysLock.Lock()
6✔
167
        defer ms.keysLock.Unlock()
6✔
168

6✔
169
        // First, update metadata if needed
6✔
170
        if title != "" || description != "" {
10✔
171
                updateFields := bson.M{}
4✔
172
                if title != "" {
8✔
173
                        updateFields["title"] = title
4✔
174
                }
4✔
175
                if description != "" {
8✔
176
                        updateFields["description"] = description
4✔
177
                }
4✔
178
                updateFields["updatedAt"] = time.Now()
4✔
179

4✔
180
                metadataUpdate := bson.D{{Key: "$set", Value: updateFields}}
4✔
181
                _, err = ms.orgMemberGroups.UpdateOne(ctx, filter, metadataUpdate)
4✔
182
                if err != nil {
4✔
183
                        return err
×
184
                }
×
185
        }
186

187
        // Get the updated group to ensure we have the latest state
188
        updatedGroup, err := ms.OrganizationMemberGroup(groupID, orgAddress)
6✔
189
        if err != nil {
6✔
190
                return err
×
191
        }
×
192

193
        // Now handle member updates if needed
194
        if len(addedMembers) > 0 || len(removedMembers) > 0 {
10✔
195
                // Calculate the final list of members
4✔
196
                finalMembers := make([]string, 0, len(updatedGroup.MemberIDs)+len(addedMembers))
4✔
197

4✔
198
                // Add existing members that aren't in the removedMembers list
4✔
199
                for _, id := range updatedGroup.MemberIDs {
13✔
200
                        if !contains(removedMembers, id) {
15✔
201
                                finalMembers = append(finalMembers, id)
6✔
202
                        }
6✔
203
                }
204

205
                // Add new members that aren't already in the list
206
                for _, id := range addedMembers {
7✔
207
                        if !contains(finalMembers, id) {
6✔
208
                                finalMembers = append(finalMembers, id)
3✔
209
                        }
3✔
210
                }
211

212
                // Update the member list
213
                memberUpdate := bson.D{{Key: "$set", Value: bson.M{
4✔
214
                        "memberIds": finalMembers,
4✔
215
                        "updatedAt": time.Now(),
4✔
216
                }}}
4✔
217

4✔
218
                _, err = ms.orgMemberGroups.UpdateOne(ctx, filter, memberUpdate)
4✔
219
                if err != nil {
4✔
220
                        return err
×
221
                }
×
222
        }
223

224
        return nil
6✔
225
}
226

227
// DeleteOrganizationMemberGroup deletes an organization member group by its ID
228
func (ms *MongoStorage) DeleteOrganizationMemberGroup(groupID, orgAddress string) error {
6✔
229
        // create a context with a timeout
6✔
230
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
6✔
231
        defer cancel()
6✔
232

6✔
233
        objID, err := primitive.ObjectIDFromHex(groupID)
6✔
234
        if err != nil {
7✔
235
                return fmt.Errorf("invalid group ID: %w", err)
1✔
236
        }
1✔
237

238
        // Only lock the mutex during the actual database operations
239
        ms.keysLock.Lock()
5✔
240
        defer ms.keysLock.Unlock()
5✔
241

5✔
242
        // delete the group from the database
5✔
243
        filter := bson.M{"_id": objID, "orgAddress": orgAddress}
5✔
244
        if _, err := ms.orgMemberGroups.DeleteOne(ctx, filter); err != nil {
5✔
245
                return fmt.Errorf("could not delete organization members group: %w", err)
×
246
        }
×
247
        return nil
5✔
248
}
249

250
// ListOrganizationMemberGroup lists all the members of an organization member group and the total number of members
251
func (ms *MongoStorage) ListOrganizationMemberGroup(
252
        groupID, orgAddress string,
253
        page, pageSize int64,
254
) (int, []*OrgMember, error) {
9✔
255
        // get the group
9✔
256
        group, err := ms.OrganizationMemberGroup(groupID, orgAddress)
9✔
257
        if err != nil {
12✔
258
                return 0, nil, fmt.Errorf("could not retrieve organization members group: %w", err)
3✔
259
        }
3✔
260

261
        return ms.orgMembersByIDs(
6✔
262
                orgAddress,
6✔
263
                group.MemberIDs,
6✔
264
                page,
6✔
265
                pageSize,
6✔
266
        )
6✔
267
}
268

269
// CheckOrgMemberAuthFields checks if the provided orgFields are valid for authentication
270
// Checks the entire member base of an organization creating a projection that contains only
271
// the provided auth fields and verifies that the resulting data do not have duplicates or
272
// missing fields. Returns the corrsponding informative errors concerning duplicates or columns with empty values
273
// The authFields are checked for empties and duplicates while the twoFaFields are only checked for empties
274
func (ms *MongoStorage) CheckGroupMembersFields(
275
        orgAddress string,
276
        groupID string,
277
        authFields OrgMemberAuthFields,
278
        twoFaFields OrgMemberTwoFaFields,
279
) (*OrgMemberAggregationResults, error) {
19✔
280
        if len(orgAddress) == 0 {
20✔
281
                return nil, ErrInvalidData
1✔
282
        }
1✔
283
        if len(authFields) == 0 && len(twoFaFields) == 0 {
19✔
284
                return nil, fmt.Errorf("no auth or twoFa fields provided")
1✔
285
        }
1✔
286

287
        // group, err := ms.OrganizationMemberGroup(groupID, orgAddress)
288
        // if err != nil {
289
        //         if err == ErrNotFound {
290
        //                 return nil, fmt.Errorf("group %s not found for organization %s: %w", groupID, orgAddress, ErrInvalidData)
291
        //         }
292
        //         return nil, fmt.Errorf("failed to fetch group %s for organization %s: %w", groupID, orgAddress, err)
293
        // }
294
        // // Check if the group has members
295
        // if len(group.MemberIDs) == 0 {
296
        //         return nil, fmt.Errorf("no members in group %s for organization %s", groupID, orgAddress)
297
        // }
298
        // objectIDs := make([]primitive.ObjectID, len(group.MemberIDs))
299
        // for i, id := range group.MemberIDs {
300
        //         objID, err := primitive.ObjectIDFromHex(id)
301
        //         if err != nil {
302
        //                 return nil, fmt.Errorf("invalid member ID %s: %w", id, ErrInvalidData)
303
        //         }
304
        //         objectIDs[i] = objID
305
        // }
306

307
        // create a context with a timeout
308

309
        // 1) Build find filter and projection
310
        // filter := bson.D{
311
        //         {Key: "orgAddress", Value: orgAddress},
312
        //         {Key: "_id", Value: bson.M{"$in": objectIDs}},
313
        // }
314
        // proj := bson.D{
315
        //         {Key: "_id", Value: 1},
316
        //         {Key: orgAddress, Value: 1},
317
        // }
318
        // for _, f := range orgFields {
319
        //         proj = append(proj, bson.E{Key: string(f), Value: 1})
320
        // }
321
        // findOpts := options.Find().SetProjection(proj)
322

323
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
17✔
324
        defer cancel()
17✔
325

17✔
326
        // 2) Fetch all matching docs
17✔
327
        cur, err := ms.getGroupMembersFields(ctx, orgAddress, groupID, authFields, twoFaFields)
17✔
328
        // ms.orgMembers.Find(ctx, filter, findOpts)
17✔
329
        if err != nil {
20✔
330
                return nil, err
3✔
331
        }
3✔
332
        defer func() {
28✔
333
                if err := cur.Close(ctx); err != nil {
14✔
NEW
334
                        log.Warnw("error closing cursor", "error", err)
×
NEW
335
                }
×
336
        }()
337

338
        results := OrgMemberAggregationResults{
14✔
339
                Members:    make([]primitive.ObjectID, 0),
14✔
340
                Duplicates: make([]primitive.ObjectID, 0),
14✔
341
                Empties:    make([]primitive.ObjectID, 0),
14✔
342
        }
14✔
343

14✔
344
        seenKeys := make(map[string]primitive.ObjectID, 50000)
14✔
345

14✔
346
        // 4) Iterate and detect
14✔
347
        for cur.Next(ctx) {
43✔
348
                // decode into a map so we can handle dynamic fields
29✔
349
                var m OrgMember
29✔
350
                var bm bson.M
29✔
351
                if err := cur.Decode(&m); err != nil {
29✔
NEW
352
                        return nil, err
×
NEW
353
                }
×
354
                if err := cur.Decode(&bm); err != nil {
29✔
NEW
355
                        return nil, err
×
NEW
356
                }
×
357

358
                // build composite key & check for empty rows
359
                keyParts := make([]string, len(authFields))
29✔
360
                rowEmpty := false
29✔
361
                for i, f := range authFields {
60✔
362
                        rawVal := bm[string(f)]
31✔
363
                        s := fmt.Sprint(rawVal)
31✔
364
                        if rawVal == nil || s == "" {
34✔
365
                                rowEmpty = true
3✔
366
                                break
3✔
367
                        }
368
                        keyParts[i] = s
28✔
369
                }
370
                for _, f := range twoFaFields {
42✔
371
                        rawVal := bm[string(f)]
13✔
372
                        s := fmt.Sprint(rawVal)
13✔
373
                        if rawVal == nil || s == "" {
16✔
374
                                rowEmpty = true
3✔
375
                                break
3✔
376
                        }
377
                }
378
                if rowEmpty {
35✔
379
                        // if any of the fields are empty, add to empties
6✔
380
                        // and continue to the next member
6✔
381
                        // we do not check for duplicates in empty rows
6✔
382
                        results.Empties = append(results.Empties, m.ID)
6✔
383
                        continue
6✔
384
                }
385

386
                key := strings.Join(keyParts, "|")
23✔
387
                if val, seen := seenKeys[key]; seen {
31✔
388
                        // if the key is already seen, add to duplicates
8✔
389
                        // and continue to the next member
8✔
390
                        results.Duplicates = append(results.Duplicates, m.ID)
8✔
391
                        results.Duplicates = append(results.Duplicates, val)
8✔
392
                        continue
8✔
393
                } else {
15✔
394
                        // neither empty nor duplicate, so we add it to the seen keys
15✔
395
                        seenKeys[key] = m.ID
15✔
396
                        // append the member ID to the results
15✔
397
                        results.Members = append(results.Members, m.ID)
15✔
398
                }
15✔
399
        }
400
        if err := cur.Err(); err != nil {
14✔
NEW
401
                return nil, err
×
NEW
402
        }
×
403

404
        return &results, nil
14✔
405
}
406

407
// getGroupMembersAuthFields creates a projection of a set of members that
408
// contains only the chosen AuthFields
409
func (ms *MongoStorage) getGroupMembersFields(
410
        ctx context.Context,
411
        orgAddress string,
412
        groupID string,
413
        authFields OrgMemberAuthFields,
414
        twoFaFields OrgMemberTwoFaFields,
415
) (*mongo.Cursor, error) {
17✔
416
        // 1) Build find filter and projection
17✔
417
        filter := bson.D{
17✔
418
                {Key: "orgAddress", Value: orgAddress},
17✔
419
        }
17✔
420
        // in case a groupID is provided, fetch the group and its members and
17✔
421
        // extend the filter to include only those members
17✔
422
        if len(groupID) > 0 {
31✔
423

14✔
424
                group, err := ms.OrganizationMemberGroup(groupID, orgAddress)
14✔
425
                if err != nil {
17✔
426
                        if err == ErrNotFound {
5✔
427
                                return nil, fmt.Errorf("group %s not found for organization %s: %w", groupID, orgAddress, ErrInvalidData)
2✔
428
                        }
2✔
429
                        return nil, fmt.Errorf("failed to fetch group %s for organization %s: %w", groupID, orgAddress, err)
1✔
430
                }
431
                // Check if the group has members
432
                if len(group.MemberIDs) == 0 {
11✔
NEW
433
                        return nil, fmt.Errorf("no members in group %s for organization %s", groupID, orgAddress)
×
NEW
434
                }
×
435
                objectIDs := make([]primitive.ObjectID, len(group.MemberIDs))
11✔
436
                for i, id := range group.MemberIDs {
40✔
437
                        objID, err := primitive.ObjectIDFromHex(id)
29✔
438
                        if err != nil {
29✔
NEW
439
                                return nil, fmt.Errorf("invalid member ID %s: %w", id, ErrInvalidData)
×
NEW
440
                        }
×
441
                        objectIDs[i] = objID
29✔
442
                }
443
                if len(objectIDs) > 0 {
22✔
444
                        filter = append(filter, bson.E{Key: "_id", Value: bson.M{"$in": objectIDs}})
11✔
445
                }
11✔
446
        }
447

448
        proj := bson.D{
14✔
449
                {Key: "_id", Value: 1},
14✔
450
                {Key: orgAddress, Value: 1},
14✔
451
        }
14✔
452

14✔
453
        for _, f := range authFields {
29✔
454
                proj = append(proj, bson.E{Key: string(f), Value: 1})
15✔
455
        }
15✔
456
        for _, f := range twoFaFields {
21✔
457
                proj = append(proj, bson.E{Key: string(f), Value: 1})
7✔
458
        }
7✔
459

460
        findOpts := options.Find().SetProjection(proj)
14✔
461

14✔
462
        // 2) Fetch all matching docs
14✔
463
        return ms.orgMembers.Find(ctx, filter, findOpts)
14✔
464
}
465

466
// Helper function to check if a string is in a slice
467
func contains(slice []string, item string) bool {
16✔
468
        for _, s := range slice {
35✔
469
                if s == item {
26✔
470
                        return true
7✔
471
                }
7✔
472
        }
473
        return false
9✔
474
}
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