From d2ad98655891822c8d0b787631a9d9457fb91972 Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Tue, 21 Apr 2026 17:11:15 +1000 Subject: [PATCH] Improve local dev override docs and add read-only example --- README.md | 336 ++++++++---------- examples/read_only/main.tf | 12 + examples/read_only/outputs.tf | 24 ++ examples/read_only/providers.tf | 18 + examples/read_only/terraform.tfvars.example | 6 + examples/read_only/variables.tf | 12 + internal/dynuclient/client.go | 16 +- internal/provider/data_source_dns_records.go | 4 +- internal/provider/data_source_domain.go | 4 +- internal/provider/data_source_domains.go | 2 +- internal/provider/diagnostics.go | 23 ++ .../provider/provider_integration_test.go | 53 +++ internal/provider/provider_test.go | 64 ++++ 13 files changed, 388 insertions(+), 186 deletions(-) create mode 100644 examples/read_only/main.tf create mode 100644 examples/read_only/outputs.tf create mode 100644 examples/read_only/providers.tf create mode 100644 examples/read_only/terraform.tfvars.example create mode 100644 examples/read_only/variables.tf create mode 100644 internal/provider/diagnostics.go diff --git a/README.md b/README.md index 7032c40..f6d9959 100644 --- a/README.md +++ b/README.md @@ -2,212 +2,188 @@ A standalone Terraform provider for Dynu DNS. -> Status: **read-only milestone**. This provider currently implements provider configuration and data sources only. +> Status: **read-only milestone**. This provider currently implements provider configuration plus read-only data sources. -## Feature scope +## Quick start (local dev with `dev_overrides`) -Implemented: -- Provider authentication using `api_key` or `DYNU_API_KEY` -- Optional provider `base_url` override for local test/dev setups -- Data sources: - - `dynu_domains` - - `dynu_domain` - - `dynu_dns_records` +This provider is **not published** to the Terraform Registry yet. For local development, use Terraform CLI `dev_overrides` and your local provider binary. -Not implemented yet: -- Terraform resources (no create/update/delete) -- Any write API operations +1. Build provider binary in repo root: -## Provider source and module path +```bash +go build -o terraform-provider-dynu +``` -- Terraform provider source address: `dynu/dynu` -- Go module path: `github.com/dynu/terraform-provider-dynu` - -The repository can be hosted elsewhere during development, but module and provider source naming are kept aligned with planned public registry publishing. - -## Requirements - -- Terraform `>= 1.5` -- Go `>= 1.23` -- Dynu API key for live API usage - -## Authentication - -Option 1: Terraform configuration. +2. Configure `~/.terraformrc`: ```hcl +provider_installation { + dev_overrides { + "dynu/dynu" = "/path/to/terraform-dynu-provider" + } + + direct {} +} +``` + +3. Run the runnable read-only example: + +```bash +cd examples/read_only +cp terraform.tfvars.example terraform.tfvars +terraform validate +terraform plan +``` + +> With `dev_overrides`, Terraform uses your local binary for `dynu/dynu`. `terraform init` is not the primary local-dev loop here and may still try registry/network operations. + +## Copy/paste starter configuration + +```hcl +terraform { + required_providers { + dynu = { + source = "dynu/dynu" + } + } +} + provider "dynu" { - api_key = var.dynu_api_key + api_key = var.dynu_api_key # optional if DYNU_API_KEY is set } variable "dynu_api_key" { type = string + default = null sensitive = true } + +data "dynu_domains" "all" {} + +# Use a real hostname from your Dynu account. +data "dynu_domain" "selected" { + hostname = "www.example.com" +} + +data "dynu_dns_records" "selected" { + hostname = "www.example.com" +} ``` -Option 2: Environment variable. +## Provider schema reference -```bash -export DYNU_API_KEY="your-dynu-api-key" +### Provider: `dynu` + +Optional arguments: +- `api_key` (String, Sensitive) + - Falls back to `DYNU_API_KEY` environment variable when omitted. +- `base_url` (String) + - Test/dev override for Dynu API base URL. + +No provider resources are implemented yet. + +## Data source schema reference + +### `dynu_domains` + +Arguments: +- none + +Attributes: +- `domains` (List(Object)): + - `id`, `name`, `unicode_name`, `token` (sensitive), `state`, `group` + - `ipv4_address`, `ipv6_address`, `ttl` + - `ipv4`, `ipv6`, `ipv4_wildcard_alias`, `ipv6_wildcard_alias` + - `allow_zone_transfer`, `dnssec`, `created_on`, `updated_on` + +Example: + +```hcl +data "dynu_domains" "all" {} ``` -## Data source examples +### `dynu_domain` -See the `examples/` directory: -- `examples/provider/provider.tf` -- `examples/data-sources/dynu_domains/data-source.tf` -- `examples/data-sources/dynu_domain/data-source.tf` -- `examples/data-sources/dynu_dns_records/data-source.tf` +Arguments: +- `hostname` (String, required) + +Attributes: +- `domain` (Object) with the same fields as `dynu_domains.domains[*]`. + +Example: + +```hcl +data "dynu_domain" "selected" { + hostname = "www.example.com" +} +``` + +### `dynu_dns_records` + +Arguments: +- `hostname` (String, required) + +Attributes: +- `domain_id` (Number) +- `domain_name` (String) +- `records` (List(Object)) with: + - `id`, `domain_id`, `domain_name`, `node_name`, `hostname`, `record_type` + - `ttl`, `state`, `content`, `updated_on`, `group`, `host` + +Example: + +```hcl +data "dynu_dns_records" "selected" { + hostname = "www.example.com" +} +``` + +## Examples + +- Runnable local workflow: `examples/read_only/` +- Provider block example: `examples/provider/provider.tf` +- Individual data source snippets: + - `examples/data-sources/dynu_domains/data-source.tf` + - `examples/data-sources/dynu_domain/data-source.tf` + - `examples/data-sources/dynu_dns_records/data-source.tf` + +## Troubleshooting local dev + +- **Unsupported provider arguments** + - Symptom: errors such as `Unsupported argument` (for example `username`). + - Fix: use only `api_key` and/or `base_url` in `provider "dynu"`. + +- **Bad API credentials** + - Symptom: diagnostics mention authentication failures. + - Fix: verify `api_key` or `DYNU_API_KEY` and re-run `terraform plan`. + +- **Unknown data source arguments** + - Symptom: unsupported argument errors in data blocks. + - Fix: `dynu_domain` and `dynu_dns_records` require only `hostname`; `dynu_domains` takes no arguments. + +- **Stale provider binary after code changes** + - Symptom: Terraform behavior doesn't reflect latest code. + - Fix: rebuild binary (`go build -o terraform-provider-dynu`) and run `terraform plan` again. ## Developer workflow -- `./scripts/setup-dev.sh` - validate local Go/Terraform toolchains and environment health (no changes) -- `./scripts/setup-dev.sh --fix` - attempt safe remediation with installed version managers (`mise`, `asdf`, `tfenv`, `tenv`) -- `./scripts/setup-dev.sh --strict` - require Terraform to be installed -- `./scripts/check.sh` - formatting, vet, and unit tests (Tier A) -- `./scripts/test-integration.sh` - local mock-backed provider integration tests (Tier B) -- `./scripts/testacc.sh` - default: Tier B; live mode available with `--live` (Tier C) +- `./scripts/setup-dev.sh` - validate local toolchain requirements +- `./scripts/check.sh` - formatting, vet, and unit tests +- `./scripts/test-integration.sh` - local mock-backed provider integration tests +- `./scripts/testacc.sh` - acceptance/integration test wrapper (live tests opt-in) -### setup-dev behavior - -`scripts/setup-dev.sh` now performs robust validation and troubleshooting checks: - -- Always reports resolved paths for `bash`, `git`, `go`, and `terraform` (Terraform is warning-only unless `--strict` is used). -- Enforces minimum versions: - - Go `>= 1.23` (error if missing or too old) - - Terraform `>= 1.5` (warning if missing by default, error if present but too old) -- Detects common broken Go setups: - - manual `GOROOT` conflicting with `go env GOROOT` - - stale stdlib tree mismatches (for example missing `slices`, `maps`, `math/rand/v2`) -- Detects malformed `GOPROXY` and prints the recommended non-destructive fix: - - `go env -w GOPROXY=https://proxy.golang.org,direct` -- Supports optional safe auto-fix mode: - - only uses already-installed user-space managers - - does **not** run distro package managers, `sudo`, or shell-profile edits - - re-validates tools after attempted remediation - -The script is safe for both normal shells and VS Code integrated terminals, and includes guidance when terminal/session restart may be needed after changing versions. - -### Standalone repository guarantee - -This repository is intentionally self-contained: -- no dependency on sibling repositories -- no dependency on external helper scripts (for example `services-up.sh`) -- no hardcoded local paths (for example `/workspace/...` or `/home/...`) - -### setup-dev troubleshooting quick reference - -- **Malformed GOPROXY** - - Symptom: warning that GOPROXY has no valid entries. - - Fix: `go env -w GOPROXY=https://proxy.golang.org,direct` - -- **Broken GOROOT** - - Symptom: `GOROOT` environment value differs from `go env GOROOT`, or stdlib package checks fail. - - Fix: usually remove manual override with `unset GOROOT`, then ensure the intended `go` binary is first in `PATH`. - -- **Go version / stdlib path mismatch** - - Symptom: `go version` looks modern but build errors mention missing stdlib packages (for example `slices`, `maps`, `math/rand/v2`). - - Cause: stale or mismatched Go installation path. - - Fix: re-select/install Go via your version manager and re-run `./scripts/setup-dev.sh`. - -- **VS Code integrated terminal stale environment** - - Symptom: command paths or versions do not match your expected shell setup. - - Fix: restart the integrated terminal (or reload the VS Code window) and run `./scripts/setup-dev.sh` again. - -### Build - -```bash -go build ./... -``` - -## Testing model - -The provider now has three explicit test tiers: - -### Tier A: unit tests (fast, no network) - -Covers focused package behavior (client parsing, mappers, provider helper logic). - -```bash -./scripts/check.sh -go test ./... -``` - -### Troubleshooting Go dependency downloads - -If you see an error like: - -```text -GOPROXY list is not the empty string, but contains no entries -``` - -your local Go environment is misconfigured. A common fix is: - -```bash -go env -w GOPROXY=https://proxy.golang.org,direct -``` - -Helpful diagnostics: - -```bash -go env GOPROXY -echo "$GOPROXY" -``` - -### Acceptance tests -### Tier B: local integration tests (mock Dynu API, no real credentials) - -These tests use an `httptest` fake Dynu API server and run the Terraform provider end-to-end against deterministic fixtures. - -- No Dynu account required -- Dummy API key is used in test provider configuration -- Exercises provider wiring, schema/state mapping, hostname resolution flow, and diagnostic behavior - -```bash -./scripts/test-integration.sh -./scripts/testacc.sh -``` - -### Tier C: live acceptance tests (opt-in) - -These tests call the real Dynu API and are read-only. - -Required environment variables: +Live acceptance tests are read-only and require: - `TF_ACC=1` - `DYNU_API_KEY` +- optional `DYNU_DOMAIN` for domain-specific coverage -Optional: -- `DYNU_DOMAIN` (required for domain-specific acceptance tests such as `dynu_domain` and `dynu_dns_records`) +## Feature scope -```bash -TF_ACC=1 DYNU_API_KEY="your-dynu-api-key" DYNU_DOMAIN="www.example.com" ./scripts/testacc.sh --live -# or -LIVE=1 TF_ACC=1 DYNU_API_KEY="your-dynu-api-key" ./scripts/testacc.sh -``` +Implemented: +- Provider authentication via `api_key` or `DYNU_API_KEY` +- Optional provider `base_url` override +- Data sources: `dynu_domains`, `dynu_domain`, `dynu_dns_records` -If `DYNU_DOMAIN` is omitted, domain-specific live tests skip cleanly. - -## CI - -GitHub Actions CI runs on push and pull requests and executes: -- gofmt verification -- `go vet ./...` -- `go test ./...` - -Live acceptance tests are intentionally excluded from default CI. - -## Documentation - -Registry-style markdown docs are stored in `docs/`. - -## Limitations - -- Dynu timestamps are currently exposed as strings exactly as returned by Dynu API. -- Data returned from Dynu is sorted in provider state for Terraform stability. -- Read-only operations only. - -## Roadmap - -Next planned milestone after this testing foundation: -- first writable resource (`dynu_dns_record`) with strict schema validation, import support, mock-first integration tests, and then live acceptance coverage. +Not implemented yet: +- Terraform resources (create/update/delete) +- Any write API operations diff --git a/examples/read_only/main.tf b/examples/read_only/main.tf new file mode 100644 index 0000000..4ce1a75 --- /dev/null +++ b/examples/read_only/main.tf @@ -0,0 +1,12 @@ +# Lists all Dynu domains available to the configured API key. +data "dynu_domains" "all" {} + +# Resolves the Dynu root domain for a specific hostname. +data "dynu_domain" "selected" { + hostname = var.hostname +} + +# Lists DNS records for the root domain resolved from the same hostname. +data "dynu_dns_records" "selected" { + hostname = var.hostname +} diff --git a/examples/read_only/outputs.tf b/examples/read_only/outputs.tf new file mode 100644 index 0000000..b985bf8 --- /dev/null +++ b/examples/read_only/outputs.tf @@ -0,0 +1,24 @@ +output "domains" { + description = "All visible Dynu domains." + value = data.dynu_domains.all.domains +} + +output "resolved_domain" { + description = "Resolved Dynu root domain details for var.hostname." + value = data.dynu_domain.selected.domain +} + +output "resolved_dns_records" { + description = "DNS records for the resolved root domain." + value = data.dynu_dns_records.selected.records +} + +output "resolved_domain_id" { + description = "Domain ID returned by dynu_dns_records." + value = data.dynu_dns_records.selected.domain_id +} + +output "resolved_domain_name" { + description = "Root domain name returned by dynu_dns_records." + value = data.dynu_dns_records.selected.domain_name +} diff --git a/examples/read_only/providers.tf b/examples/read_only/providers.tf new file mode 100644 index 0000000..c84a49c --- /dev/null +++ b/examples/read_only/providers.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + dynu = { + source = "dynu/dynu" + } + } +} + +# Local development note: +# This source address stays "dynu/dynu" while using ~/.terraformrc dev_overrides. +# Terraform will load your local terraform-provider-dynu binary instead of the public registry. +provider "dynu" { + # Use DYNU_API_KEY environment variable by default. + # Set var.dynu_api_key in terraform.tfvars to override. + api_key = var.dynu_api_key +} diff --git a/examples/read_only/terraform.tfvars.example b/examples/read_only/terraform.tfvars.example new file mode 100644 index 0000000..2eff749 --- /dev/null +++ b/examples/read_only/terraform.tfvars.example @@ -0,0 +1,6 @@ +# Copy to terraform.tfvars and adjust values for your environment. +# dynu_api_key can be omitted when DYNU_API_KEY is exported in your shell. +# dynu_api_key = "replace-with-dynu-api-key" + +# Use a hostname that exists in your Dynu account. +hostname = "www.example.com" diff --git a/examples/read_only/variables.tf b/examples/read_only/variables.tf new file mode 100644 index 0000000..0d4034d --- /dev/null +++ b/examples/read_only/variables.tf @@ -0,0 +1,12 @@ +variable "dynu_api_key" { + description = "Dynu API key. Leave null to use DYNU_API_KEY from environment." + type = string + default = null + sensitive = true +} + +variable "hostname" { + description = "Fully-qualified hostname used by dynu_domain and dynu_dns_records data sources." + type = string + default = "www.example.com" +} diff --git a/internal/dynuclient/client.go b/internal/dynuclient/client.go index 821506d..3f2094d 100644 --- a/internal/dynuclient/client.go +++ b/internal/dynuclient/client.go @@ -56,6 +56,16 @@ type apiException struct { Message string `json:"message"` } +type APIError struct { + StatusCode int + Type string + Message string +} + +func (e *APIError) Error() string { + return fmt.Sprintf("dynu API error %d (%s): %s", e.StatusCode, e.Type, e.Message) +} + type apiResponse struct { StatusCode int `json:"statusCode"` Exception *apiException `json:"exception"` @@ -202,5 +212,9 @@ func parseAPIException(payload []byte) error { return nil } - return fmt.Errorf("dynu API error %d (%s): %s", apiResult.Exception.StatusCode, apiResult.Exception.Type, apiResult.Exception.Message) + return &APIError{ + StatusCode: apiResult.Exception.StatusCode, + Type: apiResult.Exception.Type, + Message: apiResult.Exception.Message, + } } diff --git a/internal/provider/data_source_dns_records.go b/internal/provider/data_source_dns_records.go index 8986160..ffd9c78 100644 --- a/internal/provider/data_source_dns_records.go +++ b/internal/provider/data_source_dns_records.go @@ -115,13 +115,13 @@ func (d *dnsRecordsDataSource) Read(ctx context.Context, req datasource.ReadRequ domainID, domainName, err := d.clientProvider.client.GetRootDomain(ctx, config.Hostname.ValueString()) if err != nil { - resp.Diagnostics.AddError("Unable to resolve Dynu domain from hostname", err.Error()) + resp.Diagnostics.AddError(diagnosticSummary("Unable to resolve Dynu domain from hostname", err), err.Error()) return } records, err := d.clientProvider.client.ListDNSRecords(ctx, domainID) if err != nil { - resp.Diagnostics.AddError("Unable to list Dynu DNS records", err.Error()) + resp.Diagnostics.AddError(diagnosticSummary("Unable to list Dynu DNS records", err), err.Error()) return } diff --git a/internal/provider/data_source_domain.go b/internal/provider/data_source_domain.go index a48b6aa..8a45d25 100644 --- a/internal/provider/data_source_domain.go +++ b/internal/provider/data_source_domain.go @@ -88,13 +88,13 @@ func (d *domainDataSource) Read(ctx context.Context, req datasource.ReadRequest, domainID, _, err := d.clientProvider.client.GetRootDomain(ctx, config.Hostname.ValueString()) if err != nil { - resp.Diagnostics.AddError("Unable to resolve Dynu domain from hostname", err.Error()) + resp.Diagnostics.AddError(diagnosticSummary("Unable to resolve Dynu domain from hostname", err), err.Error()) return } domain, err := d.clientProvider.client.GetDomainByID(ctx, domainID) if err != nil { - resp.Diagnostics.AddError("Unable to get Dynu domain", err.Error()) + resp.Diagnostics.AddError(diagnosticSummary("Unable to get Dynu domain", err), err.Error()) return } diff --git a/internal/provider/data_source_domains.go b/internal/provider/data_source_domains.go index 3c6efe8..0161a6e 100644 --- a/internal/provider/data_source_domains.go +++ b/internal/provider/data_source_domains.go @@ -80,7 +80,7 @@ func (d *domainsDataSource) Configure(_ context.Context, req datasource.Configur func (d *domainsDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) { domains, err := d.clientProvider.client.ListDomains(ctx) if err != nil { - resp.Diagnostics.AddError("Unable to list Dynu domains", err.Error()) + resp.Diagnostics.AddError(diagnosticSummary("Unable to list Dynu domains", err), err.Error()) return } diff --git a/internal/provider/diagnostics.go b/internal/provider/diagnostics.go new file mode 100644 index 0000000..b2ebd3d --- /dev/null +++ b/internal/provider/diagnostics.go @@ -0,0 +1,23 @@ +package provider + +import ( + "errors" + + "github.com/dynu/terraform-provider-dynu/internal/dynuclient" +) + +func diagnosticSummary(defaultSummary string, err error) string { + var apiErr *dynuclient.APIError + if !errors.As(err, &apiErr) { + return defaultSummary + } + + switch apiErr.StatusCode { + case 401, 403: + return defaultSummary + " (authentication failed)" + case 404: + return defaultSummary + " (not found)" + default: + return defaultSummary + } +} diff --git a/internal/provider/provider_integration_test.go b/internal/provider/provider_integration_test.go index 4171d73..c0acf26 100644 --- a/internal/provider/provider_integration_test.go +++ b/internal/provider/provider_integration_test.go @@ -157,6 +157,59 @@ func TestIntegrationDataSourceDiagnosticsFromAPIError(t *testing.T) { } } +func TestIntegrationDataSourceDiagnosticsFromAuthError(t *testing.T) { + fake := fakedynu.NewServer() + defer fake.Close() + fake.SetAPIError("/dns/getroot/www.a.example.com", fakedynu.APIError{HTTPStatus: 401, StatusCode: 401, Type: "Unauthorized", Message: "invalid api key"}) + + ds := NewDomainDataSource().(*domainDataSource) + configureDataSource(t, ds, fake.BaseURL()) + + var schemaResp datasource.SchemaResponse + ds.Schema(context.Background(), datasource.SchemaRequest{}, &schemaResp) + + req := datasource.ReadRequest{Config: tfsdk.Config{ + Schema: schemaResp.Schema, + Raw: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{"hostname": tftypes.String}}, map[string]tftypes.Value{ + "hostname": tftypes.NewValue(tftypes.String, "www.a.example.com"), + }), + }} + resp := datasource.ReadResponse{State: tfsdk.State{Schema: schemaResp.Schema}} + ds.Read(context.Background(), req, &resp) + if !resp.Diagnostics.HasError() { + t.Fatal("expected diagnostics error") + } + if !strings.Contains(resp.Diagnostics[0].Summary(), "authentication failed") { + t.Fatalf("unexpected diagnostics summary: %s", resp.Diagnostics[0].Summary()) + } +} + +func TestIntegrationDataSourceDiagnosticsFromNotFoundError(t *testing.T) { + fake := fakedynu.NewServer() + defer fake.Close() + + ds := NewDomainDataSource().(*domainDataSource) + configureDataSource(t, ds, fake.BaseURL()) + + var schemaResp datasource.SchemaResponse + ds.Schema(context.Background(), datasource.SchemaRequest{}, &schemaResp) + + req := datasource.ReadRequest{Config: tfsdk.Config{ + Schema: schemaResp.Schema, + Raw: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{"hostname": tftypes.String}}, map[string]tftypes.Value{ + "hostname": tftypes.NewValue(tftypes.String, "missing.a.example.com"), + }), + }} + resp := datasource.ReadResponse{State: tfsdk.State{Schema: schemaResp.Schema}} + ds.Read(context.Background(), req, &resp) + if !resp.Diagnostics.HasError() { + t.Fatal("expected diagnostics error") + } + if !strings.Contains(resp.Diagnostics[0].Summary(), "not found") { + t.Fatalf("unexpected diagnostics summary: %s", resp.Diagnostics[0].Summary()) + } +} + func configureDataSource(t *testing.T, ds datasource.DataSourceWithConfigure, baseURL string) { t.Helper() resp := datasource.ConfigureResponse{} diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index b47ccd7..84479bb 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -1,8 +1,14 @@ package provider import ( + "context" + "os" + "path/filepath" + "strings" "testing" + "github.com/dynu/terraform-provider-dynu/internal/dynuclient" + "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -28,3 +34,61 @@ func TestResolveAPIKey(t *testing.T) { }) } } + +func TestProviderSchema(t *testing.T) { + p := New("test")() + var resp provider.SchemaResponse + p.Schema(context.Background(), provider.SchemaRequest{}, &resp) + + apiKeyAttr, ok := resp.Schema.Attributes["api_key"] + if !ok { + t.Fatal("expected api_key in provider schema") + } + if !apiKeyAttr.IsOptional() { + t.Fatal("expected api_key to be optional") + } + + baseURLAttr, ok := resp.Schema.Attributes["base_url"] + if !ok { + t.Fatal("expected base_url in provider schema") + } + if !baseURLAttr.IsOptional() { + t.Fatal("expected base_url to be optional") + } +} + +func TestDiagnosticSummary(t *testing.T) { + tests := []struct { + name string + err error + summary string + want string + }{ + {name: "non api error", err: os.ErrNotExist, summary: "Unable to list Dynu domains", want: "Unable to list Dynu domains"}, + {name: "auth error", err: &dynuclient.APIError{StatusCode: 401, Type: "Unauthorized", Message: "invalid"}, summary: "Unable to list Dynu domains", want: "Unable to list Dynu domains (authentication failed)"}, + {name: "not found", err: &dynuclient.APIError{StatusCode: 404, Type: "Not Found", Message: "missing"}, summary: "Unable to resolve Dynu domain from hostname", want: "Unable to resolve Dynu domain from hostname (not found)"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := diagnosticSummary(tc.summary, tc.err); got != tc.want { + t.Fatalf("diagnosticSummary() = %q, want %q", got, tc.want) + } + }) + } +} + +func TestReadOnlyExampleUsesValidProviderArguments(t *testing.T) { + contents, err := os.ReadFile(filepath.Join("..", "..", "examples", "read_only", "providers.tf")) + if err != nil { + t.Fatalf("read example providers.tf: %v", err) + } + + config := string(contents) + if strings.Contains(config, "username") { + t.Fatal("example providers.tf must not use unsupported provider argument 'username'") + } + if !strings.Contains(config, "api_key") { + t.Fatal("example providers.tf must include api_key argument") + } +}