Add TestController (#50)

This can be used in tests to get access to a controller that behaves
just like a dev controller but doesn't require the normal Command bits
(UI, etc).

This can be enhanced over time as needed; for now it's pretty simple but
it's good enough to be used for API tests. A simple project create/read
API test is added as well, which serves to show the programmatic model
(as well as test those functions at the API level).
pull/60/head
Jeff Mitchell 6 years ago committed by GitHub
parent 2aca5fff03
commit f8b456f3a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,41 @@
package scopes
import (
"testing"
"github.com/hashicorp/watchtower/api"
"github.com/hashicorp/watchtower/internal/servers/controller"
"github.com/stretchr/testify/assert"
)
func TestProjects_Crud(t *testing.T) {
tc := controller.NewTestController(t, nil)
defer tc.Shutdown()
client := tc.Client()
org := &Organization{
Client: client,
}
name := "foo"
checkProject := func(step string, p *Project, apiErr *api.Error, err error) {
assert := assert.New(t)
assert.NoError(err, step)
assert.Nil(apiErr, step)
assert.NotNil(p, "returned project", step)
assert.NotNil(p, "returned project name", step)
assert.Equal(name, *p.Name, step)
}
p, apiErr, err := org.CreateProject(tc.Context(), &Project{Name: api.String(name)})
checkProject("create", p, apiErr, err)
p, apiErr, err = org.ReadProject(tc.Context(), &Project{Id: p.Id})
checkProject("read", p, apiErr, err)
// TODO: Update and Delete
// TODO: Error conditions once the proper errors are being returned.
// Probably as parallel subtests against the same DB.
}

@ -17,7 +17,7 @@ require (
github.com/golang/protobuf v1.4.1
github.com/grpc-ecosystem/grpc-gateway v1.14.5
github.com/hashicorp/errwrap v1.0.0
github.com/hashicorp/go-alpnmux v0.0.0-20200323180452-dee08f00df54
github.com/hashicorp/go-alpnmux v0.0.0-20200513011953-0293f5d23c31
github.com/hashicorp/go-cleanhttp v0.5.1
github.com/hashicorp/go-hclog v0.13.0
github.com/hashicorp/go-kms-wrapping v0.5.8
@ -27,7 +27,7 @@ require (
github.com/hashicorp/go-sockaddr v1.0.2
github.com/hashicorp/go-uuid v1.0.2
github.com/hashicorp/hcl v1.0.0
github.com/hashicorp/vault v1.2.1-0.20200511170424-4928e34922e8
github.com/hashicorp/vault v1.2.1-0.20200513002223-fc89625cd196
github.com/hashicorp/vault/sdk v0.1.14-0.20200511170424-4928e34922e8
github.com/jackc/pgx/v4 v4.6.0
github.com/jinzhu/gorm v1.9.12

@ -570,6 +570,8 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-alpnmux v0.0.0-20200323180452-dee08f00df54 h1:WhMHPQosFuXFjt2wpRzX2eqR1WpnS+35MFzmG3trg3Y=
github.com/hashicorp/go-alpnmux v0.0.0-20200323180452-dee08f00df54/go.mod h1:KvpteZzIafT4tRAuQ9vVRBgZyqeVCS2B2177fNAyEZc=
github.com/hashicorp/go-alpnmux v0.0.0-20200513011953-0293f5d23c31 h1:pxqI71/0R1WIASjQEJ9W9skCKYiREEkRoXFvHCZH1pg=
github.com/hashicorp/go-alpnmux v0.0.0-20200513011953-0293f5d23c31/go.mod h1:KvpteZzIafT4tRAuQ9vVRBgZyqeVCS2B2177fNAyEZc=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
@ -656,16 +658,18 @@ github.com/hashicorp/raft-snapshot v1.0.2-0.20190827162939-8117efcc5aab/go.mod h
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/serf v0.8.3 h1:MWYcmct5EtKz0efYooPcL0yNkem+7kWxqXDi/UIh+8k=
github.com/hashicorp/serf v0.8.3/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc1oal+k=
github.com/hashicorp/vault v1.2.1-0.20200511170424-4928e34922e8 h1:++KBH9oO+9rr5qXvNuPV1tlwcUVmwxuXjxUi2tp5he4=
github.com/hashicorp/vault v1.2.1-0.20200511170424-4928e34922e8/go.mod h1:1/d/RuAcpBW/XXJ7RbRdezTGXc3ZLK/AAj6gPg4wIf8=
github.com/hashicorp/vault v1.2.1-0.20200513002223-fc89625cd196 h1:QplfqcymYufhcbkqY8aMrp5ukBveoZ3+rQK0CotAFhw=
github.com/hashicorp/vault v1.2.1-0.20200513002223-fc89625cd196/go.mod h1:1/d/RuAcpBW/XXJ7RbRdezTGXc3ZLK/AAj6gPg4wIf8=
github.com/hashicorp/vault-plugin-auth-alicloud v0.5.5 h1:JYf3VYpKs7mOdtcwZWi73S82oXrC/JR7uoPVUd8c4Hk=
github.com/hashicorp/vault-plugin-auth-alicloud v0.5.5/go.mod h1:sQ+VNwPQlemgXHXikYH6onfH9gPwDZ1GUVRLz0ZvHx8=
github.com/hashicorp/vault-plugin-auth-azure v0.5.5 h1:kN79ai+aMVU9hUmwscHjmweW2fGa8V/t+ScIchPZGrk=
github.com/hashicorp/vault-plugin-auth-azure v0.5.5/go.mod h1:RCVBsf8AJndh4c6iGZtvVZFui9SG0Bj9fnF0SodNIkw=
github.com/hashicorp/vault-plugin-auth-centrify v0.5.5 h1:YXxXt6o6I1rOkYW+hADK0vd+uVMj4C6Qs3jBrQlKQcY=
github.com/hashicorp/vault-plugin-auth-centrify v0.5.5/go.mod h1:GfRoy7NHsuR/ogmZtbExdJXUwbfwcxPrS9xzkyy2J/c=
github.com/hashicorp/vault-plugin-auth-cf v0.5.4 h1:2wl+qK7cLpr4u/lkv5DgvkNoKKhHC69H1QmoXOnArLw=
github.com/hashicorp/vault-plugin-auth-cf v0.5.4/go.mod h1:idkFYHc6ske2BE7fe00SpH+SBIlqDKz8vk/IPLJuX2o=
github.com/hashicorp/vault-plugin-auth-gcp v0.5.1/go.mod h1:eLj92eX8MPI4vY1jaazVLF2sVbSAJ3LRHLRhF/pUmlI=
github.com/hashicorp/vault-plugin-auth-gcp v0.6.1 h1:WXTuja3WC2BdZekYCnzuZGoVvZTAGH8kSDUHzOK2PQY=
github.com/hashicorp/vault-plugin-auth-gcp v0.6.1/go.mod h1:8eBRzg+JIhAaDBfDndDAQKIhDrQ3WW8OPklxAYftNFs=
github.com/hashicorp/vault-plugin-auth-jwt v0.6.2 h1:fp6Rk89iPjDS8dyEK7lEauYE/UhkgkHbmwRZKuQA01U=
github.com/hashicorp/vault-plugin-auth-jwt v0.6.2/go.mod h1:SFadxIfoLGzugEjwUUmUaCGbsYEz2/jJymZDDQjEqYg=

@ -3,7 +3,6 @@ package base
import (
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
@ -37,7 +36,7 @@ type WorkerAuthCertInfo struct {
}
// Factory is the factory function to create a listener.
type ListenerFactory func(*configutil.Listener, io.Writer, hclog.Logger, cli.Ui) (*alpnmux.ALPNMux, map[string]string, reloadutil.ReloadFunc, error)
type ListenerFactory func(*configutil.Listener, hclog.Logger, cli.Ui) (*alpnmux.ALPNMux, map[string]string, reloadutil.ReloadFunc, error)
// BuiltinListeners is the list of built-in listener types.
var BuiltinListeners = map[string]ListenerFactory{
@ -46,16 +45,16 @@ var BuiltinListeners = map[string]ListenerFactory{
// New creates a new listener of the given type with the given
// configuration. The type is looked up in the BuiltinListeners map.
func NewListener(l *configutil.Listener, w io.Writer, logger hclog.Logger, ui cli.Ui) (*alpnmux.ALPNMux, map[string]string, reloadutil.ReloadFunc, error) {
func NewListener(l *configutil.Listener, logger hclog.Logger, ui cli.Ui) (*alpnmux.ALPNMux, map[string]string, reloadutil.ReloadFunc, error) {
f, ok := BuiltinListeners[l.Type]
if !ok {
return nil, nil, nil, fmt.Errorf("unknown listener type: %q", l.Type)
}
return f(l, w, logger, ui)
return f(l, logger, ui)
}
func tcpListenerFactory(l *configutil.Listener, _ io.Writer, logger hclog.Logger, ui cli.Ui) (*alpnmux.ALPNMux, map[string]string, reloadutil.ReloadFunc, error) {
func tcpListenerFactory(l *configutil.Listener, logger hclog.Logger, ui cli.Ui) (*alpnmux.ALPNMux, map[string]string, reloadutil.ReloadFunc, error) {
if l.Address == "" {
if len(l.Purpose) == 1 && l.Purpose[0] == "cluster" {
l.Address = "127.0.0.1:9201"
@ -72,6 +71,15 @@ func tcpListenerFactory(l *configutil.Listener, _ io.Writer, logger hclog.Logger
bindProto = "tcp4"
}
if l.RandomPort {
colon := strings.Index(l.Address, ":")
if colon != -1 {
// colon+1 because it needs to end in a colon to be automatically
// assigned by Go
l.Address = l.Address[0 : colon+1]
}
}
ln, err := net.Listen(bindProto, l.Address)
if err != nil {
return nil, nil, nil, err

@ -17,6 +17,7 @@ import (
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-hclog"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/internalshared/configutil"
"github.com/hashicorp/vault/internalshared/gatedwriter"
"github.com/hashicorp/vault/internalshared/reloadutil"
@ -239,7 +240,7 @@ func (b *Server) SetupListeners(ui cli.Ui, config *configutil.SharedConfig) erro
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
}
lnMux, props, reloadFunc, err := NewListener(lnConfig, b.GatedWriter, b.Logger, ui)
lnMux, props, reloadFunc, err := NewListener(lnConfig, b.Logger, ui)
if err != nil {
return fmt.Errorf("Error initializing listener of type %s: %w", lnConfig.Type, err)
}
@ -356,16 +357,18 @@ func (b *Server) SetupKMSes(ui cli.Ui, config *configutil.SharedConfig, purposes
return nil
}
func (b *Server) RunShutdownFuncs(ui cli.Ui) {
func (b *Server) RunShutdownFuncs() error {
var mErr *multierror.Error
for _, f := range b.ShutdownFuncs {
if err := f(); err != nil {
ui.Error(fmt.Sprintf("Error running a shutdown task: %s", err.Error()))
mErr = multierror.Append(mErr, err)
}
}
return mErr.ErrorOrNil()
}
func (b *Server) CreateDevDatabase(dialect string) error {
c, url, err := db.InitDbInDocker(dialect)
c, url, container, err := db.InitDbInDocker(dialect)
if err != nil {
c()
return fmt.Errorf("unable to start dev database with dialect %s: %w", dialect, err)
@ -376,6 +379,10 @@ func (b *Server) CreateDevDatabase(dialect string) error {
b.InfoKeys = append(b.InfoKeys, "dev database url")
b.Info["dev database url"] = b.DevDatabaseUrl
if container != "" {
b.InfoKeys = append(b.InfoKeys, "dev database container")
b.Info["dev database container"] = strings.TrimPrefix(container, "/")
}
dbase, err := gorm.Open(dialect, url)
if err != nil {

@ -259,7 +259,11 @@ func (c *Command) Run(args []string) int {
c.ShutdownFuncs = append(c.ShutdownFuncs, c.DestroyDevDatabase)
}
defer c.RunShutdownFuncs(c.UI)
defer func() {
if err := c.RunShutdownFuncs(); err != nil {
c.UI.Error(fmt.Errorf("Error running shutdown tasks: %w", err).Error())
}
}()
c.PrintInfo(c.UI, "controller")
c.ReleaseLogGate()

@ -220,7 +220,11 @@ func (c *Command) Run(args []string) int {
return 1
}
defer c.RunShutdownFuncs(c.UI)
defer func() {
if err := c.RunShutdownFuncs(); err != nil {
c.UI.Error(fmt.Errorf("Error running shutdown tasks: %w", err).Error())
}
}()
if err := c.CreateDevDatabase("postgres"); err != nil {
c.UI.Error(fmt.Errorf("Error creating dev database container: %w", err).Error())

@ -176,7 +176,11 @@ func (c *Command) Run(args []string) int {
return 1
}
defer c.RunShutdownFuncs(c.UI)
defer func() {
if err := c.RunShutdownFuncs(); err != nil {
c.UI.Error(fmt.Errorf("Error running shutdown tasks: %w", err).Error())
}
}()
c.PrintInfo(c.UI, "worker")
c.ReleaseLogGate()

@ -57,31 +57,31 @@ func Migrate(connectionUrl string, migrationsDirectory string) error {
}
// InitDbInDocker initializes the data store within docker or an existing PG_URL
func InitDbInDocker(dialect string) (cleanup func() error, retURL string, err error) {
func InitDbInDocker(dialect string) (cleanup func() error, retURL, container string, err error) {
switch dialect {
case "postgres":
if os.Getenv("PG_URL") != "" {
if err := InitStore(dialect, func() error { return nil }, os.Getenv("PG_URL")); err != nil {
return func() error { return nil }, os.Getenv("PG_URL"), fmt.Errorf("error initializing store: %w", err)
return func() error { return nil }, os.Getenv("PG_URL"), "", fmt.Errorf("error initializing store: %w", err)
}
return func() error { return nil }, os.Getenv("PG_URL"), nil
return func() error { return nil }, os.Getenv("PG_URL"), "", nil
}
}
c, url, err := StartDbInDocker(dialect)
c, url, container, err := StartDbInDocker(dialect)
if err != nil {
return func() error { return nil }, "", fmt.Errorf("could not start docker: %w", err)
return func() error { return nil }, "", "", fmt.Errorf("could not start docker: %w", err)
}
if err := InitStore(dialect, c, url); err != nil {
return func() error { return nil }, "", fmt.Errorf("error initializing store: %w", err)
return func() error { return nil }, "", "", fmt.Errorf("error initializing store: %w", err)
}
return c, url, nil
return c, url, container, nil
}
// StartDbInDocker
func StartDbInDocker(dialect string) (cleanup func() error, retURL string, err error) {
func StartDbInDocker(dialect string) (cleanup func() error, retURL, container string, err error) {
pool, err := dockertest.NewPool("")
if err != nil {
return func() error { return nil }, "", fmt.Errorf("could not connect to docker: %w", err)
return func() error { return nil }, "", "", fmt.Errorf("could not connect to docker: %w", err)
}
var resource *dockertest.Resource
@ -94,7 +94,7 @@ func StartDbInDocker(dialect string) (cleanup func() error, retURL string, err e
panic(fmt.Sprintf("unknown dialect %q", dialect))
}
if err != nil {
return func() error { return nil }, "", fmt.Errorf("could not start resource: %w", err)
return func() error { return nil }, "", "", fmt.Errorf("could not start resource: %w", err)
}
cleanup = func() error {
@ -115,10 +115,10 @@ func StartDbInDocker(dialect string) (cleanup func() error, retURL string, err e
defer db.Close()
return nil
}); err != nil {
return func() error { return nil }, "", fmt.Errorf("could not connect to docker: %w", err)
return func() error { return nil }, "", "", fmt.Errorf("could not connect to docker: %w", err)
}
return cleanup, url, nil
return cleanup, url, resource.Container.Name, nil
}
// InitStore will execute the migrations needed to initialize the store for tests

@ -5,7 +5,7 @@ import (
)
func TestOpen(t *testing.T) {
cleanup, url, err := StartDbInDocker("postgres")
cleanup, url, _, err := StartDbInDocker("postgres")
if err != nil {
t.Fatal(err)
}
@ -56,7 +56,7 @@ func TestOpen(t *testing.T) {
}
func TestMigrate(t *testing.T) {
cleanup, url, err := StartDbInDocker("postgres")
cleanup, url, _, err := StartDbInDocker("postgres")
if err != nil {
t.Fatal(err)
}

@ -16,7 +16,7 @@ func TestSetup(t *testing.T, dialect string) (func() error, *gorm.DB, string) {
cleanup := func() error { return nil }
var url string
var err error
cleanup, url, err = InitDbInDocker(dialect)
cleanup, url, _, err = InitDbInDocker(dialect)
if err != nil {
t.Fatal(err)
}
@ -43,4 +43,3 @@ func TestWrapper(t *testing.T) wrapping.Wrapper {
}
return root
}

@ -0,0 +1,190 @@
package controller
import (
"context"
"fmt"
"net"
"testing"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/watchtower/api"
"github.com/hashicorp/watchtower/internal/cmd/base"
"github.com/hashicorp/watchtower/internal/cmd/config"
)
// TestController wraps a base.Server and Controller to provide a
// fully-programmatic controller for tests. Error checking (for instance, for
// valid config) is not stringent at the moment.
type TestController struct {
b *base.Server
c *Controller
t *testing.T
client *api.Client
ctx context.Context
cancel context.CancelFunc
}
// Controller returns the underlying controller
func (tc *TestController) Controller() *Controller {
return tc.c
}
func (tc *TestController) Client() *api.Client {
return tc.client
}
func (tc *TestController) Context() context.Context {
return tc.ctx
}
func (tc *TestController) Cancel() {
tc.cancel()
}
func (tc *TestController) buildClient() {
var apiLn *base.ServerListener
for _, listener := range tc.b.Listeners {
if listener.Config.Purpose[0] == "api" {
apiLn = listener
break
}
}
if apiLn == nil {
tc.t.Fatal("could not find api listener")
}
tcpAddr, ok := apiLn.Mux.Addr().(*net.TCPAddr)
if !ok {
tc.t.Fatal("could not parse address as a TCP addr")
}
addr := fmt.Sprintf("http://%s:%d", tcpAddr.IP.String(), tcpAddr.Port)
client, err := api.NewClient(nil)
if err != nil {
tc.t.Fatal(fmt.Errorf("error creating client: %w", err))
}
if err := client.SetAddr(addr); err != nil {
tc.t.Fatal(fmt.Errorf("error setting client address: %w", err))
}
tc.client = client
}
// Shutdown runs any cleanup functions; be sure to run this after your test is
// done
func (tc *TestController) Shutdown() {
if tc.b != nil {
close(tc.b.ShutdownCh)
}
tc.cancel()
if tc.c != nil {
if err := tc.c.Shutdown(); err != nil {
tc.t.Error(err)
}
}
if tc.b != nil {
if err := tc.b.RunShutdownFuncs(); err != nil {
tc.t.Error(err)
}
if tc.b.DestroyDevDatabase() != nil {
if err := tc.b.DestroyDevDatabase(); err != nil {
tc.t.Error(err)
}
}
}
}
type TestControllerOpts struct {
// Config; if not provided a dev one will be created
Config *config.Config
// DefaultOrgId is the default org ID to use, if set. Can also be provided
// in the normal config.
DefaultOrgId string
// DisableDatabaseCreation can be set true to disable creating a dev
// database
DisableDatabaseCreation bool
}
func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController {
ctx, cancel := context.WithCancel(context.Background())
tc := &TestController{
t: t,
ctx: ctx,
cancel: cancel,
}
if opts == nil {
opts = new(TestControllerOpts)
}
// Base server
tc.b = base.NewServer(nil)
tc.b.Command = &base.Command{
ShutdownCh: make(chan struct{}),
}
// Get dev config, or use a provided one
var err error
if opts.Config == nil {
opts.Config, err = config.DevController()
if err != nil {
t.Fatal(err)
}
}
// Set default org ID, preferring one passed in from opts over config
if opts.Config.DefaultOrgId != "" {
tc.b.DefaultOrgId = opts.Config.DefaultOrgId
}
if opts.DefaultOrgId != "" {
tc.b.DefaultOrgId = opts.DefaultOrgId
}
// Start a logger
tc.b.Logger = hclog.New(&hclog.LoggerOptions{
Level: hclog.Trace,
})
// Set up KMSes
if err := tc.b.SetupKMSes(nil, opts.Config.SharedConfig, []string{"controller", "worker-auth"}); err != nil {
t.Fatal(err)
}
// Ensure the listeners use random port allocation
for _, listener := range opts.Config.Listeners {
listener.RandomPort = true
}
if err := tc.b.SetupListeners(nil, opts.Config.SharedConfig); err != nil {
t.Fatal(err)
}
if !opts.DisableDatabaseCreation {
if err := tc.b.CreateDevDatabase("postgres"); err != nil {
t.Fatal(err)
}
}
conf := &Config{
RawConfig: opts.Config,
Server: tc.b,
}
tc.c, err = New(conf)
if err != nil {
tc.Shutdown()
t.Fatal(err)
}
tc.buildClient()
if err := tc.c.Start(); err != nil {
tc.Shutdown()
t.Fatal(err)
}
return tc
}

@ -0,0 +1,13 @@
package controller
import (
"testing"
)
func Test_TestController(t *testing.T) {
t.Run("startup and shutdown", func(t *testing.T) {
t.Parallel()
tc := NewTestController(t, nil)
defer tc.Shutdown()
})
}
Loading…
Cancel
Save