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

vocdoni / saas-backend / 20214988780

14 Dec 2025 06:08PM UTC coverage: 62.63% (-0.4%) from 63.071%
20214988780

Pull #364

github

altergui
test(api): dedup code with helper funcs

* new package ./internal/testutil/
Pull Request #364: api test refactor

127 of 138 new or added lines in 6 files covered. (92.03%)

58 existing lines in 2 files now uncovered.

6677 of 10661 relevant lines covered (62.63%)

34.15 hits per line

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

76.86
/errors/errors.go
1
package errors
2

3
import (
4
        "encoding/json"
5
        stderrors "errors"
6
        "fmt"
7
        "net/http"
8
        "runtime"
9

10
        "go.vocdoni.io/dvote/log"
11
)
12

13
// Error is used by handler functions to wrap errors, assigning a unique error code
14
// and also specifying which HTTP Status should be used.
15
type Error struct {
16
        Err        error  `json:"error"` // Original error
17
        Code       int    // Error code
18
        HTTPstatus int    // HTTP status code to return
19
        LogLevel   string // Log level for this error (defaults to "debug")
20
        Data       any    // Optional data to include in the error response
21
}
22

23
// MarshalJSON returns a JSON containing Err.Error() and Code. Field HTTPstatus is ignored.
24
//
25
// Example output: {"error":"account not found","code":4003}
26
func (e Error) MarshalJSON() ([]byte, error) {
160✔
27
        // This anon struct is needed to actually include the error string,
160✔
28
        // since it wouldn't be marshaled otherwise. (json.Marshal doesn't call Err.Error())
160✔
29
        return json.Marshal(
160✔
30
                struct {
160✔
31
                        Error string `json:"error"`
160✔
32
                        Code  int    `json:"code"`
160✔
33
                        Data  any    `json:"data,omitempty"`
160✔
34
                }{
160✔
35
                        Error: e.Err.Error(),
160✔
36
                        Code:  e.Code,
160✔
37
                        Data:  e.Data,
160✔
38
                })
160✔
39
}
160✔
40

41
// UnmarshalJSON parses a JSON containing error, code and optionally data.
42
//
43
// Example input: {"error":"account not found","code":4003}
44
func (e *Error) UnmarshalJSON(data []byte) error {
15✔
45
        // This anon struct is needed to actually set the error string,
15✔
46
        // since it wouldn't be unmarshaled otherwise. (cannot json.Unmarshal string into type error)
15✔
47
        parsed := struct {
15✔
48
                Error string `json:"error"`
15✔
49
                Code  int    `json:"code"`
15✔
50
                Data  any    `json:"data,omitempty"`
15✔
51
        }{}
15✔
52
        if err := json.Unmarshal(data, &parsed); err != nil {
15✔
NEW
53
                return err
×
NEW
54
        }
×
55
        e.Err = fmt.Errorf("%s", parsed.Error)
15✔
56
        e.Code = parsed.Code
15✔
57
        e.Data = parsed.Data
15✔
58
        return nil
15✔
59
}
60

61
// Error returns the Message contained inside the APIerror
62
func (e Error) Error() string {
167✔
63
        return e.Err.Error()
167✔
64
}
167✔
65

66
// Unwrap returns the error contained inside
67
func (e Error) Unwrap() error {
×
68
        return e.Err
×
69
}
×
70

71
// Write serializes a JSON msg using Error.Err and Error.Code
72
// and passes that to http.Error(). It also logs the error with appropriate level.
73
func (e Error) Write(w http.ResponseWriter) {
155✔
74
        msg, err := json.Marshal(e)
155✔
75
        if err != nil {
155✔
76
                log.Warn(err)
×
77
                http.Error(w, "marshal failed", http.StatusInternalServerError)
×
78
                return
×
79
        }
×
80

81
        // Get caller information for better logging
82
        pc, file, line, _ := runtime.Caller(1)
155✔
83
        caller := runtime.FuncForPC(pc).Name()
155✔
84

155✔
85
        // Log the error with appropriate level
155✔
86
        logLevel := e.LogLevel
155✔
87
        if logLevel == "" {
226✔
88
                // Default log level based on HTTP status
71✔
89
                if e.HTTPstatus >= 500 {
71✔
90
                        logLevel = "error"
×
91
                } else {
71✔
92
                        logLevel = "debug"
71✔
93
                }
71✔
94
        }
95

96
        // For 5xx errors, always log with Error level and include internal error details
97
        if e.HTTPstatus >= 500 {
173✔
98
                // For internal errors, log the full error details
18✔
99
                log.Errorw(e.Err, fmt.Sprintf("API error response [%d]: %s (code: %d, caller: %s, file: %s:%d)",
18✔
100
                        e.HTTPstatus, e.Error(), e.Code, caller, file, line))
18✔
101
        } else if log.Level() == log.LogLevelDebug {
292✔
102
                // For 4xx errors, log with debug level
137✔
103
                errMsg := fmt.Sprintf("API error response [%d]: %s (code: %d, caller: %s)",
137✔
104
                        e.HTTPstatus, e.Error(), e.Code, caller)
137✔
105

137✔
106
                switch logLevel {
137✔
107
                case "debug":
71✔
108
                        log.Debugw(errMsg)
71✔
109
                case "info":
66✔
110
                        log.Infow(errMsg)
66✔
111
                case "warn":
×
112
                        log.Warnw(errMsg)
×
113
                default:
×
114
                        log.Debugw(errMsg) // Default to debug level for unknown log levels
×
115
                }
116
        }
117

118
        // Set the content type to JSON
119
        w.Header().Set("Content-Type", "application/json")
155✔
120
        http.Error(w, string(msg), e.HTTPstatus)
155✔
121
}
122

123
// Withf returns a copy of Error with the Sprintf formatted string appended at the end of e.Err
124
func (e Error) Withf(format string, args ...any) Error {
71✔
125
        return Error{
71✔
126
                Err:        fmt.Errorf("%w: %v", e.Err, fmt.Sprintf(format, args...)),
71✔
127
                Code:       e.Code,
71✔
128
                HTTPstatus: e.HTTPstatus,
71✔
129
                LogLevel:   e.LogLevel,
71✔
130
        }
71✔
131
}
71✔
132

133
// With returns a copy of Error with the string appended at the end of e.Err
134
func (e Error) With(s string) Error {
17✔
135
        return Error{
17✔
136
                Err:        fmt.Errorf("%w: %v", e.Err, s),
17✔
137
                Code:       e.Code,
17✔
138
                HTTPstatus: e.HTTPstatus,
17✔
139
                LogLevel:   e.LogLevel,
17✔
140
        }
17✔
141
}
17✔
142

143
// WithErr returns a copy of Error with err.Error() appended at the end of e.Err
144
// The original error is preserved for logging purposes
145
func (e Error) WithErr(err error) Error {
7✔
146
        return Error{
7✔
147
                Err:        fmt.Errorf("%w: %v", e.Err, err.Error()),
7✔
148
                Code:       e.Code,
7✔
149
                HTTPstatus: e.HTTPstatus,
7✔
150
                LogLevel:   e.LogLevel,
7✔
151
        }
7✔
152
}
7✔
153

154
// WithLogLevel returns a copy of Error with the specified log level
155
func (e Error) WithLogLevel(level string) Error {
×
156
        return Error{
×
157
                Err:        e.Err,
×
158
                Code:       e.Code,
×
159
                HTTPstatus: e.HTTPstatus,
×
160
                LogLevel:   level,
×
161
        }
×
162
}
×
163

164
func (e Error) WithData(data any) Error {
4✔
165
        return Error{
4✔
166
                Err:        e.Err,
4✔
167
                Code:       e.Code,
4✔
168
                HTTPstatus: e.HTTPstatus,
4✔
169
                LogLevel:   e.LogLevel,
4✔
170
                Data:       data,
4✔
171
        }
4✔
172
}
4✔
173

174
// As finds the first error in err's tree that matches target, and if one is found, sets
175
// target to that error value and returns true. Otherwise, it returns false.
176
//
177
// The tree consists of err itself, followed by the errors obtained by repeatedly
178
// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple
179
// errors, As examines err followed by a depth-first traversal of its children.
180
//
181
// An error matches target if the error's concrete value is assignable to the value
182
// pointed to by target, or if the error has a method As(any) bool such that
183
// As(target) returns true. In the latter case, the As method is responsible for
184
// setting target.
185
//
186
// An error type might provide an As method so it can be treated as if it were a
187
// different error type.
188
//
189
// As panics if target is not a non-nil pointer to either a type that implements
190
// error, or to any interface type.
NEW
191
func As(err error, target any) bool {
×
NEW
192
        return stderrors.As(err, target)
×
NEW
193
}
×
194

195
// Is reports whether any error in err's tree matches target.
196
//
197
// The tree consists of err itself, followed by the errors obtained by repeatedly
198
// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple
199
// errors, Is examines err followed by a depth-first traversal of its children.
200
//
201
// An error is considered to match a target if it is equal to that target or if
202
// it implements a method Is(error) bool such that Is(target) returns true.
203
//
204
// An error type might provide an Is method so it can be treated as equivalent
205
// to an existing error. For example, if MyError defines
206
//
207
//        func (m MyError) Is(target error) bool { return target == fs.ErrExist }
208
//
209
// then Is(MyError{}, fs.ErrExist) returns true. See [syscall.Errno.Is] for
210
// an example in the standard library. An Is method should only shallowly
211
// compare err and the target and not call [Unwrap] on either.
NEW
212
func Is(err error, target error) bool {
×
NEW
213
        return stderrors.Is(err, target)
×
NEW
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