chore: more gateway tests

This commit is contained in:
Julian Sommer
2026-02-11 12:24:00 +10:00
parent 2d30743ba5
commit 467eb0252d
8 changed files with 1126 additions and 0 deletions
@@ -0,0 +1,239 @@
package gateway_ec2_instance
import (
"encoding/json"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mulgadc/hive/hive/utils"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDescribeInstanceTypes_SingleNode(t *testing.T) {
_, nc := startTestNATSServer(t)
nc.Subscribe("ec2.DescribeInstanceTypes", func(msg *nats.Msg) {
data, _ := json.Marshal(&ec2.DescribeInstanceTypesOutput{
InstanceTypes: []*ec2.InstanceTypeInfo{
{InstanceType: aws.String("t3.micro")},
{InstanceType: aws.String("t3.small")},
},
})
msg.Respond(data)
})
input := &ec2.DescribeInstanceTypesInput{}
output, err := DescribeInstanceTypes(input, nc, 1)
require.NoError(t, err)
require.NotNil(t, output)
assert.Len(t, output.InstanceTypes, 2)
assert.Equal(t, "t3.micro", *output.InstanceTypes[0].InstanceType)
assert.Equal(t, "t3.small", *output.InstanceTypes[1].InstanceType)
}
func TestDescribeInstanceTypes_DeduplicatesAcrossNodes(t *testing.T) {
_, nc := startTestNATSServer(t)
// Node 1
nc.Subscribe("ec2.DescribeInstanceTypes", func(msg *nats.Msg) {
data, _ := json.Marshal(&ec2.DescribeInstanceTypesOutput{
InstanceTypes: []*ec2.InstanceTypeInfo{
{InstanceType: aws.String("t3.micro")},
{InstanceType: aws.String("t3.small")},
},
})
msg.Respond(data)
})
// Node 2 reports same types
nc2, err := nats.Connect(nc.ConnectedUrl())
require.NoError(t, err)
defer nc2.Close()
nc2.Subscribe("ec2.DescribeInstanceTypes", func(msg *nats.Msg) {
data, _ := json.Marshal(&ec2.DescribeInstanceTypesOutput{
InstanceTypes: []*ec2.InstanceTypeInfo{
{InstanceType: aws.String("t3.micro")},
{InstanceType: aws.String("m5.large")},
},
})
msg.Respond(data)
})
nc.Flush()
nc2.Flush()
input := &ec2.DescribeInstanceTypesInput{}
output, err := DescribeInstanceTypes(input, nc, 2)
require.NoError(t, err)
require.NotNil(t, output)
// t3.micro should appear once, t3.small once, m5.large once = 3 unique
assert.Len(t, output.InstanceTypes, 3)
seen := make(map[string]int)
for _, it := range output.InstanceTypes {
seen[*it.InstanceType]++
}
assert.Equal(t, 1, seen["t3.micro"])
assert.Equal(t, 1, seen["t3.small"])
assert.Equal(t, 1, seen["m5.large"])
}
func TestDescribeInstanceTypes_CapacityFilterShowsDuplicates(t *testing.T) {
_, nc := startTestNATSServer(t)
// Node 1
nc.Subscribe("ec2.DescribeInstanceTypes", func(msg *nats.Msg) {
data, _ := json.Marshal(&ec2.DescribeInstanceTypesOutput{
InstanceTypes: []*ec2.InstanceTypeInfo{
{InstanceType: aws.String("t3.micro")},
},
})
msg.Respond(data)
})
// Node 2 reports same type
nc2, err := nats.Connect(nc.ConnectedUrl())
require.NoError(t, err)
defer nc2.Close()
nc2.Subscribe("ec2.DescribeInstanceTypes", func(msg *nats.Msg) {
data, _ := json.Marshal(&ec2.DescribeInstanceTypesOutput{
InstanceTypes: []*ec2.InstanceTypeInfo{
{InstanceType: aws.String("t3.micro")},
},
})
msg.Respond(data)
})
nc.Flush()
nc2.Flush()
// With capacity filter = true, duplicates should be preserved
input := &ec2.DescribeInstanceTypesInput{
Filters: []*ec2.Filter{
{
Name: aws.String("capacity"),
Values: []*string{aws.String("true")},
},
},
}
output, err := DescribeInstanceTypes(input, nc, 2)
require.NoError(t, err)
require.NotNil(t, output)
// Both slots should appear since capacity filter is enabled
assert.Len(t, output.InstanceTypes, 2)
}
func TestDescribeInstanceTypes_CapacityFilterFalseDeduplicates(t *testing.T) {
_, nc := startTestNATSServer(t)
nc.Subscribe("ec2.DescribeInstanceTypes", func(msg *nats.Msg) {
data, _ := json.Marshal(&ec2.DescribeInstanceTypesOutput{
InstanceTypes: []*ec2.InstanceTypeInfo{
{InstanceType: aws.String("t3.micro")},
{InstanceType: aws.String("t3.micro")},
},
})
msg.Respond(data)
})
// capacity=false should still deduplicate
input := &ec2.DescribeInstanceTypesInput{
Filters: []*ec2.Filter{
{
Name: aws.String("capacity"),
Values: []*string{aws.String("false")},
},
},
}
output, err := DescribeInstanceTypes(input, nc, 1)
require.NoError(t, err)
require.NotNil(t, output)
assert.Len(t, output.InstanceTypes, 1)
}
func TestDescribeInstanceTypes_NoSubscribers(t *testing.T) {
_, nc := startTestNATSServer(t)
input := &ec2.DescribeInstanceTypesInput{}
output, err := DescribeInstanceTypes(input, nc, 0)
require.NoError(t, err)
require.NotNil(t, output)
assert.Empty(t, output.InstanceTypes)
}
func TestDescribeInstanceTypes_NodeReturnsError(t *testing.T) {
_, nc := startTestNATSServer(t)
nc.Subscribe("ec2.DescribeInstanceTypes", func(msg *nats.Msg) {
errorPayload := utils.GenerateErrorPayload("InternalError")
msg.Respond(errorPayload)
})
input := &ec2.DescribeInstanceTypesInput{}
output, err := DescribeInstanceTypes(input, nc, 1)
require.NoError(t, err)
require.NotNil(t, output)
assert.Empty(t, output.InstanceTypes)
}
func TestDescribeInstanceTypes_MalformedJSON(t *testing.T) {
_, nc := startTestNATSServer(t)
nc.Subscribe("ec2.DescribeInstanceTypes", func(msg *nats.Msg) {
msg.Respond([]byte(`not valid json`))
})
input := &ec2.DescribeInstanceTypesInput{}
output, err := DescribeInstanceTypes(input, nc, 1)
require.NoError(t, err)
require.NotNil(t, output)
assert.Empty(t, output.InstanceTypes)
}
func TestDescribeInstanceTypes_NilInstanceTypeSkipped(t *testing.T) {
_, nc := startTestNATSServer(t)
nc.Subscribe("ec2.DescribeInstanceTypes", func(msg *nats.Msg) {
data, _ := json.Marshal(&ec2.DescribeInstanceTypesOutput{
InstanceTypes: []*ec2.InstanceTypeInfo{
{InstanceType: aws.String("t3.micro")},
{InstanceType: nil}, // nil InstanceType should be skipped in dedup
nil, // nil entry should be skipped
},
})
msg.Respond(data)
})
input := &ec2.DescribeInstanceTypesInput{}
output, err := DescribeInstanceTypes(input, nc, 1)
require.NoError(t, err)
require.NotNil(t, output)
assert.Len(t, output.InstanceTypes, 1)
assert.Equal(t, "t3.micro", *output.InstanceTypes[0].InstanceType)
}
func TestDescribeInstanceTypes_ClosedConnection(t *testing.T) {
_, nc := startTestNATSServer(t)
closedNC, err := nats.Connect(nc.ConnectedUrl())
require.NoError(t, err)
closedNC.Close()
input := &ec2.DescribeInstanceTypesInput{}
_, err = DescribeInstanceTypes(input, closedNC, 1)
require.Error(t, err)
}
@@ -0,0 +1,256 @@
package gateway_ec2_instance
import (
"encoding/json"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mulgadc/hive/hive/utils"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDescribeInstances_SingleNode(t *testing.T) {
_, nc := startTestNATSServer(t)
reservation := &ec2.Reservation{
ReservationId: aws.String("r-abc123"),
Instances: []*ec2.Instance{
{
InstanceId: aws.String("i-001"),
InstanceType: aws.String("t3.micro"),
State: &ec2.InstanceState{Code: aws.Int64(16), Name: aws.String("running")},
},
},
}
_, err := nc.Subscribe("ec2.DescribeInstances", func(msg *nats.Msg) {
data, _ := json.Marshal(&ec2.DescribeInstancesOutput{
Reservations: []*ec2.Reservation{reservation},
})
msg.Respond(data)
})
require.NoError(t, err)
input := &ec2.DescribeInstancesInput{}
output, err := DescribeInstances(input, nc, 1)
require.NoError(t, err)
require.NotNil(t, output)
require.Len(t, output.Reservations, 1)
assert.Equal(t, "r-abc123", *output.Reservations[0].ReservationId)
assert.Equal(t, "i-001", *output.Reservations[0].Instances[0].InstanceId)
}
func TestDescribeInstances_MultipleNodes(t *testing.T) {
_, nc := startTestNATSServer(t)
// Two nodes each return different instances
nc.Subscribe("ec2.DescribeInstances", func(msg *nats.Msg) {
data, _ := json.Marshal(&ec2.DescribeInstancesOutput{
Reservations: []*ec2.Reservation{
{
ReservationId: aws.String("r-node1"),
Instances: []*ec2.Instance{
{InstanceId: aws.String("i-node1-001")},
},
},
},
})
msg.Respond(data)
})
nc2, err := nats.Connect(nc.ConnectedUrl())
require.NoError(t, err)
defer nc2.Close()
nc2.Subscribe("ec2.DescribeInstances", func(msg *nats.Msg) {
data, _ := json.Marshal(&ec2.DescribeInstancesOutput{
Reservations: []*ec2.Reservation{
{
ReservationId: aws.String("r-node2"),
Instances: []*ec2.Instance{
{InstanceId: aws.String("i-node2-001")},
{InstanceId: aws.String("i-node2-002")},
},
},
},
})
msg.Respond(data)
})
// Wait for subscriptions to propagate
nc.Flush()
nc2.Flush()
input := &ec2.DescribeInstancesInput{}
output, err := DescribeInstances(input, nc, 2)
require.NoError(t, err)
require.NotNil(t, output)
assert.Len(t, output.Reservations, 2)
}
func TestDescribeInstances_NoSubscribers(t *testing.T) {
_, nc := startTestNATSServer(t)
input := &ec2.DescribeInstancesInput{}
output, err := DescribeInstances(input, nc, 0)
// No subscribers means timeout — function returns empty reservations, no error
require.NoError(t, err)
require.NotNil(t, output)
assert.Empty(t, output.Reservations)
}
func TestDescribeInstances_NodeReturnsError(t *testing.T) {
_, nc := startTestNATSServer(t)
nc.Subscribe("ec2.DescribeInstances", func(msg *nats.Msg) {
errorPayload := utils.GenerateErrorPayload("InternalError")
msg.Respond(errorPayload)
})
input := &ec2.DescribeInstancesInput{}
output, err := DescribeInstances(input, nc, 1)
// Error responses from nodes are logged but don't fail the overall call
require.NoError(t, err)
require.NotNil(t, output)
assert.Empty(t, output.Reservations)
}
func TestDescribeInstances_MixedResponses(t *testing.T) {
_, nc := startTestNATSServer(t)
// Node 1: returns valid data
nc.Subscribe("ec2.DescribeInstances", func(msg *nats.Msg) {
data, _ := json.Marshal(&ec2.DescribeInstancesOutput{
Reservations: []*ec2.Reservation{
{
ReservationId: aws.String("r-good"),
Instances: []*ec2.Instance{{InstanceId: aws.String("i-good")}},
},
},
})
msg.Respond(data)
})
// Node 2: returns error
nc2, err := nats.Connect(nc.ConnectedUrl())
require.NoError(t, err)
defer nc2.Close()
nc2.Subscribe("ec2.DescribeInstances", func(msg *nats.Msg) {
errorPayload := utils.GenerateErrorPayload("InternalError")
msg.Respond(errorPayload)
})
nc.Flush()
nc2.Flush()
input := &ec2.DescribeInstancesInput{}
output, err := DescribeInstances(input, nc, 2)
require.NoError(t, err)
require.NotNil(t, output)
// Only the valid node's reservation should appear
assert.Len(t, output.Reservations, 1)
assert.Equal(t, "r-good", *output.Reservations[0].ReservationId)
}
func TestDescribeInstances_MalformedJSON(t *testing.T) {
_, nc := startTestNATSServer(t)
nc.Subscribe("ec2.DescribeInstances", func(msg *nats.Msg) {
msg.Respond([]byte(`{invalid json`))
})
input := &ec2.DescribeInstancesInput{}
output, err := DescribeInstances(input, nc, 1)
// Malformed JSON from a node is skipped, not fatal
require.NoError(t, err)
require.NotNil(t, output)
assert.Empty(t, output.Reservations)
}
func TestDescribeInstances_EmptyReservations(t *testing.T) {
_, nc := startTestNATSServer(t)
nc.Subscribe("ec2.DescribeInstances", func(msg *nats.Msg) {
data, _ := json.Marshal(&ec2.DescribeInstancesOutput{
Reservations: nil,
})
msg.Respond(data)
})
input := &ec2.DescribeInstancesInput{}
output, err := DescribeInstances(input, nc, 1)
require.NoError(t, err)
require.NotNil(t, output)
assert.Empty(t, output.Reservations)
}
func TestDescribeInstances_TimeoutCollection(t *testing.T) {
_, nc := startTestNATSServer(t)
// Node responds after a delay (but within timeout)
nc.Subscribe("ec2.DescribeInstances", func(msg *nats.Msg) {
time.Sleep(500 * time.Millisecond)
data, _ := json.Marshal(&ec2.DescribeInstancesOutput{
Reservations: []*ec2.Reservation{
{ReservationId: aws.String("r-delayed")},
},
})
msg.Respond(data)
})
input := &ec2.DescribeInstancesInput{}
output, err := DescribeInstances(input, nc, 0) // 0 = timeout-based collection
require.NoError(t, err)
require.NotNil(t, output)
assert.Len(t, output.Reservations, 1)
}
func TestDescribeInstances_EarlyExitWithExpectedNodes(t *testing.T) {
_, nc := startTestNATSServer(t)
nc.Subscribe("ec2.DescribeInstances", func(msg *nats.Msg) {
data, _ := json.Marshal(&ec2.DescribeInstancesOutput{
Reservations: []*ec2.Reservation{
{ReservationId: aws.String("r-fast")},
},
})
msg.Respond(data)
})
input := &ec2.DescribeInstancesInput{}
start := time.Now()
output, err := DescribeInstances(input, nc, 1)
duration := time.Since(start)
require.NoError(t, err)
require.NotNil(t, output)
assert.Len(t, output.Reservations, 1)
// Should exit early, well before the 3-second timeout
assert.Less(t, duration, 2*time.Second)
}
func TestDescribeInstances_ClosedConnection(t *testing.T) {
_, nc := startTestNATSServer(t)
closedNC, err := nats.Connect(nc.ConnectedUrl())
require.NoError(t, err)
closedNC.Close()
input := &ec2.DescribeInstancesInput{}
_, err = DescribeInstances(input, closedNC, 1)
require.Error(t, err)
}
@@ -115,6 +115,68 @@ func TestParseRunInstances(t *testing.T) {
want: errors.New(awserrors.ErrorInvalidAMIIDMalformed),
},
{
name: "NilInput",
input: nil,
want: errors.New(awserrors.ErrorMissingParameter),
},
{
name: "NilMinCount",
input: &ec2.RunInstancesInput{
ImageId: defaults.ImageId,
InstanceType: defaults.InstanceType,
MinCount: nil,
MaxCount: aws.Int64(1),
KeyName: defaults.KeyName,
SecurityGroupIds: defaults.SecurityGroupIds,
SubnetId: defaults.SubnetId,
},
want: errors.New(awserrors.ErrorMissingParameter),
},
{
name: "MinCountGreaterThanMaxCount",
input: &ec2.RunInstancesInput{
ImageId: defaults.ImageId,
InstanceType: defaults.InstanceType,
MinCount: aws.Int64(5),
MaxCount: aws.Int64(2),
KeyName: defaults.KeyName,
SecurityGroupIds: defaults.SecurityGroupIds,
SubnetId: defaults.SubnetId,
},
want: errors.New(awserrors.ErrorInvalidParameterValue),
},
{
name: "NilImageId",
input: &ec2.RunInstancesInput{
ImageId: nil,
InstanceType: defaults.InstanceType,
MinCount: aws.Int64(1),
MaxCount: aws.Int64(1),
KeyName: defaults.KeyName,
SecurityGroupIds: defaults.SecurityGroupIds,
SubnetId: defaults.SubnetId,
},
want: errors.New(awserrors.ErrorMissingParameter),
},
{
name: "NilInstanceType",
input: &ec2.RunInstancesInput{
ImageId: defaults.ImageId,
InstanceType: nil,
MinCount: aws.Int64(1),
MaxCount: aws.Int64(1),
KeyName: defaults.KeyName,
SecurityGroupIds: defaults.SecurityGroupIds,
SubnetId: defaults.SubnetId,
},
want: errors.New(awserrors.ErrorMissingParameter),
},
// Successful test
{
name: "ValidTest",
@@ -0,0 +1,165 @@
package gateway_ec2_instance
import (
"encoding/json"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mulgadc/hive/hive/qmp"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStartInstances_Success(t *testing.T) {
_, nc := startTestNATSServer(t)
instanceID := "i-0123456789abcdef0"
// Mock subscriber for the instance command topic
nc.Subscribe("ec2.cmd."+instanceID, func(msg *nats.Msg) {
var cmd qmp.Command
err := json.Unmarshal(msg.Data, &cmd)
require.NoError(t, err)
assert.Equal(t, instanceID, cmd.ID)
assert.Equal(t, "cont", cmd.QMPCommand.Execute)
assert.True(t, cmd.Attributes.StartInstance)
assert.False(t, cmd.Attributes.StopInstance)
assert.False(t, cmd.Attributes.TerminateInstance)
msg.Respond([]byte(`{"return":{}}`))
})
input := &ec2.StartInstancesInput{
InstanceIds: []*string{aws.String(instanceID)},
}
output, err := StartInstances(input, nc)
require.NoError(t, err)
require.NotNil(t, output)
require.Len(t, output.StartingInstances, 1)
sc := output.StartingInstances[0]
assert.Equal(t, instanceID, *sc.InstanceId)
assert.Equal(t, int64(0), *sc.CurrentState.Code)
assert.Equal(t, "pending", *sc.CurrentState.Name)
assert.Equal(t, int64(80), *sc.PreviousState.Code)
assert.Equal(t, "stopped", *sc.PreviousState.Name)
}
func TestStartInstances_MultipleInstances(t *testing.T) {
_, nc := startTestNATSServer(t)
ids := []string{"i-001", "i-002", "i-003"}
for _, id := range ids {
id := id
nc.Subscribe("ec2.cmd."+id, func(msg *nats.Msg) {
msg.Respond([]byte(`{"return":{}}`))
})
}
input := &ec2.StartInstancesInput{
InstanceIds: []*string{aws.String(ids[0]), aws.String(ids[1]), aws.String(ids[2])},
}
output, err := StartInstances(input, nc)
require.NoError(t, err)
require.NotNil(t, output)
assert.Len(t, output.StartingInstances, 3)
for i, sc := range output.StartingInstances {
assert.Equal(t, ids[i], *sc.InstanceId)
assert.Equal(t, "pending", *sc.CurrentState.Name)
}
}
func TestStartInstances_EmptyInstanceIds(t *testing.T) {
_, nc := startTestNATSServer(t)
input := &ec2.StartInstancesInput{
InstanceIds: []*string{},
}
_, err := StartInstances(input, nc)
require.Error(t, err)
assert.Contains(t, err.Error(), "no instance IDs provided")
}
func TestStartInstances_NilInstanceIdSkipped(t *testing.T) {
_, nc := startTestNATSServer(t)
instanceID := "i-valid"
nc.Subscribe("ec2.cmd."+instanceID, func(msg *nats.Msg) {
msg.Respond([]byte(`{"return":{}}`))
})
input := &ec2.StartInstancesInput{
InstanceIds: []*string{nil, aws.String(instanceID), nil},
}
output, err := StartInstances(input, nc)
require.NoError(t, err)
require.NotNil(t, output)
// Only the valid ID should produce a state change
assert.Len(t, output.StartingInstances, 1)
assert.Equal(t, instanceID, *output.StartingInstances[0].InstanceId)
}
func TestStartInstances_NATSRequestFails(t *testing.T) {
_, nc := startTestNATSServer(t)
// No subscriber for this instance topic, so NATS request will fail
instanceID := "i-nosubscriber"
input := &ec2.StartInstancesInput{
InstanceIds: []*string{aws.String(instanceID)},
}
output, err := StartInstances(input, nc)
require.NoError(t, err) // Function itself doesn't error, it records state change
require.NotNil(t, output)
require.Len(t, output.StartingInstances, 1)
// On NATS failure, state should reflect "still stopped"
sc := output.StartingInstances[0]
assert.Equal(t, instanceID, *sc.InstanceId)
assert.Equal(t, int64(80), *sc.CurrentState.Code)
assert.Equal(t, "stopped", *sc.CurrentState.Name)
assert.Equal(t, int64(80), *sc.PreviousState.Code)
assert.Equal(t, "stopped", *sc.PreviousState.Name)
}
func TestStartInstances_MixedSuccessAndFailure(t *testing.T) {
_, nc := startTestNATSServer(t)
goodID := "i-good"
badID := "i-bad" // no subscriber
nc.Subscribe("ec2.cmd."+goodID, func(msg *nats.Msg) {
msg.Respond([]byte(`{"return":{}}`))
})
input := &ec2.StartInstancesInput{
InstanceIds: []*string{aws.String(goodID), aws.String(badID)},
}
output, err := StartInstances(input, nc)
require.NoError(t, err)
require.Len(t, output.StartingInstances, 2)
// First instance succeeded: state → pending
assert.Equal(t, goodID, *output.StartingInstances[0].InstanceId)
assert.Equal(t, "pending", *output.StartingInstances[0].CurrentState.Name)
// Second instance failed: state → stopped (unchanged)
assert.Equal(t, badID, *output.StartingInstances[1].InstanceId)
assert.Equal(t, "stopped", *output.StartingInstances[1].CurrentState.Name)
}
@@ -0,0 +1,159 @@
package gateway_ec2_instance
import (
"encoding/json"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mulgadc/hive/hive/qmp"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStopInstances_Success(t *testing.T) {
_, nc := startTestNATSServer(t)
instanceID := "i-0123456789abcdef0"
nc.Subscribe("ec2.cmd."+instanceID, func(msg *nats.Msg) {
var cmd qmp.Command
err := json.Unmarshal(msg.Data, &cmd)
require.NoError(t, err)
assert.Equal(t, instanceID, cmd.ID)
assert.Equal(t, "system_powerdown", cmd.QMPCommand.Execute)
assert.True(t, cmd.Attributes.StopInstance)
assert.False(t, cmd.Attributes.TerminateInstance)
msg.Respond([]byte(`{"return":{}}`))
})
input := &ec2.StopInstancesInput{
InstanceIds: []*string{aws.String(instanceID)},
}
output, err := StopInstances(input, nc)
require.NoError(t, err)
require.NotNil(t, output)
require.Len(t, output.StoppingInstances, 1)
sc := output.StoppingInstances[0]
assert.Equal(t, instanceID, *sc.InstanceId)
assert.Equal(t, int64(64), *sc.CurrentState.Code)
assert.Equal(t, "stopping", *sc.CurrentState.Name)
assert.Equal(t, int64(16), *sc.PreviousState.Code)
assert.Equal(t, "running", *sc.PreviousState.Name)
}
func TestStopInstances_MultipleInstances(t *testing.T) {
_, nc := startTestNATSServer(t)
ids := []string{"i-001", "i-002"}
for _, id := range ids {
id := id
nc.Subscribe("ec2.cmd."+id, func(msg *nats.Msg) {
msg.Respond([]byte(`{"return":{}}`))
})
}
input := &ec2.StopInstancesInput{
InstanceIds: []*string{aws.String(ids[0]), aws.String(ids[1])},
}
output, err := StopInstances(input, nc)
require.NoError(t, err)
require.Len(t, output.StoppingInstances, 2)
for i, sc := range output.StoppingInstances {
assert.Equal(t, ids[i], *sc.InstanceId)
assert.Equal(t, "stopping", *sc.CurrentState.Name)
assert.Equal(t, "running", *sc.PreviousState.Name)
}
}
func TestStopInstances_EmptyInstanceIds(t *testing.T) {
_, nc := startTestNATSServer(t)
input := &ec2.StopInstancesInput{
InstanceIds: []*string{},
}
_, err := StopInstances(input, nc)
require.Error(t, err)
assert.Contains(t, err.Error(), "no instance IDs provided")
}
func TestStopInstances_NilInstanceIdSkipped(t *testing.T) {
_, nc := startTestNATSServer(t)
instanceID := "i-valid"
nc.Subscribe("ec2.cmd."+instanceID, func(msg *nats.Msg) {
msg.Respond([]byte(`{"return":{}}`))
})
input := &ec2.StopInstancesInput{
InstanceIds: []*string{nil, aws.String(instanceID)},
}
output, err := StopInstances(input, nc)
require.NoError(t, err)
assert.Len(t, output.StoppingInstances, 1)
assert.Equal(t, instanceID, *output.StoppingInstances[0].InstanceId)
}
func TestStopInstances_NATSRequestFails(t *testing.T) {
_, nc := startTestNATSServer(t)
instanceID := "i-nosubscriber"
input := &ec2.StopInstancesInput{
InstanceIds: []*string{aws.String(instanceID)},
}
output, err := StopInstances(input, nc)
require.NoError(t, err)
require.Len(t, output.StoppingInstances, 1)
// On NATS failure, state should reflect "still running"
sc := output.StoppingInstances[0]
assert.Equal(t, instanceID, *sc.InstanceId)
assert.Equal(t, int64(16), *sc.CurrentState.Code)
assert.Equal(t, "running", *sc.CurrentState.Name)
assert.Equal(t, int64(16), *sc.PreviousState.Code)
assert.Equal(t, "running", *sc.PreviousState.Name)
}
func TestStopInstances_MixedSuccessAndFailure(t *testing.T) {
_, nc := startTestNATSServer(t)
goodID := "i-good"
badID := "i-bad"
nc.Subscribe("ec2.cmd."+goodID, func(msg *nats.Msg) {
msg.Respond([]byte(`{"return":{}}`))
})
input := &ec2.StopInstancesInput{
InstanceIds: []*string{aws.String(goodID), aws.String(badID)},
}
output, err := StopInstances(input, nc)
require.NoError(t, err)
require.Len(t, output.StoppingInstances, 2)
// First: success → stopping
assert.Equal(t, goodID, *output.StoppingInstances[0].InstanceId)
assert.Equal(t, "stopping", *output.StoppingInstances[0].CurrentState.Name)
// Second: failure → still running
assert.Equal(t, badID, *output.StoppingInstances[1].InstanceId)
assert.Equal(t, "running", *output.StoppingInstances[1].CurrentState.Name)
}
@@ -0,0 +1,183 @@
package gateway_ec2_instance
import (
"encoding/json"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mulgadc/hive/hive/qmp"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTerminateInstances_Success(t *testing.T) {
_, nc := startTestNATSServer(t)
instanceID := "i-0123456789abcdef0"
nc.Subscribe("ec2.cmd."+instanceID, func(msg *nats.Msg) {
var cmd qmp.Command
err := json.Unmarshal(msg.Data, &cmd)
require.NoError(t, err)
assert.Equal(t, instanceID, cmd.ID)
assert.Equal(t, "system_powerdown", cmd.QMPCommand.Execute)
assert.True(t, cmd.Attributes.StopInstance)
assert.True(t, cmd.Attributes.TerminateInstance)
msg.Respond([]byte(`{"return":{}}`))
})
input := &ec2.TerminateInstancesInput{
InstanceIds: []*string{aws.String(instanceID)},
}
output, err := TerminateInstances(input, nc)
require.NoError(t, err)
require.NotNil(t, output)
require.Len(t, output.TerminatingInstances, 1)
sc := output.TerminatingInstances[0]
assert.Equal(t, instanceID, *sc.InstanceId)
assert.Equal(t, int64(32), *sc.CurrentState.Code)
assert.Equal(t, "shutting-down", *sc.CurrentState.Name)
assert.Equal(t, int64(16), *sc.PreviousState.Code)
assert.Equal(t, "running", *sc.PreviousState.Name)
}
func TestTerminateInstances_MultipleInstances(t *testing.T) {
_, nc := startTestNATSServer(t)
ids := []string{"i-001", "i-002", "i-003"}
for _, id := range ids {
id := id
nc.Subscribe("ec2.cmd."+id, func(msg *nats.Msg) {
msg.Respond([]byte(`{"return":{}}`))
})
}
input := &ec2.TerminateInstancesInput{
InstanceIds: []*string{aws.String(ids[0]), aws.String(ids[1]), aws.String(ids[2])},
}
output, err := TerminateInstances(input, nc)
require.NoError(t, err)
require.Len(t, output.TerminatingInstances, 3)
for i, sc := range output.TerminatingInstances {
assert.Equal(t, ids[i], *sc.InstanceId)
assert.Equal(t, "shutting-down", *sc.CurrentState.Name)
assert.Equal(t, "running", *sc.PreviousState.Name)
}
}
func TestTerminateInstances_EmptyInstanceIds(t *testing.T) {
_, nc := startTestNATSServer(t)
input := &ec2.TerminateInstancesInput{
InstanceIds: []*string{},
}
_, err := TerminateInstances(input, nc)
require.Error(t, err)
assert.Contains(t, err.Error(), "no instance IDs provided")
}
func TestTerminateInstances_NilInstanceIdSkipped(t *testing.T) {
_, nc := startTestNATSServer(t)
instanceID := "i-valid"
nc.Subscribe("ec2.cmd."+instanceID, func(msg *nats.Msg) {
msg.Respond([]byte(`{"return":{}}`))
})
input := &ec2.TerminateInstancesInput{
InstanceIds: []*string{nil, aws.String(instanceID), nil},
}
output, err := TerminateInstances(input, nc)
require.NoError(t, err)
assert.Len(t, output.TerminatingInstances, 1)
assert.Equal(t, instanceID, *output.TerminatingInstances[0].InstanceId)
}
func TestTerminateInstances_NATSRequestFails(t *testing.T) {
_, nc := startTestNATSServer(t)
instanceID := "i-nosubscriber"
input := &ec2.TerminateInstancesInput{
InstanceIds: []*string{aws.String(instanceID)},
}
output, err := TerminateInstances(input, nc)
require.NoError(t, err)
require.Len(t, output.TerminatingInstances, 1)
// On NATS failure, state should reflect "still running"
sc := output.TerminatingInstances[0]
assert.Equal(t, instanceID, *sc.InstanceId)
assert.Equal(t, int64(16), *sc.CurrentState.Code)
assert.Equal(t, "running", *sc.CurrentState.Name)
assert.Equal(t, int64(16), *sc.PreviousState.Code)
assert.Equal(t, "running", *sc.PreviousState.Name)
}
func TestTerminateInstances_MixedSuccessAndFailure(t *testing.T) {
_, nc := startTestNATSServer(t)
goodID := "i-good"
badID := "i-bad"
nc.Subscribe("ec2.cmd."+goodID, func(msg *nats.Msg) {
msg.Respond([]byte(`{"return":{}}`))
})
input := &ec2.TerminateInstancesInput{
InstanceIds: []*string{aws.String(goodID), aws.String(badID)},
}
output, err := TerminateInstances(input, nc)
require.NoError(t, err)
require.Len(t, output.TerminatingInstances, 2)
// First: success → shutting-down
assert.Equal(t, goodID, *output.TerminatingInstances[0].InstanceId)
assert.Equal(t, "shutting-down", *output.TerminatingInstances[0].CurrentState.Name)
// Second: failure → still running
assert.Equal(t, badID, *output.TerminatingInstances[1].InstanceId)
assert.Equal(t, "running", *output.TerminatingInstances[1].CurrentState.Name)
}
func TestTerminateInstances_VerifiesQMPAttributes(t *testing.T) {
_, nc := startTestNATSServer(t)
instanceID := "i-verify"
var receivedCmd qmp.Command
nc.Subscribe("ec2.cmd."+instanceID, func(msg *nats.Msg) {
json.Unmarshal(msg.Data, &receivedCmd)
msg.Respond([]byte(`{"return":{}}`))
})
input := &ec2.TerminateInstancesInput{
InstanceIds: []*string{aws.String(instanceID)},
}
_, err := TerminateInstances(input, nc)
require.NoError(t, err)
// Terminate should set both stop and terminate flags
assert.True(t, receivedCmd.Attributes.StopInstance, "StopInstance should be true for terminate")
assert.True(t, receivedCmd.Attributes.TerminateInstance, "TerminateInstance should be true")
assert.False(t, receivedCmd.Attributes.StartInstance, "StartInstance should be false")
}
+42
View File
@@ -0,0 +1,42 @@
package gateway_ec2_instance
import (
"testing"
"time"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/require"
)
// startTestNATSServer starts an embedded NATS server for testing
func startTestNATSServer(t *testing.T) (*server.Server, *nats.Conn) {
t.Helper()
opts := &server.Options{
Host: "127.0.0.1",
Port: -1,
JetStream: false,
NoLog: true,
NoSigs: true,
}
ns, err := server.NewServer(opts)
require.NoError(t, err, "Failed to create NATS server")
go ns.Start()
if !ns.ReadyForConnections(5 * time.Second) {
t.Fatal("NATS server failed to start")
}
nc, err := nats.Connect(ns.ClientURL())
require.NoError(t, err, "Failed to connect to NATS")
t.Cleanup(func() {
nc.Close()
ns.Shutdown()
})
return ns, nc
}
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
# Run Go tests with coverage and report total code coverage.
COVERPROFILE=$(mktemp /tmp/hive-coverage-XXXXXX.out)
trap 'rm -f "$COVERPROFILE"' EXIT
echo "Running tests with coverage..."
echo ""
LOG_IGNORE=1 go test -timeout 120s -coverprofile="$COVERPROFILE" -covermode=atomic ./hive/... 2>&1
if [[ ! -s "$COVERPROFILE" ]]; then
echo "No coverage data collected."
exit 1
fi
echo ""
echo "=== Total ==="
go tool cover -func="$COVERPROFILE" | awk '/^total:/ { print $NF }'