• 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

17.56
/db/mongo.go
1
package db
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "fmt"
7
        "os"
8
        "strings"
9
        "sync"
10
        "time"
11

12
        "go.mongodb.org/mongo-driver/bson"
13
        "go.mongodb.org/mongo-driver/mongo"
14
        "go.mongodb.org/mongo-driver/mongo/options"
15
        "go.mongodb.org/mongo-driver/mongo/readpref"
16
        "go.mongodb.org/mongo-driver/x/mongo/driver/connstring"
17
        "go.vocdoni.io/dvote/log"
18
)
19

20
const (
21
        // connectTimeout is used for connection timeout
22
        connectTimeout = 10 * time.Second
23
        // defaultTimeout is used for simple operations (FindOne, UpdateOne, DeleteOne)
24
        defaultTimeout = 10 * time.Second
25
        // batchTimeout is used for batch operations (BulkWrite)
26
        batchTimeout = 20 * time.Second
27
        // exportTimeout is used for export/import operations (String, Import)
28
        exportTimeout = 30 * time.Second
29
)
30

31
// MongoStorage uses an external MongoDB service for stoting the user data and election details.
32
type MongoStorage struct {
33
        database    string
34
        DBClient    *mongo.Client
35
        keysLock    sync.RWMutex
36
        stripePlans []*Plan
37

38
        users               *mongo.Collection
39
        verifications       *mongo.Collection
40
        organizations       *mongo.Collection
41
        organizationInvites *mongo.Collection
42
        plans               *mongo.Collection
43
        objects             *mongo.Collection
44
        orgParticipants     *mongo.Collection
45
        orgMemberGroups     *mongo.Collection
46
        censusMemberships   *mongo.Collection
47
        censuses            *mongo.Collection
48
        publishedCensuses   *mongo.Collection
49
        processes           *mongo.Collection
50
        processBundles      *mongo.Collection
51
        cspTokens           *mongo.Collection
52
        cspTokensStatus     *mongo.Collection
53
}
54

55
type Options struct {
56
        MongoURL string
57
        Database string
58
}
59

60
func New(url, database string, plans []*Plan) (*MongoStorage, error) {
8✔
61
        var err error
8✔
62
        ms := &MongoStorage{}
8✔
63
        if url == "" {
8✔
64
                return nil, fmt.Errorf("mongo URL is not defined")
×
65
        }
×
66
        cs, err := connstring.ParseAndValidate(url)
8✔
67
        if err != nil {
8✔
68
                return nil, fmt.Errorf("cannot parse the connection string: %w", err)
×
69
        }
×
70
        // set the database name if it is not empty, if it is empty, try to parse it
71
        // from the URL
72
        switch {
8✔
73
        case cs.Database == "" && database == "":
×
74
                return nil, fmt.Errorf("database name is not defined")
×
75
        case database != "":
8✔
76
                cs.Database = database
8✔
77
                ms.database = database
8✔
78
        default:
×
79
                ms.database = cs.Database
×
80
        }
81
        // if the auth source is not set, set it to admin (append the param or
82
        // create it if no other params are present)
83
        if !cs.AuthSourceSet {
16✔
84
                var sb strings.Builder
8✔
85
                params := "authSource=admin"
8✔
86
                sb.WriteString(url)
8✔
87
                if strings.Contains(url, "?") {
8✔
88
                        sb.WriteString("&")
×
89
                } else if strings.HasSuffix(url, "/") {
8✔
90
                        sb.WriteString("?")
×
91
                } else {
8✔
92
                        sb.WriteString("/?")
8✔
93
                }
8✔
94
                sb.WriteString(params)
8✔
95
                url = sb.String()
8✔
96
        }
97
        log.Infow("connecting to mongodb", "url", url)
8✔
98
        // preparing connection
8✔
99
        opts := options.Client()
8✔
100
        opts.ApplyURI(url)
8✔
101
        opts.SetMaxConnecting(200)
8✔
102
        opts.SetConnectTimeout(connectTimeout)
8✔
103
        // create a new client with the connection options
8✔
104
        ctx, cancel := context.WithTimeout(context.Background(), connectTimeout)
8✔
105
        defer cancel()
8✔
106
        client, err := mongo.Connect(ctx, opts)
8✔
107
        if err != nil {
8✔
108
                return nil, fmt.Errorf("cannot connect to mongodb: %w", err)
×
109
        }
×
110
        // check if the connection is successful
111
        ctx, cancel2 := context.WithTimeout(context.Background(), connectTimeout)
8✔
112
        defer cancel2()
8✔
113
        // try to ping the database
8✔
114
        if err = client.Ping(ctx, readpref.Primary()); err != nil {
8✔
115
                return nil, fmt.Errorf("cannot ping to mongodb: %w", err)
×
116
        }
×
117
        // init the database client
118
        ms.DBClient = client
8✔
119
        if len(plans) > 0 {
10✔
120
                ms.stripePlans = plans
2✔
121
        }
2✔
122
        // init the collections
123
        if err := ms.initCollections(ms.database); err != nil {
8✔
124
                return nil, err
×
125
        }
×
126
        // if reset flag is enabled, Reset drops the database documents and recreates indexes
127
        // else, just init collections and create indexes
128
        if reset := os.Getenv("VOCDONI_MONGO_RESET_DB"); reset != "" {
9✔
129
                if err := ms.Reset(); err != nil {
1✔
130
                        return nil, err
×
131
                }
×
132
        } else {
7✔
133
                // create indexes
7✔
134
                if err := ms.createIndexes(); err != nil {
7✔
135
                        return nil, err
×
136
                }
×
137
        }
138
        return ms, nil
8✔
139
}
140

141
func (ms *MongoStorage) Close() {
1✔
142
        ctx, cancel := context.WithTimeout(context.Background(), connectTimeout)
1✔
143
        defer cancel()
1✔
144
        if err := ms.DBClient.Disconnect(ctx); err != nil {
1✔
145
                log.Warnw("disconnect error", "error", err)
×
146
        }
×
147
}
148

149
func (ms *MongoStorage) Reset() error {
89✔
150
        log.Infow("resetting database")
89✔
151
        ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
89✔
152
        defer cancel()
89✔
153

89✔
154
        // Drop all collections
89✔
155
        for _, collectionPtr := range ms.collectionsMap() {
1,424✔
156
                if *collectionPtr != nil {
2,670✔
157
                        if err := (*collectionPtr).Drop(ctx); err != nil {
1,335✔
158
                                return err
×
159
                        }
×
160
                }
161
        }
162
        // init the collections
163
        if err := ms.initCollections(ms.database); err != nil {
89✔
164
                return err
×
165
        }
×
166

167
        // create indexes
168
        return ms.createIndexes()
89✔
169
}
170

171
func (ms *MongoStorage) String() string {
×
172
        ms.keysLock.RLock()
×
173
        defer ms.keysLock.RUnlock()
×
174
        // get all users
×
175
        ctx, cancel := context.WithTimeout(context.Background(), exportTimeout)
×
176
        defer cancel()
×
177
        userCur, err := ms.users.Find(ctx, bson.D{{}})
×
178
        if err != nil {
×
179
                log.Warnw("error decoding user", "error", err)
×
180
                return "{}"
×
181
        }
×
182
        // append all users to the export data
183
        ctx, cancel2 := context.WithTimeout(context.Background(), exportTimeout)
×
184
        defer cancel2()
×
185
        var users UserCollection
×
186
        for userCur.Next(ctx) {
×
187
                var user User
×
188
                err := userCur.Decode(&user)
×
189
                if err != nil {
×
190
                        log.Warnw("error finding users", "error", err)
×
191
                }
×
192
                users.Users = append(users.Users, user)
×
193
        }
194
        // get all user verifications
195
        ctx, cancel3 := context.WithTimeout(context.Background(), exportTimeout)
×
196
        defer cancel3()
×
197
        verCur, err := ms.verifications.Find(ctx, bson.D{{}})
×
198
        if err != nil {
×
199
                log.Warnw("error decoding verification", "error", err)
×
200
                return "{}"
×
201
        }
×
202
        // append all user verifications to the export data
203
        ctx, cancel4 := context.WithTimeout(context.Background(), exportTimeout)
×
204
        defer cancel4()
×
205
        var verifications UserVerifications
×
206
        for verCur.Next(ctx) {
×
207
                var ver UserVerification
×
208
                err := verCur.Decode(&ver)
×
209
                if err != nil {
×
210
                        log.Warnw("error finding verifications", "error", err)
×
211
                }
×
212
                verifications.Verifications = append(verifications.Verifications, ver)
×
213
        }
214
        // get all organizations
215
        ctx, cancel5 := context.WithTimeout(context.Background(), exportTimeout)
×
216
        defer cancel5()
×
217
        orgCur, err := ms.organizations.Find(ctx, bson.D{{}})
×
218
        if err != nil {
×
219
                log.Warnw("error decoding organization", "error", err)
×
220
                return "{}"
×
221
        }
×
222
        // append all organizations to the export data
223
        ctx, cancel6 := context.WithTimeout(context.Background(), exportTimeout)
×
224
        defer cancel6()
×
225
        var organizations OrganizationCollection
×
226
        for orgCur.Next(ctx) {
×
227
                var org Organization
×
228
                err := orgCur.Decode(&org)
×
229
                if err != nil {
×
230
                        log.Warnw("error finding organizations", "error", err)
×
231
                }
×
232
                organizations.Organizations = append(organizations.Organizations, org)
×
233
        }
234

235
        // get all censuses
236
        ctx, cancel7 := context.WithTimeout(context.Background(), exportTimeout)
×
237
        defer cancel7()
×
238
        censusCur, err := ms.censuses.Find(ctx, bson.D{{}})
×
239
        if err != nil {
×
240
                log.Warnw("error decoding census", "error", err)
×
241
                return "{}"
×
242
        }
×
243
        // append all censuses to the export data
244
        ctx, cancel8 := context.WithTimeout(context.Background(), exportTimeout)
×
245
        defer cancel8()
×
246
        var censuses CensusCollection
×
247
        for censusCur.Next(ctx) {
×
248
                var census Census
×
249
                err := censusCur.Decode(&census)
×
250
                if err != nil {
×
251
                        log.Warnw("error finding censuses", "error", err)
×
252
                }
×
253
                censuses.Censuses = append(censuses.Censuses, census)
×
254
        }
255

256
        // get all census participants
257
        ctx, cancel9 := context.WithTimeout(context.Background(), exportTimeout)
×
258
        defer cancel9()
×
259
        censusPartCur, err := ms.orgParticipants.Find(ctx, bson.D{{}})
×
260
        if err != nil {
×
261
                log.Warnw("error decoding census participant", "error", err)
×
262
                return "{}"
×
263
        }
×
264
        // append all census participants to the export data
265
        ctx, cancel10 := context.WithTimeout(context.Background(), exportTimeout)
×
266
        defer cancel10()
×
267
        var orgParticipants OrgParticipantsCollection
×
268
        for censusPartCur.Next(ctx) {
×
269
                var censusPart OrgParticipant
×
270
                err := censusPartCur.Decode(&censusPart)
×
271
                if err != nil {
×
272
                        log.Warnw("error finding census participants", "error", err)
×
273
                }
×
274
                orgParticipants.OrgParticipants = append(orgParticipants.OrgParticipants, censusPart)
×
275
        }
276

277
        // get all census memberships
278
        ctx, cancel11 := context.WithTimeout(context.Background(), exportTimeout)
×
279
        defer cancel11()
×
280
        censusMemCur, err := ms.censusMemberships.Find(ctx, bson.D{{}})
×
281
        if err != nil {
×
282
                log.Warnw("error decoding census membership", "error", err)
×
283
                return "{}"
×
284
        }
×
285
        // append all census memberships to the export data
286
        ctx, cancel12 := context.WithTimeout(context.Background(), exportTimeout)
×
287
        defer cancel12()
×
288
        var censusMemberships CensusMembershipsCollection
×
289
        for censusMemCur.Next(ctx) {
×
290
                var censusMem CensusMembership
×
291
                err := censusMemCur.Decode(&censusMem)
×
292
                if err != nil {
×
293
                        log.Warnw("error finding census memberships", "error", err)
×
294
                }
×
295
                censusMemberships.CensusMemberships = append(censusMemberships.CensusMemberships, censusMem)
×
296
        }
297

298
        // get all published censuses
299
        ctx, cancel13 := context.WithTimeout(context.Background(), exportTimeout)
×
300
        defer cancel13()
×
301
        pubCensusCur, err := ms.publishedCensuses.Find(ctx, bson.D{{}})
×
302
        if err != nil {
×
303
                log.Warnw("error decoding published census", "error", err)
×
304
                return "{}"
×
305
        }
×
306
        // append all published censuses to the export data
307
        ctx, cancel14 := context.WithTimeout(context.Background(), exportTimeout)
×
308
        defer cancel14()
×
309
        var publishedCensuses PublishedCensusesCollection
×
310
        for pubCensusCur.Next(ctx) {
×
311
                var pubCensus PublishedCensus
×
312
                err := pubCensusCur.Decode(&pubCensus)
×
313
                if err != nil {
×
314
                        log.Warnw("error finding published censuses", "error", err)
×
315
                }
×
316
                publishedCensuses.PublishedCensuses = append(publishedCensuses.PublishedCensuses, pubCensus)
×
317
        }
318

319
        // get all processes
320
        ctx, cancel15 := context.WithTimeout(context.Background(), exportTimeout)
×
321
        defer cancel15()
×
322
        processCur, err := ms.processes.Find(ctx, bson.D{{}})
×
323
        if err != nil {
×
324
                log.Warnw("error decoding process", "error", err)
×
325
                return "{}"
×
326
        }
×
327
        // append all processes to the export data
328
        ctx, cancel16 := context.WithTimeout(context.Background(), exportTimeout)
×
329
        defer cancel16()
×
330
        var processes ProcessesCollection
×
331
        for processCur.Next(ctx) {
×
332
                var process Process
×
333
                err := processCur.Decode(&process)
×
334
                if err != nil {
×
335
                        log.Warnw("error finding processes", "error", err)
×
336
                }
×
337
                processes.Processes = append(processes.Processes, process)
×
338
        }
339
        // get all organization invites
340
        ctx, cancel17 := context.WithTimeout(context.Background(), exportTimeout)
×
341
        defer cancel17()
×
342
        invCur, err := ms.organizationInvites.Find(ctx, bson.D{{}})
×
343
        if err != nil {
×
344
                log.Warnw("error decoding organization invite", "error", err)
×
345
                return "{}"
×
346
        }
×
347
        // append all organization invites to the export data
348
        ctx, cancel18 := context.WithTimeout(context.Background(), exportTimeout)
×
349
        defer cancel18()
×
350
        var organizationInvites OrganizationInvitesCollection
×
351
        for invCur.Next(ctx) {
×
352
                var inv OrganizationInvite
×
353
                err := invCur.Decode(&inv)
×
354
                if err != nil {
×
355
                        log.Warnw("error finding organization invites", "error", err)
×
356
                }
×
357
                organizationInvites.OrganizationInvites = append(organizationInvites.OrganizationInvites, inv)
×
358
        }
359

360
        // get all organization groups
NEW
361
        ctx, cancel19 := context.WithTimeout(context.Background(), exportTimeout)
×
NEW
362
        defer cancel19()
×
NEW
363
        orgGroupsCursor, err := ms.orgMemberGroups.Find(ctx, bson.D{{}})
×
NEW
364
        if err != nil {
×
NEW
365
                log.Warnw("error decoding organization groups", "error", err)
×
NEW
366
                return "{}"
×
NEW
367
        }
×
368

369
        // append all organization groups to the export data
NEW
370
        ctx, cancel20 := context.WithTimeout(context.Background(), exportTimeout)
×
NEW
371
        defer cancel20()
×
NEW
372
        var orgGroups OrgMembersGroupsCollection
×
NEW
373
        for orgGroupsCursor.Next(ctx) {
×
NEW
374
                var orgGroup OrganizationMemberGroup
×
NEW
375
                err := orgGroupsCursor.Decode(&orgGroup)
×
NEW
376
                if err != nil {
×
NEW
377
                        log.Warnw("error finding organization groups", "error", err)
×
NEW
378
                }
×
NEW
379
                orgGroups.OrgParticipantsGroups = append(orgGroups.OrgParticipantsGroups, orgGroup)
×
380
        }
381

382
        // encode the data to JSON and return it
383
        data, err := json.Marshal(&Collection{
×
384
                users, verifications, organizations, organizationInvites, censuses,
×
NEW
385
                orgParticipants, orgGroups, censusMemberships, publishedCensuses, processes,
×
386
        })
×
387
        if err != nil {
×
388
                log.Warnw("error marshaling data", "error", err)
×
389
        }
×
390
        return string(data)
×
391
}
392

393
// Import imports a JSON dataset produced by String() into the database.
394
func (ms *MongoStorage) Import(jsonData []byte) error {
×
395
        ms.keysLock.Lock()
×
396
        defer ms.keysLock.Unlock()
×
397
        // decode import data
×
398
        log.Infow("importing database")
×
399
        var collection Collection
×
400
        err := json.Unmarshal(jsonData, &collection)
×
401
        if err != nil {
×
402
                return err
×
403
        }
×
404
        // create global context to import data
405
        ctx, cancel := context.WithTimeout(context.Background(), 4*exportTimeout)
×
406
        defer cancel()
×
407
        // upsert users collection
×
408
        log.Infow("importing users", "count", len(collection.Users))
×
409
        for _, user := range collection.Users {
×
410
                filter := bson.M{"_id": user.ID}
×
411
                update := bson.M{"$set": user}
×
412
                opts := options.Update().SetUpsert(true)
×
413
                _, err := ms.users.UpdateOne(ctx, filter, update, opts)
×
414
                if err != nil {
×
415
                        log.Warnw("error upserting user", "error", err, "user", user.ID)
×
416
                }
×
417
        }
418
        // upsert organizations collection
419
        log.Infow("importing organizations", "count", len(collection.Organizations))
×
420
        for _, org := range collection.Organizations {
×
421
                filter := bson.M{"_id": org.Address}
×
422
                update := bson.M{"$set": org}
×
423
                opts := options.Update().SetUpsert(true)
×
424
                _, err := ms.organizations.UpdateOne(ctx, filter, update, opts)
×
425
                if err != nil {
×
426
                        log.Warnw("error upserting organization", "error", err, "organization", org.Address)
×
427
                }
×
428
        }
429
        log.Infow("imported database")
×
430

×
431
        // upsert censuses collection
×
432
        log.Infow("importing censuses", "count", len(collection.Censuses))
×
433
        for _, census := range collection.Censuses {
×
434
                filter := bson.M{"_id": census.ID}
×
435
                update := bson.M{"$set": census}
×
436
                opts := options.Update().SetUpsert(true)
×
437
                _, err := ms.censuses.UpdateOne(ctx, filter, update, opts)
×
438
                if err != nil {
×
439
                        log.Warnw("error upserting census", "error", err, "census", census.ID)
×
440
                }
×
441
        }
442

443
        // upsert census participants collection
444
        log.Infow("importing census participants", "count", len(collection.OrgParticipants))
×
445
        for _, censusPart := range collection.OrgParticipants {
×
446
                filter := bson.M{"_id": censusPart.ID}
×
447
                update := bson.M{"$set": censusPart}
×
448
                opts := options.Update().SetUpsert(true)
×
449
                _, err := ms.orgParticipants.UpdateOne(ctx, filter, update, opts)
×
450
                if err != nil {
×
451
                        log.Warnw("error upserting census participant", "error", err, "orgParticipant", censusPart.ID)
×
452
                }
×
453
        }
454

455
        // upsert census memberships collection
456
        log.Infow("importing census memberships", "count", len(collection.CensusMemberships))
×
457
        for _, censusMem := range collection.CensusMemberships {
×
458
                filter := bson.M{
×
459
                        "participantNo": censusMem.ParticipantNo,
×
460
                        "censusId":      censusMem.CensusID,
×
461
                }
×
462
                update := bson.M{"$set": censusMem}
×
463
                opts := options.Update().SetUpsert(true)
×
464
                _, err := ms.censusMemberships.UpdateOne(ctx, filter, update, opts)
×
465
                if err != nil {
×
466
                        log.Warnw("error upserting census membership", "error", err, "censusMembership", censusMem.ParticipantNo)
×
467
                }
×
468
        }
469

470
        // upsert published censuses collection
471
        log.Infow("importing published censuses", "count", len(collection.PublishedCensuses))
×
472
        for _, pubCensus := range collection.PublishedCensuses {
×
473
                filter := bson.M{"root": pubCensus.Root, "uri": pubCensus.URI}
×
474
                update := bson.M{"$set": pubCensus}
×
475
                opts := options.Update().SetUpsert(true)
×
476
                _, err := ms.publishedCensuses.UpdateOne(ctx, filter, update, opts)
×
477
                if err != nil {
×
478
                        log.Warnw("error upserting published census", "error", err, "publishedCensus", pubCensus.Root)
×
479
                }
×
480
        }
481

482
        // upsert processes collection
483
        log.Infow("importing processes", "count", len(collection.Processes))
×
484
        for _, process := range collection.Processes {
×
485
                filter := bson.M{"_id": process.ID}
×
486
                update := bson.M{"$set": process}
×
487
                opts := options.Update().SetUpsert(true)
×
488
                _, err := ms.processes.UpdateOne(ctx, filter, update, opts)
×
489
                if err != nil {
×
490
                        log.Warnw("error upserting process", "error", err, "process", process.ID.String())
×
491
                }
×
492
        }
493
        return nil
×
494
}
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