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

vocdoni / saas-backend / 19272512635

11 Nov 2025 04:46PM UTC coverage: 60.484% (+0.03%) from 60.453%
19272512635

push

github

emmdim
api: limit creation of process drafts

* extend TestDraftProcess to check limits enforcement

* cleanup leftovers introduced in "api: support creating a draft process"

* new subscriptions.OrgHasPermission

36 of 53 new or added lines in 3 files covered. (67.92%)

4 existing lines in 2 files now uncovered.

6329 of 10464 relevant lines covered (60.48%)

37.0 hits per line

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

51.42
/api/process.go
1
package api
2

3
import (
4
        "encoding/json"
5
        "net/http"
6

7
        "github.com/ethereum/go-ethereum/common"
8
        "github.com/go-chi/chi/v5"
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/subscriptions"
13
        "go.mongodb.org/mongo-driver/bson/primitive"
14
)
15

16
// createProcessHandler godoc
17
//
18
//        @Summary                Create a new voting process
19
//        @Description        Create a new voting process. Requires Manager/Admin role.
20
//        @Tags                        process
21
//        @Accept                        json
22
//        @Produce                json
23
//        @Security                BearerAuth
24
//        @Param                        request        body                apicommon.CreateProcessRequest        true        "Process creation information"
25
//        @Success                200                {object}        primitive.ObjectID                                "Process ID"
26
//        @Failure                400                {object}        errors.Error                                        "Invalid input data"
27
//        @Failure                401                {object}        errors.Error                                        "Unauthorized"
28
//        @Failure                404                {object}        errors.Error                                        "Published census not found"
29
//        @Failure                409                {object}        errors.Error                                        "Process already exists"
30
//        @Failure                500                {object}        errors.Error                                        "Internal server error"
31
//        @Router                        /process [post]
32
func (a *API) createProcessHandler(w http.ResponseWriter, r *http.Request) {
12✔
33
        // parse the process info from the request body
12✔
34
        processInfo := &apicommon.CreateProcessRequest{}
12✔
35
        if err := json.NewDecoder(r.Body).Decode(&processInfo); err != nil {
12✔
36
                errors.ErrMalformedBody.Write(w)
×
37
                return
×
38
        }
×
39

40
        // get the user from the request context
41
        user, ok := apicommon.UserFromContext(r.Context())
12✔
42
        if !ok {
12✔
43
                errors.ErrUnauthorized.Write(w)
×
44
                return
×
45
        }
×
46

47
        // if it's a draft process
48
        if processInfo.Address.Equals(nil) && processInfo.OrgAddress == (common.Address{}) {
14✔
49
                errors.ErrMalformedBody.Withf("draft processes must provide an org address").Write(w)
2✔
50
                return
2✔
51
        }
2✔
52

53
        // Create or update the process
54
        process := &db.Process{
10✔
55
                Metadata: processInfo.Metadata,
10✔
56
        }
10✔
57

10✔
58
        var orgAddress common.Address
10✔
59
        if processInfo.CensusID != nil {
20✔
60
                var err error
10✔
61
                census, err := a.db.Census(processInfo.CensusID.String())
10✔
62
                if err != nil {
10✔
63
                        if err == db.ErrNotFound {
×
64
                                errors.ErrMalformedURLParam.Withf("invalid census provided").Write(w)
×
65
                                return
×
66
                        }
×
67
                        errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
68
                        return
×
69
                }
70
                process.Census = *census
10✔
71
                orgAddress = census.OrgAddress
10✔
72
        } else if processInfo.OrgAddress != (common.Address{}) {
×
73
                orgAddress = processInfo.OrgAddress
×
74
        } else {
×
75
                errors.ErrMalformedBody.Withf("either census ID or organization address must be provided").Write(w)
×
76
                return
×
77
        }
×
78

79
        // check the user has the necessary permissions
80
        if !user.HasRoleFor(orgAddress, db.ManagerRole) && !user.HasRoleFor(orgAddress, db.AdminRole) {
10✔
81
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
82
                return
×
83
        }
×
84

85
        process.OrgAddress = orgAddress
10✔
86
        if !processInfo.Address.Equals(nil) {
12✔
87
                process.Address = processInfo.Address
2✔
88
        }
2✔
89

90
        // if it's a new draft process
91
        if process.Address.Equals(nil) && process.ID == primitive.NilObjectID {
18✔
92
                if err := a.subscriptions.OrgHasPermission(process.OrgAddress, subscriptions.CreateDraft); err != nil {
10✔
93
                        if apierr, ok := err.(errors.Error); ok {
4✔
94
                                apierr.Write(w)
2✔
95
                                return
2✔
96
                        }
2✔
NEW
97
                        errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
NEW
98
                        return
×
99
                }
100
        }
101

102
        processID, err := a.db.SetProcess(process)
8✔
103
        if err != nil {
8✔
104
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
105
                return
×
106
        }
×
107

108
        apicommon.HTTPWriteJSON(w, processID)
8✔
109
}
110

111
// updateProcessHandler godoc
112
//
113
//        @Summary                Update an existing voting process
114
//        @Description        Update an existing voting process. Requires Manager/Admin role.
115
//        @Tags                        process
116
//        @Accept                        json
117
//        @Produce                json
118
//        @Security                BearerAuth
119
//        @Param                        processId        path                string                                                        true        "Process ID"
120
//        @Param                        request                body                apicommon.CreateProcessRequest        true        "Process update information"
121
//        @Success                200                        {string}        string                                                        "OK"
122
//        @Failure                400                        {object}        errors.Error                                        "Invalid input data"
123
//        @Failure                401                        {object}        errors.Error                                        "Unauthorized"
124
//        @Failure                404                        {object}        errors.Error                                        "Process not found"
125
//        @Failure                500                        {object}        errors.Error                                        "Internal server error"
126
//        @Router                        /process/{processId} [put]
127
func (a *API) updateProcessHandler(w http.ResponseWriter, r *http.Request) {
2✔
128
        processID := chi.URLParam(r, "processId")
2✔
129
        if processID == "" {
2✔
130
                errors.ErrMalformedURLParam.Withf("missing process ID").Write(w)
×
131
                return
×
132
        }
×
133
        parsedID, err := primitive.ObjectIDFromHex(processID)
2✔
134
        if err != nil {
2✔
135
                errors.ErrMalformedURLParam.Withf("invalid process ID").Write(w)
×
136
                return
×
137
        }
×
138

139
        // parse the process info from the request body
140
        processInfo := &apicommon.UpdateProcessRequest{}
2✔
141
        if err := json.NewDecoder(r.Body).Decode(&processInfo); err != nil {
2✔
142
                errors.ErrMalformedBody.Write(w)
×
143
                return
×
144
        }
×
145

146
        // get the user from the request context
147
        user, ok := apicommon.UserFromContext(r.Context())
2✔
148
        if !ok {
2✔
149
                errors.ErrUnauthorized.Write(w)
×
150
                return
×
151
        }
×
152

153
        existingProcess, err := a.db.Process(parsedID)
2✔
154
        if err != nil {
2✔
155
                if err == db.ErrNotFound {
×
156
                        errors.ErrProcessNotFound.Write(w)
×
157
                        return
×
158
                }
×
159
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
160
                return
×
161
        }
162

163
        // check if it's a draft process and can be overwritten
164
        if !existingProcess.Address.Equals(nil) {
3✔
165
                errors.ErrDuplicateConflict.Withf("process already exists and is not in draft mode").Write(w)
1✔
166
                return
1✔
167
        }
1✔
168

169
        // check the user has the necessary permissions
170
        if !user.HasRoleFor(existingProcess.OrgAddress, db.ManagerRole) &&
1✔
171
                !user.HasRoleFor(existingProcess.OrgAddress, db.AdminRole) {
1✔
172
                errors.ErrUnauthorized.Withf("user is not admin or manager of the organization that owns this process").Write(w)
×
173
                return
×
174
        }
×
175

176
        var census *db.Census
1✔
177
        if !processInfo.CensusID.Equals(nil) {
1✔
UNCOV
178
                census, err = a.db.Census(processInfo.CensusID.String())
×
UNCOV
179
                if err != nil {
×
180
                        if err == db.ErrNotFound {
×
181
                                errors.ErrMalformedURLParam.Withf("census not found").Write(w)
×
182
                                return
×
183
                        }
×
184
                        errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
185
                        return
×
186
                }
UNCOV
187
                existingProcess.Census = *census
×
188
        }
189

190
        if len(processInfo.Metadata) > 0 {
2✔
191
                existingProcess.Metadata = processInfo.Metadata
1✔
192
        }
1✔
193

194
        if !processInfo.Address.Equals(nil) {
2✔
195
                existingProcess.Address = processInfo.Address
1✔
196
        }
1✔
197

198
        _, err = a.db.SetProcess(existingProcess)
1✔
199
        if err != nil {
1✔
200
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
201
                return
×
202
        }
×
203

204
        apicommon.HTTPWriteOK(w)
1✔
205
}
206

207
// processInfoHandler godoc
208
//
209
//        @Summary                Get process information
210
//        @Description        Retrieve voting process information by ID. Returns process details including census and metadata.
211
//        @Tags                        process
212
//        @Accept                        json
213
//        @Produce                json
214
//        @Param                        processId        path                string        true        "Process ID"
215
//        @Success                200                        {object}        db.Process
216
//        @Failure                400                        {object}        errors.Error        "Invalid process ID"
217
//        @Failure                404                        {object}        errors.Error        "Process not found"
218
//        @Failure                500                        {object}        errors.Error        "Internal server error"
219
//        @Router                        /process/{processId} [get]
220
func (a *API) processInfoHandler(w http.ResponseWriter, r *http.Request) {
10✔
221
        processID := chi.URLParam(r, "processId")
10✔
222
        if len(processID) == 0 {
10✔
223
                errors.ErrMalformedURLParam.Withf("missing process ID").Write(w)
×
224
                return
×
225
        }
×
226
        parsedID, err := primitive.ObjectIDFromHex(processID)
10✔
227
        if err != nil {
11✔
228
                errors.ErrMalformedURLParam.Withf("invalid process ID").Write(w)
1✔
229
                return
1✔
230
        }
1✔
231

232
        process, err := a.db.Process(parsedID)
9✔
233
        if err != nil {
10✔
234
                if err == db.ErrNotFound {
2✔
235
                        errors.ErrProcessNotFound.Write(w)
1✔
236
                        return
1✔
237
                }
1✔
238
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
239
                return
×
240
        }
241

242
        apicommon.HTTPWriteJSON(w, process)
8✔
243
}
244

245
// organizationListProcessDraftsHandler godoc
246
//
247
//        @Summary                Get paginated list of process drafts
248
//        @Description        Returns a list of voting process drafts.
249
//        @Tags                        process
250
//        @Accept                        json
251
//        @Produce                json
252
//        @Success                200        {object}        apicommon.ListOrganizationProcesses
253
//        @Failure                404        {object}        errors.Error        "Process not found"
254
//        @Failure                500        {object}        errors.Error        "Internal server error"
255
//        @Router                        /organizations/{address}/processes/drafts [get]
256
func (a *API) organizationListProcessDraftsHandler(w http.ResponseWriter, r *http.Request) {
4✔
257
        // get the organization info from the request context
4✔
258
        org, _, ok := a.organizationFromRequest(r)
4✔
259
        if !ok {
4✔
260
                errors.ErrNoOrganizationProvided.Write(w)
×
261
                return
×
262
        }
×
263
        // get the user from the request context
264
        user, ok := apicommon.UserFromContext(r.Context())
4✔
265
        if !ok {
4✔
266
                errors.ErrUnauthorized.Write(w)
×
267
                return
×
268
        }
×
269
        // check the user has the necessary permissions
270
        if !user.HasRoleFor(org.Address, db.ManagerRole) && !user.HasRoleFor(org.Address, db.AdminRole) {
4✔
271
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
272
                return
×
273
        }
×
274

275
        params, err := parsePaginationParams(r.URL.Query().Get(ParamPage), r.URL.Query().Get(ParamLimit))
4✔
276
        if err != nil {
4✔
277
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
×
278
                return
×
279
        }
×
280

281
        // retrieve the orgMembers with pagination
282
        totalItems, processes, err := a.db.ListProcesses(org.Address, params.Page, params.Limit, db.DraftOnly)
4✔
283
        if err != nil {
4✔
284
                errors.ErrGenericInternalServerError.Withf("could not get processes: %v", err).Write(w)
×
285
                return
×
286
        }
×
287
        pagination, err := calculatePagination(params.Page, params.Limit, totalItems)
4✔
288
        if err != nil {
4✔
289
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
×
290
                return
×
291
        }
×
292

293
        apicommon.HTTPWriteJSON(w, &apicommon.ListOrganizationProcesses{
4✔
294
                Pagination: pagination,
4✔
295
                Processes:  processes,
4✔
296
        })
4✔
297
}
298

299
// deleteProcessHandler godoc
300
//
301
//        @Summary                Delete a voting process
302
//        @Description        Delete a voting process. Requires Manager/Admin role.
303
//        @Tags                        process
304
//        @Accept                        json
305
//        @Produce                json
306
//        @Security                BearerAuth
307
//        @Param                        processId        path                string                        true        "Process ID"
308
//        @Success                200                        {string}        string                        "OK"
309
//        @Failure                400                        {object}        errors.Error        "Invalid process ID"
310
//        @Failure                401                        {object}        errors.Error        "Unauthorized"
311
//        @Failure                404                        {object}        errors.Error        "Process not found"
312
//        @Failure                500                        {object}        errors.Error        "Internal server error"
313
//        @Router                        /process/{processId} [delete]
314
func (a *API) deleteProcessHandler(w http.ResponseWriter, r *http.Request) {
1✔
315
        processID := chi.URLParam(r, "processId")
1✔
316
        if processID == "" {
1✔
317
                errors.ErrMalformedURLParam.Withf("missing process ID").Write(w)
×
318
                return
×
319
        }
×
320
        parsedID, err := primitive.ObjectIDFromHex(processID)
1✔
321
        if err != nil {
1✔
322
                errors.ErrMalformedURLParam.Withf("invalid process ID").Write(w)
×
323
                return
×
324
        }
×
325

326
        // get the user from the request context
327
        user, ok := apicommon.UserFromContext(r.Context())
1✔
328
        if !ok {
1✔
329
                errors.ErrUnauthorized.Write(w)
×
330
                return
×
331
        }
×
332

333
        existingProcess, err := a.db.Process(parsedID)
1✔
334
        if err != nil {
1✔
335
                if err == db.ErrNotFound {
×
336
                        errors.ErrProcessNotFound.Withf("process not found").Write(w)
×
337
                        return
×
338
                }
×
339
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
340
                return
×
341
        }
342

343
        // check the user has the necessary permissions
344
        if !user.HasRoleFor(existingProcess.OrgAddress, db.ManagerRole) &&
1✔
345
                !user.HasRoleFor(existingProcess.OrgAddress, db.AdminRole) {
1✔
346
                errors.ErrUnauthorized.Withf("user is not admin or manager of the organization that owns this process").Write(w)
×
347
                return
×
348
        }
×
349

350
        err = a.db.DelProcess(parsedID)
1✔
351
        if err != nil {
1✔
352
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
353
                return
×
354
        }
×
355

356
        apicommon.HTTPWriteOK(w)
1✔
357
}
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

© 2026 Coveralls, Inc