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

vocdoni / saas-backend / 17557469823

08 Sep 2025 04:25PM UTC coverage: 58.777% (-0.06%) from 58.841%
17557469823

Pull #213

github

altergui
fix
Pull Request #213: api: standardize parameters ProcessID, CensusID, GroupID, JobID, UserID, BundleID

254 of 345 new or added lines in 22 files covered. (73.62%)

19 existing lines in 7 files now uncovered.

5652 of 9616 relevant lines covered (58.78%)

32.01 hits per line

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

63.9
/api/census.go
1
package api
2

3
import (
4
        "encoding/json"
5
        "net/http"
6
        "sync"
7
        "time"
8

9
        "github.com/vocdoni/saas-backend/api/apicommon"
10
        "github.com/vocdoni/saas-backend/db"
11
        "github.com/vocdoni/saas-backend/errors"
12
        "github.com/vocdoni/saas-backend/internal"
13
        "go.vocdoni.io/dvote/log"
14
)
15

16
const (
17
        // CensusTypeSMSOrMail is the CSP based type of census that supports both SMS and mail.
18
        CensusTypeSMSOrMail = "sms_or_mail"
19
        CensusTypeMail      = "mail"
20
        CensusTypeSMS       = "sms"
21
)
22

23
// addParticipantsToCensusWorkers is a map of job identifiers to the progress of adding participants to a census.
24
// This is used to check the progress of the job.
25
var addParticipantsToCensusWorkers sync.Map
26

27
// createCensusHandler godoc
28
//
29
//        @Summary                Create a new census
30
//        @Description        Create a new census for an organization. Requires Manager/Admin role.
31
//        @Description        Creates either a regular census or a group-based census if GroupID is provided.
32
//        @Description        Validates that either AuthFields or TwoFaFields are provided and checks for duplicates or empty fields.
33
//        @Tags                        census
34
//        @Accept                        json
35
//        @Produce                json
36
//        @Security                BearerAuth
37
//        @Param                        request        body                apicommon.CreateCensusRequest        true        "Census information"
38
//        @Success                200                {object}        apicommon.CreateCensusResponse        "Returns the created census ID"
39
//        @Failure                400                {object}        errors.Error                                        "Invalid input data or missing required fields"
40
//        @Failure                401                {object}        errors.Error                                        "Unauthorized"
41
//        @Failure                500                {object}        errors.Error                                        "Internal server error"
42
//        @Router                        /census [post]
43
func (a *API) createCensusHandler(w http.ResponseWriter, r *http.Request) {
12✔
44
        // Parse request
12✔
45
        censusInfo := &apicommon.CreateCensusRequest{}
12✔
46
        if err := json.NewDecoder(r.Body).Decode(&censusInfo); err != nil {
12✔
47
                errors.ErrMalformedBody.Write(w)
×
48
                return
×
49
        }
×
50

51
        // get the user from the request context
52
        user, ok := apicommon.UserFromContext(r.Context())
12✔
53
        if !ok {
12✔
54
                errors.ErrUnauthorized.Write(w)
×
55
                return
×
56
        }
×
57

58
        // check the user has the necessary permissions
59
        if !user.HasRoleFor(censusInfo.OrgAddress, db.ManagerRole) && !user.HasRoleFor(censusInfo.OrgAddress, db.AdminRole) {
13✔
60
                errors.ErrUnauthorized.Withf("user does not have the necessary permissions in the organization").Write(w)
1✔
61
                return
1✔
62
        }
1✔
63

64
        census := &db.Census{
11✔
65
                OrgAddress:  censusInfo.OrgAddress,
11✔
66
                AuthFields:  censusInfo.AuthFields,
11✔
67
                TwoFaFields: censusInfo.TwoFaFields,
11✔
68
                CreatedAt:   time.Now(),
11✔
69
        }
11✔
70

11✔
71
        // In the regular census, members will be added later so we just create the DB entry
11✔
72
        censusID, err := a.db.SetCensus(census)
11✔
73
        if err != nil {
11✔
74
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
75
                return
×
76
        }
×
77

78
        apicommon.HTTPWriteJSON(w, apicommon.CreateCensusResponse{
11✔
79
                ID: censusID,
11✔
80
        })
11✔
81
}
82

83
// censusInfoHandler godoc
84
//
85
//        @Summary                Get census information
86
//        @Description        Retrieve census information by ID. Returns census type, organization address, and creation time.
87
//        @Tags                        census
88
//        @Accept                        json
89
//        @Produce                json
90
//        @Param                        censusId        path                string        true        "Census ID"
91
//        @Success                200                        {object}        apicommon.OrganizationCensus
92
//        @Failure                400                        {object}        errors.Error        "Invalid census ID"
93
//        @Failure                404                        {object}        errors.Error        "Census not found"
94
//        @Failure                500                        {object}        errors.Error        "Internal server error"
95
//        @Router                        /census/{censusId} [get]
96
func (a *API) censusInfoHandler(w http.ResponseWriter, r *http.Request) {
2✔
97
        censusID, err := apicommon.CensusIDFromRequest(r)
2✔
98
        if err != nil {
3✔
99
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
1✔
100
                return
1✔
101
        }
1✔
102
        census, err := a.db.Census(censusID)
1✔
103
        if err != nil {
1✔
104
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
105
                return
×
106
        }
×
107
        apicommon.HTTPWriteJSON(w, apicommon.OrganizationCensusFromDB(census))
1✔
108
}
109

110
// addCensusParticipantsHandler godoc
111
//
112
//        @Summary                Add participants to a census
113
//        @Description        Add multiple participants to a census. Requires Manager/Admin role.
114
//        @Tags                        census
115
//        @Accept                        json
116
//        @Produce                json
117
//        @Security                BearerAuth
118
//        @Param                        censusId        path                string                                                true        "Census ID"
119
//        @Param                        async                query                boolean                                                false        "Process asynchronously and return job ID"
120
//        @Param                        request                body                apicommon.AddMembersRequest        true        "Participants to add"
121
//        @Success                200                        {object}        apicommon.AddMembersResponse
122
//        @Failure                400                        {object}        errors.Error        "Invalid input data"
123
//        @Failure                401                        {object}        errors.Error        "Unauthorized"
124
//        @Failure                404                        {object}        errors.Error        "Census not found"
125
//        @Failure                500                        {object}        errors.Error        "Internal server error"
126
//        @Router                        /census/{censusId} [post]
127
func (a *API) addCensusParticipantsHandler(w http.ResponseWriter, r *http.Request) {
5✔
128
        censusID, err := apicommon.CensusIDFromRequest(r)
5✔
129
        if err != nil {
6✔
130
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
1✔
131
                return
1✔
132
        }
1✔
133
        // get the user from the request context
134
        user, ok := apicommon.UserFromContext(r.Context())
4✔
135
        if !ok {
4✔
136
                errors.ErrUnauthorized.Write(w)
×
137
                return
×
138
        }
×
139
        // get the async flag
140
        async := r.URL.Query().Get("async") == "true"
4✔
141

4✔
142
        // retrieve census
4✔
143
        census, err := a.db.Census(censusID)
4✔
144
        if err != nil {
4✔
145
                if err == db.ErrNotFound {
×
146
                        errors.ErrMalformedURLParam.Withf("census not found").Write(w)
×
147
                        return
×
148
                }
×
149
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
150
                return
×
151
        }
152
        // check the user has the necessary permissions
153
        if !user.HasRoleFor(census.OrgAddress, db.ManagerRole) && !user.HasRoleFor(census.OrgAddress, db.AdminRole) {
4✔
154
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
155
                return
×
156
        }
×
157
        // decode the participants from the request body
158
        members := &apicommon.AddMembersRequest{}
4✔
159
        if err := json.NewDecoder(r.Body).Decode(members); err != nil {
4✔
160
                log.Error(err)
×
161
                errors.ErrMalformedBody.Withf("missing participants").Write(w)
×
162
                return
×
163
        }
×
164
        // check if there are participants to add
165
        if len(members.Members) == 0 {
5✔
166
                apicommon.HTTPWriteJSON(w, &apicommon.AddMembersResponse{Added: 0})
1✔
167
                return
1✔
168
        }
1✔
169
        org, err := a.db.Organization(census.OrgAddress)
3✔
170
        if err != nil {
3✔
171
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
172
                return
×
173
        }
×
174

175
        // add the org members as census participants in the database
176
        progressChan, err := a.db.SetBulkCensusOrgMemberParticipant(
3✔
177
                org,
3✔
178
                passwordSalt,
3✔
179
                censusID,
3✔
180
                members.ToDB(),
3✔
181
        )
3✔
182
        if err != nil {
3✔
183
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
184
                return
×
185
        }
×
186

187
        if !async {
5✔
188
                // Wait for the channel to be closed (100% completion)
2✔
189
                var lastProgress *db.BulkCensusParticipantStatus
2✔
190
                for p := range progressChan {
6✔
191
                        lastProgress = p
4✔
192
                        // Just drain the channel until it's closed
4✔
193
                        log.Debugw("census add participants",
4✔
194
                                "census", censusID,
4✔
195
                                "org", census.OrgAddress,
4✔
196
                                "progress", p.Progress,
4✔
197
                                "added", p.Added,
4✔
198
                                "total", p.Total)
4✔
199
                }
4✔
200
                // Return the number of participants added
201
                apicommon.HTTPWriteJSON(w, &apicommon.AddMembersResponse{Added: uint32(lastProgress.Added)})
2✔
202
                return
2✔
203
        }
204

205
        // if async create a new job identifier
206
        jobID := internal.NewObjectID()
1✔
207

1✔
208
        // Create persistent job record
1✔
209
        if err := a.db.CreateJob(jobID, db.JobTypeCensusParticipants, census.OrgAddress, len(members.Members)); err != nil {
1✔
NEW
210
                log.Warnw("failed to create persistent job record", "error", err, "jobId", jobID)
×
211
                // Continue with in-memory only (fallback)
×
212
        }
×
213

214
        go func() {
2✔
215
                for p := range progressChan {
3✔
216
                        // We need to drain the channel to avoid blocking
2✔
217
                        addParticipantsToCensusWorkers.Store(jobID, p)
2✔
218

2✔
219
                        // When job completes, persist final results
2✔
220
                        if p.Progress == 100 {
3✔
221
                                // we pass CompleteJob an empty errors slice, because SetBulkCensusOrgMemberParticipant
1✔
222
                                // doesn't collect errors, it only reports progress over the channel.
1✔
223
                                if err := a.db.CompleteJob(jobID, p.Added, []string{}); err != nil {
1✔
224
                                        log.Warnw("failed to persist job completion", "error", err, "jobId", jobID.String())
×
225
                                }
×
226
                                addParticipantsToCensusWorkers.Delete(jobID)
1✔
227
                        }
228
                }
229
        }()
230

231
        apicommon.HTTPWriteJSON(w, &apicommon.AddMembersResponse{JobID: jobID})
1✔
232
}
233

234
// censusAddParticipantsJobStatusHandler godoc
235
//
236
//        @Summary                Check the progress of adding participants
237
//        @Description        Check the progress of a job to add participants to a census. Returns the progress of the job.
238
//        @Description        If the job is completed, the job is deleted after 60 seconds.
239
//        @Tags                        census
240
//        @Accept                        json
241
//        @Produce                json
242
//        @Param                        jobId        path                string        true        "Job ID"
243
//        @Success                200                {object}        db.BulkCensusParticipantStatus
244
//        @Failure                400                {object}        errors.Error        "Invalid job ID"
245
//        @Failure                404                {object}        errors.Error        "Job not found"
246
//        @Router                        /census/job/{jobId} [get]
247
func (a *API) censusAddParticipantsJobStatusHandler(w http.ResponseWriter, r *http.Request) {
4✔
248
        jobID, err := apicommon.JobIDFromRequest(r)
4✔
249
        if err != nil {
4✔
NEW
250
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
×
251
                return
×
252
        }
×
253

254
        // First check in-memory for active jobs
255
        if v, ok := addParticipantsToCensusWorkers.Load(jobID); ok {
7✔
256
                p, ok := v.(*db.BulkCensusParticipantStatus)
3✔
257
                if !ok {
3✔
258
                        errors.ErrGenericInternalServerError.Withf("invalid job status type").Write(w)
×
259
                        return
×
260
                }
×
261
                apicommon.HTTPWriteJSON(w, p)
3✔
262
                return
3✔
263
        }
264

265
        // If not in memory, check database for completed jobs
266
        job, err := a.db.Job(jobID)
1✔
267
        if err != nil {
1✔
268
                if err == db.ErrNotFound {
×
NEW
269
                        errors.ErrJobNotFound.Withf("%s", jobID).Write(w)
×
270
                        return
×
271
                }
×
272
                errors.ErrGenericInternalServerError.Withf("failed to get job: %v", err).Write(w)
×
273
                return
×
274
        }
275

276
        // Return persistent job data in the same format as BulkCensusParticipantStatus
277
        apicommon.HTTPWriteJSON(w, &db.BulkCensusParticipantStatus{
1✔
278
                Progress: 100, // Completed jobs are always 100%
1✔
279
                Total:    job.Total,
1✔
280
                Added:    job.Added,
1✔
281
        })
1✔
282
}
283

284
// publishCensusHandler godoc
285
//
286
//        @Summary                Publish a census for voting
287
//        @Description        Publish a census for voting. Requires Manager/Admin role. Returns published census with credentials.
288
//        @Tags                        census
289
//        @Accept                        json
290
//        @Produce                json
291
//        @Security                BearerAuth
292
//        @Param                        censusId        path                string        true        "Census ID"
293
//        @Success                200                        {object}        apicommon.PublishedCensusResponse
294
//        @Failure                400                        {object}        errors.Error        "Invalid census ID"
295
//        @Failure                401                        {object}        errors.Error        "Unauthorized"
296
//        @Failure                404                        {object}        errors.Error        "Census not found"
297
//        @Failure                500                        {object}        errors.Error        "Internal server error"
298
//        @Router                        /census/{censusId}/publish [post]
299
func (a *API) publishCensusHandler(w http.ResponseWriter, r *http.Request) {
3✔
300
        censusID, err := apicommon.CensusIDFromRequest(r)
3✔
301
        if err != nil {
4✔
302
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
1✔
303
                return
1✔
304
        }
1✔
305

306
        // get the user from the request context
307
        user, ok := apicommon.UserFromContext(r.Context())
2✔
308
        if !ok {
2✔
309
                errors.ErrUnauthorized.Write(w)
×
310
                return
×
311
        }
×
312

313
        // retrieve census
314
        census, err := a.db.Census(censusID)
2✔
315
        if err != nil {
2✔
316
                errors.ErrCensusNotFound.Write(w)
×
317
                return
×
318
        }
×
319

320
        // check the user has the necessary permissions
321
        if !user.HasRoleFor(census.OrgAddress, db.ManagerRole) && !user.HasRoleFor(census.OrgAddress, db.AdminRole) {
2✔
322
                errors.ErrUnauthorized.Withf("user does not have the necessary permissions in the organization").Write(w)
×
323
                return
×
324
        }
×
325

326
        if len(census.Published.Root) > 0 {
2✔
327
                // if the census is already published, return the censusInfo
×
328
                apicommon.HTTPWriteJSON(w, &apicommon.PublishedCensusResponse{
×
329
                        URI:  census.Published.URI,
×
330
                        Root: census.Published.Root,
×
331
                })
×
332
                return
×
333
        }
×
334

335
        // if census.Type == CensusTypeSMSOrMail || census.Type == CenT {
336
        // build the census and store it
337
        cspSignerPubKey := a.account.PubKey // TODO: use a different key based on the censusID
2✔
338
        switch census.Type {
2✔
339
        case CensusTypeSMSOrMail, CensusTypeMail, CensusTypeSMS:
2✔
340
                census.Published.Root = cspSignerPubKey
2✔
341
                census.Published.URI = a.serverURL + "/process"
2✔
342
                census.Published.CreatedAt = time.Now()
2✔
343

344
        default:
×
345
                errors.ErrCensusTypeNotFound.Write(w)
×
346
                return
×
347
        }
348

349
        if _, err := a.db.SetCensus(census); err != nil {
2✔
350
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
351
                return
×
352
        }
×
353

354
        apicommon.HTTPWriteJSON(w, &apicommon.PublishedCensusResponse{
2✔
355
                URI:  census.Published.URI,
2✔
356
                Root: cspSignerPubKey,
2✔
357
        })
2✔
358
}
359

360
// publishCensusGroupHandler godoc
361
//
362
//        @Summary                Publish a group-based census for voting
363
//        @Description        Publish a census based on a specific organization members group for voting. Requires Manager/Admin role.
364
//        @Description        Returns published census with credentials.
365
//        @Tags                        census
366
//        @Accept                        json
367
//        @Produce                json
368
//        @Security                BearerAuth
369
//        @Param                        censusId        path                string                                                                true        "Census ID"
370
//        @Param                        groupId                path                string                                                                true        "Group ID"
371
//        @Param                        request                body                apicommon.PublishCensusGroupRequest        true        "Census authentication configuration"
372
//        @Success                200                        {object}        apicommon.PublishedCensusResponse
373
//        @Failure                400                        {object}        errors.Error        "Invalid census ID or group ID"
374
//        @Failure                401                        {object}        errors.Error        "Unauthorized"
375
//        @Failure                404                        {object}        errors.Error        "Census not found"
376
//        @Failure                500                        {object}        errors.Error        "Internal server error"
377
//        @Router                        /census/{censusId}/publish/group/{groupId} [post]
378
func (a *API) publishCensusGroupHandler(w http.ResponseWriter, r *http.Request) {
13✔
379
        censusID, err := apicommon.CensusIDFromRequest(r)
13✔
380
        if err != nil {
14✔
381
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
1✔
382
                return
1✔
383
        }
1✔
384

385
        // get the group ID from the request path
386
        groupID, err := apicommon.GroupIDFromRequest(r)
12✔
387
        if err != nil {
13✔
388
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
1✔
389
                return
1✔
390
        }
1✔
391

392
        // get the user from the request context
393
        user, ok := apicommon.UserFromContext(r.Context())
11✔
394
        if !ok {
11✔
395
                errors.ErrUnauthorized.Write(w)
×
396
                return
×
397
        }
×
398

399
        // retrieve census
400
        census, err := a.db.Census(censusID)
11✔
401
        if err != nil {
12✔
402
                errors.ErrCensusNotFound.Write(w)
1✔
403
                return
1✔
404
        }
1✔
405

406
        // check the user has the necessary permissions
407
        if !user.HasRoleFor(census.OrgAddress, db.ManagerRole) && !user.HasRoleFor(census.OrgAddress, db.AdminRole) {
11✔
408
                errors.ErrUnauthorized.Withf("user does not have the necessary permissions in the organization").Write(w)
1✔
409
                return
1✔
410
        }
1✔
411

412
        // Parse request
413
        publishInfo := &apicommon.PublishCensusGroupRequest{}
9✔
414
        if err := json.NewDecoder(r.Body).Decode(&publishInfo); err != nil {
9✔
415
                errors.ErrMalformedBody.Write(w)
×
416
                return
×
417
        }
×
418
        census.AuthFields = publishInfo.AuthFields
9✔
419
        census.TwoFaFields = publishInfo.TwoFaFields
9✔
420

9✔
421
        if len(census.Published.Root) > 0 {
11✔
422
                // if the census is already published, return the censusInfo
2✔
423
                apicommon.HTTPWriteJSON(w, &apicommon.PublishedCensusResponse{
2✔
424
                        URI:  census.Published.URI,
2✔
425
                        Root: census.Published.Root,
2✔
426
                })
2✔
427
                return
2✔
428
        }
2✔
429

430
        inserted, err := a.db.PopulateGroupCensus(census, groupID)
7✔
431
        if err != nil {
7✔
432
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
433
                return
×
434
        }
×
435

436
        // build the census and store it
437
        cspSignerPubKey, err := a.csp.PubKey()
7✔
438
        if err != nil {
7✔
439
                errors.ErrGenericInternalServerError.Withf("failed to get CSP public key").Write(w)
×
440
                return
×
441
        }
×
442
        var rootHex internal.HexBytes
7✔
443
        if err := rootHex.ParseString(cspSignerPubKey.String()); err != nil {
7✔
444
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
445
                return
×
446
        }
×
447
        if len(census.TwoFaFields) == 0 && len(census.AuthFields) == 0 {
7✔
448
                // non CSP censuses
×
449
                errors.ErrCensusTypeNotFound.Write(w)
×
450
                return
×
451
        }
×
452

453
        census.Published.Root = rootHex
7✔
454
        census.Published.URI = a.serverURL + "/process"
7✔
455
        census.Published.CreatedAt = time.Now()
7✔
456

7✔
457
        if _, err := a.db.SetCensus(census); err != nil {
7✔
458
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
459
                return
×
460
        }
×
461

462
        apicommon.HTTPWriteJSON(w, &apicommon.PublishedCensusResponse{
7✔
463
                URI:  census.Published.URI,
7✔
464
                Root: rootHex,
7✔
465
                Size: inserted,
7✔
466
        })
7✔
467
}
468

469
// censusParticipantsHandler godoc
470
//
471
//        @Summary                Get census participants
472
//        @Description        Retrieve participants of a census by ID. Requires Manager/Admin role.
473
//        @Tags                        census
474
//        @Accept                        json
475
//        @Produce                json
476
//        @Security                BearerAuth
477
//        @Param                        censusId        path                string        true        "Census ID"
478
//        @Success                200                        {object}        apicommon.CensusParticipantsResponse
479
//        @Failure                400                        {object}        errors.Error        "Invalid census ID"
480
//        @Failure                401                        {object}        errors.Error        "Unauthorized"
481
//        @Failure                404                        {object}        errors.Error        "Census not found"
482
//        @Failure                500                        {object}        errors.Error        "Internal server error"
483
//        @Router                        /census/{censusId}/participants [get]
484
func (a *API) censusParticipantsHandler(w http.ResponseWriter, r *http.Request) {
1✔
485
        censusID, err := apicommon.CensusIDFromRequest(r)
1✔
486
        if err != nil {
1✔
NEW
487
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
×
488
                return
×
489
        }
×
490

491
        // retrieve census
492
        census, err := a.db.Census(censusID)
1✔
493
        if err != nil {
1✔
494
                if err == db.ErrNotFound {
×
495
                        errors.ErrCensusNotFound.Write(w)
×
496
                        return
×
497
                }
×
498
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
499
                return
×
500
        }
501

502
        // get the user from the request context
503
        user, ok := apicommon.UserFromContext(r.Context())
1✔
504
        if !ok {
1✔
505
                errors.ErrUnauthorized.Write(w)
×
506
                return
×
507
        }
×
508

509
        // check the user has the necessary permissions
510
        if !user.HasRoleFor(census.OrgAddress, db.ManagerRole) && !user.HasRoleFor(census.OrgAddress, db.AdminRole) {
1✔
511
                errors.ErrUnauthorized.Withf("user does not have the necessary permissions in the organization").Write(w)
×
512
                return
×
513
        }
×
514

515
        participants, err := a.db.CensusParticipants(censusID)
1✔
516
        if err != nil {
1✔
517
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
518
                return
×
519
        }
×
520
        participantMemberIDs := make([]internal.ObjectID, len(participants))
1✔
521
        for i, p := range participants {
3✔
522
                participantMemberIDs[i] = p.ParticipantID
2✔
523
        }
2✔
524

525
        apicommon.HTTPWriteJSON(w, &apicommon.CensusParticipantsResponse{
1✔
526
                CensusID:  censusID,
1✔
527
                MemberIDs: participantMemberIDs,
1✔
528
        })
1✔
529
}
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