Skip to content

Commit d7e766c

Browse files
test(iaas): rewrite mock-based test to use resource.UnitTest instead
Relates to STACKITTPR-782
1 parent 2c2559d commit d7e766c

1 file changed

Lines changed: 108 additions & 175 deletions

File tree

Lines changed: 108 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -1,202 +1,135 @@
1-
package volume
1+
package volume_test
22

33
import (
4-
"context"
54
"encoding/json"
65
"fmt"
76
"io"
87
"net/http"
9-
"net/http/httptest"
108
"testing"
119

12-
"github.com/hashicorp/terraform-plugin-framework/resource"
13-
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
14-
"github.com/hashicorp/terraform-plugin-framework/types"
15-
"github.com/hashicorp/terraform-plugin-go/tftypes"
16-
sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config"
17-
iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api"
10+
"github.com/google/uuid"
11+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
1812

19-
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
20-
)
13+
iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api"
2114

22-
const (
23-
testProjectId = "4e684f79-a12c-449d-aa89-bcd9d8aafaf2"
24-
testRegion = "eu01"
25-
testVolumeId = "3dee3fb9-59f0-4f97-8eeb-a4da37d05a00"
26-
testKeyPayloadBase64 = "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIDEzIGxhenkgZG9ncy4="
15+
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
2716
)
2817

29-
// buildCreateRequest builds a resource.CreateRequest from a plan and a config model.
30-
// Terraform populates write-only attribute values only in the config model - never in the plan or state model.
31-
// That's why we need both, the plan model AND config model to build the request.
32-
func buildCreateRequest(ctx context.Context, t *testing.T, schemaResp *resource.SchemaResponse, planModel, configModel *Model) resource.CreateRequest {
33-
t.Helper()
34-
35-
req := resource.CreateRequest{}
36-
req.Plan = tfsdk.Plan{
37-
Schema: schemaResp.Schema,
38-
Raw: tftypes.NewValue(tftypes.DynamicPseudoType, nil),
39-
}
40-
if diags := req.Plan.Set(ctx, planModel); diags.HasError() {
41-
t.Fatalf("Failed to set plan: %v", diags.Errors())
42-
}
43-
44-
configScratch := tfsdk.Plan{
45-
Schema: schemaResp.Schema,
46-
Raw: tftypes.NewValue(tftypes.DynamicPseudoType, nil),
47-
}
48-
if diags := configScratch.Set(ctx, configModel); diags.HasError() {
49-
t.Fatalf("Failed to set config: %v", diags.Errors())
50-
}
51-
req.Config = tfsdk.Config{
52-
Schema: schemaResp.Schema,
53-
Raw: configScratch.Raw,
54-
}
55-
56-
return req
57-
}
58-
59-
type volumeFixture struct {
60-
server *httptest.Server
61-
capturedKeyPayload *string
62-
createCalled bool
63-
}
64-
65-
// newVolumeFixture spins up a mock IaaS API server handling volume creation and the subsequent
66-
// polling of the wait handler. The create handler decodes the request body and records the
67-
// encryption key payload that the provider sent to the API.
68-
func newVolumeFixture(t *testing.T) *volumeFixture {
69-
t.Helper()
70-
fixture := &volumeFixture{}
71-
72-
mux := http.NewServeMux()
73-
// Create volume
74-
mux.HandleFunc(fmt.Sprintf("POST /v2/projects/%s/regions/%s/volumes", testProjectId, testRegion), func(w http.ResponseWriter, r *http.Request) {
75-
fixture.createCalled = true
76-
body, err := io.ReadAll(r.Body)
77-
if err != nil {
78-
t.Errorf("Failed to read create request body: %v", err)
79-
w.WriteHeader(http.StatusBadRequest)
80-
return
81-
}
82-
var payload iaas.CreateVolumePayload
83-
if err := json.Unmarshal(body, &payload); err != nil {
84-
t.Errorf("Failed to unmarshal create request body: %v", err)
85-
w.WriteHeader(http.StatusBadRequest)
86-
return
87-
}
88-
if payload.EncryptionParameters != nil {
89-
fixture.capturedKeyPayload = payload.EncryptionParameters.KeyPayload
90-
}
91-
92-
w.Header().Set("content-type", "application/json")
93-
volumeId := testVolumeId
94-
_ = json.NewEncoder(w).Encode(iaas.Volume{Id: &volumeId})
95-
})
96-
// Get volume (used by the create wait handler and by mapFields via the response of the wait handler)
97-
mux.HandleFunc(fmt.Sprintf("GET /v2/projects/%s/regions/%s/volumes/%s", testProjectId, testRegion, testVolumeId), func(w http.ResponseWriter, _ *http.Request) {
98-
w.Header().Set("content-type", "application/json")
99-
volumeId := testVolumeId
100-
status := "AVAILABLE"
101-
_ = json.NewEncoder(w).Encode(iaas.Volume{
102-
Id: &volumeId,
103-
Status: &status,
104-
AvailabilityZone: "eu01-1",
105-
})
106-
})
107-
108-
fixture.server = httptest.NewServer(mux)
109-
t.Cleanup(fixture.server.Close)
110-
return fixture
111-
}
112-
113-
// newTestVolumeResource builds a volumeResource with the client's URL being set to the mock URL
114-
func newTestVolumeResource(t *testing.T, server *httptest.Server) *volumeResource {
115-
t.Helper()
116-
client, err := iaas.NewAPIClient(
117-
sdkConfig.WithEndpoint(server.URL),
118-
sdkConfig.WithoutAuthentication(),
18+
// TestCreateWriteOnlyKeyPayload is a regression test for the bug where the write-only key payload
19+
// was read from the plan model instead of the config model.
20+
// The test asserts that the value configured via key_payload_base64_wo is actually
21+
// sent to the API in the create request.
22+
func TestCreateWriteOnlyKeyPayload(t *testing.T) {
23+
projectId := uuid.NewString()
24+
volumeId := uuid.NewString()
25+
kekKeyId := uuid.NewString()
26+
kekKeyringId := uuid.NewString()
27+
const (
28+
region = "eu01"
29+
availabilityZone = "eu01-1"
30+
name = "test-volume"
31+
size = 16
32+
serviceAccount = "test-sa@sa.stackit.cloud"
33+
testKeyPayload = "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIDEzIGxhenkgZG9ncy4="
34+
volumeStatusCreated = "AVAILABLE"
11935
)
120-
if err != nil {
121-
t.Fatalf("Failed to initialize client: %v", err)
122-
}
123-
return &volumeResource{
124-
client: client,
125-
providerData: core.ProviderData{
126-
DefaultRegion: testRegion,
127-
},
128-
}
36+
s := testutil.NewMockServer(t)
37+
t.Cleanup(s.Server.Close)
38+
tfConfig := fmt.Sprintf(`
39+
provider "stackit" {
40+
default_region = "%s"
41+
iaas_custom_endpoint = "%s"
42+
service_account_token = "mock-server-needs-no-auth"
12943
}
13044
131-
func encryptionParametersTestModel() *encryptionParametersModel {
132-
return &encryptionParametersModel{
133-
KekKeyId: types.StringValue("11111111-1111-1111-1111-111111111111"),
134-
KekKeyVersion: types.Int64Value(1),
135-
KekKeyringId: types.StringValue("22222222-2222-2222-2222-222222222222"),
136-
KeyPayloadBase64: types.StringNull(),
137-
KeyPayloadBase64WriteOnly: types.StringNull(), // will be set manually for the config model
138-
KeyPayloadBase64WriteOnlyVersion: types.Int64Value(1),
139-
ServiceAccount: types.StringValue("test-sa@sa.stackit.cloud"),
45+
resource "stackit_volume" "volume" {
46+
project_id = "%s"
47+
availability_zone = "%s"
48+
name = "%s"
49+
size = %d
50+
encryption_parameters = {
51+
kek_key_id = "%s"
52+
kek_key_version = 1
53+
kek_keyring_id = "%s"
54+
key_payload_base64_wo = "%s"
55+
key_payload_base64_wo_version = 1
56+
service_account = "%s"
14057
}
14158
}
142-
143-
func baseTestModel() Model {
144-
return Model{
145-
ProjectId: types.StringValue(testProjectId),
146-
Region: types.StringValue(testRegion),
147-
AvailabilityZone: types.StringValue("eu01-1"),
148-
Name: types.StringValue("test-volume"),
149-
Size: types.Int64Value(16),
150-
Labels: types.MapNull(types.StringType),
151-
Source: types.ObjectNull(sourceTypes),
59+
`, region, s.Server.URL, projectId, availabilityZone, name, size, kekKeyId, kekKeyringId, testKeyPayload, serviceAccount)
60+
61+
volumeName := name
62+
volumeSize := int64(size)
63+
volume := iaas.Volume{
64+
Id: &volumeId,
65+
Status: new(volumeStatusCreated),
66+
AvailabilityZone: availabilityZone,
67+
Name: &volumeName,
68+
Size: &volumeSize,
15269
}
153-
}
15470

155-
// TestCreate_WriteOnlyKeyPayload is a regression test for the bug where the write-only key payload
156-
// was read from the plan model instead of the config model.
157-
// The test asserts that the value configured via key_payload_base64_wo is actually
158-
// sent to the API in the create request.
159-
func TestCreate_WriteOnlyKeyPayload(t *testing.T) {
160-
ctx := context.Background()
161-
162-
// Usually terraform will only ever write write-only fields in the config model, not the plan.
163-
// Since we're setting the models manually here, we have to ensure this is done correctly.
164-
// Ensuring that the write-only fields never go into the state/plan model is not part of this test's scope here
165-
planModel := baseTestModel()
166-
planModel.EncryptionParameters = encryptionParametersTestModel()
167-
168-
configModel := baseTestModel()
169-
configEncryptionParams := encryptionParametersTestModel()
170-
configEncryptionParams.KeyPayloadBase64WriteOnly = types.StringValue(testKeyPayloadBase64)
171-
configModel.EncryptionParameters = configEncryptionParams
172-
173-
fixture := newVolumeFixture(t)
174-
iaasRessource := newTestVolumeResource(t, fixture.server)
175-
176-
schemaResp := &resource.SchemaResponse{}
177-
iaasRessource.Schema(ctx, resource.SchemaRequest{}, schemaResp)
178-
179-
req := buildCreateRequest(ctx, t, schemaResp, &planModel, &configModel)
180-
// we have to set an initial empty state so it is != nil
181-
resp := &resource.CreateResponse{}
182-
resp.State = tfsdk.State{
183-
Schema: schemaResp.Schema,
184-
Raw: tftypes.NewValue(tftypes.DynamicPseudoType, nil),
71+
var capturedKeyPayload *string
72+
createCalled := false
73+
createVolume := testutil.MockResponse{
74+
Description: "create",
75+
Handler: func(w http.ResponseWriter, req *http.Request) {
76+
expected := fmt.Sprintf("/v2/projects/%s/regions/%s/volumes", projectId, region)
77+
if req.URL.Path != expected {
78+
t.Errorf("expected request to %s, got %s", expected, req.URL.Path)
79+
}
80+
createCalled = true
81+
body, err := io.ReadAll(req.Body)
82+
if err != nil {
83+
t.Errorf("failed to read create request body: %v", err)
84+
w.WriteHeader(http.StatusBadRequest)
85+
return
86+
}
87+
var payload iaas.CreateVolumePayload
88+
if err := json.Unmarshal(body, &payload); err != nil {
89+
t.Errorf("failed to unmarshal create request body: %v", err)
90+
w.WriteHeader(http.StatusBadRequest)
91+
return
92+
}
93+
if payload.EncryptionParameters != nil {
94+
capturedKeyPayload = payload.EncryptionParameters.KeyPayload
95+
}
96+
97+
w.Header().Set("content-type", "application/json")
98+
_ = json.NewEncoder(w).Encode(iaas.Volume{Id: &volumeId})
99+
},
185100
}
186101

187-
iaasRessource.Create(ctx, req, resp)
102+
resource.UnitTest(t, resource.TestCase{
103+
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
104+
Steps: []resource.TestStep{
105+
{
106+
PreConfig: func() {
107+
s.Reset(
108+
createVolume,
109+
testutil.MockResponse{Description: "create waiter", ToJsonBody: volume},
110+
testutil.MockResponse{Description: "get", ToJsonBody: volume},
111+
testutil.MockResponse{Description: "delete", StatusCode: http.StatusAccepted},
112+
testutil.MockResponse{Description: "delete waiter", StatusCode: http.StatusNotFound},
113+
)
114+
},
115+
Config: tfConfig,
116+
Check: resource.ComposeTestCheckFunc(
117+
resource.TestCheckResourceAttr("stackit_volume.volume", "volume_id", volumeId),
118+
resource.TestCheckResourceAttr("stackit_volume.volume", "region", region),
119+
resource.TestCheckNoResourceAttr("stackit_volume.volume", "encryption_parameters.key_payload_base64_wo"),
120+
resource.TestCheckResourceAttr("stackit_volume.volume", "encryption_parameters.key_payload_base64_wo_version", "1"),
121+
),
122+
},
123+
},
124+
})
188125

189-
if resp.Diagnostics.HasError() {
190-
t.Fatalf("Create should succeed, but got errors: %v", resp.Diagnostics.Errors())
191-
}
192-
if !fixture.createCalled {
126+
if !createCalled {
193127
t.Fatalf("Expected the create endpoint to be called")
194128
}
195-
196-
if fixture.capturedKeyPayload == nil {
197-
t.Fatalf("Expected key payload %q to be sent to the API, but none was sent", testKeyPayloadBase64)
129+
if capturedKeyPayload == nil {
130+
t.Fatalf("Expected key payload %q to be sent to the API, but none was sent", testKeyPayload)
198131
}
199-
if *fixture.capturedKeyPayload != testKeyPayloadBase64 {
200-
t.Fatalf("Wrong key payload sent to the API: expected %q, got %q", testKeyPayloadBase64, *fixture.capturedKeyPayload)
132+
if *capturedKeyPayload != testKeyPayload {
133+
t.Fatalf("Wrong key payload sent to the API: expected %q, got %q", testKeyPayload, *capturedKeyPayload)
201134
}
202135
}

0 commit comments

Comments
 (0)