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

vocdoni / saas-backend / 18781003905

24 Oct 2025 01:18PM UTC coverage: 60.307% (+0.2%) from 60.112%
18781003905

Pull #84

github

emmdim
api: add process update endpoint with PUT method

Add new PUT endpoint `/process/{processId}` to enable updating existing voting processes. Includes new `UpdateProcessRequest` type with fields for process address, census ID, metadata, and draft status. Updates API router to register the new update handler alongside existing process creation functionality.
Pull Request #84: api: support creating (and listing) draft processes

126 of 198 new or added lines in 3 files covered. (63.64%)

327 existing lines in 10 files now uncovered.

6278 of 10410 relevant lines covered (60.31%)

36.45 hits per line

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

54.6
/api/process.go
1
package api
2

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

8
        "github.com/ethereum/go-ethereum/common"
9
        "github.com/go-chi/chi/v5"
10
        "github.com/vocdoni/saas-backend/api/apicommon"
11
        "github.com/vocdoni/saas-backend/db"
12
        "github.com/vocdoni/saas-backend/errors"
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                {string}        string                                                        "OK"
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✔
42
                errors.ErrUnauthorized.Write(w)
×
43
                return
×
44
        }
×
45

46
        if processInfo.Draft && len(processInfo.OrgAddress) == 0 {
5✔
NEW
47
                errors.ErrMalformedBody.Withf("draft processes must provide an org address").Write(w)
×
NEW
48
                return
×
NEW
49
        }
×
50

51
        var orgAddress common.Address
5✔
52
        var census *db.Census
5✔
53
        if processInfo.CensusID != nil {
9✔
54
                var err error
4✔
55
                census, err = a.db.Census(processInfo.CensusID.String())
4✔
56
                if err != nil {
5✔
57
                        if err == db.ErrNotFound {
1✔
NEW
58
                                errors.ErrMalformedURLParam.Withf("invalid census provided").Write(w)
×
NEW
59
                                return
×
NEW
60
                        }
×
61
                        errors.ErrGenericInternalServerError.WithErr(err).Write(w)
1✔
62
                        return
1✔
63
                }
64
                orgAddress = census.OrgAddress
3✔
65
        } else if len(processInfo.Address) > 0 {
1✔
NEW
66
                orgAddress = common.HexToAddress(processInfo.Address.Address().Hex())
×
67
        } else {
1✔
68
                errors.ErrMalformedBody.Withf("either census ID or organization address must be provided").Write(w)
1✔
69
                return
1✔
70
        }
1✔
71

72
        // check the user has the necessary permissions
73
        if !user.HasRoleFor(orgAddress, db.ManagerRole) && !user.HasRoleFor(orgAddress, db.AdminRole) {
3✔
74
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
75
                return
×
76
        }
×
77

78
        // Create or update the process
79
        process := &db.Process{
3✔
80
                Census:     *census,
3✔
81
                Metadata:   processInfo.Metadata,
3✔
82
                OrgAddress: orgAddress,
3✔
83
                Draft:      processInfo.Draft,
3✔
84
        }
3✔
85
        if len(processInfo.Address) > 0 {
3✔
86
                process.Address = processInfo.Address
×
87
        }
×
88

89
        processID, err := a.db.SetProcess(process)
3✔
90
        if err != nil {
3✔
91
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
92
                return
×
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) {
1✔
115
        processID := chi.URLParam(r, "processId")
1✔
116
        if len(processID) == 0 {
1✔
NEW
117
                errors.ErrMalformedURLParam.Withf("missing process ID").Write(w)
×
NEW
118
                return
×
NEW
119
        }
×
120

121
        // parse the process info from the request body
122
        processInfo := &apicommon.UpdateProcessRequest{}
1✔
123
        if err := json.NewDecoder(r.Body).Decode(&processInfo); err != nil {
1✔
NEW
124
                errors.ErrMalformedBody.Write(w)
×
NEW
125
                return
×
NEW
126
        }
×
127

128
        // get the user from the request context
129
        user, ok := apicommon.UserFromContext(r.Context())
1✔
130
        if !ok {
1✔
NEW
131
                errors.ErrUnauthorized.Write(w)
×
NEW
132
                return
×
NEW
133
        }
×
134

135
        existingProcess, err := a.db.Process(processID)
1✔
136
        if err != nil {
1✔
NEW
137
                if err == db.ErrNotFound {
×
NEW
138
                        errors.ErrMalformedURLParam.Withf("process not found").Write(w)
×
NEW
139
                        return
×
NEW
140
                }
×
NEW
141
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
NEW
142
                return
×
143
        }
144

145
        // check the user has the necessary permissions
146
        if !user.HasRoleFor(existingProcess.OrgAddress, db.ManagerRole) &&
1✔
147
                !user.HasRoleFor(existingProcess.OrgAddress, db.AdminRole) {
1✔
NEW
148
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
NEW
149
                return
×
NEW
150
        }
×
151

152
        var census *db.Census
1✔
153
        if processInfo.CensusID != nil {
2✔
154
                census, err = a.db.Census(processInfo.CensusID.String())
1✔
155
                if err != nil {
1✔
NEW
156
                        if err == db.ErrNotFound {
×
NEW
157
                                errors.ErrMalformedURLParam.Withf("census not found").Write(w)
×
NEW
158
                                return
×
NEW
159
                        }
×
NEW
160
                        errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
NEW
161
                        return
×
162
                }
163
        }
164

165
        if processInfo.Draft != nil {
2✔
166
                existingProcess.Draft = *processInfo.Draft
1✔
167
        }
1✔
168

169
        if len(processInfo.Metadata) > 0 {
2✔
170
                existingProcess.Metadata = processInfo.Metadata
1✔
171
        }
1✔
172

173
        if len(processInfo.Address) > 0 {
1✔
NEW
174
                existingProcess.Address = processInfo.Address
×
NEW
175
        }
×
176

177
        if len(processInfo.CensusID) > 0 {
2✔
178
                existingProcess.Census = *census
1✔
179
        }
1✔
180

181
        _, err = a.db.SetProcess(existingProcess)
1✔
182
        if err != nil {
1✔
NEW
183
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
NEW
184
                return
×
NEW
185
        }
×
186

187
        apicommon.HTTPWriteJSON(w, "Process updated successfully")
1✔
188
}
189

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

210
        process, err := a.db.Process(processID)
4✔
211
        if err != nil {
5✔
212
                if err == db.ErrNotFound {
1✔
213
                        errors.ErrMalformedURLParam.Withf("process not found").Write(w)
×
214
                        return
×
215
                }
×
216
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
1✔
217
                return
1✔
218
        }
219

220
        apicommon.HTTPWriteJSON(w, process)
3✔
221
}
222

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

253
        // Parse pagination parameters from query string
254
        page := 1      // Default page number
2✔
255
        pageSize := 10 // Default page size
2✔
256

2✔
257
        if pageStr := r.URL.Query().Get("page"); pageStr != "" {
2✔
NEW
258
                if pageVal, err := strconv.Atoi(pageStr); err == nil && pageVal > 0 {
×
NEW
259
                        page = pageVal
×
NEW
260
                }
×
261
        }
262

263
        if pageSizeStr := r.URL.Query().Get("pageSize"); pageSizeStr != "" {
2✔
NEW
264
                if pageSizeVal, err := strconv.Atoi(pageSizeStr); err == nil && pageSizeVal >= 0 {
×
NEW
265
                        pageSize = pageSizeVal
×
NEW
266
                }
×
267
        }
268

269
        // retrieve the orgMembers with pagination
270
        pages, processes, err := a.db.ListProcesses(org.Address, page, pageSize, true)
2✔
271
        if err != nil {
2✔
NEW
272
                errors.ErrGenericInternalServerError.Withf("could not get processes: %v", err).Write(w)
×
NEW
273
                return
×
NEW
274
        }
×
275

276
        apicommon.HTTPWriteJSON(w, &apicommon.ListOrganizationProcesses{
2✔
277
                TotalPages:  pages,
2✔
278
                CurrentPage: page,
2✔
279
                Processes:   processes,
2✔
280
        })
2✔
281
}
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