feat: test suite

- updated tests to new api throughout
- expanded tests where appropriate
- lint issues
- minor code review comments addressed
This commit is contained in:
Luke K 2020-08-29 00:47:24 +09:00
parent 6e0f4caa93
commit d33fb2d694
No known key found for this signature in database
GPG Key ID: 4896F75BAF2E1966
25 changed files with 669 additions and 959 deletions

2
.gitignore vendored
View File

@ -7,3 +7,5 @@
target
node_modules
/coverage.out
/bin

192
client.go
View File

@ -18,8 +18,6 @@ const (
// Client for managing Function instances.
type Client struct {
verbose bool // print verbose logs
local bool // Run in local-only mode
internal bool // Deploy without publicly accessible route
builder Builder // Builds a runnable image from Function source
pusher Pusher // Pushes the image assocaited with a Function.
deployer Deployer // Deploys a Function
@ -151,20 +149,6 @@ func WithVerbose(v bool) Option {
}
}
// WithLocal sets the local mode
func WithLocal(l bool) Option {
return func(c *Client) {
c.local = l
}
}
// WithInternal sets the internal (no public route) mode for deployed Function.
func WithInternal(i bool) Option {
return func(c *Client) {
c.internal = i
}
}
// WithBuilder provides the concrete implementation of a builder.
func WithBuilder(d Builder) Option {
return func(c *Client) {
@ -266,6 +250,60 @@ func WithRepository(repository string) Option {
}
}
// Create a Function.
// Includes Initialization, Building, and Deploying.
func (c *Client) Create(cfg Function) (err error) {
c.progressListener.SetTotal(4)
defer c.progressListener.Done()
// Initialize, writing out a template implementation and a config file.
// TODO: the Function's Initialize parameters are slightly different than
// the Initializer interface, and can thus cause confusion (one passes an
// optional name the other passes root path). This could easily cause
// confusion and thus we may want to rename Initalizer to the more specific
// task it performs: ContextTemplateWriter or similar.
c.progressListener.Increment("Initializing new Function project")
err = c.Initialize(cfg)
if err != nil {
return
}
// Load the now-initialized Function.
f, err := NewFunction(cfg.Root)
if err != nil {
return
}
// Build the now-initialized Function
c.progressListener.Increment("Building container image")
if err = c.Build(f.Root); err != nil {
return
}
// Deploy the initialized Function, returning its publicly
// addressible name for possible registration.
c.progressListener.Increment("Deploying Function to cluster")
if err = c.Deploy(f.Root); err != nil {
return
}
// Create an external route to the Function
c.progressListener.Increment("Creating route to Function")
if err = c.Route(f.Root); err != nil {
return
}
c.progressListener.Complete("Create complete")
// TODO: use the knative client during deployment such that the actual final
// route can be returned from the deployment step, passed to the DNS Router
// for routing actual traffic, and returned here.
if c.verbose {
fmt.Printf("https://%v/\n", f.Name)
}
return
}
// Initialize creates a new Function project locally using the settings
// provided on a Function object.
func (c *Client) Initialize(cfg Function) (err error) {
@ -275,17 +313,8 @@ func (c *Client) Initialize(cfg Function) (err error) {
return
}
// Do not initialize if already initialized.
if f.Initialized() {
err = fmt.Errorf("Function at '%v' already initialized.", cfg.Root)
return
}
// Do not re-initialize unless the directory is empty This is to protect the
// user from inadvertently overwriting local files which share the same name
// as those in the applied template if, for instance, an incorrect path is
// supplied for the new Function. This assertion is that the target path is
// empty of all but unrelated hidden files.
// Assert the specified root is free of visible files and contentious
// hidden files (the ConfigFile, which indicates it is already initialized)
if err = assertEmptyRoot(f.Root); err != nil {
return
}
@ -304,8 +333,6 @@ func (c *Client) Initialize(cfg Function) (err error) {
f.Runtime = cfg.Runtime
if f.Runtime == "" {
f.Runtime = DefaultRuntime
} else {
f.Runtime = cfg.Runtime
}
// Assert trigger was provided, or default.
@ -404,61 +431,6 @@ func (c *Client) Route(path string) (err error) {
return c.dnsProvider.Provide(f)
}
// Create a Function of the given runtime.
// Name and Root are optional:
// Name is derived from root if possible.
// Root is defaulted to the current working directory.
func (c *Client) Create(cfg Function) (err error) {
c.progressListener.SetTotal(4)
defer c.progressListener.Done()
// Initialize, writing out a template implementation and a config file.
// TODO: the Function's Initialize parameters are slightly different than
// the Initializer interface, and can thus cause confusion (one passes an
// optional name the other passes root path). This could easily cause
// confusion and thus we may want to rename Initalizer to the more specific
// task it performs: ContextTemplateWriter or similar.
c.progressListener.Increment("Initializing new Function project")
err = c.Initialize(cfg)
if err != nil {
return
}
// Load the now-initialized Function.
f, err := NewFunction(cfg.Root)
if err != nil {
return
}
// Build the now-initialized Function
c.progressListener.Increment("Building container image")
err = c.Build(f.Root)
if err != nil {
return
}
// Deploy the initialized Function, returning its publicly
// addressible name for possible registration.
c.progressListener.Increment("Deploying Function to cluster")
if err = c.Deploy(f.Root); err != nil {
return
}
// Create an external route to the Function
c.progressListener.Increment("Creating route to Function")
if err = c.Route(f.Root); err != nil {
return
}
c.progressListener.Complete("Create complete")
// TODO: use the knative client during deployment such that the actual final
// route can be returned from the deployment step, passed to the DNS Router
// for routing actual traffic, and returned here.
fmt.Printf("https://%v/\n", f.Name)
return
}
// Update a previously created Function.
func (c *Client) Update(root string) (err error) {
@ -539,20 +511,20 @@ func (c *Client) Describe(name, root string) (fd FunctionDescription, err error)
}
// Remove a Function. Name takes precidence. If no name is provided,
// the Function defined at root is used.
func (c *Client) Remove(name, root string) error {
// the Function defined at root is used if it exists.
func (c *Client) Remove(cfg Function) error {
// If name is provided, it takes precidence.
// Otherwise load the Function deined at root.
if name != "" {
return c.remover.Remove(name)
if cfg.Name != "" {
return c.remover.Remove(cfg.Name)
}
f, err := NewFunction(root)
f, err := NewFunction(cfg.Root)
if err != nil {
return err
}
if !f.Initialized() {
return fmt.Errorf("%v is not initialized", f.Name)
return fmt.Errorf("Function at %v can not be removed unless initialized. Try removing by name.", f.Root)
}
return c.remover.Remove(f.Name)
}
@ -567,59 +539,35 @@ func (c *Client) Remove(name, root string) error {
type noopBuilder struct{ output io.Writer }
func (n *noopBuilder) Build(_ Function) error {
fmt.Fprintln(n.output, "skipping build: client not initialized WithBuilder")
return nil
}
func (n *noopBuilder) Build(_ Function) error { return nil }
type noopPusher struct{ output io.Writer }
func (n *noopPusher) Push(_ Function) error {
fmt.Fprintln(n.output, "skipping push: client not initialized WithPusher")
return nil
}
func (n *noopPusher) Push(_ Function) error { return nil }
type noopDeployer struct{ output io.Writer }
func (n *noopDeployer) Deploy(_ Function) error {
fmt.Fprintln(n.output, "skipping deploy: client not initialized WithDeployer")
return nil
}
func (n *noopDeployer) Deploy(_ Function) error { return nil }
type noopUpdater struct{ output io.Writer }
func (n *noopUpdater) Update(_ Function) error {
fmt.Fprintln(n.output, "skipping deploy: client not initialized WithDeployer")
return nil
}
func (n *noopUpdater) Update(_ Function) error { return nil }
type noopRunner struct{ output io.Writer }
func (n *noopRunner) Run(_ Function) error {
fmt.Fprintln(n.output, "skipping run: client not initialized WithRunner")
return nil
}
func (n *noopRunner) Run(_ Function) error { return nil }
type noopRemover struct{ output io.Writer }
func (n *noopRemover) Remove(string) error {
fmt.Fprintln(n.output, "skipping remove: client not initialized WithRemover")
return nil
}
func (n *noopRemover) Remove(string) error { return nil }
type noopLister struct{ output io.Writer }
func (n *noopLister) List() ([]string, error) {
fmt.Fprintln(n.output, "skipping list: client not initialized WithLister")
return []string{}, nil
}
func (n *noopLister) List() ([]string, error) { return []string{}, nil }
type noopDNSProvider struct{ output io.Writer }
func (n *noopDNSProvider) Provide(_ Function) error {
// Note: at this time manual DNS provisioning required for name -> knative serving netowrk load-balancer
return nil
}
func (n *noopDNSProvider) Provide(_ Function) error { return nil }
type noopProgressListener struct{}

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
package faas

View File

@ -31,11 +31,13 @@ func runDelete(cmd *cobra.Command, args []string) (err error) {
remover := knative.NewRemover()
remover.Verbose = config.Verbose
function := faas.Function{Root: config.Path, Name: config.Name}
client := faas.New(
faas.WithVerbose(verbose),
faas.WithRemover(remover))
return client.Remove(config.Name, config.Path)
return client.Remove(function)
}
type deleteConfig struct {

View File

@ -19,9 +19,7 @@ func init() {
initCmd.Flags().StringP("trigger", "t", faas.DefaultTrigger, "Function trigger (ex: 'http','events') - $FAAS_TRIGGER")
initCmd.Flags().BoolP("yes", "y", false, "When in interactive mode (attached to a TTY), skip prompts. - $FAAS_YES")
var err error
err = initCmd.RegisterFlagCompletionFunc("runtime", CompleteRuntimeList)
if err != nil {
if err := initCmd.RegisterFlagCompletionFunc("runtime", CompleteRuntimeList); err != nil {
fmt.Println("Error while calling RegisterFlagCompletionFunc: ", err)
}
}

View File

@ -4,7 +4,6 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"github.com/ory/viper"
"github.com/spf13/cobra"
@ -99,6 +98,8 @@ func newListConfig() listConfig {
}
// DEPRECATED BELOW (?):
// TODO: regenerate completions, which may necessitate the below change:
/*
var validFormats []string
@ -129,3 +130,4 @@ func fmtYAML(writer io.Writer, names []string) error {
encoder := yaml.NewEncoder(writer)
return encoder.Encode(names)
}
*/

View File

@ -8,12 +8,12 @@ import (
"gopkg.in/yaml.v2"
)
// ConfigFileName is the name of the config's serialized form.
const ConfigFileName = ".faas.config"
// ConfigFile is the name of the config's serialized form.
const ConfigFile = ".faas.yaml"
// Config represents the serialized state of a Function's metadata.
// See the Function struct for attribute documentation.
type Config struct {
type config struct {
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
Runtime string `yaml:"runtime"`
@ -26,8 +26,8 @@ type Config struct {
// errors accessing an extant config file, or the contents of the file do not
// unmarshall. A missing file at a valid path does not error but returns the
// empty value of Config.
func newConfig(root string) (c Config, err error) {
filename := filepath.Join(root, ConfigFileName)
func newConfig(root string) (c config, err error) {
filename := filepath.Join(root, ConfigFile)
if _, err = os.Stat(filename); os.IsNotExist(err) {
err = nil // do not consider a missing config file an error
return // return the zero value of the config
@ -42,7 +42,7 @@ func newConfig(root string) (c Config, err error) {
// fromConfig returns a Function populated from config.
// Note that config does not include ancillary fields not serialized, such as Root.
func fromConfig(c Config) (f Function) {
func fromConfig(c config) (f Function) {
return Function{
Name: c.Name,
Namespace: c.Namespace,
@ -52,8 +52,8 @@ func fromConfig(c Config) (f Function) {
}
// toConfig serializes a Function to a config object.
func toConfig(f Function) Config {
return Config{
func toConfig(f Function) config {
return config{
Name: f.Name,
Namespace: f.Namespace,
Runtime: f.Runtime,
@ -63,9 +63,9 @@ func toConfig(f Function) Config {
// writeConfig for the given Function out to disk at root.
func writeConfig(f Function) (err error) {
path := filepath.Join(f.Root, ConfigFileName)
path := filepath.Join(f.Root, ConfigFile)
c := toConfig(f)
bb := []byte{}
var bb []byte
if bb, err = yaml.Marshal(&c); err != nil {
return
}

View File

@ -97,6 +97,13 @@ func (f Function) Initialized() bool {
// Default if not provided is --repository (a required global setting)
// followed by the provided (or derived) image name.
func DerivedImage(root, repository string) (image string, err error) {
// Repository is currently required until such time as we support
// pushing to an implicitly-available in-cluster registry by default.
if repository == "" {
err = errors.New("Repository name is required.")
return
}
f, err := NewFunction(root)
if err != nil {
// an inability to load the funciton means it is not yet initialized
@ -127,6 +134,12 @@ func DerivedImage(root, repository string) (image string, err error) {
} else {
err = fmt.Errorf("repository should be either 'namespace' or 'registry/namespace'")
}
// Explicitly append :latest. We currently expect source control to drive
// versioning, rather than rely on Docker Hub tags with explicit version
// numbers, as is seen in many serverless solutions. This will be updated
// to branch name when we add source-driven canary/ bluegreen deployments.
image = image + ":latest"
return
}
@ -165,7 +178,7 @@ func assertEmptyRoot(path string) (err error) {
// contentiousFiles are files which, if extant, preclude the creation of a
// Function rooted in the given directory.
var contentiousFiles = []string{
".faas.yaml",
ConfigFile,
".appsody-config.yaml",
}
@ -182,14 +195,8 @@ func contentiousFilesIn(dir string) (contentious []string, err error) {
return
}
// effectivelyEmpty directories are those which have no visible files,
// and none of the explicitly enumerated contentious files.
// effectivelyEmpty directories are those which have no visible files
func isEffectivelyEmpty(dir string) (bool, error) {
// Check for contentious files
if contentious, err := contentiousFilesIn(dir); len(contentious) > 0 {
return false, err
}
// Check for any non-hidden files
files, err := ioutil.ReadDir(dir)
if err != nil {

View File

@ -1,10 +1,6 @@
package faas
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
@ -77,66 +73,3 @@ func TestPathToDomain(t *testing.T) {
}
}
}
// TestApplyConfig ensures that
// - a directory with no config has nothing applied without error.
// - a directory with an invalid config errors.
// - a directory with a valid config has it applied.
func TestApplyConfig(t *testing.T) {
// Create a temporary directory
root := "./testdata/example.com/cfgtest"
cfgFile := filepath.Join(root, ConfigFileName)
err := os.MkdirAll(root, 0700)
if err != nil {
fmt.Println("Error on TestApplyConfig: ", err)
return
}
defer os.RemoveAll(root)
c := Function{Name: "staticDefault"}
// Assert config optional.
// Ensure that applying a directory with no config does not error.
if err := applyConfig(&c, root); err != nil {
t.Fatalf("unexpected error applying a nonexistent config: %v", err)
}
// Assert an extant, but empty config file causes no errors,
// and leaves data intact on the client instance.
if err := ioutil.WriteFile(cfgFile, []byte(""), 0644); err != nil {
t.Fatal(err)
}
if err := applyConfig(&c, root); err != nil {
t.Fatalf("unexpected error applying an empty config: %v", err)
}
// Assert an unparseable config file errors
if err := ioutil.WriteFile(cfgFile, []byte("=invalid="), 0644); err != nil {
t.Fatal(err)
}
if err := applyConfig(&c, root); err == nil {
t.Fatal("Did not receive expected error from invalid config.")
}
// Assert a valid config with no value zeroes out the default
if err := ioutil.WriteFile(cfgFile, []byte("name:"), 0644); err != nil {
t.Fatal(err)
}
if err := applyConfig(&c, root); err != nil {
t.Fatal(err)
}
if c.Name != "" {
t.Fatalf("Expected name to be zeroed by config, but got '%v'", c.Name)
}
// Assert a valid config with a value for name is applied.
if err := ioutil.WriteFile(cfgFile, []byte("name: www.example.com"), 0644); err != nil {
t.Fatal(err)
}
if err := applyConfig(&c, root); err != nil {
t.Fatal(err)
}
if c.Name != "www.example.com" {
t.Fatalf("Expected name 'www.example.com', got '%v'", c.Name)
}
}

View File

@ -1,17 +1,19 @@
package mock
import "github.com/boson-project/faas"
type Builder struct {
BuildInvoked bool
BuildFn func(tag string) (image string, err error)
BuildFn func(faas.Function) error
}
func NewBuilder() *Builder {
return &Builder{
BuildFn: func(string) (string, error) { return "", nil },
BuildFn: func(faas.Function) error { return nil },
}
}
func (i *Builder) Build(tag string) (string, error) {
func (i *Builder) Build(f faas.Function) error {
i.BuildInvoked = true
return i.BuildFn(tag)
return i.BuildFn(f)
}

View File

@ -1,17 +1,19 @@
package mock
import "github.com/boson-project/faas"
type Deployer struct {
DeployInvoked bool
DeployFn func(name, image string) (address string, err error)
DeployFn func(faas.Function) error
}
func NewDeployer() *Deployer {
return &Deployer{
DeployFn: func(string, string) (string, error) { return "", nil },
DeployFn: func(faas.Function) error { return nil },
}
}
func (i *Deployer) Deploy(name, image string) (address string, err error) {
func (i *Deployer) Deploy(f faas.Function) error {
i.DeployInvoked = true
return i.DeployFn(name, image)
return i.DeployFn(f)
}

View File

@ -1,17 +1,19 @@
package mock
import "github.com/boson-project/faas"
type Pusher struct {
PushInvoked bool
PushFn func(tag string) error
PushFn func(faas.Function) error
}
func NewPusher() *Pusher {
return &Pusher{
PushFn: func(tag string) error { return nil },
PushFn: func(faas.Function) error { return nil },
}
}
func (i *Pusher) Push(tag string) error {
func (i *Pusher) Push(f faas.Function) error {
i.PushInvoked = true
return i.PushFn(tag)
return i.PushFn(f)
}

View File

@ -1,5 +1,7 @@
package mock
import "github.com/boson-project/faas"
type Runner struct {
RunInvoked bool
RootRequested string
@ -9,8 +11,8 @@ func NewRunner() *Runner {
return &Runner{}
}
func (r *Runner) Run(root string) error {
func (r *Runner) Run(f faas.Function) error {
r.RunInvoked = true
r.RootRequested = root
r.RootRequested = f.Root
return nil
}

View File

@ -1,17 +1,19 @@
package mock
import "github.com/boson-project/faas"
type Updater struct {
UpdateInvoked bool
UpdateFn func(name, image string) error
UpdateFn func(faas.Function) error
}
func NewUpdater() *Updater {
return &Updater{
UpdateFn: func(string, string) error { return nil },
UpdateFn: func(faas.Function) error { return nil },
}
}
func (i *Updater) Update(name, image string) error {
func (i *Updater) Update(f faas.Function) error {
i.UpdateInvoked = true
return i.UpdateFn(name, image)
return i.UpdateFn(f)
}

View File

@ -20,8 +20,8 @@ import (
// an HTTP Handler ("http") and Cloud Events ("events")
const DefaultTemplate = "http"
// FileAccessor encapsulates methods for accessing template files.
type FileAccessor interface {
// fileAccessor encapsulates methods for accessing template files.
type fileAccessor interface {
Stat(name string) (os.FileInfo, error)
Open(p string) (file, error)
}
@ -35,7 +35,7 @@ type file interface {
// When pkger is run, code analysis detects this Include statement,
// triggering the serializaation of the templates directory and all
// its contents into pkged.go, which is then made available via
// a pkger FileAccessor.
// a pkger fileAccessor.
// Path is relative to the go module root.
func init() {
_ = pkger.Include("/templates")
@ -112,7 +112,7 @@ func (a filesystemAccessor) Open(path string) (file, error) {
return os.Open(path)
}
func copy(src, dest string, accessor FileAccessor) (err error) {
func copy(src, dest string, accessor fileAccessor) (err error) {
node, err := accessor.Stat(src)
if err != nil {
return
@ -124,7 +124,7 @@ func copy(src, dest string, accessor FileAccessor) (err error) {
}
}
func copyNode(src, dest string, accessor FileAccessor) (err error) {
func copyNode(src, dest string, accessor fileAccessor) (err error) {
node, err := accessor.Stat(src)
if err != nil {
return
@ -135,7 +135,7 @@ func copyNode(src, dest string, accessor FileAccessor) (err error) {
return
}
children, err := ReadDir(src, accessor)
children, err := readDir(src, accessor)
if err != nil {
return
}
@ -147,7 +147,7 @@ func copyNode(src, dest string, accessor FileAccessor) (err error) {
return
}
func ReadDir(src string, accessor FileAccessor) ([]os.FileInfo, error) {
func readDir(src string, accessor fileAccessor) ([]os.FileInfo, error) {
f, err := accessor.Open(src)
if err != nil {
return nil, err
@ -161,7 +161,7 @@ func ReadDir(src string, accessor FileAccessor) ([]os.FileInfo, error) {
return list, nil
}
func copyLeaf(src, dest string, accessor FileAccessor) (err error) {
func copyLeaf(src, dest string, accessor fileAccessor) (err error) {
srcFile, err := accessor.Open(src)
if err != nil {
return

View File

@ -6,108 +6,19 @@ import (
"testing"
)
// TestInitialize ensures that on initialization of a the reference runtime
// (Go), the template is written.
func TestInitialize(t *testing.T) {
var (
path = "testdata/example.org/www"
testFile = "handle.go"
template = "http"
)
err := os.MkdirAll(path, 0744)
if err != nil {
panic(err)
}
defer os.RemoveAll(path)
err = NewInitializer("").Initialize("go", template, path)
if err != nil {
t.Fatal(err)
}
// Test that the directory is not empty
if _, err := os.Stat(filepath.Join(path, testFile)); os.IsNotExist(err) {
t.Fatalf("Initialize did not result in '%v' being written to '%v'", testFile, path)
}
}
// TestDefaultTemplate ensures that if no template is provided, files are still written.
func TestDefaultTemplate(t *testing.T) {
var (
path = "testdata/example.org/www"
testFile = "handle.go"
template = ""
)
err := os.MkdirAll(path, 0744)
if err != nil {
panic(err)
}
defer os.RemoveAll(path)
err = NewInitializer("").Initialize("go", template, path)
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(path, testFile)); os.IsNotExist(err) {
t.Fatalf("Initializing without providing a template did not result in '%v' being written to '%v'", testFile, path)
}
}
// TestCustom ensures that a custom repository can be used as a template.
// Custom repository location is not defined herein but expected to be
// provided because, for example, a CLI may want to use XDG_CONFIG_HOME.
// Assuming a repository path $FAAS_TEMPLATES, a Go template named 'json'
// which is provided in the repository repository 'boson-experimental',
// would be expected to be in the location:
// $FAAS_TEMPLATES/boson-experimental/go/json
// See the CLI for full details, but a standard default location is
// $HOME/.config/templates/boson-experimental/go/json
func TestCustom(t *testing.T) {
var (
path = "testdata/example.org/www"
testFile = "handle.go"
template = "boson-experimental/json"
// repos = "testdata/templates"
)
err := os.MkdirAll(path, 0744)
if err != nil {
panic(err)
}
defer os.RemoveAll(path)
// Unrecognized runtime/template should error
err = NewInitializer("").Initialize("go", template, path)
if err == nil {
t.Fatal("An unrecognized runtime/template should generate an error")
}
// Recognized external (non-embedded) path should succeed
err = NewInitializer("testdata/templates").Initialize("go", template, path)
if err != nil {
t.Fatal(err)
}
// The template should have been written to the given path.
if _, err := os.Stat(filepath.Join(path, testFile)); os.IsNotExist(err) {
t.Fatalf("Initializing a custom did not result in the expected '%v' being written to '%v'", testFile, path)
} else if err != nil {
t.Fatal(err)
}
}
// TestEmbeddedFileMode ensures that files from the embedded templates are
// TestTemplatesEmbeddedFileMode ensures that files from the embedded templates are
// written with the same mode from whence they came
func TestEmbeddedFileMode(t *testing.T) {
var path = "testdata/example.org/www"
func TestTemplatesEmbeddedFileMode(t *testing.T) {
var path = "testdata/example.com/www"
err := os.MkdirAll(path, 0744)
if err != nil {
panic(err)
}
defer os.RemoveAll(path)
// Initialize a quarkus app from the embedded templates.
if err := NewInitializer("").Initialize("quarkus", "events", path); err != nil {
client := New()
function := Function{Root: path, Runtime: "quarkus", Trigger: "events"}
if err := client.Initialize(function); err != nil {
t.Fatal(err)
}
@ -124,12 +35,13 @@ func TestEmbeddedFileMode(t *testing.T) {
}
}
// TestCustomFileMode ensures that files from a file-system derived repository
// of templates are written with the same mode from whence they came
func TestFileMode(t *testing.T) {
// TestTemplatesExtensibleFileMode ensures that files from a file-system
// derived template is written with mode retained.
func TestTemplatesExtensibleFileMode(t *testing.T) {
var (
path = "testdata/example.org/www"
template = "boson-experimental/http"
path = "testdata/example.com/www"
template = "boson-experimental/http"
templates = "testdata/templates"
)
err := os.MkdirAll(path, 0744)
if err != nil {
@ -137,8 +49,9 @@ func TestFileMode(t *testing.T) {
}
defer os.RemoveAll(path)
// Initialize a quarkus app from the custom repo in ./testdata
if err = NewInitializer("testdata/templates").Initialize("quarkus", template, path); err != nil {
client := New(WithTemplates(templates))
function := Function{Root: path, Runtime: "quarkus", Trigger: template}
if err := client.Initialize(function); err != nil {
t.Fatal(err)
}

3
testdata/README.md vendored
View File

@ -1,5 +1,4 @@
# testdata
Used by tests to hold files necessary for completing and as a place to create
service functions of varying configurations.
Contains test templates and directory targets for domain and subdomain-level tests.

View File

@ -0,0 +1,3 @@
# region1
Used as a test target.

View File

@ -1,3 +0,0 @@
id: "20200507014534.69605855"
project-name: www-example-com
stack: quay.io/boson/go-ce-functions:0.0

View File

@ -1,2 +0,0 @@
name: www.example.com
runtime: go

View File

@ -1,5 +0,0 @@
module function
go 1.13
require github.com/cloudevents/sdk-go v0.10.2

View File

@ -1,32 +0,0 @@
package function
import (
"context"
"fmt"
"os"
cloudevents "github.com/cloudevents/sdk-go"
)
// Handle a CloudEvent.
// Supported function signatures:
// func()
// func() error
// func(context.Context)
// func(context.Context) error
// func(cloudevents.Event)
// func(cloudevents.Event) error
// func(context.Context, cloudevents.Event)
// func(context.Context, cloudevents.Event) error
// func(cloudevents.Event, *cloudevents.EventResponse)
// func(cloudevents.Event, *cloudevents.EventResponse) error
// func(context.Context, cloudevents.Event, *cloudevents.EventResponse)
// func(context.Context, cloudevents.Event, *cloudevents.EventResponse) error
func Handle(ctx context.Context, event cloudevents.Event) error {
if err := event.Validate(); err != nil {
fmt.Fprintf(os.Stderr, "invalid event received. %v", err)
return err
}
fmt.Printf("%v\n", event)
return nil
}

View File

@ -1,115 +0,0 @@
package function
import (
"context"
"fmt"
"net/url"
"testing"
cloudevents "github.com/cloudevents/sdk-go"
)
// TestHandle ensures that Handle accepts a valid CloudEvent without error.
func TestHandle(t *testing.T) {
// A minimal, but valid, event.
event := cloudevents.NewEvent()
event.SetID("TEST-EVENT-01")
event.SetType("com.example.cloudevents.test")
event.SetSource("http://localhost:8080/")
// Invoke the defined handler.
if err := Handle(context.Background(), event); err != nil {
t.Fatal(err)
}
}
// TestHandleInvalid ensures that an invalid input event generates an error.
func TestInvalidInput(t *testing.T) {
invalidEvent := cloudevents.NewEvent() // missing required fields
// Attempt to handle the invalid event, ensuring that the handler validats events.
if err := Handle(context.Background(), invalidEvent); err == nil {
t.Fatalf("handler did not generate error on invalid event. Missing .Validate() check?")
}
}
// TestE2E also tests the Handle function, but does so by creating an actual
// CloudEvents HTTP sending and receiving clients. This is a bit redundant
// with TestHandle, but illustrates how clients are configured and used.
func TestE2E(t *testing.T) {
var (
receiver cloudevents.Client
address string // at which the receiver beings listening (os-chosen port)
sender cloudevents.Client // sends an event to the receiver via HTTP
handler = Handle // test the user-defined Handler
err error
)
if receiver, address, err = newReceiver(t); err != nil {
t.Fatal(err)
}
if sender, err = newSender(t, address); err != nil {
t.Fatal(err)
}
go func() {
if err := receiver.StartReceiver(context.Background(), handler); err != nil {
t.Fatal(err)
}
}()
_, resp, err := sender.Send(context.Background(), newEvent(t, TestData{Sequence: 1, Message: "test message"}))
if err != nil {
t.Fatal(err)
}
fmt.Printf("OK:\n%v\n", resp)
}
type TestData struct {
Sequence int `json:"id"`
Message string `json:"message"`
}
func newReceiver(t *testing.T) (c cloudevents.Client, address string, err error) {
t.Helper()
transport, err := cloudevents.NewHTTPTransport(
cloudevents.WithPort(0), // use an OS-chosen unused port.
cloudevents.WithPath("/"))
if err != nil {
return
}
address = fmt.Sprintf("http://127.0.0.1:%v/", transport.GetPort())
c, err = cloudevents.NewClient(transport)
return
}
func newSender(t *testing.T, address string) (c cloudevents.Client, err error) {
t.Helper()
transport, err := cloudevents.NewHTTPTransport(
cloudevents.WithTarget(address),
cloudevents.WithEncoding(cloudevents.HTTPStructuredV01))
if err != nil {
return
}
return cloudevents.NewClient(transport, cloudevents.WithTimeNow())
}
func newEvent(t *testing.T, data TestData) (event cloudevents.Event) {
source, err := url.Parse("https://example.com/cloudfunction/cloudevent/cmd/runner")
if err != nil {
t.Fatal(err)
}
contentType := "application/json"
event = cloudevents.Event{
Context: cloudevents.EventContextV01{
EventID: "test-event-01",
EventType: "com.cloudevents.sample.sent",
Source: cloudevents.URLRef{URL: *source},
ContentType: &contentType,
}.AsV01(),
Data: &data,
}
return
}

View File