Skip to content

Commit

Permalink
Merge pull request #2 from mittwald/tests/auth-tests
Browse files Browse the repository at this point in the history
Add tests for authentication functions
  • Loading branch information
martin-helmich authored Jan 23, 2025
2 parents 338c355 + 761e3a8 commit 0052251
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 0 deletions.
91 changes: 91 additions & 0 deletions mittwaldv2/client_opt_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package mittwaldv2_test

import (
"context"
"github.com/mittwald/api-client-go/mittwaldv2"
"github.com/mittwald/api-client-go/mittwaldv2/generated/clients/user"
"github.com/mittwald/api-client-go/pkg/httpclient_mock"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"net/http"
"os"
)

var _ = Describe("Client authentication", func() {
Describe("WithAccessToken", func() {
It("should append the provided access token to all requests", func() {
ctx := context.Background()

runner := &httpclient_mock.MockRequestRunner{}
runner.ExpectRequest(http.MethodGet, "/v2/users/self/personal-information", httpclient_mock.WithJSONResponse(map[string]any{}))

client, err := mittwaldv2.New(ctx, mittwaldv2.WithHTTPClient(runner), mittwaldv2.WithAccessToken("FOOBAR"))

Expect(err).NotTo(HaveOccurred())

_, _, err = client.User().GetOwnAccount(ctx, user.GetOwnAccountRequest{})

Expect(err).NotTo(HaveOccurred())
Expect(runner.Requests).To(HaveLen(1))
Expect(runner.Requests[0].Header.Get("X-Access-Token")).To(Equal("FOOBAR"))
})
})

Describe("WithAccessTokenFromEnv", func() {
It("should retrieve the access token from the environment", func() {
Expect(os.Setenv("MITTWALD_API_TOKEN", "FOOBAR")).To(Succeed())

ctx := context.Background()

runner := &httpclient_mock.MockRequestRunner{}
runner.ExpectRequest(http.MethodGet, "/v2/users/self/personal-information", httpclient_mock.WithJSONResponse(map[string]any{}))

client, err := mittwaldv2.New(ctx, mittwaldv2.WithHTTPClient(runner), mittwaldv2.WithAccessTokenFromEnv())

Expect(err).NotTo(HaveOccurred())

_, _, err = client.User().GetOwnAccount(ctx, user.GetOwnAccountRequest{})

Expect(err).NotTo(HaveOccurred())
Expect(runner.Requests).To(HaveLen(1))
Expect(runner.Requests[0].Header.Get("X-Access-Token")).To(Equal("FOOBAR"))
})
})

Describe("WithUsernamePassword", func() {
It("should retrieve the access token from an actual login", func() {
ctx := context.Background()

runner := &httpclient_mock.MockRequestRunner{}
runner.ExpectRequest(http.MethodPost, "/v2/authenticate", httpclient_mock.WithJSONResponse(map[string]any{"token": "FOOBAR"}))
runner.ExpectRequest(http.MethodGet, "/v2/users/self/personal-information", httpclient_mock.WithJSONResponse(map[string]any{}))

client, err := mittwaldv2.New(ctx, mittwaldv2.WithHTTPClient(runner), mittwaldv2.WithUsernamePassword("[email protected]", "secret"))

Expect(err).NotTo(HaveOccurred())

_, _, err = client.User().GetOwnAccount(ctx, user.GetOwnAccountRequest{})

Expect(err).NotTo(HaveOccurred())
Expect(runner.Requests).To(HaveLen(2))
Expect(runner.Requests[0].Header.Get("X-Access-Token")).To(Equal(""))
Expect(runner.Requests[1].Header.Get("X-Access-Token")).To(Equal("FOOBAR"))
})

It("should return an error when 2FA is required", func() {
ctx := context.Background()

runner := &httpclient_mock.MockRequestRunner{}
runner.ExpectRequest(
http.MethodPost,
"/v2/authenticate",
httpclient_mock.WithStatus(http.StatusAccepted),
httpclient_mock.WithJSONResponse(map[string]any{"name": "SecondFactorRequired"}))

_, err := mittwaldv2.New(ctx, mittwaldv2.WithHTTPClient(runner), mittwaldv2.WithUsernamePassword("[email protected]", "secret"))

Expect(err).To(HaveOccurred())
Expect(err).To(MatchError("second factor required; use an API token instead"))
})
})
})
13 changes: 13 additions & 0 deletions mittwaldv2/suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package mittwaldv2_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestTypes(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "mittwaldv2 client initialization")
}
55 changes: 55 additions & 0 deletions pkg/httpclient_mock/client_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package httpclient_mock

import (
"fmt"
"io"
"net/http"
"strings"
)

// MockRequestRunner is a helper class that implements the httpclient.RequestRunner
// interface and may be used as a mock implementation during testing.
type MockRequestRunner struct {
Requests []http.Request
Matchers map[string]func(r *http.Request) *http.Response
}

// ExpectRequest configures the mock client to expect an HTTP request with a
// given method and path. The response that should be returned can be configured
// by providing a list of ResponseOption's.
func (m *MockRequestRunner) ExpectRequest(method, path string, opts ...ResponseOption) {
bodyReader := strings.NewReader("")
bodyReadCloser := io.NopCloser(bodyReader)

resp := http.Response{Body: bodyReadCloser, StatusCode: 204, Status: http.StatusText(204)}

for _, o := range opts {
o(&resp)
}

m.ExpectRequestWithResponse(method, path, &resp)
}

func (m *MockRequestRunner) ExpectRequestWithResponse(method, path string, resp *http.Response) {
m.ExpectRequestWithResponseFunc(method, path, func(*http.Request) *http.Response { return resp })
}

func (m *MockRequestRunner) ExpectRequestWithResponseFunc(method, path string, resp func(r *http.Request) *http.Response) {
if m.Matchers == nil {
m.Matchers = make(map[string]func(r *http.Request) *http.Response)
}

key := strings.ToLower(method + "_" + path)
m.Matchers[key] = resp
}

func (m *MockRequestRunner) Do(request *http.Request) (*http.Response, error) {
m.Requests = append(m.Requests, *request)

key := strings.ToLower(request.Method + "_" + request.URL.Path)
if handler, ok := m.Matchers[key]; ok {
return handler(request), nil
}

return nil, fmt.Errorf("unexpected %s request to %s", request.Method, request.URL)
}
36 changes: 36 additions & 0 deletions pkg/httpclient_mock/request_opt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package httpclient_mock

import (
"bytes"
"encoding/json"
"io"
"net/http"
)

type ResponseOption func(*http.Response)

func WithStatus(status int) ResponseOption {
return func(resp *http.Response) {
resp.StatusCode = status
resp.Status = http.StatusText(status)
}
}

func WithJSONResponse(body any) ResponseOption {
return func(resp *http.Response) {
j, _ := json.Marshal(body)

jsonReader := bytes.NewReader(j)
jsonReadCloser := io.NopCloser(jsonReader)

resp.Body = jsonReadCloser
if resp.Header == nil {
resp.Header = make(http.Header)
}
resp.Header.Set("Content-Type", "application/json")

if resp.StatusCode == 0 {
WithStatus(200)(resp)
}
}
}

0 comments on commit 0052251

Please sign in to comment.