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

vocdoni / saas-backend / 18940750079

30 Oct 2025 12:33PM UTC coverage: 60.068% (-0.04%) from 60.11%
18940750079

Pull #84

github

altergui
fixup
Pull Request #84: api: support creating (and listing) draft processes

109 of 174 new or added lines in 3 files covered. (62.64%)

10 existing lines in 2 files now uncovered.

6211 of 10340 relevant lines covered (60.07%)

37.11 hits per line

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

51.46
/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
        "go.mongodb.org/mongo-driver/bson/primitive"
13
)
14

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

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

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

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

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

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

84
        process.OrgAddress = orgAddress
3✔
85
        if processInfo.Address != nil {
3✔
NEW
86
                process.Address = processInfo.Address
×
NEW
87
        }
×
88

89
        processID, err := a.db.SetProcess(process)
3✔
90
        if err != nil {
3✔
NEW
91
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
NEW
92
                return
×
NEW
93
        }
×
94

95
        apicommon.HTTPWriteJSON(w, processID)
3✔
96
}
97

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

126
        // parse the process info from the request body
127
        processInfo := &apicommon.UpdateProcessRequest{}
2✔
128
        if err := json.NewDecoder(r.Body).Decode(&processInfo); err != nil {
2✔
NEW
129
                errors.ErrMalformedBody.Write(w)
×
UNCOV
130
                return
×
UNCOV
131
        }
×
132

133
        // get the user from the request context
134
        user, ok := apicommon.UserFromContext(r.Context())
2✔
135
        if !ok {
2✔
136
                errors.ErrUnauthorized.Write(w)
×
137
                return
×
138
        }
×
139

140
        existingProcess, err := a.db.Process(parsedID)
2✔
141
        if err != nil {
2✔
UNCOV
142
                if err == db.ErrNotFound {
×
NEW
143
                        errors.ErrMalformedURLParam.Withf("process not found").Write(w)
×
144
                        return
×
145
                }
×
UNCOV
146
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
UNCOV
147
                return
×
148
        }
149

150
        // check if it's a draft process and can be overwritten
151
        if existingProcess.Address != nil {
3✔
152
                errors.ErrDuplicateConflict.Withf("process already exists and is not in draft mode").Write(w)
1✔
153
                return
1✔
154
        }
1✔
155

156
        // check the user has the necessary permissions
157
        if !user.HasRoleFor(existingProcess.OrgAddress, db.ManagerRole) &&
1✔
158
                !user.HasRoleFor(existingProcess.OrgAddress, db.AdminRole) {
1✔
NEW
159
                errors.ErrUnauthorized.Withf("user is not admin or manager of the organization that owns this process").Write(w)
×
160
                return
×
161
        }
×
162

163
        var census *db.Census
1✔
164
        if processInfo.CensusID != nil {
2✔
165
                census, err = a.db.Census(processInfo.CensusID.String())
1✔
166
                if err != nil {
1✔
NEW
167
                        if err == db.ErrNotFound {
×
NEW
168
                                errors.ErrMalformedURLParam.Withf("census not found").Write(w)
×
NEW
169
                                return
×
NEW
170
                        }
×
NEW
171
                        errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
NEW
172
                        return
×
173
                }
174
        }
175

176
        if processInfo.Metadata != nil {
2✔
177
                existingProcess.Metadata = processInfo.Metadata
1✔
178
        }
1✔
179

180
        if processInfo.Address != nil {
2✔
181
                existingProcess.Address = processInfo.Address
1✔
182
        }
1✔
183

184
        if processInfo.CensusID != nil {
2✔
185
                existingProcess.Census = *census
1✔
186
        }
1✔
187

188
        _, err = a.db.SetProcess(existingProcess)
1✔
189
        if err != nil {
1✔
190
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
191
                return
×
192
        }
×
193

194
        apicommon.HTTPWriteOK(w)
1✔
195
}
196

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

222
        process, err := a.db.Process(parsedID)
3✔
223
        if err != nil {
3✔
224
                if err == db.ErrNotFound {
×
225
                        errors.ErrMalformedURLParam.Withf("process not found").Write(w)
×
226
                        return
×
227
                }
×
228
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
229
                return
×
230
        }
231

232
        apicommon.HTTPWriteJSON(w, process)
3✔
233
}
234

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

265
        params, err := parsePaginationParams(r.URL.Query().Get(ParamPage), r.URL.Query().Get(ParamLimit))
2✔
266
        if err != nil {
2✔
NEW
267
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
×
NEW
268
                return
×
NEW
269
        }
×
270

271
        // retrieve the orgMembers with pagination
272
        totalItems, processes, err := a.db.ListProcesses(org.Address, params.Page, params.Limit, db.DraftOnly)
2✔
273
        if err != nil {
2✔
NEW
274
                errors.ErrGenericInternalServerError.Withf("could not get processes: %v", err).Write(w)
×
NEW
275
                return
×
NEW
276
        }
×
277
        pagination, err := calculatePagination(params.Page, params.Limit, totalItems)
2✔
278
        if err != nil {
2✔
NEW
279
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
×
NEW
280
                return
×
NEW
281
        }
×
282

283
        apicommon.HTTPWriteJSON(w, &apicommon.ListOrganizationProcesses{
2✔
284
                Pagination: pagination,
2✔
285
                Processes:  processes,
2✔
286
        })
2✔
287
}
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