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 ( dbcb3a...736483 )
by Victor Hugo
01:11 queued 12s
created

mollie/mollie.go   B

Size/Duplication

Total Lines 355
Duplicated Lines 0 %

Test Coverage

Coverage 94.4%

Importance

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