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

vocdoni / saas-backend / 18557389217

16 Oct 2025 09:53AM UTC coverage: 57.599% (+0.1%) from 57.49%
18557389217

Pull #84

github

altergui
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
Pull Request #84: api: support creating (and listing) draft processes

83 of 117 new or added lines in 3 files covered. (70.94%)

5 existing lines in 2 files now uncovered.

5988 of 10396 relevant lines covered (57.6%)

33.99 hits per line

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

55.28
/api/process.go
1
package api
2

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

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/internal"
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
//        @Description        When draft=true, the process can be updated (overwritten).
20
//        @Tags                        process
21
//        @Accept                        json
22
//        @Produce                json
23
//        @Security                BearerAuth
24
//        @Param                        processId        path                string                                                        true        "Process ID"
25
//        @Param                        request                body                apicommon.CreateProcessRequest        true        "Process creation information"
26
//        @Success                200                        {string}        string                                                        "OK"
27
//        @Failure                400                        {object}        errors.Error                                        "Invalid input data"
28
//        @Failure                401                        {object}        errors.Error                                        "Unauthorized"
29
//        @Failure                404                        {object}        errors.Error                                        "Published census not found"
30
//        @Failure                409                        {object}        errors.Error                                        "Process already exists"
31
//        @Failure                500                        {object}        errors.Error                                        "Internal server error"
32
//        @Router                        /process/{processId} [post]
33
func (a *API) createProcessHandler(w http.ResponseWriter, r *http.Request) {
6✔
34
        processID := internal.HexBytes{}
6✔
35
        if err := processID.ParseString(chi.URLParam(r, "processId")); err != nil {
7✔
36
                errors.ErrMalformedURLParam.Withf("missing process ID").Write(w)
1✔
37
                return
1✔
38
        }
1✔
39

40
        processInfo := &apicommon.CreateProcessRequest{}
5✔
41
        if err := json.NewDecoder(r.Body).Decode(&processInfo); err != nil {
5✔
42
                errors.ErrMalformedBody.Write(w)
×
43
                return
×
44
        }
×
45

46
        if processInfo.PublishedCensusRoot == nil || processInfo.CensusID == nil {
6✔
47
                errors.ErrMalformedBody.Withf("missing published census root or ID").Write(w)
1✔
48
                return
1✔
49
        }
1✔
50

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

58
        census, err := a.db.Census(processInfo.CensusID.String())
4✔
59
        if err != nil {
4✔
60
                if err == db.ErrNotFound {
×
61
                        errors.ErrMalformedURLParam.Withf("census not found").Write(w)
×
62
                        return
×
63
                }
×
64
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
65
                return
×
66
        }
67

68
        if processInfo.PublishedCensusRoot.String() != census.Published.Root.String() ||
4✔
69
                processInfo.PublishedCensusURI != census.Published.URI {
4✔
70
                errors.ErrMalformedBody.Withf("published census root or URI does not match census").Write(w)
×
71
                return
×
72
        }
×
73

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

80
        // check if the process exists
81
        existingProcess, err := a.db.Process(processID)
4✔
82
        if err == nil {
6✔
83
                // Process exists, check if it's in draft mode and can be overwritten
2✔
84
                if !existingProcess.Draft {
3✔
85
                        errors.ErrDuplicateConflict.Withf("process already exists and is not in draft mode").Write(w)
1✔
86
                        return
1✔
87
                }
1✔
88

89
                // Check if the user has permission to modify this process
90
                if !user.HasRoleFor(existingProcess.OrgAddress, db.ManagerRole) && !user.HasRoleFor(existingProcess.OrgAddress, db.AdminRole) {
1✔
NEW
91
                        errors.ErrUnauthorized.Withf("user is not admin or manager of the organization that owns this process").Write(w)
×
NEW
92
                        return
×
NEW
93
                }
×
94
        } else if err != db.ErrNotFound {
2✔
NEW
95
                // Some other error occurred
×
NEW
96
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
97
                return
×
98
        }
×
99

100
        // Create or update the process
101
        process := &db.Process{
3✔
102
                ID:         processID,
3✔
103
                Census:     *census,
3✔
104
                Metadata:   processInfo.Metadata,
3✔
105
                OrgAddress: census.OrgAddress,
3✔
106
                Draft:      processInfo.Draft,
3✔
107
        }
3✔
108

3✔
109
        if err := a.db.SetProcess(process); err != nil {
3✔
110
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
111
                return
×
112
        }
×
113

114
        apicommon.HTTPWriteOK(w)
3✔
115
}
116

117
// processInfoHandler godoc
118
//
119
//        @Summary                Get process information
120
//        @Description        Retrieve voting process information by ID. Returns process details including census and metadata.
121
//        @Tags                        process
122
//        @Accept                        json
123
//        @Produce                json
124
//        @Param                        processId        path                string        true        "Process ID"
125
//        @Success                200                        {object}        db.Process
126
//        @Failure                400                        {object}        errors.Error        "Invalid process ID"
127
//        @Failure                404                        {object}        errors.Error        "Process not found"
128
//        @Failure                500                        {object}        errors.Error        "Internal server error"
129
//        @Router                        /process/{processId} [get]
130
func (a *API) processInfoHandler(w http.ResponseWriter, r *http.Request) {
3✔
131
        processIDStr := chi.URLParam(r, "processId")
3✔
132
        if len(processIDStr) == 0 {
3✔
133
                errors.ErrMalformedURLParam.Withf("missing process ID").Write(w)
×
134
                return
×
135
        }
×
136

137
        processID := internal.HexBytes{}
3✔
138
        if err := processID.ParseString(processIDStr); err != nil {
4✔
139
                errors.ErrMalformedURLParam.Withf("invalid process ID format").Write(w)
1✔
140
                return
1✔
141
        }
1✔
142

143
        process, err := a.db.Process(processID)
2✔
144
        if err != nil {
2✔
UNCOV
145
                if err == db.ErrNotFound {
×
146
                        errors.ErrMalformedURLParam.Withf("process not found").Write(w)
×
147
                        return
×
148
                }
×
UNCOV
149
                errors.ErrGenericInternalServerError.WithErr(err).Write(w)
×
UNCOV
150
                return
×
151
        }
152

153
        apicommon.HTTPWriteJSON(w, process)
2✔
154
}
155

156
// organizationListProcessDraftsHandler godoc
157
//
158
//        @Summary                Get paginated list of process drafts
159
//        @Description        Returns a list of voting process drafts.
160
//        @Tags                        process
161
//        @Accept                        json
162
//        @Produce                json
163
//        @Success                200        {object}        apicommon.ListOrganizationProcesses
164
//        @Failure                404        {object}        errors.Error        "Process not found"
165
//        @Failure                500        {object}        errors.Error        "Internal server error"
166
//        @Router                        /organizations/{address}/processes/drafts [get]
167
func (a *API) organizationListProcessDraftsHandler(w http.ResponseWriter, r *http.Request) {
2✔
168
        // get the organization info from the request context
2✔
169
        org, _, ok := a.organizationFromRequest(r)
2✔
170
        if !ok {
2✔
NEW
171
                errors.ErrNoOrganizationProvided.Write(w)
×
NEW
172
                return
×
NEW
173
        }
×
174
        // get the user from the request context
175
        user, ok := apicommon.UserFromContext(r.Context())
2✔
176
        if !ok {
2✔
NEW
177
                errors.ErrUnauthorized.Write(w)
×
NEW
178
                return
×
NEW
179
        }
×
180
        // check the user has the necessary permissions
181
        if !user.HasRoleFor(org.Address, db.ManagerRole) && !user.HasRoleFor(org.Address, db.AdminRole) {
2✔
NEW
182
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
×
NEW
183
                return
×
NEW
184
        }
×
185

186
        // Parse pagination parameters from query string
187
        page := 1      // Default page number
2✔
188
        pageSize := 10 // Default page size
2✔
189

2✔
190
        if pageStr := r.URL.Query().Get("page"); pageStr != "" {
2✔
NEW
191
                if pageVal, err := strconv.Atoi(pageStr); err == nil && pageVal > 0 {
×
NEW
192
                        page = pageVal
×
NEW
193
                }
×
194
        }
195

196
        if pageSizeStr := r.URL.Query().Get("pageSize"); pageSizeStr != "" {
2✔
NEW
197
                if pageSizeVal, err := strconv.Atoi(pageSizeStr); err == nil && pageSizeVal >= 0 {
×
NEW
198
                        pageSize = pageSizeVal
×
NEW
199
                }
×
200
        }
201

202
        // retrieve the orgMembers with pagination
203
        pages, processes, err := a.db.ListProcesses(org.Address, page, pageSize, true)
2✔
204
        if err != nil {
2✔
NEW
205
                errors.ErrGenericInternalServerError.Withf("could not get processes: %v", err).Write(w)
×
NEW
206
                return
×
NEW
207
        }
×
208

209
        apicommon.HTTPWriteJSON(w, &apicommon.ListOrganizationProcesses{
2✔
210
                TotalPages:  pages,
2✔
211
                CurrentPage: page,
2✔
212
                Processes:   processes,
2✔
213
        })
2✔
214
}
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