Skip to content

Commit 761e3a8

Browse files
chore: add tests for authentication functions
1 parent 338c355 commit 761e3a8

File tree

4 files changed

+195
-0
lines changed

4 files changed

+195
-0
lines changed

mittwaldv2/client_opt_auth_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package mittwaldv2_test
2+
3+
import (
4+
"context"
5+
"github.com/mittwald/api-client-go/mittwaldv2"
6+
"github.com/mittwald/api-client-go/mittwaldv2/generated/clients/user"
7+
"github.com/mittwald/api-client-go/pkg/httpclient_mock"
8+
. "github.com/onsi/ginkgo/v2"
9+
. "github.com/onsi/gomega"
10+
"net/http"
11+
"os"
12+
)
13+
14+
var _ = Describe("Client authentication", func() {
15+
Describe("WithAccessToken", func() {
16+
It("should append the provided access token to all requests", func() {
17+
ctx := context.Background()
18+
19+
runner := &httpclient_mock.MockRequestRunner{}
20+
runner.ExpectRequest(http.MethodGet, "/v2/users/self/personal-information", httpclient_mock.WithJSONResponse(map[string]any{}))
21+
22+
client, err := mittwaldv2.New(ctx, mittwaldv2.WithHTTPClient(runner), mittwaldv2.WithAccessToken("FOOBAR"))
23+
24+
Expect(err).NotTo(HaveOccurred())
25+
26+
_, _, err = client.User().GetOwnAccount(ctx, user.GetOwnAccountRequest{})
27+
28+
Expect(err).NotTo(HaveOccurred())
29+
Expect(runner.Requests).To(HaveLen(1))
30+
Expect(runner.Requests[0].Header.Get("X-Access-Token")).To(Equal("FOOBAR"))
31+
})
32+
})
33+
34+
Describe("WithAccessTokenFromEnv", func() {
35+
It("should retrieve the access token from the environment", func() {
36+
Expect(os.Setenv("MITTWALD_API_TOKEN", "FOOBAR")).To(Succeed())
37+
38+
ctx := context.Background()
39+
40+
runner := &httpclient_mock.MockRequestRunner{}
41+
runner.ExpectRequest(http.MethodGet, "/v2/users/self/personal-information", httpclient_mock.WithJSONResponse(map[string]any{}))
42+
43+
client, err := mittwaldv2.New(ctx, mittwaldv2.WithHTTPClient(runner), mittwaldv2.WithAccessTokenFromEnv())
44+
45+
Expect(err).NotTo(HaveOccurred())
46+
47+
_, _, err = client.User().GetOwnAccount(ctx, user.GetOwnAccountRequest{})
48+
49+
Expect(err).NotTo(HaveOccurred())
50+
Expect(runner.Requests).To(HaveLen(1))
51+
Expect(runner.Requests[0].Header.Get("X-Access-Token")).To(Equal("FOOBAR"))
52+
})
53+
})
54+
55+
Describe("WithUsernamePassword", func() {
56+
It("should retrieve the access token from an actual login", func() {
57+
ctx := context.Background()
58+
59+
runner := &httpclient_mock.MockRequestRunner{}
60+
runner.ExpectRequest(http.MethodPost, "/v2/authenticate", httpclient_mock.WithJSONResponse(map[string]any{"token": "FOOBAR"}))
61+
runner.ExpectRequest(http.MethodGet, "/v2/users/self/personal-information", httpclient_mock.WithJSONResponse(map[string]any{}))
62+
63+
client, err := mittwaldv2.New(ctx, mittwaldv2.WithHTTPClient(runner), mittwaldv2.WithUsernamePassword("martin@foo.example", "secret"))
64+
65+
Expect(err).NotTo(HaveOccurred())
66+
67+
_, _, err = client.User().GetOwnAccount(ctx, user.GetOwnAccountRequest{})
68+
69+
Expect(err).NotTo(HaveOccurred())
70+
Expect(runner.Requests).To(HaveLen(2))
71+
Expect(runner.Requests[0].Header.Get("X-Access-Token")).To(Equal(""))
72+
Expect(runner.Requests[1].Header.Get("X-Access-Token")).To(Equal("FOOBAR"))
73+
})
74+
75+
It("should return an error when 2FA is required", func() {
76+
ctx := context.Background()
77+
78+
runner := &httpclient_mock.MockRequestRunner{}
79+
runner.ExpectRequest(
80+
http.MethodPost,
81+
"/v2/authenticate",
82+
httpclient_mock.WithStatus(http.StatusAccepted),
83+
httpclient_mock.WithJSONResponse(map[string]any{"name": "SecondFactorRequired"}))
84+
85+
_, err := mittwaldv2.New(ctx, mittwaldv2.WithHTTPClient(runner), mittwaldv2.WithUsernamePassword("martin@foo.example", "secret"))
86+
87+
Expect(err).To(HaveOccurred())
88+
Expect(err).To(MatchError("second factor required; use an API token instead"))
89+
})
90+
})
91+
})

mittwaldv2/suite_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package mittwaldv2_test
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestTypes(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "mittwaldv2 client initialization")
13+
}

pkg/httpclient_mock/client_mock.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package httpclient_mock
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"strings"
8+
)
9+
10+
// MockRequestRunner is a helper class that implements the httpclient.RequestRunner
11+
// interface and may be used as a mock implementation during testing.
12+
type MockRequestRunner struct {
13+
Requests []http.Request
14+
Matchers map[string]func(r *http.Request) *http.Response
15+
}
16+
17+
// ExpectRequest configures the mock client to expect an HTTP request with a
18+
// given method and path. The response that should be returned can be configured
19+
// by providing a list of ResponseOption's.
20+
func (m *MockRequestRunner) ExpectRequest(method, path string, opts ...ResponseOption) {
21+
bodyReader := strings.NewReader("")
22+
bodyReadCloser := io.NopCloser(bodyReader)
23+
24+
resp := http.Response{Body: bodyReadCloser, StatusCode: 204, Status: http.StatusText(204)}
25+
26+
for _, o := range opts {
27+
o(&resp)
28+
}
29+
30+
m.ExpectRequestWithResponse(method, path, &resp)
31+
}
32+
33+
func (m *MockRequestRunner) ExpectRequestWithResponse(method, path string, resp *http.Response) {
34+
m.ExpectRequestWithResponseFunc(method, path, func(*http.Request) *http.Response { return resp })
35+
}
36+
37+
func (m *MockRequestRunner) ExpectRequestWithResponseFunc(method, path string, resp func(r *http.Request) *http.Response) {
38+
if m.Matchers == nil {
39+
m.Matchers = make(map[string]func(r *http.Request) *http.Response)
40+
}
41+
42+
key := strings.ToLower(method + "_" + path)
43+
m.Matchers[key] = resp
44+
}
45+
46+
func (m *MockRequestRunner) Do(request *http.Request) (*http.Response, error) {
47+
m.Requests = append(m.Requests, *request)
48+
49+
key := strings.ToLower(request.Method + "_" + request.URL.Path)
50+
if handler, ok := m.Matchers[key]; ok {
51+
return handler(request), nil
52+
}
53+
54+
return nil, fmt.Errorf("unexpected %s request to %s", request.Method, request.URL)
55+
}

pkg/httpclient_mock/request_opt.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package httpclient_mock
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
)
9+
10+
type ResponseOption func(*http.Response)
11+
12+
func WithStatus(status int) ResponseOption {
13+
return func(resp *http.Response) {
14+
resp.StatusCode = status
15+
resp.Status = http.StatusText(status)
16+
}
17+
}
18+
19+
func WithJSONResponse(body any) ResponseOption {
20+
return func(resp *http.Response) {
21+
j, _ := json.Marshal(body)
22+
23+
jsonReader := bytes.NewReader(j)
24+
jsonReadCloser := io.NopCloser(jsonReader)
25+
26+
resp.Body = jsonReadCloser
27+
if resp.Header == nil {
28+
resp.Header = make(http.Header)
29+
}
30+
resp.Header.Set("Content-Type", "application/json")
31+
32+
if resp.StatusCode == 0 {
33+
WithStatus(200)(resp)
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)