Skip to content
This repository has been archived by the owner on Aug 11, 2020. It is now read-only.

Improve tests of pkg/db package #30

Merged
merged 3 commits into from
Sep 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.bak
.coverage.out

.pebble/
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ GOLINT := golint
GOLANGCI_LINT := golangci-lint
DOCKER_COMPOSE := docker-compose
PROTOC := protoc
SED := sed

HOST_IP := $(shell $(GO) run scripts/dev/hostip/main.go)

Expand Down Expand Up @@ -39,7 +40,9 @@ $(COVERAGE_FILE): $(GO_FILES)
# would instrument dependencies as well.
#
# See https://golang.org/doc/go1.10#test
@$(GO) test -race -covermode=atomic -coverprofile=$(COVERAGE_FILE) -coverpkg=$(GO_PACKAGES) ./... 2> /dev/null
@$(GO) test -race -covermode=atomic -coverprofile=$@ -coverpkg=$(GO_PACKAGES) ./... 2> /dev/null
$(SED) -i.bak '/\.pb\.go/d' $@
$(SED) -i.bak '/testing\.go/d' $@

.PHONY: coverage
coverage: $(COVERAGE_FILE) ## Compute and display the current total code coverage
Expand Down Expand Up @@ -123,6 +126,7 @@ clean: ## Remove all intermediate directories and files
rm -rf $(BIN_DIR)
rm -rf $(PEBBLE_DIR)
rm -rf $(COVERAGE_FILE)
rm -rf $(COVERAGE_FILE).bak

.PHONY: help
help: ## Display this help message
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,27 @@ To shut down the test environment issue:
make dev-env-down
```

#### Firewall configuration

For the tests it is necessary that the instance of pebble running in
Docker is able to access the test server running on your host machine.
If you have a firewall it must allow access to port `5002`.

Furthermore the firewall must forward packages to the internal network
created by docker compose.

For `nftables` execute the following prior to executing the tests for
the first time.

```sh
sudo nft insert rule inet filter input tcp dport 5002 accept
sudo nft insert rule inet filter forward ip daddr 10.30.50.0/24 accept
```

The commands above assume you have a table called `filter` for the
`inet` address family. The `filter` table must define two chains
`input` and `forward`.

### Linter

`acmeproxy` uses GolangCI. The full report can be found
Expand Down
67 changes: 67 additions & 0 deletions pkg/db/bucket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package db

import (
"encoding"

"github.com/pkg/errors"
"go.etcd.io/bbolt"
)

type bucket struct {
Bkt *bbolt.Bucket
Err error
}

func (b *bucket) writeRecord(id, record encoding.BinaryMarshaler) {
var (
idBytes []byte
recordBytes []byte
)
if b.Err != nil {
return
}
idBytes, b.Err = id.MarshalBinary()
if b.Err != nil {
return
}
recordBytes, b.Err = record.MarshalBinary()
if b.Err != nil {
return
}
b.Err = b.Bkt.Put(idBytes, recordBytes)
if b.Err != nil {
b.Err = errors.Wrap(b.Err, "put record")
return
}
}

func (b *bucket) readRecord(id encoding.BinaryMarshaler, target encoding.BinaryUnmarshaler) {
var (
idBytes []byte
recordBytes []byte
)
if b.Err != nil {
return
}
idBytes, b.Err = id.MarshalBinary()
if b.Err != nil {
return
}
recordBytes = b.Bkt.Get(idBytes)
if recordBytes == nil {
return
}
b.Err = target.UnmarshalBinary(recordBytes)
if b.Err != nil {
return
}
}

// do executes f iff b.Err is nil. do makes it easy to write code that should
// only execute if the bucket has no error yet.
func (b *bucket) do(f func() error) {
if b.Err != nil {
return
}
b.Err = f()
}
255 changes: 255 additions & 0 deletions pkg/db/bucket_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package db

import (
"testing"

"github.com/fhofherr/acmeproxy/pkg/db/internal/dbrecords"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func TestWriteAndReadRecord(t *testing.T) {
var (
actual string
id = uuid.Must(uuid.NewRandom())
record = "some record"
bucketName = "some_bucket"
)

fx := NewTestFixture(t)
defer fx.Close()

err := fx.DB.updateBucket(bucketName, func(b *bucket) error {
b.writeRecord(id, &dbrecords.BinaryMarshaller{V: record})
return b.Err
})
if !assert.NoError(t, err) {
return
}
err = fx.DB.viewBucket(bucketName, func(b *bucket) error {
target := &dbrecords.BinaryUnmarshaller{V: &actual}
b.readRecord(id, target)
return b.Err
})

assert.NoError(t, err)
assert.Equal(t, record, actual)
}

func TestReadRecordCantMarshalID(t *testing.T) {
fx := NewTestFixture(t)
defer fx.Close()

marshalErr := errors.Errorf("can't marshal")
id := newMockBinaryMarshaller(t)
id.On("MarshalBinary").Return([]byte(nil), marshalErr)
target := newMockBinaryUnmarshaller(t)

bucketName := "some_bucket"
fx.CreateBucket(bucketName)
err := fx.DB.viewBucket(bucketName, func(b *bucket) error {
b.readRecord(id, target)
return b.Err
})

assert.Error(t, err)
assert.Equal(t, marshalErr, errors.Cause(err))
id.AssertCalled(t, "MarshalBinary")
target.AssertNotCalled(t, "UnmarshalBinary", mock.Anything)
}

func TestReadRecordCantUnmarshalRecord(t *testing.T) {
fx := NewTestFixture(t)
defer fx.Close()

record := "some record"
unmarshalErr := errors.New("can't unmarshal")
id := uuid.Must(uuid.NewRandom())
target := newMockBinaryUnmarshaller(t)
target.On("UnmarshalBinary", []byte(record)).Return(unmarshalErr)

bucketName := "some_bucket"
fx.CreateBucketWithKey(bucketName, id, &dbrecords.BinaryMarshaller{V: record})

err := fx.DB.viewBucket(bucketName, func(b *bucket) error {
b.readRecord(id, target)
return b.Err
})
assert.Error(t, err)
assert.Error(t, unmarshalErr, errors.Cause(err))
target.AssertCalled(t, "UnmarshalBinary", []byte(record))
}

func TestReadRecordDoesNothingIfBucketHasError(t *testing.T) {
fx := NewTestFixture(t)
defer fx.Close()

bucketErr := errors.Errorf("bucket error")
id := newMockBinaryMarshaller(t)
record := newMockBinaryUnmarshaller(t)

bucketName := "some_bucket"
fx.CreateBucket(bucketName)
err := fx.DB.viewBucket(bucketName, func(b *bucket) error {
b.Err = bucketErr
b.readRecord(id, record)
return b.Err
})

assert.Error(t, err)
assert.Equal(t, bucketErr, err)
}

func TestWriteRecordCantMarshalID(t *testing.T) {
fx := NewTestFixture(t)
defer fx.Close()

marshalErr := errors.Errorf("can't marshal id")
id := newMockBinaryMarshaller(t)
id.On("MarshalBinary").Return([]byte(nil), marshalErr)
record := newMockBinaryMarshaller(t)

err := fx.DB.updateBucket("some_bucket", func(b *bucket) error {
b.writeRecord(id, record)
return b.Err
})
assert.Error(t, err)
assert.Equal(t, marshalErr, errors.Cause(err))
id.AssertCalled(t, "MarshalBinary")
record.AssertNotCalled(t, "MarshalBinary")
}

func TestWriteRecordCantMarshalRecord(t *testing.T) {
fx := NewTestFixture(t)
defer fx.Close()

id := uuid.Must(uuid.NewRandom())
marshalErr := errors.Errorf("can't marshal record")
record := newMockBinaryMarshaller(t)
record.On("MarshalBinary").Return([]byte(nil), marshalErr)

err := fx.DB.updateBucket("some_bucket", func(b *bucket) error {
b.writeRecord(id, record)
return b.Err
})
assert.Error(t, err)
assert.Equal(t, marshalErr, errors.Cause(err))
record.AssertCalled(t, "MarshalBinary")
}

func TestWriteRecordPutToBucketFails(t *testing.T) {
fx := NewTestFixture(t)
defer fx.Close()

record := "some record"
err := fx.DB.updateBucket("some_bucket", func(b *bucket) error {
b.writeRecord(
// An empty id makes bolt fail, as the id is used as key which may
// not be empty.
&dbrecords.BinaryMarshaller{V: ""},
&dbrecords.BinaryMarshaller{V: record},
)
return b.Err
})
assert.Error(t, err)
}

func TestWriteRecordDoesNothingIfBucketHasError(t *testing.T) {
fx := NewTestFixture(t)
defer fx.Close()

bucketErr := errors.New("bucket error")
id := newMockBinaryMarshaller(t)
record := newMockBinaryMarshaller(t)
err := fx.DB.updateBucket("some_bucket", func(b *bucket) error {
b.Err = bucketErr
b.writeRecord(id, record)
return b.Err
})

assert.Error(t, err)
assert.Equal(t, bucketErr, err)
}

func TestDoExecutesCodeIffBucketIsOk(t *testing.T) {
tests := []struct {
name string
bucketError error
funcError error
funcCalled bool
}{
{
name: "bucket is ok",
funcCalled: true,
},
{
name: "bucket has error",
bucketError: errors.New("bucket error"),
funcCalled: false,
},
{
name: "function fails bucket",
funcError: errors.New("function error"),
funcCalled: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fx := NewTestFixture(t)
defer fx.Close()

bucketName := "some_bucket"
fx.CreateBucket(bucketName)

called := false
f := func() error {
called = true
return tt.funcError
}
err := fx.DB.viewBucket(bucketName, func(b *bucket) error {
b.Err = tt.bucketError
b.do(f)
return nil
})
if tt.funcError != nil {
assert.Equal(t, tt.funcError, err)
} else {
assert.Equal(t, tt.bucketError, err)
}
assert.Equal(t, tt.funcCalled, called)
})
}
}

type mockBinaryMarshaller struct {
mock.Mock
}

func newMockBinaryMarshaller(t *testing.T) *mockBinaryMarshaller {
m := &mockBinaryMarshaller{}
m.Test(t)
return m
}

func (m *mockBinaryMarshaller) MarshalBinary() ([]byte, error) {
args := m.Called()
return args.Get(0).([]byte), args.Error(1)
}

type mockBinaryUnmarshaller struct {
mock.Mock
}

func newMockBinaryUnmarshaller(t *testing.T) *mockBinaryUnmarshaller {
m := &mockBinaryUnmarshaller{}
m.Test(t)
return m
}

func (m *mockBinaryUnmarshaller) UnmarshalBinary(data []byte) error {
args := m.Called(data)
return args.Error(0)
}
Loading