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

vocdoni / saas-backend / 18944970466

30 Oct 2025 02:55PM UTC coverage: 60.072% (-0.04%) from 60.11%
18944970466

push

github

emmdim
api: support creating a draft process

* api: fix processInfoHandler, now parses processId correctly
* api: new endpoint GET /organizations/{address}/processes/drafts
* api: new TestDraftProcess
* db: SetProcess now creates or updates a process
* db: new ListProcesses retrieves all processes from the DB for an organization

111 of 176 new or added lines in 4 files covered. (63.07%)

10 existing lines in 2 files now uncovered.

6212 of 10341 relevant lines covered (60.07%)

37.09 hits per line

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

50.89
/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.Equals(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.Equals(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.Equals(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.Equals(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
                existingProcess.Census = *census
1✔
175
        }
176

177
        if len(processInfo.Metadata) > 0 {
2✔
178
                existingProcess.Metadata = processInfo.Metadata
1✔
179
        }
1✔
180

181
        if !processInfo.Address.Equals(nil) {
2✔
182
                existingProcess.Address = processInfo.Address
1✔
183
        }
1✔
184

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

191
        apicommon.HTTPWriteOK(w)
1✔
192
}
193

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

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

229
        apicommon.HTTPWriteJSON(w, process)
3✔
230
}
231

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

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

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

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