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

vocdoni / saas-backend / 29813731602

21 Jul 2026 08:17AM UTC coverage: 62.421% (-0.01%) from 62.433%
29813731602

push

github

web-flow
chore(lint): fix all golangci-lint issues for v2.12 (#584)

18 of 29 new or added lines in 10 files covered. (62.07%)

3 existing lines in 1 file now uncovered.

11795 of 18896 relevant lines covered (62.42%)

45.45 hits per line

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

18.5
/cmd/client/api_client.go
1
package main
2

3
import (
4
        "bytes"
5
        "encoding/json"
6
        "fmt"
7
        "io"
8
        "net/http"
9
        "net/url"
10
        "strconv"
11
        "strings"
12
        "time"
13

14
        "github.com/ethereum/go-ethereum/common"
15
        "github.com/vocdoni/saas-backend/api/apicommon"
16
)
17

18
// Client wraps authenticated HTTP calls to the SaaS API.
19
type Client struct {
20
        http    *http.Client
21
        baseURL string
22
        token   string
23
}
24

25
func newClient(baseURL string) *Client {
2✔
26
        return &Client{
2✔
27
                http:    &http.Client{Timeout: 30 * time.Second},
2✔
28
                baseURL: strings.TrimRight(baseURL, "/"),
2✔
29
        }
2✔
30
}
2✔
31

32
// doJSON sends an HTTP request and decodes a JSON response into target when provided.
33
func (c *Client) doJSON(method, path string, query url.Values, body, target any) error {
2✔
34
        fullURL := c.baseURL + path
2✔
35
        if len(query) > 0 {
2✔
36
                fullURL = fullURL + "?" + query.Encode()
×
37
        }
×
38

39
        var bodyReader io.Reader
2✔
40
        if body != nil {
4✔
41
                payload, err := json.Marshal(body)
2✔
42
                if err != nil {
2✔
43
                        return fmt.Errorf("marshal request body: %w", err)
×
44
                }
×
45
                bodyReader = bytes.NewReader(payload)
2✔
46
        }
47

48
        req, err := http.NewRequest(method, fullURL, bodyReader)
2✔
49
        if err != nil {
2✔
50
                return fmt.Errorf("build request %s %s: %w", method, fullURL, err)
×
51
        }
×
52
        req.Header.Set("Content-Type", "application/json")
2✔
53
        if c.token != "" {
2✔
54
                req.Header.Set("Authorization", "Bearer "+c.token)
×
55
        }
×
56

57
        resp, err := c.http.Do(req)
2✔
58
        if err != nil {
2✔
59
                return fmt.Errorf("send request %s %s: %w", method, fullURL, err)
×
60
        }
×
61
        defer func() {
4✔
62
                if closeErr := resp.Body.Close(); closeErr != nil {
2✔
63
                        fmt.Printf("warning: close response body: %v\n", closeErr)
×
64
                }
×
65
        }()
66

67
        if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
2✔
68
                respBody, _ := io.ReadAll(resp.Body)
×
69
                trimmed := strings.TrimSpace(string(respBody))
×
70
                if trimmed == "" {
×
71
                        trimmed = "no response body"
×
72
                }
×
73
                return fmt.Errorf("request %s %s failed with status %d: %s", method, fullURL, resp.StatusCode, trimmed)
×
74
        }
75

76
        if target == nil {
2✔
77
                if _, err := io.Copy(io.Discard, resp.Body); err != nil {
×
78
                        return fmt.Errorf("drain response body: %w", err)
×
79
                }
×
80
                return nil
×
81
        }
82

83
        if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
2✔
84
                return fmt.Errorf("decode response for %s %s: %w", method, fullURL, err)
×
85
        }
×
86
        return nil
2✔
87
}
88

89
func (c *Client) login(email, password string) error {
×
90
        var loginResp apicommon.LoginResponse
×
91
        req := apicommon.UserInfo{
×
92
                Email:    email,
×
93
                Password: password,
×
94
        }
×
95
        if err := c.doJSON(
×
96
                http.MethodPost,
×
97
                "/auth/login",
×
98
                nil,
×
99
                req,
×
100
                &loginResp,
×
101
        ); err != nil {
×
102
                return fmt.Errorf("POST /auth/login: %w", err)
×
103
        }
×
104
        if loginResp.Token == "" {
×
105
                return fmt.Errorf("POST /auth/login: empty token in response")
×
106
        }
×
107
        c.token = loginResp.Token
×
108
        return nil
×
109
}
110

111
func (c *Client) organization(address common.Address) (*apicommon.OrganizationInfo, error) {
×
112
        var resp apicommon.OrganizationInfo
×
113
        path := fmt.Sprintf("/organizations/%s", url.PathEscape(address.Hex()))
×
114
        if err := c.doJSON(http.MethodGet, path, nil, nil, &resp); err != nil {
×
115
                return nil, fmt.Errorf("GET %s: %w", path, err)
×
116
        }
×
117
        return &resp, nil
×
118
}
119

120
func (c *Client) organizationMembers(
121
        address common.Address,
122
        search string,
123
        page int,
124
        limit int,
125
) (*apicommon.OrganizationMembersResponse, error) {
×
126
        query := url.Values{}
×
127
        if search != "" {
×
128
                query.Set("search", search)
×
129
        }
×
130
        if page > 0 {
×
131
                query.Set("page", strconv.Itoa(page))
×
132
        }
×
133
        if limit > 0 {
×
134
                query.Set("limit", strconv.Itoa(limit))
×
135
        }
×
136

137
        var resp apicommon.OrganizationMembersResponse
×
138
        path := fmt.Sprintf("/organizations/%s/members", url.PathEscape(address.Hex()))
×
139
        if err := c.doJSON(http.MethodGet, path, query, nil, &resp); err != nil {
×
140
                return nil, fmt.Errorf("GET %s: %w", path, err)
×
141
        }
×
142
        return &resp, nil
×
143
}
144

145
func (c *Client) upsertMember(address common.Address, member apicommon.OrgMember) (string, error) {
2✔
146
        var resp apicommon.OrgMember
2✔
147
        path := fmt.Sprintf("/organizations/%s/members", url.PathEscape(address.Hex()))
2✔
148
        if err := c.doJSON(http.MethodPut, path, nil, member, &resp); err != nil {
2✔
149
                return "", fmt.Errorf("PUT %s: %w", path, err)
×
150
        }
×
151
        if resp.ID == "" {
2✔
152
                return "", fmt.Errorf("PUT %s: empty member ID in response", path)
×
153
        }
×
154
        return resp.ID, nil
2✔
155
}
156

157
func (c *Client) addMembersToCensus(censusID string, memberIDs []string) (*apicommon.AddMembersResponse, error) {
×
158
        var resp apicommon.AddMembersResponse
×
159
        path := fmt.Sprintf("/census/%s", url.PathEscape(censusID))
×
160
        req := apicommon.AddCensusParticipantsRequest{MemberIDs: memberIDs}
×
161
        if err := c.doJSON(http.MethodPost, path, nil, req, &resp); err != nil {
×
162
                return nil, fmt.Errorf("POST %s: %w", path, err)
×
163
        }
×
164
        return &resp, nil
×
165
}
166

167
func (c *Client) censusParticipants(censusID string) (*apicommon.CensusParticipantsResponse, error) {
×
168
        var resp apicommon.CensusParticipantsResponse
×
169
        path := fmt.Sprintf("/census/%s/participants", url.PathEscape(censusID))
×
170
        if err := c.doJSON(http.MethodGet, path, nil, nil, &resp); err != nil {
×
171
                return nil, fmt.Errorf("GET %s: %w", path, err)
×
172
        }
×
173
        return &resp, nil
×
174
}
175

176
func (c *Client) processBundle(bundleID string) (map[string]any, error) {
×
177
        var resp map[string]any
×
178
        path := fmt.Sprintf("/process/bundle/%s", url.PathEscape(bundleID))
×
179
        if err := c.doJSON(http.MethodGet, path, nil, nil, &resp); err != nil {
×
180
                return nil, fmt.Errorf("GET %s: %w", path, err)
×
181
        }
×
182
        return resp, nil
×
183
}
184

185
// censusIDByBundle fetches a process bundle and extracts its census identifier.
186
// It supports both `census.id` and `census._id` payload shapes.
187
func (c *Client) censusIDByBundle(bundleID string) (string, error) {
×
188
        bundleID = strings.TrimSpace(bundleID)
×
189
        if bundleID == "" {
×
190
                return "", fmt.Errorf("bundle ID is empty")
×
191
        }
×
192

193
        bundle, err := c.processBundle(bundleID)
×
194
        if err != nil {
×
195
                return "", fmt.Errorf("retrieve bundle %s: %w", bundleID, err)
×
196
        }
×
197

198
        censusRaw, ok := bundle["census"]
×
199
        if !ok || censusRaw == nil {
×
200
                return "", fmt.Errorf("bundle %s missing census field", bundleID)
×
201
        }
×
202

203
        census, ok := censusRaw.(map[string]any)
×
204
        if !ok {
×
205
                return "", fmt.Errorf(
×
206
                        "bundle %s census has unexpected type %T",
×
207
                        bundleID,
×
208
                        censusRaw,
×
209
                )
×
210
        }
×
211

212
        censusID := stringField(census, "_id")
×
213
        if censusID == "" {
×
214
                censusID = stringField(census, "id")
×
215
        }
×
216
        if censusID == "" {
×
217
                return "", fmt.Errorf(
×
218
                        "bundle %s census ID not found in census._id or census.id",
×
219
                        bundleID,
×
220
                )
×
221
        }
×
222
        return censusID, nil
×
223
}
224

225
func stringField(obj map[string]any, key string) string {
×
226
        raw, ok := obj[key]
×
227
        if !ok || raw == nil {
×
228
                return ""
×
229
        }
×
230

231
        switch value := raw.(type) {
×
232
        case string:
×
233
                return strings.TrimSpace(value)
×
234
        case map[string]any:
×
235
                // Mongo Extended JSON representation, e.g. {"$oid":"..."}.
×
236
                if oid, ok := value["$oid"].(string); ok {
×
237
                        return strings.TrimSpace(oid)
×
238
                }
×
NEW
239
        default:
×
240
                // unsupported type, fall back to the empty string below
241
        }
242
        return ""
×
243
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc