Skip to content

Commit

Permalink
add package mock
Browse files Browse the repository at this point in the history
  • Loading branch information
majewsky committed Aug 16, 2023
1 parent 87e8e6f commit 4f0adea
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ reusability. Feel free to add to this.
* [httpext](./httpext) adds some convenience functions to [net/http](https://golang.org/pkg/http/).
* [jobloop](./jobloop) contains the Job trait, which abstracts over reusable implementations of worker loops.
* [logg](./logg) adds some convenience functions to [log](https://golang.org/pkg/log/).
* [mock](./mock) contains basic mocks and test doubles.
* [must](./must) contains convenience functions for quickly exiting on fatal errors without the need for excessive `if err != nil`.
* [osext](./osext) contains extensions to the standard library package "os", mostly relating to parsing of environment variables.
* [pluggable](./pluggable) is a tiny plugin factory library, for constructing different objects implementing a common interface based on a configurable type selector.
Expand Down
2 changes: 1 addition & 1 deletion gopherpolicy/pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import (

// Validator is the interface provided by TokenValidator. Application code
// should prefer to reference this interface to allow for substituation by a
// test double.
// test double (such as type mock.Validator).
type Validator interface {
//CheckToken checks the validity of the request's X-Auth-Token in Keystone, and
//returns a Token instance for checking authorization. Any errors that occur
Expand Down
55 changes: 55 additions & 0 deletions mock/clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/******************************************************************************
*
* Copyright 2023 SAP SE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/

package mock

import (
"time"
)

// Clock is a deterministic clock for unit tests. It starts at the Unix epoch
// and only advances when Clock.StepBy() is called.
type Clock struct {
currentTime int64
listeners []func(time.Time)
}

// NewClock starts a new Clock at the Unix epoch.
func NewClock() *Clock {
return &Clock{currentTime: 0}
}

// AddListener registers a callback that will be called whenever the clock is
// advanced. It will also be called once immediately.
func (c *Clock) AddListener(callback func(time.Time)) {
c.listeners = append(c.listeners, callback)
callback(c.Now())
}

// Now reads the clock. This function can be used as a test double for time.Now().
func (c *Clock) Now() time.Time {
return time.Unix(c.currentTime, 0).UTC()
}

// StepBy advances the clock by the given duration.
func (c *Clock) StepBy(d time.Duration) {
c.currentTime += int64(d / time.Second)
for _, callback := range c.listeners {
callback(c.Now())
}
}
50 changes: 50 additions & 0 deletions mock/clock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/******************************************************************************
*
* Copyright 2023 SAP SE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/

package mock

import (
"testing"
"time"

"github.com/sapcc/go-bits/assert"
)

func TestClock(t *testing.T) {
//clock should start at zero
c := NewClock()
assert.DeepEqual(t, "Clock.Now as Unix timestamp", c.Now().Unix(), int64(0))

//clock should not advance on its own
assert.DeepEqual(t, "Clock.Now as Unix timestamp", c.Now().Unix(), int64(0))

//clock should advance when asked to
c.StepBy(5 * time.Minute)
assert.DeepEqual(t, "Clock.Now as Unix timestamp", c.Now().Unix(), int64(300))

//adding a listener should invoke the callback immediately
currentTime := int64(-1)
c.AddListener(func(t time.Time) {
currentTime = t.Unix()
})
assert.DeepEqual(t, "Unix timestamp from callback", currentTime, int64(300))

//advancing the clock should invoke the callback
c.StepBy(time.Second)
assert.DeepEqual(t, "Unix timestamp from callback", currentTime, int64(301))
}
49 changes: 49 additions & 0 deletions mock/enforcer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/******************************************************************************
*
* Copyright 2023 SAP SE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/

package mock

import policy "github.com/databus23/goslo.policy"

// Enforcer implements the gopherpolicy.Enforcer interface. During enforcement,
// all accesses are allowed by default. More restrictive policies can be
// configured with Forbid() and Allow(). Request attributes cannot be checked.
type Enforcer struct {
forbiddenRules map[string]bool
}

// NewEnforcer initializes an Enforcer instance.
func NewEnforcer() *Enforcer {
return &Enforcer{make(map[string]bool)}
}

// Forbid will cause all subsequent calls to Enforce() to return false when
// called for this rule.
func (e *Enforcer) Forbid(rule string) {
e.forbiddenRules[rule] = true
}

// Allow reverses a previous Forbid call and allows the given policy rule.
func (e *Enforcer) Allow(rule string) {
e.forbiddenRules[rule] = false
}

// Enforce implements the gopherpolicy.Enforcer interface.
func (e *Enforcer) Enforce(rule string, ctx policy.Context) bool {
return !e.forbiddenRules[rule]
}
20 changes: 20 additions & 0 deletions mock/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/******************************************************************************
*
* Copyright 2023 SAP SE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/

// Package mock contains basic mocks and test doubles.
package mock
58 changes: 58 additions & 0 deletions mock/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/******************************************************************************
*
* Copyright 2023 SAP SE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/

package mock

import (
"net/http"

policy "github.com/databus23/goslo.policy"

"github.com/sapcc/go-bits/gopherpolicy"
)

// Validator implements the gopherpolicy.Validator and gopherpolicy.Enforcer
// interfaces.
//
// During validation, the X-Auth-Token header on the request is not inspected
// at all. Instead, auth success is always assumed and a token is built from
// the Auth parameters provided during New(), using the mock itself as Enforcer.
//
// During enforcement, all accesses are allowed by default. More restrictive
// policies can be configured with Forbid() and Allow().
type Validator[E gopherpolicy.Enforcer] struct {
Enforcer E
Auth map[string]string
}

// NewValidator initializes a new Validator. The provided auth variables will
// be mirrored into all gopherpolicy.Token instances returned by this Validator.
func NewValidator[E gopherpolicy.Enforcer](enforcer E, auth map[string]string) *Validator[E] {
return &Validator[E]{enforcer, auth}
}

// CheckToken implements the gopherpolicy.Validator interface.
func (v *Validator[E]) CheckToken(r *http.Request) *gopherpolicy.Token {
return &gopherpolicy.Token{
Enforcer: v.Enforcer,
Context: policy.Context{
Auth: v.Auth,
Request: map[string]string{},
},
}
}
69 changes: 69 additions & 0 deletions mock/validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/******************************************************************************
*
* Copyright 2023 SAP SE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/

package mock

import (
"net/http"
"testing"

"github.com/sapcc/go-bits/assert"
)

func TestValidator(t *testing.T) {
v := NewValidator(NewEnforcer(), nil)

//setup a simple HTTP handler that just outputs status 204, 401 or 403 depending on auth result
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !v.CheckToken(r).Require(w, "api:access") {
return
}
w.WriteHeader(http.StatusNoContent)
})

//the default behavior is permissive
assert.HTTPRequest{
Method: http.MethodGet,
Path: "/",
ExpectStatus: http.StatusNoContent,
}.Check(t, h)

//Forbid() on an unrelated rule does not affect the result
v.Enforcer.Forbid("api:details")
assert.HTTPRequest{
Method: http.MethodGet,
Path: "/",
ExpectStatus: http.StatusNoContent,
}.Check(t, h)

//Forbid() on the relevant rule causes 403 error
v.Enforcer.Forbid("api:access")
assert.HTTPRequest{
Method: http.MethodGet,
Path: "/",
ExpectStatus: http.StatusForbidden,
}.Check(t, h)

//explicit Allow() reverses an earlier Forbid
v.Enforcer.Allow("api:access")
assert.HTTPRequest{
Method: http.MethodGet,
Path: "/",
ExpectStatus: http.StatusNoContent,
}.Check(t, h)
}

0 comments on commit 4f0adea

Please sign in to comment.