-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathbehavior_test.go
More file actions
366 lines (331 loc) · 12.8 KB
/
Copy pathbehavior_test.go
File metadata and controls
366 lines (331 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
package global_connection
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/dbt-labs/terraform-provider-dbtcloud/pkg/dbt_cloud"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// These are plain Go unit tests (no TF_ACC / live API needed) that exercise the
// exact code paths fixed for GitHub issue #709: importing/refreshing a
// bigquery_v1 dbtcloud_global_connection left use_latest_adapter unset in
// state, which the ModifyPlan guard then misread as a user-requested adapter
// change, and Update unconditionally sent the bigquery_v0-only
// timeout_seconds field even for bigquery_v1 connections.
func newTestClient(t *testing.T, server *httptest.Server) *dbt_cloud.Client {
t.Helper()
u, err := url.Parse(server.URL)
if err != nil {
t.Fatalf("failed to parse test server URL: %v", err)
}
return &dbt_cloud.Client{
HostURL: u,
HTTPClient: server.Client(),
AccountID: 1,
MaxRetries: 1,
}
}
func testResourceSchema(t *testing.T) resource.SchemaResponse {
t.Helper()
r := &globalConnectionResource{}
var resp resource.SchemaResponse
r.Schema(context.Background(), resource.SchemaRequest{}, &resp)
if resp.Diagnostics.HasError() {
t.Fatalf("unexpected schema error: %v", resp.Diagnostics)
}
return resp
}
func bigQueryModel(useLatestAdapter bool, timeoutSeconds int64) *GlobalConnectionResourceModel {
return &GlobalConnectionResourceModel{
ID: types.Int64Value(123),
AdapterVersion: types.StringValue("bigquery_v1"),
Name: types.StringValue("bq-conn"),
IsSshTunnelEnabled: types.BoolValue(false),
PrivateLinkEndpointId: types.StringNull(),
OauthConfigurationId: types.Int64Null(),
BigQueryConfig: &BigQueryConfig{
GCPProjectID: types.StringValue("my-project"),
TimeoutSeconds: types.Int64Value(timeoutSeconds),
PrivateKeyID: types.StringNull(),
PrivateKey: types.StringNull(),
ClientEmail: types.StringNull(),
ClientID: types.StringNull(),
AuthURI: types.StringNull(),
TokenURI: types.StringNull(),
AuthProviderX509CertURL: types.StringNull(),
ClientX509CertURL: types.StringNull(),
Retries: types.Int64Null(),
Scopes: nil,
Priority: types.StringNull(),
Location: types.StringNull(),
MaximumBytesBilled: types.Int64Null(),
ExecutionProject: types.StringNull(),
ImpersonateServiceAccount: types.StringNull(),
JobRetryDeadlineSeconds: types.Int64Null(),
JobCreationTimeoutSeconds: types.Int64Null(),
ApplicationID: types.StringNull(),
ApplicationSecret: types.StringNull(),
GcsBucket: types.StringNull(),
DataprocRegion: types.StringNull(),
DataprocClusterName: types.StringNull(),
UseLatestAdapter: types.BoolValue(useLatestAdapter),
JobExecutionTimeoutSeconds: types.Int64Null(),
DeploymentEnvAuthType: types.StringValue("service-account-json"),
},
}
}
// TestReadGeneric_BigQueryV1_SetsUseLatestAdapter reproduces the core of #709:
// before the fix, readGeneric (shared by Read and ImportState) never set
// BigQueryConfig.UseLatestAdapter, so it stayed null after import/refresh.
func TestReadGeneric_BigQueryV1_SetsUseLatestAdapter(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"status": {"code": 200, "is_success": true},
"data": {
"id": 123,
"name": "bq-v1-conn",
"account_id": 1,
"adapter_version": "bigquery_v1",
"private_link_endpoint_id": null,
"oauth_configuration_id": null,
"config": {
"gcp_project_id": "my-project",
"priority": null,
"location": null,
"maximum_bytes_billed": null,
"execution_project": null,
"impersonate_service_account": null,
"job_retry_deadline_seconds": null,
"job_creation_timeout_seconds": null,
"gcs_bucket": null,
"dataproc_region": null,
"dataproc_cluster_name": null
}
}
}`))
}))
defer server.Close()
client := newTestClient(t, server)
state := &GlobalConnectionResourceModel{
ID: types.Int64Value(123),
BigQueryConfig: &BigQueryConfig{},
}
newState, action, err := readGeneric(client, state, "")
if err != nil {
t.Fatalf("readGeneric returned an unexpected error: %v", err)
}
if action != "" {
t.Fatalf("expected no removal action, got %q", action)
}
if newState.BigQueryConfig.UseLatestAdapter.IsNull() {
t.Fatalf("regression: use_latest_adapter is still null after reading a bigquery_v1 connection")
}
if !newState.BigQueryConfig.UseLatestAdapter.ValueBool() {
t.Fatalf("expected use_latest_adapter=true for a bigquery_v1 connection, got false")
}
}
// TestReadGeneric_BigQueryV0_SetsUseLatestAdapterFalse confirms the derivation
// discriminates correctly rather than always reporting true.
func TestReadGeneric_BigQueryV0_SetsUseLatestAdapterFalse(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"status": {"code": 200, "is_success": true},
"data": {
"id": 456,
"name": "bq-v0-conn",
"account_id": 1,
"adapter_version": "bigquery_v0",
"private_link_endpoint_id": null,
"oauth_configuration_id": null,
"config": {
"gcp_project_id": "my-project",
"timeout_seconds": 300,
"priority": null,
"location": null,
"maximum_bytes_billed": null,
"execution_project": null,
"impersonate_service_account": null,
"job_retry_deadline_seconds": null,
"job_creation_timeout_seconds": null,
"gcs_bucket": null,
"dataproc_region": null,
"dataproc_cluster_name": null
}
}
}`))
}))
defer server.Close()
client := newTestClient(t, server)
state := &GlobalConnectionResourceModel{
ID: types.Int64Value(456),
BigQueryConfig: &BigQueryConfig{},
}
newState, _, err := readGeneric(client, state, "")
if err != nil {
t.Fatalf("readGeneric returned an unexpected error: %v", err)
}
if newState.BigQueryConfig.UseLatestAdapter.IsNull() || newState.BigQueryConfig.UseLatestAdapter.ValueBool() {
t.Fatalf("expected use_latest_adapter=false for a bigquery_v0 connection")
}
}
// TestModifyPlan_BigQueryV1_NoFalsePositiveWhenAdapterUnchanged reproduces the
// user-facing symptom of #709: a plain no-op plan on an imported bigquery_v1
// connection used to fail with "Adapter version cannot be changed" because
// state.BigQueryConfig.UseLatestAdapter was null. With the read-path fix,
// state now correctly says true, so the guard must not fire.
func TestModifyPlan_BigQueryV1_NoFalsePositiveWhenAdapterUnchanged(t *testing.T) {
schemaResp := testResourceSchema(t)
plan := bigQueryModel(true, 300)
state := bigQueryModel(true, 300) // what readGeneric now produces after the fix
tfPlan := tfsdk.Plan{Schema: schemaResp.Schema}
if diags := tfPlan.Set(context.Background(), plan); diags.HasError() {
t.Fatalf("failed to build plan fixture: %v", diags)
}
tfState := tfsdk.State{Schema: schemaResp.Schema}
if diags := tfState.Set(context.Background(), state); diags.HasError() {
t.Fatalf("failed to build state fixture: %v", diags)
}
r := globalConnectionResource{}
var resp resource.ModifyPlanResponse
r.ModifyPlan(
context.Background(),
resource.ModifyPlanRequest{Plan: tfPlan, State: tfState},
&resp,
)
if resp.Diagnostics.HasError() {
t.Fatalf("ModifyPlan unexpectedly errored on a no-op plan (this is the #709 regression): %v", resp.Diagnostics)
}
}
// TestModifyPlan_BigQueryV1_StillBlocksGenuineAdapterChange makes sure the fix
// didn't neuter the guard: a real v0 -> v1 change must still be rejected.
func TestModifyPlan_BigQueryV1_StillBlocksGenuineAdapterChange(t *testing.T) {
schemaResp := testResourceSchema(t)
plan := bigQueryModel(true, 300)
state := bigQueryModel(false, 300)
tfPlan := tfsdk.Plan{Schema: schemaResp.Schema}
if diags := tfPlan.Set(context.Background(), plan); diags.HasError() {
t.Fatalf("failed to build plan fixture: %v", diags)
}
tfState := tfsdk.State{Schema: schemaResp.Schema}
if diags := tfState.Set(context.Background(), state); diags.HasError() {
t.Fatalf("failed to build state fixture: %v", diags)
}
r := globalConnectionResource{}
var resp resource.ModifyPlanResponse
r.ModifyPlan(
context.Background(),
resource.ModifyPlanRequest{Plan: tfPlan, State: tfState},
&resp,
)
if !resp.Diagnostics.HasError() {
t.Fatalf("expected ModifyPlan to still block a genuine adapter version change")
}
}
func captureUpdatePatchBody(t *testing.T, capture *map[string]interface{}) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPatch {
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("failed to read request body: %v", err)
}
if err := json.Unmarshal(body, capture); err != nil {
t.Fatalf("failed to unmarshal request body: %v", err)
}
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"status": {"code": 200, "is_success": true},
"data": {"id": 123, "account_id": 1, "config": {"gcp_project_id": "my-project"}}
}`))
}))
}
// TestUpdate_BigQueryV1_DoesNotSendTimeoutSecondsToAPI reproduces the second
// half of #709: Update used to send timeout_seconds unconditionally whenever
// it differed from state, even though the field is bigquery_v0-only and the
// API rejects it for bigquery_v1.
func TestUpdate_BigQueryV1_DoesNotSendTimeoutSecondsToAPI(t *testing.T) {
var captured map[string]interface{}
server := captureUpdatePatchBody(t, &captured)
defer server.Close()
schemaResp := testResourceSchema(t)
client := newTestClient(t, server)
// state carries a stale timeout_seconds value (e.g. left over before the
// connection was switched to the v1 adapter), plan has a different value -
// this is exactly the condition that used to trigger the send.
state := bigQueryModel(true, 600)
plan := bigQueryModel(true, 300)
tfPlan := tfsdk.Plan{Schema: schemaResp.Schema}
if diags := tfPlan.Set(context.Background(), plan); diags.HasError() {
t.Fatalf("failed to build plan fixture: %v", diags)
}
tfState := tfsdk.State{Schema: schemaResp.Schema}
if diags := tfState.Set(context.Background(), state); diags.HasError() {
t.Fatalf("failed to build state fixture: %v", diags)
}
r := &globalConnectionResource{client: client}
resp := &resource.UpdateResponse{State: tfsdk.State{Schema: schemaResp.Schema}}
r.Update(
context.Background(),
resource.UpdateRequest{Plan: tfPlan, State: tfState},
resp,
)
if resp.Diagnostics.HasError() {
t.Fatalf("Update returned an unexpected error: %v", resp.Diagnostics)
}
if captured == nil {
t.Fatalf("expected a PATCH request to have been sent")
}
config, _ := captured["config"].(map[string]interface{})
if _, present := config["timeout_seconds"]; present {
t.Fatalf("regression: timeout_seconds was sent to the API for a bigquery_v1 connection: %v", config)
}
}
// TestUpdate_BigQueryV0_StillSendsTimeoutSecondsToAPI confirms the new guard
// only excludes timeout_seconds for v1, and doesn't break v0 behavior.
func TestUpdate_BigQueryV0_StillSendsTimeoutSecondsToAPI(t *testing.T) {
var captured map[string]interface{}
server := captureUpdatePatchBody(t, &captured)
defer server.Close()
schemaResp := testResourceSchema(t)
client := newTestClient(t, server)
state := bigQueryModel(false, 600)
plan := bigQueryModel(false, 300)
tfPlan := tfsdk.Plan{Schema: schemaResp.Schema}
if diags := tfPlan.Set(context.Background(), plan); diags.HasError() {
t.Fatalf("failed to build plan fixture: %v", diags)
}
tfState := tfsdk.State{Schema: schemaResp.Schema}
if diags := tfState.Set(context.Background(), state); diags.HasError() {
t.Fatalf("failed to build state fixture: %v", diags)
}
r := &globalConnectionResource{client: client}
resp := &resource.UpdateResponse{State: tfsdk.State{Schema: schemaResp.Schema}}
r.Update(
context.Background(),
resource.UpdateRequest{Plan: tfPlan, State: tfState},
resp,
)
if resp.Diagnostics.HasError() {
t.Fatalf("Update returned an unexpected error: %v", resp.Diagnostics)
}
if captured == nil {
t.Fatalf("expected a PATCH request to have been sent")
}
config, _ := captured["config"].(map[string]interface{})
got, present := config["timeout_seconds"]
if !present {
t.Fatalf("expected timeout_seconds to still be sent for a bigquery_v0 connection")
}
if got != float64(300) {
t.Fatalf("expected timeout_seconds=300, got %v", got)
}
}