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 (#207)
by Victor Hugo
01:35
created

mollie/mollie.go   B

Size/Duplication

Total Lines 357
Duplicated Lines 0 %

Test Coverage

Coverage 94.44%

Importance

Changes 0
Metric Value
cc 43
eloc 207
dl 0
loc 357
ccs 119
cts 126
cp 0.9444
crap 43.3178
rs 8.96
c 0
b 0
f 0

13 Methods

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