| // Copyright IBM Corp. 2014, 2026 |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package command |
| |
| import ( |
| "context" |
| "fmt" |
| "io" |
| "net/http" |
| "net/http/httptest" |
| "os" |
| "path/filepath" |
| "testing" |
| |
| "github.com/hashicorp/terraform/internal/backend" |
| "github.com/hashicorp/terraform/internal/backend/backendrun" |
| "github.com/hashicorp/terraform/internal/command/arguments" |
| "github.com/hashicorp/terraform/internal/configs" |
| "github.com/hashicorp/terraform/internal/states/statemgr" |
| "github.com/hashicorp/terraform/internal/terraform" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| |
| backendLocal "github.com/hashicorp/terraform/internal/backend/local" |
| ) |
| |
| func cloudTestServerWithVars(t *testing.T) *httptest.Server { |
| t.Helper() |
| mux := http.NewServeMux() |
| |
| initialPath, _ := os.Getwd() |
| |
| mux.HandleFunc("/api/cloudplugin/v1/manifest", func(w http.ResponseWriter, r *http.Request) { |
| fileToSend, _ := os.Open(filepath.Join(initialPath, "testdata/cloud-archives/manifest.json")) |
| defer fileToSend.Close() |
| io.Copy(w, fileToSend) |
| }) |
| |
| // Respond to service version constraints calls. |
| mux.HandleFunc("/v1/versions/", func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| io.WriteString(w, fmt.Sprintf(`{ |
| "service": "%s", |
| "product": "terraform", |
| "minimum": "0.1.0", |
| "maximum": "10.0.0" |
| }`, filepath.Base(r.URL.Path))) |
| }) |
| |
| // Respond to pings to get the API version header. |
| mux.HandleFunc("/api/v2/ping", func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| w.Header().Set("TFP-API-Version", "2.5") |
| }) |
| |
| // Respond to the initial query to read the hashicorp org entitlements. |
| mux.HandleFunc("/api/v2/organizations/hashicorp/entitlement-set", func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/vnd.api+json") |
| io.WriteString(w, `{ |
| "data": { |
| "id": "org-GExadygjSbKP8hsY", |
| "type": "entitlement-sets", |
| "attributes": { |
| "operations": true, |
| "private-module-registry": true, |
| "sentinel": true, |
| "state-storage": true, |
| "teams": true, |
| "vcs-integrations": true |
| } |
| } |
| }`) |
| }) |
| |
| // Respond to our "test" workspace |
| mux.HandleFunc("/api/v2/organizations/hashicorp/workspaces/test", func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/vnd.api+json") |
| io.WriteString(w, `{ |
| "data": { |
| "id": "ws-GExadygjSbKP8hsY", |
| "type": "workspaces", |
| "attributes": { |
| "name": "test", |
| "terraform-version": "1.15.0", |
| "execution-mode": "remote" |
| } |
| } |
| }`) |
| }) |
| |
| // Respond to the "default" workspace request, will always be requested by Terraform |
| mux.HandleFunc("/api/v2/organizations/hashicorp/workspaces/default", func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/vnd.api+json") |
| io.WriteString(w, `{ |
| "data": { |
| "id": "ws-GExadygjSbKP8hsX", |
| "type": "workspaces", |
| "attributes": { |
| "name": "default", |
| "terraform-version": "1.15.0", |
| "execution-mode": "remote" |
| } |
| } |
| }`) |
| }) |
| |
| // Respond to the variables for the "test" workspace: |
| // module_name -> example |
| mux.HandleFunc("/api/v2/workspaces/ws-GExadygjSbKP8hsY/all-vars", func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/vnd.api+json") |
| io.WriteString(w, `{ |
| "data": [ |
| { |
| "id": "var-1234", |
| "type": "vars", |
| "attributes": { |
| "key": "module_name", |
| "value": "example", |
| "category": "terraform", |
| "hcl": false, |
| "sensitive": false |
| } |
| } |
| ] |
| }`) |
| }) |
| |
| return httptest.NewServer(mux) |
| } |
| |
| // TestVariableBackend is a backend used in test that are fetching variable |
| // values for constant values. |
| type TestVariableBackend struct { |
| *backendLocal.Local |
| |
| vars map[string]string |
| } |
| |
| func TestNewVariableBackend(result map[string]string) backend.Backend { |
| return &TestVariableBackend{ |
| Local: backendLocal.New(), |
| vars: result, |
| } |
| } |
| |
| var _ backendrun.ConstVariableSupplier = (*TestVariableBackend)(nil) |
| var _ backendrun.Local = (*TestVariableBackend)(nil) |
| |
| func (b *TestVariableBackend) FetchVariables(ctx context.Context, workspace string) (map[string]arguments.UnparsedVariableValue, tfdiags.Diagnostics) { |
| result := make(map[string]arguments.UnparsedVariableValue) |
| |
| for name, value := range b.vars { |
| result[name] = testUnparsedVariableValueString{ |
| str: value, |
| name: name, |
| } |
| } |
| |
| return result, nil |
| } |
| |
| func (b *TestVariableBackend) LocalRun(ctx context.Context, op *backendrun.Operation) (*backendrun.LocalRun, statemgr.Full, tfdiags.Diagnostics) { |
| // Sometimes a command (like graph) requires a local backend. The cloud |
| // backend implements LocalRun and will fetch variables from the backend. |
| // But our mock TestVariableBackend will fail in these tests, because it |
| // embends the backendrun.Local backend and never calls FetchVariables. |
| // |
| // We now set the variables manually here and defer to the regular backend |
| // run implementation, this helps us test the desired behavior. |
| if op.Variables == nil { |
| op.Variables = make(map[string]arguments.UnparsedVariableValue) |
| } |
| fetchedVars, _ := b.FetchVariables(context.Background(), op.Workspace) |
| for k, v := range fetchedVars { |
| if _, ok := op.Variables[k]; !ok { |
| op.Variables[k] = v |
| } |
| } |
| |
| return b.Local.LocalRun(ctx, op) |
| } |
| |
| type testUnparsedVariableValueString struct { |
| str string |
| name string |
| } |
| |
| func (v testUnparsedVariableValueString) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| |
| val, hclDiags := mode.Parse(v.name, v.str) |
| diags = diags.Append(hclDiags) |
| |
| return &terraform.InputValue{ |
| Value: val, |
| SourceType: terraform.ValueFromInput, |
| }, diags |
| } |