func/client_test.go

680 lines
20 KiB
Go

package faas_test
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/boson-project/faas"
"github.com/boson-project/faas/mock"
)
// TestNew ensures that instantiation succeeds or fails as expected.
func TestNew(t *testing.T) {
// New client with all defaults
_, err := faas.New()
if err != nil {
t.Fatal(err)
}
// New client with optional verbosity enabled
_, err = faas.New(faas.WithVerbose(true))
if err != nil {
t.Fatal(err)
}
}
// TestNewWithInterferingFiles asserts that attempting to create a new client rooted
// to a directory with any visible files or any known contentious files (configs) fails.
func TestNewWithInterferingFiles(t *testing.T) {
// TODO
}
// TestCreate ensures that creation of a supported runtime succeeds with all
// defaults (base case).
func TestCreate(t *testing.T) {
// Client with all defaults other than an initializer that verifies the
// specified runtime.
client, err := faas.New(
faas.WithInitializer(mock.NewInitializer()))
if err != nil {
t.Fatal(err)
}
// Create the test function root
root := "testdata/example.com/admin"
err = os.MkdirAll(root, 0700)
if err != nil {
panic(err)
}
defer os.RemoveAll(root)
// A supported langauge should not error
if err := client.Create("go", "", "", root); err != nil {
t.Fatal(err)
}
}
// TestCreateUnderivableName ensures that attempting to create a new function
// when the name is underivable (and no explicit name is provided) generates
// an error.
func TestCreateUnderivableName(t *testing.T) {
// Create the test function root
root := "testdata/example.com/admin"
err := os.MkdirAll(root, 0700)
if err != nil {
panic(err)
}
defer os.RemoveAll(root)
// Instantiation without an explicit service name, but no derivable service
// name (because of limiting path recursion) should fail.
client, err := faas.New(
faas.WithDomainSearchLimit(0)) // limit ability to derive from path.
if err != nil {
t.Fatal(err)
}
// create a Function with a missing name, but when the name is
// underivable (in this case due to limited recursion, but would equally
// apply if run from /tmp or similar)
if err := client.Create("go", "", "", ""); err == nil {
t.Fatal("did not receive expected error")
}
}
// TestCreateMissingRuntime ensures that instantiation fails if the required
// runtime parameter is not passed to Create.
func TestCreateMissingRuntime(t *testing.T) {
client, err := faas.New(
faas.WithInitializer(mock.NewInitializer()))
if err != nil {
t.Fatal(err)
}
// create a Function call missing runtime should error
if err := client.Create("", "", "", ""); err == nil {
t.Fatal("missing runtime did not generate error")
}
}
// TestCreateUnsupportedRuntime ensures that instantiation fails if the required
// runtime parameter is of an unsupported runtime.
func TestCreateUnsupportedRuntime(t *testing.T) {
client, err := faas.New(
faas.WithInitializer(mock.NewInitializer())) // validtes runtime passed
if err != nil {
t.Fatal(err)
}
// create a Function call witn an unsupported runtime should bubble
// the error generated by the underlying initializer.
if err := client.Create("cobol", "", "", ""); err == nil {
t.Fatal("unsupported runtime did not generate error")
}
}
// TestCreateDelegeates ensures that a call to Create invokes the Function
// Initializer, Builder, Pusher and Deployer with expected parameters.
func TestCreateDelegates(t *testing.T) {
var (
root = "testdata/example.com/admin" // .. in which to initialize
name = "admin.example.com" // expected to be derived
image = "my.hub/user/imagestamp" // expected image
route = "https://admin.example.com/" // expected final route
initializer = mock.NewInitializer()
builder = mock.NewBuilder()
pusher = mock.NewPusher()
deployer = mock.NewDeployer()
)
// Create the test function root
err := os.MkdirAll(root, 0700)
if err != nil {
panic(err)
}
defer os.RemoveAll(root)
client, err := faas.New(
faas.WithInitializer(initializer), // will receive the final value
faas.WithBuilder(builder), // builds an image
faas.WithPusher(pusher), // pushes images to a registry
faas.WithDeployer(deployer), // deploys images as a running service
)
if err != nil {
t.Fatal(err)
}
// Register function delegates on the mocks which validate assertions
// -------------
// The initializer should receive the name expected from the path,
// the passed runtime, and an absolute path to the funciton soruce.
initializer.InitializeFn = func(runtime, context, path string) error {
if runtime != "go" {
t.Fatalf("initializer expected runtime 'go', got '%v'", runtime)
}
if context != "" {
t.Fatalf("initializer expected empty context template name, got '%v'", name)
}
expectedPath, err := filepath.Abs("./testdata/example.com/admin")
if err != nil {
t.Fatal(err)
}
if path != expectedPath {
t.Fatalf("initializer expected path '%v', got '%v'", expectedPath, path)
}
return nil
}
// The builder should be invoked with a service name and path to its source
// function code. For this test, it is a name derived from the test path.
// An example image name is returned.
builder.BuildFn = func(name2, path2 string) (string, error) {
if name != name2 {
t.Fatalf("builder expected name %v, got '%v'", name, name2)
}
expectedPath, err := filepath.Abs(root)
if err != nil {
t.Fatal(err)
}
if path2 != expectedPath {
t.Fatalf("builder expected path '%v', got '%v'", expectedPath, root)
}
// The final image name will be determined by the builder implementation,
// but whatever it is (in this case fabricated); it should be returned
// and later provided to the pusher.
return image, nil
}
// The pusher should be invoked with the image to push.
pusher.PushFn = func(image2 string) error {
if image2 != image {
t.Fatalf("pusher expected image '%v', got '%v'", image, image2)
}
// image of given name wouold be pushed to the configured registry.
return nil
}
// The deployer should be invoked with the service name and image, and return
// the final accessible address.
deployer.DeployFn = func(name2, image2 string) (address string, err error) {
if name2 != name {
t.Fatalf("deployer expected name '%v', got '%v'", name, name2)
}
if image2 != image {
t.Fatalf("deployer expected image '%v', got '%v'", image, image2)
}
// service of given name would be deployed using the given image and
// allocated route returned.
return route, nil
}
// Invocation
// -------------
// Invoke the creation, triggering the function delegates, and
// perform follow-up assertions that the functions were indeed invoked.
if err := client.Create("go", "", "", root); err != nil {
t.Fatal(err)
}
// Confirm that each delegate was invoked.
if !initializer.InitializeInvoked {
t.Fatal("initializer was not invoked")
}
if !builder.BuildInvoked {
t.Fatal("builder was not invoked")
}
if !pusher.PushInvoked {
t.Fatal("pusher was not invoked")
}
if !deployer.DeployInvoked {
t.Fatal("deployer was not invoked")
}
}
// TestCreateLocal ensures that when set to local-only mode, Create only invokes
// the initializer and builder.
func TestCreateLocal(t *testing.T) {
var (
root = "testdata/example.com/admin"
initializer = mock.NewInitializer()
builder = mock.NewBuilder()
pusher = mock.NewPusher()
deployer = mock.NewDeployer()
dnsProvider = mock.NewDNSProvider()
)
// Create the test function root
err := os.MkdirAll(root, 0700)
if err != nil {
panic(err)
}
defer os.RemoveAll(root)
// Create the test function root
err = os.MkdirAll(root, 0700)
if err != nil {
panic(err)
}
defer os.RemoveAll(root)
client, err := faas.New(
faas.WithInitializer(initializer), // will receive the final value
faas.WithBuilder(builder), // builds an image
faas.WithPusher(pusher), // pushes images to a registry
faas.WithDeployer(deployer), // deploys images as a running service
faas.WithDNSProvider(dnsProvider), // will receive the final value
faas.WithLocal(true), // set to local function mode.
)
if err != nil {
t.Fatal(err)
}
// Create a new Function
if err := client.Create("go", "", "", root); err != nil {
t.Fatal(err)
}
// Ensure that none of the remote delegates were invoked
if pusher.PushInvoked {
t.Fatal("Push invoked in local mode.")
}
if deployer.DeployInvoked {
t.Fatal("Deploy invoked in local mode.")
}
if dnsProvider.ProvideInvoked {
t.Fatal("DNS provider invoked in local mode.")
}
}
// TestCreateInternal ensures that when set to internal mode, Creation invokes the deployer with the "no public route" option and subsequent updates also are flagged to not create the route.
func TestCreateInternal(t *testing.T) {
fmt.Printf("TODO: TestCreateInternal")
}
// TestCreateDomain ensures that the effective domain is dervied from
// directory structure. See the unit tests for pathToDomain for details.
func TestCreateDomain(t *testing.T) {
// Create the test function root
root := "testdata/example.com/admin"
err := os.MkdirAll(root, 0700)
if err != nil {
panic(err)
}
defer os.RemoveAll(root)
// the mock dns provider does nothing but receive the caluclated
// domain name via it's Provide(domain) method, which is the value
// being tested here.
dnsProvider := mock.NewDNSProvider()
client, err := faas.New(
faas.WithDomainSearchLimit(1), // Limit recursion to one level
faas.WithDNSProvider(dnsProvider), // will receive the final value
)
if err != nil {
t.Fatal(err)
}
if err := client.Create("go", "", "", root); err != nil {
t.Fatal(err)
}
if !dnsProvider.ProvideInvoked {
t.Fatal("dns provider was not invoked")
}
if dnsProvider.NameRequested != "admin.example.com" {
t.Fatalf("expected 'example.com', got '%v'", dnsProvider.NameRequested)
}
}
// TestCreateSubdomain ensures that a subdirectory is interpreted as a subdomain
// when calculating final domain. See the unit tests for pathToDomain for the
// details and edge cases of this caluclation.
func TestCreateSubdomain(t *testing.T) {
// Create the test function root
root := "testdata/example.com/admin"
err := os.MkdirAll(root, 0700)
if err != nil {
panic(err)
}
defer os.RemoveAll(root)
dnsProvider := mock.NewDNSProvider()
client, err := faas.New(
faas.WithDomainSearchLimit(2),
faas.WithDNSProvider(dnsProvider),
)
if err != nil {
t.Fatal(err)
}
if err := client.Create("go", "", "", root); err != nil {
t.Fatal(err)
}
if !dnsProvider.ProvideInvoked {
t.Fatal("dns provider was not invoked")
}
if dnsProvider.NameRequested != "admin.example.com" {
t.Fatalf("expected 'admin.example.com', got '%v'", dnsProvider.NameRequested)
}
}
// TestRun ensures that the runner is invoked with the absolute path requested.
func TestRun(t *testing.T) {
// a previously-initilized function's root
root := "testdata/example.com/www"
runner := mock.NewRunner()
client, err := faas.New(
faas.WithRunner(runner))
if err != nil {
t.Fatal(err)
}
if err := client.Run(root); err != nil {
t.Fatal(err)
}
if !runner.RunInvoked {
t.Fatal("run did not invoke the runner")
}
absRoot, err := filepath.Abs(root)
if err != nil {
t.Fatal(err)
}
if runner.RootRequested != absRoot {
t.Fatalf("expected path '%v', got '%v'", absRoot, runner.RootRequested)
}
}
// TestUpdate ensures that the updater properly invokes the build/push/deploy
// process, erroring if run on a directory uncreated.
func TestUpdate(t *testing.T) {
var (
root = "testdata/example.com/www" // .. expected to be initialized
name = "www.example.com" // expected to be derived
image = "my.hub/user/admin.exampe.com" // expected image
builder = mock.NewBuilder()
pusher = mock.NewPusher()
updater = mock.NewUpdater()
)
client, err := faas.New(
faas.WithBuilder(builder), // builds an image
faas.WithPusher(pusher), // pushes images to a registry
faas.WithUpdater(updater), // updates deployed image
)
if err != nil {
t.Fatal(err)
}
// Register function delegates on the mocks which validate assertions
// -------------
// The builder should be invoked with a service name and path to its source
// function code. For this test, it is a name derived from the test path.
// An example image name is returned.
builder.BuildFn = func(name2, path2 string) (string, error) {
if name != name2 {
t.Fatalf("builder expected name %v, got '%v'", name, name2)
}
// The final image name will be determined by the builder implementation,
// but whatever it is (in this case fabricated); it should be returned
// and later provided to the pusher.
return image, nil
}
// The pusher should be invoked with the image to push.
pusher.PushFn = func(image2 string) error {
if image2 != image {
t.Fatalf("pusher expected image '%v', got '%v'", image, image2)
}
// image of given name wouold be pushed to the configured registry.
return nil
}
// The updater should be invoked with the service name and image.
// Actual logic of updating is an implementation detail.
updater.UpdateFn = func(name2, image2 string) error {
if name2 != name {
t.Fatalf("updater expected name '%v', got '%v'", name, name2)
}
if image2 != image {
t.Fatalf("updater expected image '%v', got '%v'", image, image2)
}
return nil
}
// Invocation
// -------------
// Invoke the creation, triggering the function delegates, and
// perform follow-up assertions that the functions were indeed invoked.
if err := client.Update(root); err != nil {
t.Fatal(err)
}
if !builder.BuildInvoked {
t.Fatal("builder was not invoked")
}
if !pusher.PushInvoked {
t.Fatal("pusher was not invoked")
}
if !updater.UpdateInvoked {
t.Fatal("updater was not invoked")
}
}
// TestRemove ensures that the remover is invoked with the name provided, and that
func TestRemove(t *testing.T) {
var (
name = "admin.example.com"
remover = mock.NewRemover()
)
client, err := faas.New(faas.WithRemover(remover))
if err != nil {
t.Fatal(err)
}
remover.RemoveFn = func(name2 string) error {
if name2 != name {
t.Fatalf("remover expected name '%v' got '%v'", name, name2)
}
return nil
}
// Call with explicit name and no root.
if err := client.Remove(name, ""); err != nil {
t.Fatal(err)
}
// Call with explicit name and root; name should take precidence.
if err := client.Remove(name, "testdata/example.com/www"); err != nil {
t.Fatal(err)
}
}
// TestRemoveUninitializedFailes ensures that attempting to remove a function
// by path only (no name) fails unless the funciton has been initialized. I.e.
// the name will not be derived from path and the function removed by thi
// derived name; that could be unexpected and destructive.
func TestRemoveUninitializedFails(t *testing.T) {
var (
root = "testdata/example.com/admin"
remover = mock.NewRemover()
)
err := os.MkdirAll(root, 0700)
if err != nil {
panic(err)
}
defer os.RemoveAll(root)
// Create a remover delegate which fails if invoked.
remover.RemoveFn = func(name string) error {
return fmt.Errorf("remove invoked for unitialized function %v", name)
}
// Instantiate the client with the failing remover.
client, err := faas.New(faas.WithRemover(remover))
if err != nil {
t.Fatal(err)
}
// Attempt to remove by path, expecting an error.
if err := client.Remove("", root); err == nil {
t.Fatalf("did not received expeced error removing an uninitialized func")
}
}
// TestRemoveDefaultCurrent ensures that, if a name is not provided but a path is,
// the funciton defined by path will be removed.
// Note that the prior test RemoveUninitializedFails ensures that only
// initialized functions are removed, which prevents the case of a function
// being removed by path-derived name without having been initialized (an
// easily destrcutive and likely unwanted behavior)
func TestRemoveDefaultCurrent(t *testing.T) {
var (
root = "testdata/example.com/www" // an initialized function (has config)
remover = mock.NewRemover()
)
// remover delegate which ensures the correct name is received.
remover.RemoveFn = func(name2 string) error {
if name2 != "www.example.com" {
t.Fatalf("remover expected name 'www.example.com' got '%v'", name2)
}
return nil
}
client, err := faas.New(faas.WithRemover(remover))
if err != nil {
t.Fatal(err)
}
// Without a name provided, but with a path, the function will be loaded and
// its name used by default. Note that it will fail if uninitialized (no path
// derivation for removal.)
if err := client.Remove("", root); err != nil {
t.Fatal(err)
}
}
// TestWithName ensures that an explicitly passed name is used in leau of the
// path derived name when provided, and persists through instantiations.
// This also ensures that an initialized service function's name persists if
// the path is changed after creation.
func TestWithName(t *testing.T) {
// Explicit name to use
name := "service.example.com"
// Path which would derive to service.groupA.example.com were it not for the
// explicitly provided name.
path := "testdata/example.com/groupA/service"
err := os.MkdirAll(path, 0700)
if err != nil {
panic(err)
}
defer os.RemoveAll(path)
initializer := mock.NewInitializer()
c, err := faas.New(
faas.WithInitializer(initializer))
if err != nil {
t.Fatal(err)
}
// Ensure that initializing receives the specified path.
initializer.InitializeFn = func(runtime, context, path2 string) error {
expectedPath, err := filepath.Abs(path)
if err != nil {
t.Fatal(err)
}
if path2 != expectedPath {
t.Fatalf("initializer expected path '%v', got '%v'", expectedPath, path2)
}
return nil
}
// Create the service with the explict name at the non-matching path.
if err := c.Create("go", "", name, path); err != nil {
t.Fatal(err)
}
// TODO: create a Function about the path and check the name is loaded.
// Create a new client about the now initialized path and test that
// the explicitly-provided name is sent to the updater, proving that
// it was
updater := mock.NewUpdater()
c, err = faas.New(
faas.WithUpdater(updater))
if err != nil {
t.Fatal(err)
}
// Ensure that updating takes place using the previously initialized name
updater.UpdateFn = func(name2, image string) error {
if name2 != name {
t.Fatalf("updater expected name '%v', got '%v'", name, name2)
}
return nil
}
// Invoke update
if err := c.Update(path); err != nil {
t.Fatal(err)
}
}
// TestList ensures that the client invokes the configured lister.
func TestList(t *testing.T) {
var lister = mock.NewLister()
client, err := faas.New(
faas.WithLister(lister)) // lists deployed service functions.
if err != nil {
t.Fatal(err)
}
_, err = client.List()
if err != nil {
t.Fatal(err)
}
if !lister.ListInvoked {
t.Fatal("list did not invoke lister implementation")
}
}
// TestListOutsideRoot ensures that a call to a function (in this case list)
// that is not contextually dependent on being associated with a function,
// can be run from anywhere, thus ensuring that the client itself makes
// a distinction between function-scoped methods and not.
func TestListOutsideRoot(t *testing.T) {
var lister = mock.NewLister()
// Instantiate in the current working directory, with no name, and explicitly
// disallowing name path inferrence by limiting recursion. This emulates
// running the client (and subsequently list) from some arbitrary location
// without a derivable funciton context.
client, err := faas.New(
faas.WithDomainSearchLimit(0),
faas.WithLister(lister),
)
if err != nil {
t.Fatal(err)
}
_, err = client.List()
if err != nil {
t.Fatal(err)
}
if !lister.ListInvoked {
t.Fatal("list did not invoke lister implementation")
}
}