Skip to content

Commit

Permalink
test: implement new step test framework (#141)
Browse files Browse the repository at this point in the history
- Improve the test framework to test steps
- Rebuild the Hetzner Cloud mocking server
- Add tests for the create ssh step
  • Loading branch information
jooola authored Jan 4, 2024
1 parent 0aee5ba commit 0bc73eb
Show file tree
Hide file tree
Showing 7 changed files with 582 additions and 471 deletions.
274 changes: 94 additions & 180 deletions builder/hcloud/step_create_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,203 +4,117 @@
package hcloud

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/stretchr/testify/assert"

"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
)

type Checker func(requestBody string, path string) error

func TestStepCreateServer(t *testing.T) {
const snapName = "dummy-snap"
const imageName = "dummy-image"
const name = "dummy-name"
const location = "nbg1"
const serverType = "cpx11"
networks := []int64{1}

testCases := []struct {
name string
config Config
check Checker
wantAction multistep.StepAction
}{
RunStepTestCases(t, []StepTestCase{
{
name: "happy path",
wantAction: multistep.ActionContinue,
check: func(r string, path string) error {
if path == "/servers" {
payload := schema.ServerCreateRequest{}
err := json.Unmarshal([]byte(r), &payload)
if err != nil {
t.Errorf("server request not a json: got: (%s)", err)
}

if payload.Name != name {
t.Errorf("Incorrect name in request, expected '%s' found '%s'", name, payload.Name)
}

if payload.Image != imageName {
t.Errorf("Incorrect image in request, expected '%s' found '%s'", imageName, payload.Image)
}

if payload.Location != location {
t.Errorf("Incorrect location in request, expected '%s' found '%s'", location, payload.Location)
}

if payload.ServerType != serverType {
t.Errorf("Incorrect serverType in request, expected '%s' found '%s'", serverType, payload.ServerType)
}
if payload.Networks != nil {
t.Error("Networks should not be specified")
}
}
return nil
Name: "happy",
Step: &stepCreateServer{},
SetupStateFunc: func(state multistep.StateBag) {
state.Put(StateSSHKeyID, int64(1))
},
WantRequests: []Request{
{"GET", "/ssh_keys/1", nil,
200, `{
"ssh_key": { "id": 1 }
}`,
},
{"POST", "/servers",
func(t *testing.T, r *http.Request, body []byte) {
payload := schema.ServerCreateRequest{}
assert.NoError(t, json.Unmarshal(body, &payload))
assert.Equal(t, "dummy-server", payload.Name)
assert.Equal(t, "debian-12", payload.Image)
assert.Equal(t, "nbg1", payload.Location)
assert.Equal(t, "cpx11", payload.ServerType)
assert.Nil(t, payload.Networks)
},
201, `{
"server": { "id": 8, "name": "dummy-server", "public_net": { "ipv4": { "ip": "1.2.3.4" }}},
"action": { "id": 3, "status": "progress" }
}`,
},
{"GET", "/actions/3", nil,
200, `{
"action": { "id": 3, "status": "success" }
}`,
},
},
WantStepAction: multistep.ActionContinue,
WantStateFunc: func(t *testing.T, state multistep.StateBag) {
serverID, ok := state.Get(StateServerID).(int64)
assert.True(t, ok)
assert.Equal(t, int64(8), serverID)

instanceID, ok := state.Get(StateInstanceID).(int64)
assert.True(t, ok)
assert.Equal(t, int64(8), instanceID)

serverIP, ok := state.Get(StateServerIP).(string)
assert.True(t, ok)
assert.Equal(t, "1.2.3.4", serverIP)
},
},
{
name: "with netowork",
wantAction: multistep.ActionContinue,
config: Config{
Networks: networks,
Name: "happy with network",
Step: &stepCreateServer{},
SetupConfigFunc: func(c *Config) {
c.Networks = []int64{12}
},
check: func(r string, path string) error {
if path == "/servers" {
payload := schema.ServerCreateRequest{}
err := json.Unmarshal([]byte(r), &payload)
if err != nil {
t.Errorf("server request not a json: (%s)", err)
}
if payload.Networks[0] != networks[0] {
t.Errorf("network not set")
}
}
return nil
SetupStateFunc: func(state multistep.StateBag) {
state.Put(StateSSHKeyID, int64(1))
},
WantRequests: []Request{
{"GET", "/ssh_keys/1", nil,
200, `{
"ssh_key": { "id": 1 }
}`,
},
{"POST", "/servers",
func(t *testing.T, r *http.Request, body []byte) {
payload := schema.ServerCreateRequest{}
assert.NoError(t, json.Unmarshal(body, &payload))
assert.Equal(t, "dummy-server", payload.Name)
assert.Equal(t, "debian-12", payload.Image)
assert.Equal(t, "nbg1", payload.Location)
assert.Equal(t, "cpx11", payload.ServerType)
assert.Equal(t, []int64{12}, payload.Networks)
},
201, `{
"server": { "id": 8, "name": "dummy-server", "public_net": { "ipv4": { "ip": "1.2.3.4" }}},
"action": { "id": 3, "status": "progress" }
}`,
},
{"GET", "/actions/3", nil,
200, `{
"action": { "id": 3, "status": "success" }
}`,
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
errors := make(chan error, 1)
state, teardown := setupStepCreateServer(errors, tc.check)
defer teardown()

step := &stepCreateServer{}

baseConfig := Config{
ServerName: name,
Image: imageName,
SnapshotName: snapName,
ServerType: serverType,
Location: location,
SSHKeys: []string{"1"},
}

config := baseConfig
config.Networks = tc.config.Networks

if testing.Verbose() {
state.Put(StateUI, packersdk.TestUi(t))
} else {
// do not output to stdout or console
state.Put(StateUI, &packersdk.MockUi{})
}
state.Put(StateConfig, &config)
state.Put(StateSSHKeyID, int64(1))

if action := step.Run(context.Background(), state); action != tc.wantAction {
t.Errorf("step.Run: want: %v; got: %v", tc.wantAction, action)
}

select {
case err := <-errors:
t.Errorf("server: got: %s", err)
default:
}
})
}
}

// Configure a httptest server to reply to the requests done by stepCreateSnapshot.
// React with the appropriate failCause.
// Report errors on the errors channel (cannot use testing.T, it runs on a different goroutine).
// Return a tuple (state, teardown) where:
// - state (containing the client) is ready to be passed to the step.Run() method.
// - teardown is a function meant to be deferred from the test.
func setupStepCreateServer(
errors chan<- error,
checker Checker,
) (*multistep.BasicStateBag, func()) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buf, err := io.ReadAll(r.Body)
if err != nil {
errors <- fmt.Errorf("fake server: reading request: %s", err)
return
}
reqDump := fmt.Sprintf("fake server: request:\n %s %s\n body: %s",
r.Method, r.URL.Path, string(buf))
if testing.Verbose() {
fmt.Println(reqDump)
}

enc := json.NewEncoder(w)
var response interface{}
action := schema.Action{
ID: 1,
Status: "success",
}

if r.Method == http.MethodPost && r.URL.Path == "/servers" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
response = schema.ServerCreateResponse{Action: action}
}

if r.Method == http.MethodGet && r.URL.Path == "/actions/1" {
w.Header().Set("Content-Type", "application/json")
response = schema.ActionGetResponse{Action: action}
}

if r.Method == http.MethodGet && r.URL.Path == "/ssh_keys/1" {
w.Header().Set("Content-Type", "application/json")
response = schema.SSHKeyGetResponse{
SSHKey: schema.SSHKey{ID: 1},
}
}

if err := checker(string(buf), r.URL.Path); err != nil {
errors <- fmt.Errorf("Error in checker")
}

if response != nil {
if err := enc.Encode(response); err != nil {
errors <- fmt.Errorf("fake server: encoding reply: %s", err)
}
return
}

// no match: report error
w.WriteHeader(http.StatusBadRequest)
errors <- fmt.Errorf(reqDump)
}))
WantStepAction: multistep.ActionContinue,
WantStateFunc: func(t *testing.T, state multistep.StateBag) {
serverID, ok := state.Get(StateServerID).(int64)
assert.True(t, ok)
assert.Equal(t, int64(8), serverID)

state := multistep.BasicStateBag{}
client := hcloud.NewClient(hcloud.WithEndpoint(ts.URL))
state.Put(StateHCloudClient, client)
instanceID, ok := state.Get(StateInstanceID).(int64)
assert.True(t, ok)
assert.Equal(t, int64(8), instanceID)

teardown := func() {
ts.Close()
}
return &state, teardown
serverIP, ok := state.Get(StateServerIP).(string)
assert.True(t, ok)
assert.Equal(t, "1.2.3.4", serverIP)
},
},
})
}
Loading

0 comments on commit 0bc73eb

Please sign in to comment.