GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Pull Request — master (#278)
by Victor Hugo
01:25
created

mollie/mollie.go   B

Size/Duplication

Total Lines 377
Duplicated Lines 0 %

Test Coverage

Coverage 94.57%

Importance

Changes 0
Metric Value
cc 44
eloc 221
dl 0
loc 377
ccs 122
cts 129
cp 0.9457
crap 44.3099
rs 8.8798
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A mollie.newResponse 0 14 2
A mollie.*Client.WithAuthenticationValue 0 8 2
B mollie.NewClient 0 57 4
A mollie.*Client.Do 0 18 4
C mollie.*Client.NewAPIRequest 0 44 9
A mollie.CheckResponse 0 6 2
A mollie.*Client.SetIdempotencyKeyGenerator 0 2 1
A mollie.*Client.patch 0 15 3
A mollie.*Client.get 0 12 3
A mollie.*Client.delete 0 12 3
A mollie.*Client.addRequestHeaders 0 10 4
A mollie.*Client.post 0 15 3
A mollie.*Client.HasAccessToken 0 2 1
A mollie.newError 0 15 3
1
package mollie
2
3
import (
4
	"bytes"
5
	"context"
6
	"encoding/json"
7
	"errors"
8
	"fmt"
9
	"io"
10
	"net/http"
11
	"net/url"
12
	"os"
13
	"regexp"
14
	"runtime"
15
	"strings"
16
17
	"github.com/VictorAvelar/mollie-api-go/v3/mollie/tools/idempotency"
18
	"github.com/google/go-querystring/query"
19
)
20
21
// Constants holding values for client initialization and request instantiation.
22
const (
23
	BaseURL              string = "https://api.mollie.com/"
24
	AuthHeader           string = "Authorization"
25
	TokenType            string = "Bearer"
26
	APITokenEnv          string = "MOLLIE_API_TOKEN"
27
	OrgTokenEnv          string = "MOLLIE_ORG_TOKEN"
28
	RequestContentType   string = "application/json"
29
	IdempotencyKeyHeader string = "Idempotency-Key"
30
)
31
32
var (
33
	accessTokenExpr = regexp.MustCompile(`(?m)^access_`)
34
	errEmptyAuthKey = errors.New("you must provide a non-empty authentication key")
35
	errBadBaseURL   = errors.New("malformed base url, it must contain a trailing slash")
36
)
37
38
// Client manages communication with Mollie's API.
39
type Client struct {
40
	BaseURL        *url.URL
41
	authentication string
42
	userAgent      string
43
	client         *http.Client
44
	common         service // Reuse a single struct instead of allocating one for each service on the heap.
45
	config         *Config
46
	// Tools
47
	idempotencyKeyProvider idempotency.KeyGenerator
48
	// Services
49
	Payments       *PaymentsService
50
	Chargebacks    *ChargebacksService
51
	PaymentMethods *PaymentMethodsService
52
	Invoices       *InvoicesService
53
	Organizations  *OrganizationsService
54
	Profiles       *ProfilesService
55
	Refunds        *RefundsService
56
	Shipments      *ShipmentsService
57
	Orders         *OrdersService
58
	Settlements    *SettlementsService
59
	Captures       *CapturesService
60
	Subscriptions  *SubscriptionsService
61
	Customers      *CustomersService
62
	Miscellaneous  *MiscellaneousService
63
	Mandates       *MandatesService
64
	Permissions    *PermissionsService
65
	Onboarding     *OnboardingService
66
	PaymentLinks   *PaymentLinksService
67
	Partners       *PartnerService
68
	Balances       *BalancesService
69
	ClientLinks    *ClientLinksService
70
	Terminals      *TerminalsService
71
}
72
73
type service struct {
74
	client *Client
75
}
76
77
func (c *Client) get(ctx context.Context, uri string, options interface{}) (res *Response, err error) {
78 1
	if options != nil {
79 1
		v, _ := query.Values(options)
80 1
		uri = fmt.Sprintf("%s?%s", uri, v.Encode())
81
	}
82
83 1
	req, err := c.NewAPIRequest(ctx, http.MethodGet, uri, nil)
84 1
	if err != nil {
85 1
		return
86
	}
87
88 1
	return c.Do(req)
89
}
90
91
func (c *Client) post(ctx context.Context, uri string, body interface{}, options interface{}) (
92
	res *Response,
93
	err error,
94
) {
95 1
	if options != nil {
96 1
		v, _ := query.Values(options)
97 1
		uri = fmt.Sprintf("%s?%s", uri, v.Encode())
98
	}
99
100 1
	req, err := c.NewAPIRequest(ctx, http.MethodPost, uri, body)
101 1
	if err != nil {
102 1
		return
103
	}
104
105 1
	return c.Do(req)
106
}
107
108
func (c *Client) patch(ctx context.Context, uri string, body interface{}, options interface{}) (
109
	res *Response,
110
	err error,
111
) {
112 1
	if options != nil {
113
		v, _ := query.Values(options)
114
		uri = fmt.Sprintf("%s?%s", uri, v.Encode())
115
	}
116
117 1
	req, err := c.NewAPIRequest(ctx, http.MethodPatch, uri, body)
118 1
	if err != nil {
119 1
		return
120
	}
121
122 1
	return c.Do(req)
123
}
124
125
func (c *Client) delete(ctx context.Context, uri string, options interface{}) (res *Response, err error) {
126 1
	if options != nil {
127
		v, _ := query.Values(options)
128
		uri = fmt.Sprintf("%s?%s", uri, v.Encode())
129
	}
130
131 1
	req, err := c.NewAPIRequest(ctx, http.MethodDelete, uri, nil)
132 1
	if err != nil {
133 1
		return
134
	}
135
136 1
	return c.Do(req)
137
}
138
139
// WithAuthenticationValue offers a convenient setter for any of the valid authentication
140
// tokens provided by Mollie.
141
//
142
// Ideally your API key will be provided from and environment variable or
143
// a secret management engine.
144
// This should only be used when environment variables are "impossible" to be used.
145
func (c *Client) WithAuthenticationValue(k string) error {
146 1
	if k == "" {
147 1
		return errEmptyAuthKey
148
	}
149
150 1
	c.authentication = strings.TrimSpace(k)
151
152 1
	return nil
153
}
154
155
// HasAccessToken will return true when the provided authentication token
156
// complies with the access token REGEXP match check.
157
// This will enable TestMode inside the request body.
158
//
159
// See: https://github.com/VictorAvelar/mollie-api-go/issues/123
160
func (c *Client) HasAccessToken() bool {
161 1
	return accessTokenExpr.Match([]byte(c.authentication))
162
}
163
164
// SetIdempotencyKeyGenerator allows you to pass your own idempotency
165
// key generator.
166
func (c *Client) SetIdempotencyKeyGenerator(kg idempotency.KeyGenerator) {
167 1
	c.idempotencyKeyProvider = kg
168
}
169
170
// NewAPIRequest is a wrapper around the http.NewRequest function.
171
//
172
// It will setup the authentication headers/parameters according to the client config.
173
func (c *Client) NewAPIRequest(ctx context.Context, method string, uri string, body interface{}) (
174
	req *http.Request,
175
	err error,
176
) {
177
	//nolint: contextcheck
178 1
	if !strings.HasSuffix(c.BaseURL.Path, "/") {
179 1
		return nil, errBadBaseURL
180
	}
181
182 1
	url, err := c.BaseURL.Parse(uri)
183 1
	if err != nil {
184 1
		return nil, fmt.Errorf("url_parsing_error: %w", err)
185
	}
186
187 1
	if c.config.testing && c.HasAccessToken() {
188 1
		qp := url.Query()
189 1
		qp.Add("testmode", "true")
190 1
		url.RawQuery = qp.Encode()
191
	}
192
193 1
	var buf io.ReadWriter
194 1
	if body != nil {
195 1
		buf = new(bytes.Buffer)
196 1
		enc := json.NewEncoder(buf)
197 1
		enc.SetEscapeHTML(false)
198
199 1
		err := enc.Encode(body)
200 1
		if err != nil {
201 1
			return nil, fmt.Errorf("encoding_error: %w", err)
202
		}
203
	}
204
205 1
	if ctx == nil {
206 1
		ctx = context.Background()
207
	}
208
209 1
	req, err = http.NewRequestWithContext(ctx, method, url.String(), buf)
210 1
	if err != nil {
211 1
		return nil, fmt.Errorf("new_request: %w", err)
212
	}
213
214 1
	c.addRequestHeaders(req)
215
216 1
	return req, nil
217
}
218
219
func (c *Client) addRequestHeaders(req *http.Request) {
220 1
	req.Header.Add(AuthHeader, strings.Join([]string{TokenType, c.authentication}, " "))
221 1
	req.Header.Set("Content-Type", RequestContentType)
222 1
	req.Header.Set("Accept", RequestContentType)
223 1
	req.Header.Set("User-Agent", c.userAgent)
224
225 1
	if c.config.reqIdempotency &&
226
		c.idempotencyKeyProvider != nil &&
227
		req.Method == http.MethodPost {
228 1
		req.Header.Set(IdempotencyKeyHeader, c.idempotencyKeyProvider.Generate())
229
	}
230
}
231
232
// Do sends an API request and returns the API response or returned as an
233
// error if an API error has occurred.
234
func (c *Client) Do(req *http.Request) (*Response, error) {
235 1
	resp, err := c.client.Do(req)
236 1
	if err != nil {
237 1
		return nil, fmt.Errorf("http_error: %w", err)
238
	}
239 1
	defer resp.Body.Close()
240
241 1
	response, err := newResponse(resp)
242 1
	if err != nil {
243
		return response, err
244
	}
245
246 1
	err = CheckResponse(response)
247 1
	if err != nil {
248 1
		return response, err
249
	}
250
251 1
	return response, nil
252
}
253
254
// NewClient returns a new Mollie HTTP API client.
255
// You can pass a previously build http client, if none is provided then
256
// http.DefaultClient will be used.
257
//
258
// NewClient will lookup the environment for values to assign to the
259
// API token (`MOLLIE_API_TOKEN`) and the Organization token (`MOLLIE_ORG_TOKEN`)
260
// according to the provided Config object.
261
//
262
// You can also set the token values programmatically by using the Client
263
// WithAPIKey and WithOrganizationKey functions.
264
func NewClient(baseClient *http.Client, conf *Config) (mollie *Client, err error) {
265 1
	if baseClient == nil {
266 1
		baseClient = http.DefaultClient
267
	}
268
269 1
	uri, _ := url.Parse(BaseURL)
270
271 1
	mollie = &Client{
272
		BaseURL:                uri,
273
		client:                 baseClient,
274
		config:                 conf,
275
		idempotencyKeyProvider: nil,
276
	}
277
278 1
	mollie.common.client = mollie
279
280 1
	if mollie.config.reqIdempotency {
281 1
		mollie.common.client.idempotencyKeyProvider = idempotency.NewStdGenerator()
282
	}
283
284
	// services for resources
285 1
	mollie.Payments = (*PaymentsService)(&mollie.common)
286 1
	mollie.Chargebacks = (*ChargebacksService)(&mollie.common)
287 1
	mollie.PaymentMethods = (*PaymentMethodsService)(&mollie.common)
288 1
	mollie.Invoices = (*InvoicesService)(&mollie.common)
289 1
	mollie.Organizations = (*OrganizationsService)(&mollie.common)
290 1
	mollie.Profiles = (*ProfilesService)(&mollie.common)
291 1
	mollie.Refunds = (*RefundsService)(&mollie.common)
292 1
	mollie.Shipments = (*ShipmentsService)(&mollie.common)
293 1
	mollie.Orders = (*OrdersService)(&mollie.common)
294 1
	mollie.Captures = (*CapturesService)(&mollie.common)
295 1
	mollie.Settlements = (*SettlementsService)(&mollie.common)
296 1
	mollie.Subscriptions = (*SubscriptionsService)(&mollie.common)
297 1
	mollie.Customers = (*CustomersService)(&mollie.common)
298 1
	mollie.Miscellaneous = (*MiscellaneousService)(&mollie.common)
299 1
	mollie.Mandates = (*MandatesService)(&mollie.common)
300 1
	mollie.Permissions = (*PermissionsService)(&mollie.common)
301 1
	mollie.Onboarding = (*OnboardingService)(&mollie.common)
302 1
	mollie.PaymentLinks = (*PaymentLinksService)(&mollie.common)
303 1
	mollie.Partners = (*PartnerService)(&mollie.common)
304 1
	mollie.Balances = (*BalancesService)(&mollie.common)
305 1
	mollie.ClientLinks = (*ClientLinksService)(&mollie.common)
306 1
	mollie.Terminals = (*TerminalsService)(&mollie.common)
307
308 1
	mollie.userAgent = strings.Join([]string{
309
		runtime.GOOS,
310
		runtime.GOARCH,
311
		runtime.Version(),
312
	}, ";")
313
314
	// Parse authorization from specified environment variable
315 1
	tkn, ok := os.LookupEnv(mollie.config.auth)
316 1
	if ok {
317 1
		mollie.authentication = tkn
318
	}
319
320 1
	return mollie, nil
321
}
322
323
/*
324
Constructor for Error.
325
*/
326
func newError(rsp *Response) error {
327 1
	baseErr := &BaseError{}
328
329 1
	if rsp.ContentLength > 0 {
330 1
		err := json.Unmarshal(rsp.content, baseErr)
331 1
		if err != nil {
332
			return err
333
		}
334
	} else {
335 1
		baseErr.Status = rsp.StatusCode
336 1
		baseErr.Title = rsp.Status
337 1
		baseErr.Detail = string(rsp.content)
338
	}
339
340 1
	return baseErr
341
}
342
343
// Response is a Mollie API response. This wraps the standard http.Response
344
// returned from Mollie and provides convenient access to things like
345
// pagination links.
346
type Response struct {
347
	*http.Response
348
	content []byte
349
}
350
351
func newResponse(rsp *http.Response) (*Response, error) {
352 1
	res := Response{Response: rsp}
353
354 1
	data, err := io.ReadAll(rsp.Body)
355 1
	if err != nil {
356
		return &res, err
357
	}
358
359 1
	res.content = data
360
361 1
	rsp.Body = io.NopCloser(bytes.NewBuffer(data))
362 1
	res.Response = rsp
363
364 1
	return &res, nil
365
}
366
367
// CheckResponse checks the API response for errors, and returns them if
368
// present. A response is considered an error if it has a status code outside
369
// the 200 range.
370
// API error responses are expected to have either no response
371
// body, or a JSON response body.
372
func CheckResponse(r *Response) error {
373 1
	if r.StatusCode >= http.StatusMultipleChoices {
374 1
		return newError(r)
375
	}
376
377 1
	return nil
378
}
379