terraform-provider-stackitp.../stackit/internal/services/iaas/server/resource_test.go
Ruben Hönle 53a3697850
feat(iaas): support for v2 API (#1070)
relates to STACKITTPR-313
2025-12-17 15:40:46 +01:00

623 lines
19 KiB
Go

package server
import (
"context"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/stackit-sdk-go/services/iaas/wait"
)
const (
userData = "user_data"
base64EncodedUserData = "dXNlcl9kYXRh"
testTimestampValue = "2006-01-02T15:04:05Z"
)
func testTimestamp() time.Time {
timestamp, _ := time.Parse(time.RFC3339, testTimestampValue)
return timestamp
}
func TestMapFields(t *testing.T) {
type args struct {
state Model
input *iaas.Server
region string
}
tests := []struct {
description string
args args
expected Model
isValid bool
}{
{
description: "default_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
},
input: &iaas.Server{
Id: utils.Ptr("sid"),
},
region: "eu01",
},
expected: Model{
Id: types.StringValue("pid,eu01,sid"),
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Name: types.StringNull(),
AvailabilityZone: types.StringNull(),
Labels: types.MapNull(types.StringType),
ImageId: types.StringNull(),
NetworkInterfaces: types.ListNull(types.StringType),
KeypairName: types.StringNull(),
AffinityGroup: types.StringNull(),
UserData: types.StringNull(),
CreatedAt: types.StringNull(),
UpdatedAt: types.StringNull(),
LaunchedAt: types.StringNull(),
Region: types.StringValue("eu01"),
},
isValid: true,
},
{
description: "simple_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Region: types.StringValue("eu01"),
},
input: &iaas.Server{
Id: utils.Ptr("sid"),
Name: utils.Ptr("name"),
AvailabilityZone: utils.Ptr("zone"),
Labels: &map[string]interface{}{
"key": "value",
},
ImageId: utils.Ptr("image_id"),
Nics: &[]iaas.ServerNetwork{
{
NicId: utils.Ptr("nic1"),
},
{
NicId: utils.Ptr("nic2"),
},
},
KeypairName: utils.Ptr("keypair_name"),
AffinityGroup: utils.Ptr("group_id"),
CreatedAt: utils.Ptr(testTimestamp()),
UpdatedAt: utils.Ptr(testTimestamp()),
LaunchedAt: utils.Ptr(testTimestamp()),
Status: utils.Ptr("active"),
},
region: "eu02",
},
expected: Model{
Id: types.StringValue("pid,eu02,sid"),
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Name: types.StringValue("name"),
AvailabilityZone: types.StringValue("zone"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
ImageId: types.StringValue("image_id"),
NetworkInterfaces: types.ListNull(types.StringType),
KeypairName: types.StringValue("keypair_name"),
AffinityGroup: types.StringValue("group_id"),
CreatedAt: types.StringValue(testTimestampValue),
UpdatedAt: types.StringValue(testTimestampValue),
LaunchedAt: types.StringValue(testTimestampValue),
Region: types.StringValue("eu02"),
},
isValid: true,
},
{
description: "empty_labels",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
input: &iaas.Server{
Id: utils.Ptr("sid"),
},
region: "eu01",
},
expected: Model{
Id: types.StringValue("pid,eu01,sid"),
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Name: types.StringNull(),
AvailabilityZone: types.StringNull(),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
ImageId: types.StringNull(),
NetworkInterfaces: types.ListNull(types.StringType),
KeypairName: types.StringNull(),
AffinityGroup: types.StringNull(),
UserData: types.StringNull(),
CreatedAt: types.StringNull(),
UpdatedAt: types.StringNull(),
LaunchedAt: types.StringNull(),
Region: types.StringValue("eu01"),
},
isValid: true,
},
{
description: "response_nil_fail",
},
{
description: "no_resource_id",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
},
input: &iaas.Server{},
},
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
expected *iaas.CreateServerPayload
isValid bool
}{
{
description: "ok",
input: &Model{
Name: types.StringValue("name"),
AvailabilityZone: types.StringValue("zone"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
BootVolume: types.ObjectValueMust(bootVolumeTypes, map[string]attr.Value{
"performance_class": types.StringValue("class"),
"size": types.Int64Value(1),
"source_type": types.StringValue("type"),
"source_id": types.StringValue("id"),
"delete_on_termination": types.BoolUnknown(),
"id": types.StringValue("id"),
}),
ImageId: types.StringValue("image"),
KeypairName: types.StringValue("keypair"),
MachineType: types.StringValue("machine_type"),
UserData: types.StringValue(userData),
NetworkInterfaces: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("nic1"),
types.StringValue("nic2"),
}),
},
expected: &iaas.CreateServerPayload{
Name: utils.Ptr("name"),
AvailabilityZone: utils.Ptr("zone"),
Labels: &map[string]interface{}{
"key": "value",
},
BootVolume: &iaas.ServerBootVolume{
PerformanceClass: utils.Ptr("class"),
Size: utils.Ptr(int64(1)),
Source: &iaas.BootVolumeSource{
Type: utils.Ptr("type"),
Id: utils.Ptr("id"),
},
},
ImageId: utils.Ptr("image"),
KeypairName: utils.Ptr("keypair"),
MachineType: utils.Ptr("machine_type"),
UserData: utils.Ptr([]byte(base64EncodedUserData)),
Networking: &iaas.CreateServerPayloadAllOfNetworking{
CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{
NicIds: &[]string{"nic1", "nic2"},
},
},
},
isValid: true,
},
{
description: "delete on termination is set to true",
input: &Model{
Name: types.StringValue("name"),
AvailabilityZone: types.StringValue("zone"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
BootVolume: types.ObjectValueMust(bootVolumeTypes, map[string]attr.Value{
"performance_class": types.StringValue("class"),
"size": types.Int64Value(1),
"source_type": types.StringValue("image"),
"source_id": types.StringValue("id"),
"delete_on_termination": types.BoolValue(true),
"id": types.StringValue("id"),
}),
ImageId: types.StringValue("image"),
KeypairName: types.StringValue("keypair"),
MachineType: types.StringValue("machine_type"),
UserData: types.StringValue(userData),
NetworkInterfaces: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("nic1"),
types.StringValue("nic2"),
}),
},
expected: &iaas.CreateServerPayload{
Name: utils.Ptr("name"),
AvailabilityZone: utils.Ptr("zone"),
Labels: &map[string]interface{}{
"key": "value",
},
BootVolume: &iaas.ServerBootVolume{
PerformanceClass: utils.Ptr("class"),
Size: utils.Ptr(int64(1)),
Source: &iaas.BootVolumeSource{
Type: utils.Ptr("image"),
Id: utils.Ptr("id"),
},
DeleteOnTermination: utils.Ptr(true),
},
ImageId: utils.Ptr("image"),
KeypairName: utils.Ptr("keypair"),
MachineType: utils.Ptr("machine_type"),
UserData: utils.Ptr([]byte(base64EncodedUserData)),
Networking: &iaas.CreateServerPayloadAllOfNetworking{
CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{
NicIds: &[]string{"nic1", "nic2"},
},
},
},
isValid: true,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output, err := toCreatePayload(context.Background(), tt.input)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(output, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}
func TestToUpdatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
expected *iaas.UpdateServerPayload
isValid bool
}{
{
"default_ok",
&Model{
Name: types.StringValue("name"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
},
&iaas.UpdateServerPayload{
Name: utils.Ptr("name"),
Labels: &map[string]interface{}{
"key": "value",
},
},
true,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType))
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(output, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}
var _ serverControlClient = &mockServerControlClient{}
// mockServerControlClient mocks the [serverControlClient] interface with
// pluggable functions
type mockServerControlClient struct {
wait.APIClientInterface
startServerCalled int
startServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error
stopServerCalled int
stopServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error
deallocateServerCalled int
deallocateServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error
getServerCalled int
getServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) (*iaas.Server, error)
}
// DeallocateServerExecute implements serverControlClient.
func (t *mockServerControlClient) DeallocateServerExecute(ctx context.Context, projectId, region, serverId string) error {
t.deallocateServerCalled++
return t.deallocateServerExecute(t.deallocateServerCalled, ctx, projectId, region, serverId)
}
// GetServerExecute implements serverControlClient.
func (t *mockServerControlClient) GetServerExecute(ctx context.Context, projectId, region, serverId string) (*iaas.Server, error) {
t.getServerCalled++
return t.getServerExecute(t.getServerCalled, ctx, projectId, region, serverId)
}
// StartServerExecute implements serverControlClient.
func (t *mockServerControlClient) StartServerExecute(ctx context.Context, projectId, region, serverId string) error {
t.startServerCalled++
return t.startServerExecute(t.startServerCalled, ctx, projectId, region, serverId)
}
// StopServerExecute implements serverControlClient.
func (t *mockServerControlClient) StopServerExecute(ctx context.Context, projectId, region, serverId string) error {
t.stopServerCalled++
return t.stopServerExecute(t.stopServerCalled, ctx, projectId, region, serverId)
}
func Test_serverResource_updateServerStatus(t *testing.T) {
projectId := basetypes.NewStringValue("projectId")
serverId := basetypes.NewStringValue("serverId")
type fields struct {
client *mockServerControlClient
}
type args struct {
currentState *string
model Model
region string
}
type want struct {
err bool
status types.String
getServerCount int
stopCount int
startCount int
deallocatedCount int
}
tests := []struct {
name string
fields fields
args args
want want
}{
{
name: "no desired status",
fields: fields{
client: &mockServerControlClient{
getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) {
return &iaas.Server{
Id: utils.Ptr(serverId.ValueString()),
Status: utils.Ptr(wait.ServerActiveStatus),
}, nil
},
},
},
args: args{
currentState: utils.Ptr(wait.ServerActiveStatus),
model: Model{
ProjectId: projectId,
ServerId: serverId,
},
},
want: want{
getServerCount: 1,
},
},
{
name: "desired inactive state",
fields: fields{
client: &mockServerControlClient{
getServerExecute: func(no int, _ context.Context, _, _, _ string) (*iaas.Server, error) {
var state string
if no <= 1 {
state = wait.ServerActiveStatus
} else {
state = wait.ServerInactiveStatus
}
return &iaas.Server{
Id: utils.Ptr(serverId.ValueString()),
Status: &state,
}, nil
},
stopServerExecute: func(_ int, _ context.Context, _, _, _ string) error { return nil },
},
},
args: args{
currentState: utils.Ptr(wait.ServerActiveStatus),
model: Model{
ProjectId: projectId,
ServerId: serverId,
DesiredStatus: basetypes.NewStringValue("inactive"),
},
},
want: want{
getServerCount: 2,
stopCount: 1,
status: basetypes.NewStringValue("inactive"),
},
},
{
name: "desired deallocated state",
fields: fields{
client: &mockServerControlClient{
getServerExecute: func(no int, _ context.Context, _, _, _ string) (*iaas.Server, error) {
var state string
switch no {
case 1:
state = wait.ServerActiveStatus
case 2:
state = wait.ServerInactiveStatus
default:
state = wait.ServerDeallocatedStatus
}
return &iaas.Server{
Id: utils.Ptr(serverId.ValueString()),
Status: &state,
}, nil
},
deallocateServerExecute: func(_ int, _ context.Context, _, _, _ string) error { return nil },
},
},
args: args{
currentState: utils.Ptr(wait.ServerActiveStatus),
model: Model{
ProjectId: projectId,
ServerId: serverId,
DesiredStatus: basetypes.NewStringValue("deallocated"),
},
},
want: want{
getServerCount: 3,
deallocatedCount: 1,
status: basetypes.NewStringValue("deallocated"),
},
},
{
name: "don't call start if active",
fields: fields{
client: &mockServerControlClient{
getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) {
return &iaas.Server{
Id: utils.Ptr(serverId.ValueString()),
Status: utils.Ptr(wait.ServerActiveStatus),
}, nil
},
},
},
args: args{
currentState: utils.Ptr(wait.ServerActiveStatus),
model: Model{
ProjectId: projectId,
ServerId: serverId,
DesiredStatus: basetypes.NewStringValue("active"),
},
},
want: want{
status: basetypes.NewStringValue("active"),
getServerCount: 1,
},
},
{
name: "don't call stop if inactive",
fields: fields{
client: &mockServerControlClient{
getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) {
return &iaas.Server{
Id: utils.Ptr(serverId.ValueString()),
Status: utils.Ptr(wait.ServerInactiveStatus),
}, nil
},
},
},
args: args{
currentState: utils.Ptr(wait.ServerInactiveStatus),
model: Model{
ProjectId: projectId,
ServerId: serverId,
DesiredStatus: basetypes.NewStringValue("inactive"),
},
},
want: want{
status: basetypes.NewStringValue("inactive"),
getServerCount: 1,
},
},
{
name: "don't call dealloacate if deallocated",
fields: fields{
client: &mockServerControlClient{
getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) {
return &iaas.Server{
Id: utils.Ptr(serverId.ValueString()),
Status: utils.Ptr(wait.ServerDeallocatedStatus),
}, nil
},
},
},
args: args{
currentState: utils.Ptr(wait.ServerDeallocatedStatus),
model: Model{
ProjectId: projectId,
ServerId: serverId,
DesiredStatus: basetypes.NewStringValue("deallocated"),
},
},
want: want{
status: basetypes.NewStringValue("deallocated"),
getServerCount: 1,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := updateServerStatus(context.Background(), tt.fields.client, tt.args.currentState, &tt.args.model, tt.args.region)
if (err != nil) != tt.want.err {
t.Errorf("inconsistent error, want %v and got %v", tt.want.err, err)
}
if expected, actual := tt.want.status, tt.args.model.DesiredStatus; expected != actual {
t.Errorf("wanted status %s but got %s", expected, actual)
}
if expected, actual := tt.want.getServerCount, tt.fields.client.getServerCalled; expected != actual {
t.Errorf("wrong number of get server calls: Expected %d but got %d", expected, actual)
}
if expected, actual := tt.want.startCount, tt.fields.client.startServerCalled; expected != actual {
t.Errorf("wrong number of start server calls: Expected %d but got %d", expected, actual)
}
if expected, actual := tt.want.stopCount, tt.fields.client.stopServerCalled; expected != actual {
t.Errorf("wrong number of stop server calls: Expected %d but got %d", expected, actual)
}
if expected, actual := tt.want.deallocatedCount, tt.fields.client.deallocateServerCalled; expected != actual {
t.Errorf("wrong number of deallocate server calls: Expected %d but got %d", expected, actual)
}
})
}
}