Skip to content

Commit 2c2559d

Browse files
test(iaas): add mock-based test to verify write only fields functionality
Relates to STACKITTPR-782
1 parent c340cf7 commit 2c2559d

1 file changed

Lines changed: 202 additions & 0 deletions

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package volume
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/http/httptest"
10+
"testing"
11+
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"
18+
19+
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
20+
)
21+
22+
const (
23+
testProjectId = "4e684f79-a12c-449d-aa89-bcd9d8aafaf2"
24+
testRegion = "eu01"
25+
testVolumeId = "3dee3fb9-59f0-4f97-8eeb-a4da37d05a00"
26+
testKeyPayloadBase64 = "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIDEzIGxhenkgZG9ncy4="
27+
)
28+
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(),
119+
)
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+
}
129+
}
130+
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"),
140+
}
141+
}
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),
152+
}
153+
}
154+
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),
185+
}
186+
187+
iaasRessource.Create(ctx, req, resp)
188+
189+
if resp.Diagnostics.HasError() {
190+
t.Fatalf("Create should succeed, but got errors: %v", resp.Diagnostics.Errors())
191+
}
192+
if !fixture.createCalled {
193+
t.Fatalf("Expected the create endpoint to be called")
194+
}
195+
196+
if fixture.capturedKeyPayload == nil {
197+
t.Fatalf("Expected key payload %q to be sent to the API, but none was sent", testKeyPayloadBase64)
198+
}
199+
if *fixture.capturedKeyPayload != testKeyPayloadBase64 {
200+
t.Fatalf("Wrong key payload sent to the API: expected %q, got %q", testKeyPayloadBase64, *fixture.capturedKeyPayload)
201+
}
202+
}

0 commit comments

Comments
 (0)