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
Push — master ( f83009...037081 )
by Victor Hugo
01:17 queued 12s
created

mollie/mollie.go   B

Size/Duplication

Total Lines 375
Duplicated Lines 0 %

Test Coverage

Coverage 94.53%

Importance

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