Skip to content

Commit

Permalink
chore: fleshed out all the unit tests surrounding authorization heade…
Browse files Browse the repository at this point in the history
…r parsing
  • Loading branch information
johnabass committed Aug 19, 2024
1 parent 0ecd852 commit 16a52f3
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 60 deletions.
14 changes: 8 additions & 6 deletions basculehttp/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ const (
var (
// ErrInvalidAuthorization indicates an authorization header value did not
// correspond to the standard.
ErrInvalidAuthorization = errors.New("invalidation authorization")

// ErrMissingAuthorization indicates that no authorization header was
// present in the source HTTP request.
ErrMissingAuthorization = errors.New("missing authorization")
ErrInvalidAuthorization = errors.New("invalid authorization")
)

// ParseAuthorization parses an authorization value typically passed in
Expand All @@ -48,6 +44,7 @@ func ParseAuthorization(raw string) (s Scheme, v string, err error) {
return
}

// AuthorizationParserOption is a configurable option for an AuthorizationParser.
type AuthorizationParserOption interface {
apply(*AuthorizationParser) error
}
Expand Down Expand Up @@ -83,6 +80,9 @@ func WithBasic() AuthorizationParserOption {
}

// AuthorizationParsers is a bascule.TokenParser that handles the Authorization header.
//
// By default, this parser will use the standard Authorization header, which can be
// changed via with WithAuthorizationHeader option.
type AuthorizationParser struct {
header string
parsers map[Scheme]bascule.TokenParser[string]
Expand Down Expand Up @@ -111,6 +111,8 @@ func NewAuthorizationParser(opts ...AuthorizationParserOption) (*AuthorizationPa
// Parse extracts the appropriate header, Authorization by default, and parses the
// scheme and value. Schemes are case-insensitive, e.g. BASIC and Basic are the same scheme.
//
// If no authorization header is found in the request, this method returns ErrMissingCredentials.
//
// If a token parser is registered for the given scheme, that token parser is invoked.
// Otherwise, UnsupportedSchemeError is returned, indicating the scheme in question.
func (ap *AuthorizationParser) Parse(ctx context.Context, source *http.Request) (bascule.Token, error) {
Expand All @@ -121,7 +123,7 @@ func (ap *AuthorizationParser) Parse(ctx context.Context, source *http.Request)

scheme, value, err := ParseAuthorization(authValue)
if err != nil {
return nil, err
return nil, bascule.ErrInvalidCredentials
}

p, registered := ap.parsers[scheme.lower()]
Expand Down
124 changes: 124 additions & 0 deletions basculehttp/authorization_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package basculehttp

import (
"context"
"errors"
"testing"

"github.com/stretchr/testify/suite"
"github.com/xmidt-org/bascule/v1"
)

// withAuthorizationParserOptionErr is an option that returns an error.
// These tests need this since no options currently return errors
// for an AuthorizationParser.
func withAuthorizationParserOptionErr(err error) AuthorizationParserOption {
return authorizationParserOptionFunc(func(*AuthorizationParser) error {
return err
})
}

type AuthorizationTestSuite struct {
TestSuite
}

// newAuthorizationParser produces a parser from a set of options that must assert as valid.
func (suite *AuthorizationTestSuite) newAuthorizationParser(opts ...AuthorizationParserOption) *AuthorizationParser {
ap, err := NewAuthorizationParser(opts...)
suite.Require().NoError(err)
suite.Require().NotNil(ap)
return ap
}

func (suite *AuthorizationTestSuite) TestBasicAuthSuccess() {
suite.Run("DefaultHeader", func() {
var (
ap = suite.newAuthorizationParser(
WithBasic(),
)

request = suite.newBasicAuthRequest()
)

token, err := ap.Parse(context.Background(), request)
suite.NoError(err)
suite.assertBasicToken(token)
})

suite.Run("Custom", func() {
var (
ap = suite.newAuthorizationParser(
WithAuthorizationHeader("Auth-Custom"),
WithScheme(Scheme("Custom"), BasicTokenParser{}),
)

request = suite.newRequest()
)

request.Header.Set("Auth-Custom", "Custom "+suite.basicAuth())
token, err := ap.Parse(context.Background(), request)
suite.NoError(err)
suite.assertBasicToken(token)
})
}

func (suite *AuthorizationTestSuite) TestMissingCredentials() {
var (
ap = suite.newAuthorizationParser(
WithBasic(),
)

request = suite.newRequest()
)

token, err := ap.Parse(context.Background(), request)
suite.ErrorIs(err, bascule.ErrMissingCredentials)
suite.Nil(token)
}

func (suite *AuthorizationTestSuite) TestInvalidCredentials() {
var (
ap = suite.newAuthorizationParser(
WithBasic(),
)

request = suite.newRequest()
)

request.Header.Set(DefaultAuthorizationHeader, "\t")
token, err := ap.Parse(context.Background(), request)
suite.ErrorIs(err, bascule.ErrInvalidCredentials)
suite.Nil(token)
}

func (suite *AuthorizationTestSuite) TestUnsupportedScheme() {
var (
ap = suite.newAuthorizationParser(
WithBasic(),
)

request = suite.newRequest()
)

request.Header.Set(DefaultAuthorizationHeader, "Unsupported xyz")
token, err := ap.Parse(context.Background(), request)
suite.Nil(token)

var use *UnsupportedSchemeError
suite.Require().ErrorAs(err, &use)
suite.Equal(Scheme("Unsupported"), use.Scheme)
}

func (suite *AuthorizationTestSuite) TestOptionError() {
expectedErr := errors.New("expected")
ap, err := NewAuthorizationParser(withAuthorizationParserOptionErr(expectedErr))
suite.ErrorIs(err, expectedErr)
suite.Nil(ap)
}

func TestAuthorization(t *testing.T) {
suite.Run(t, new(AuthorizationTestSuite))
}
3 changes: 3 additions & 0 deletions basculehttp/basic_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package basculehttp

import (
Expand Down
58 changes: 4 additions & 54 deletions basculehttp/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,57 +16,7 @@ import (
)

type MiddlewareTestSuite struct {
suite.Suite

expectedPrincipal string
expectedPassword string
expectedToken bascule.Token
}

func (suite *MiddlewareTestSuite) SetupSuite() {
suite.expectedPrincipal = "testPrincipal"
suite.expectedPassword = "test_password"
suite.expectedToken = basicToken{
userName: suite.expectedPrincipal,
password: suite.expectedPassword,
}
}

// newRequest creates a standardized test request, devoid of any authorization.
func (suite *MiddlewareTestSuite) newRequest() *http.Request {
return httptest.NewRequest("GET", "/test", nil)
}

// newBasicAuthRequest creates a new test request configured with valid basic auth.
func (suite *MiddlewareTestSuite) newBasicAuthRequest() *http.Request {
request := suite.newRequest()
request.SetBasicAuth(suite.expectedPrincipal, suite.expectedPassword)
return request
}

// assertBasicAuthRequest asserts that the given request matches this suite's expectations.
func (suite *MiddlewareTestSuite) assertBasicAuthRequest(request *http.Request) {
suite.Require().NotNil(request)
suite.Equal("GET", request.Method)
suite.Equal("/test", request.URL.String())
}

// assertBasicAuthToken asserts that the token matches this suite's expectations.
func (suite *MiddlewareTestSuite) assertBasicAuthToken(token bascule.Token) {
suite.Require().NotNil(token)
suite.Equal(suite.expectedPrincipal, token.Principal())
suite.Require().Implements((*BasicToken)(nil), token)
suite.Equal(suite.expectedPrincipal, token.(BasicToken).UserName())
suite.Equal(suite.expectedPassword, token.(BasicToken).Password())
}

// newAuthorizationParser creates an AuthorizationParser that is expected to be valid.
// Assertions as to validity are made prior to returning.
func (suite *MiddlewareTestSuite) newAuthorizationParser(opts ...AuthorizationParserOption) *AuthorizationParser {
ap, err := NewAuthorizationParser(opts...)
suite.Require().NoError(err)
suite.Require().NotNil(ap)
return ap
TestSuite
}

// newAuthenticator creates a bascule.Authenticator that is expected to be valid.
Expand Down Expand Up @@ -325,14 +275,14 @@ func (suite *MiddlewareTestSuite) testBasicAuthSuccess() {
bascule.WithApproverFuncs(
func(_ context.Context, request *http.Request, token bascule.Token) error {
suite.assertBasicAuthRequest(request)
suite.assertBasicAuthToken(token)
suite.assertBasicToken(token)
return nil
},
),
bascule.WithAuthorizeListenerFuncs(
func(e bascule.AuthorizeEvent[*http.Request]) {
suite.assertBasicAuthRequest(e.Resource)
suite.assertBasicAuthToken(e.Token)
suite.assertBasicToken(e.Token)
authorizeEvent = true
},
),
Expand Down Expand Up @@ -434,7 +384,7 @@ func (suite *MiddlewareTestSuite) testBasicAuthAuthorizerError() {
bascule.WithApproverFuncs(
func(_ context.Context, resource *http.Request, token bascule.Token) error {
suite.assertBasicAuthRequest(resource)
suite.assertBasicAuthToken(token)
suite.assertBasicToken(token)
return expectedErr
},
),
Expand Down
28 changes: 28 additions & 0 deletions basculehttp/scheme_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package basculehttp

import (
"net/http"
"testing"

"github.com/stretchr/testify/suite"
)

type SchemeTestSuite struct {
suite.Suite
}

func (suite *SchemeTestSuite) TestUnsupportedSchemeError() {
use := &UnsupportedSchemeError{
Scheme: Scheme("Unsupported"),
}

suite.Equal(http.StatusUnauthorized, use.StatusCode())
suite.Contains(use.Error(), "Unsupported")
}

func TestScheme(t *testing.T) {
suite.Run(t, new(SchemeTestSuite))
}
64 changes: 64 additions & 0 deletions basculehttp/testSuite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2024 Comcast Cable Communications Management, LLC
// SPDX-License-Identifier: Apache-2.0

package basculehttp

import (
"net/http"
"net/http/httptest"

"github.com/stretchr/testify/suite"
"github.com/xmidt-org/bascule/v1"
)

const (
expectedPrincipal = "testPrincipal"
expectedPassword = "test_password"
)

// TestSuite is a common suite that exposes some useful behaviors.
type TestSuite struct {
suite.Suite
}

// newRequest creates a standardized test request, devoid of any authorization.
func (suite *TestSuite) newRequest() *http.Request {
return httptest.NewRequest("GET", "/test", nil)
}

// assertRequest asserts that the given request matches the one created by newRequest.
func (suite *TestSuite) assertBasicAuthRequest(request *http.Request) {
suite.Require().NotNil(request)
suite.Equal("GET", request.Method)
suite.Equal("/test", request.URL.String())
}

// newBasicAuthRequest creates a new test request configured with valid basic auth.
func (suite *TestSuite) newBasicAuthRequest() *http.Request {
request := suite.newRequest()
request.SetBasicAuth(expectedPrincipal, expectedPassword)
return request
}

// basicAuth produces a formatted basic authorization string using this suite's expectations.
func (suite *TestSuite) basicAuth() string {
return BasicAuth(expectedPrincipal, expectedPassword)
}

// assertBasicToken asserts that the token matches the one created by newBasicToken.
func (suite *TestSuite) assertBasicToken(token bascule.Token) {
suite.Require().NotNil(token)
suite.Equal(expectedPrincipal, token.Principal())
suite.Require().Implements((*BasicToken)(nil), token)
suite.Equal(expectedPrincipal, token.(BasicToken).UserName())
suite.Equal(expectedPassword, token.(BasicToken).Password())
}

// newAuthorizationParser creates an AuthorizationParser that is expected to be valid.
// Assertions as to validity are made prior to returning.
func (suite *TestSuite) newAuthorizationParser(opts ...AuthorizationParserOption) *AuthorizationParser {
ap, err := NewAuthorizationParser(opts...)
suite.Require().NoError(err)
suite.Require().NotNil(ap)
return ap
}

0 comments on commit 16a52f3

Please sign in to comment.