Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(unit): add more unit test coverage #285

Merged
merged 2 commits into from
Apr 12, 2024
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 .github/workflows/cicd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jobs:
uses: codecov/codecov-action@v4 # nosemgrep
with:
token: ${{ secrets.CODECOV_TOKEN }}
codecov_yml_path: codecov.yml

- name: Build resonate
run: go build -o resonate
Expand Down
130 changes: 130 additions & 0 deletions cmd/promises/completes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package promises

import (
"bytes"
"io"
"net/http"
"strings"
"testing"

"github.com/golang/mock/gomock"
"github.com/resonatehq/resonate/pkg/client"
"github.com/resonatehq/resonate/pkg/client/promises"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)

var (
patchPromiseResponse = &promises.PatchPromisesIdResponse{
HTTPResponse: &http.Response{
StatusCode: 201,
Body: io.NopCloser(strings.NewReader("")),
},
}
)

func TestCompletePromiseCmd(t *testing.T) {
// Set Gomock controller
ctrl := gomock.NewController(t)
defer ctrl.Finish()

// Set test cases
tcs := []struct {
name string
mockPromiseClient promises.ClientWithResponsesInterface
args []string
wantStdout string
wantStderr string
}{
{
name: "resolve a promise",
mockPromiseClient: func() *promises.MockClientWithResponsesInterface {
mock := promises.NewMockClientWithResponsesInterface(ctrl)
mock.
EXPECT().
PatchPromisesIdWithResponse(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(patchPromiseResponse, nil).
Times(1)
return mock
}(),
args: []string{"resolve", "foo", "--data", `{"foo": "bar"}`, "--header", "foo=bar"},
wantStdout: "Resolved promise: foo\n",
},
{
name: "reject a promise",
mockPromiseClient: func() *promises.MockClientWithResponsesInterface {
mock := promises.NewMockClientWithResponsesInterface(ctrl)
mock.
EXPECT().
PatchPromisesIdWithResponse(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(patchPromiseResponse, nil).
Times(1)
return mock
}(),
args: []string{"reject", "bar", "--data", `{"foo": "bar"}`},
wantStdout: "Rejected promise: bar\n",
},
{
name: "cancel a promise",
mockPromiseClient: func() *promises.MockClientWithResponsesInterface {
mock := promises.NewMockClientWithResponsesInterface(ctrl)
mock.
EXPECT().
PatchPromisesIdWithResponse(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(patchPromiseResponse, nil).
Times(1)
return mock
}(),
args: []string{"cancel", "baz"},
wantStdout: "Canceled promise: baz\n",
},
{
name: "Missing ID arg",
mockPromiseClient: func() *promises.MockClientWithResponsesInterface {
mock := promises.NewMockClientWithResponsesInterface(ctrl)
return mock
}(),
args: []string{"resolve"},
wantStderr: "Must specify promise id\n",
},
}

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
// Create buffer writer
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}

// Wire up client set
clientSet := &client.ClientSet{}
clientSet.SetPromisesV1Alpha1(tc.mockPromiseClient)

// Create commands in test
cmds := CompletePromiseCmds(clientSet)

// Find the appropriate command based on the first argument
var cmd *cobra.Command
for _, c := range cmds {
if c.Name() == tc.args[0] {
cmd = c
break
}
}

// Set streams for command
cmd.SetOut(stdout)
cmd.SetErr(stderr)

// Set args for command
cmd.SetArgs(tc.args[1:])

// Execute command
if err := cmd.Execute(); err != nil {
t.Fatalf("Received unexpected error: %v", err)
}

assert.Equal(t, tc.wantStdout, stdout.String())
assert.Equal(t, tc.wantStderr, stderr.String())
})
}
}
2 changes: 2 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
ignore:
- "cmd/*"
- "pkg/client"
- "*.pb.go"
- "test/*"
41 changes: 34 additions & 7 deletions internal/app/subsystems/aio/queuing/connections/connections.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,47 @@
package connections

import (
"errors"
"fmt"
"net/http"

http_conn "github.com/resonatehq/resonate/internal/app/subsystems/aio/queuing/connections/http"
"github.com/resonatehq/resonate/internal/app/subsystems/aio/queuing/connections/t_conn"
"github.com/resonatehq/resonate/internal/util"
)

var (
ErrMissingConnectionConfig = errors.New("connection config is nil")
ErrMissingFieldName = errors.New("missing field 'name'")
ErrMissingFieldKind = errors.New("missing field 'kind'")
ErrMissingMetadata = errors.New("missing field `metadata`")
ErrMissingMetadataProperties = errors.New("missing field `metadata.properties`")
ErrInvalidConnectionKind = errors.New("invalid connection kind")
)

func NewConnection(tasks <-chan *t_conn.ConnectionSubmission, cfg *t_conn.ConnectionConfig) (t_conn.Connection, error) {
// Validate all required fields are present.
// Validate all common required fields are present.
if cfg == nil {
return nil, fmt.Errorf("connection config is empty")
return nil, ErrMissingConnectionConfig
}
if cfg.Name == "" {
return nil, fmt.Errorf("field 'name' is empty for connection '%s'", cfg.Name)
return nil, ErrMissingFieldName
}
if cfg.Kind == "" {
return nil, fmt.Errorf("field 'kind' is empty for connection '%s'", cfg.Name)
return nil, fmt.Errorf("validation error for connection '%s': %w", cfg.Name, ErrMissingFieldKind)
}
if cfg.Metadata == nil {
return nil, fmt.Errorf("validation error for connection '%s': %w", cfg.Name, ErrMissingMetadata)
}
if cfg.Metadata.Properties == nil {
return nil, fmt.Errorf("validation error for connection '%s': %w", cfg.Name, ErrMissingMetadataProperties)
}

util.Assert(cfg != nil, "config must not be nil")
util.Assert(cfg.Name != "", "name must not be empty")
util.Assert(cfg.Kind != "", "kind must not be empty")
util.Assert(cfg.Metadata != nil, "metadata must not be nil")
util.Assert(cfg.Metadata.Properties != nil, "metadata properties must not be nil")

var (
conn t_conn.Connection
Expand All @@ -26,15 +50,18 @@ func NewConnection(tasks <-chan *t_conn.ConnectionSubmission, cfg *t_conn.Connec

switch cfg.Kind {
case t_conn.HTTP:
conn = http_conn.New()
err = conn.Init(tasks, cfg.Metadata)
param := &http.Client{}
conn = http_conn.New(param)
err = conn.Init(tasks, cfg)
default:
return nil, fmt.Errorf("invalid queuing kind: %s", cfg.Kind)
return nil, fmt.Errorf("validation error for connection '%s': %w", cfg.Name, ErrInvalidConnectionKind)
}

if err != nil {
return nil, err
}

util.Assert(conn != nil, "connection must not be nil")

return conn, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package connections

import (
"errors"
"testing"

"github.com/resonatehq/resonate/internal/app/subsystems/aio/queuing/connections/t_conn"
"github.com/resonatehq/resonate/internal/app/subsystems/aio/queuing/metadata"
)

func TestNewConnection(t *testing.T) {
testCases := []struct {
name string
config *t_conn.ConnectionConfig
expectedError error
}{
{
name: "nil config",
config: nil,
expectedError: ErrMissingConnectionConfig,
},
{
name: "empty name",
config: &t_conn.ConnectionConfig{},
expectedError: ErrMissingFieldName,
},
{
name: "empty kind",
config: &t_conn.ConnectionConfig{Name: "test"},
expectedError: ErrMissingFieldKind,
},
{
name: "nil metadata",
config: &t_conn.ConnectionConfig{Name: "test", Kind: t_conn.HTTP},
expectedError: ErrMissingMetadata,
},
{
name: "nil metadata properties",
config: &t_conn.ConnectionConfig{
Name: "test",
Kind: t_conn.HTTP,
Metadata: &metadata.Metadata{},
},
expectedError: ErrMissingMetadataProperties,
},
{
name: "invalid connection kind",
config: &t_conn.ConnectionConfig{
Name: "test",
Kind: "invalid",
Metadata: &metadata.Metadata{
Properties: map[string]interface{}{},
},
},
expectedError: ErrInvalidConnectionKind,
},
{
name: "valid config",
config: &t_conn.ConnectionConfig{
Name: "test",
Kind: t_conn.HTTP,
Metadata: &metadata.Metadata{
Properties: map[string]interface{}{
"url": "http://example.com",
},
},
},
expectedError: nil,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tasks := make(chan *t_conn.ConnectionSubmission)
_, err := NewConnection(tasks, tc.config)
if tc.expectedError != nil {
if err == nil {
t.Errorf("expected error: %s, got nil", tc.expectedError)
} else if !errors.Is(err, tc.expectedError) {
t.Errorf("expected error: %s, got: %s", tc.expectedError, err.Error())
}
} else {
if err != nil {
t.Errorf("unexpected error: %s", err.Error())
}
}
})
}
}
50 changes: 41 additions & 9 deletions internal/app/subsystems/aio/queuing/connections/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,24 @@ import (

"github.com/resonatehq/resonate/internal/app/subsystems/aio/queuing/connections/t_conn"
"github.com/resonatehq/resonate/internal/app/subsystems/aio/queuing/metadata"
"github.com/resonatehq/resonate/internal/util"
)

var (
ErrMissingURL = fmt.Errorf("missing field 'url'")
)

type (
// HTTP is a connection to an HTTP endpoint. It implements the Connection interface and
// is the only connection type that does not require a queue.
HTTP struct {
client *http.Client
client HTTPClient
tasks <-chan *t_conn.ConnectionSubmission
meta Metadata
meta *Metadata
}

HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}

Metadata struct {
Expand All @@ -36,20 +45,36 @@ type (
}
)

func New() t_conn.Connection {
return &HTTP{}
// New creates a new connection with the type specific client.
func New(c HTTPClient) t_conn.Connection {
return &HTTP{
client: c,
meta: &Metadata{},
}
}

func (c *HTTP) Init(tasks <-chan *t_conn.ConnectionSubmission, meta *metadata.Metadata) error {
c.client = &http.Client{}
// Init initializes the connection with the generic & type specific connection configuration.
func (c *HTTP) Init(tasks <-chan *t_conn.ConnectionSubmission, cfg *t_conn.ConnectionConfig) error {
util.Assert(c.client != nil, "client must not be nil")
util.Assert(c.meta != nil, "meta must not be nil")
util.Assert(tasks != nil, "tasks must not be nil")
util.Assert(cfg != nil, "config must not be nil")
util.Assert(cfg.Metadata != nil, "metadata must not be nil")
util.Assert(cfg.Metadata.Properties != nil, "metadata properties must not be nil")

c.tasks = tasks
md := Metadata{}

if err := metadata.Decode(meta.Properties, &md); err != nil {
if err := metadata.Decode(cfg.Metadata.Properties, c.meta); err != nil {
return err
}

c.meta = md
if c.meta.URL == "" {
return fmt.Errorf("validation error for connection '%s': %w", cfg.Name, ErrMissingURL)
}

util.Assert(c.client != nil, "client must not be nil")
util.Assert(c.tasks != nil, "tasks must not be nil")
util.Assert(c.meta.URL != "", "url must not be empty")

return nil
}
Expand All @@ -59,6 +84,13 @@ func (c *HTTP) Task() <-chan *t_conn.ConnectionSubmission {
}

func (c *HTTP) Execute(sub *t_conn.ConnectionSubmission) error {
util.Assert(sub != nil, "submission must not be nil")
util.Assert(sub.Queue != "", "queue must not be empty")
util.Assert(sub.TaskId != "", "task id must not be empty")
util.Assert(sub.Counter >= 0, "counter must be greater than or equal to 0")
util.Assert(sub.Links.Claim != "", "claim link must not be empty")
util.Assert(sub.Links.Complete != "", "complete link must not be empty")

// Form payload.
payload := Payload{
Queue: sub.Queue,
Expand Down
Loading