Archived
Add mock-backed integration testing tier and fake Dynu API
This commit is contained in:
@@ -8,6 +8,7 @@ A standalone Terraform provider for Dynu DNS.
|
||||
|
||||
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`
|
||||
@@ -28,7 +29,7 @@ The repository can be hosted elsewhere during development, but module and provid
|
||||
|
||||
- Terraform `>= 1.5`
|
||||
- Go `>= 1.23`
|
||||
- Dynu API key
|
||||
- Dynu API key for live API usage
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -62,8 +63,9 @@ See the `examples/` directory:
|
||||
## Developer workflow
|
||||
|
||||
- `./scripts/setup-dev.sh` - verify required local tools
|
||||
- `./scripts/check.sh` - formatting, vet, and unit tests
|
||||
- `./scripts/testacc.sh` - acceptance tests only
|
||||
- `./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)
|
||||
|
||||
### Build
|
||||
|
||||
@@ -71,15 +73,35 @@ See the `examples/` directory:
|
||||
go build ./...
|
||||
```
|
||||
|
||||
### Unit tests
|
||||
## 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 ./...
|
||||
```
|
||||
|
||||
### Acceptance tests
|
||||
### Tier B: local integration tests (mock Dynu API, no real credentials)
|
||||
|
||||
Acceptance tests are read-only and opt-in.
|
||||
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:
|
||||
- `TF_ACC=1`
|
||||
@@ -88,13 +110,13 @@ Required environment variables:
|
||||
Optional:
|
||||
- `DYNU_DOMAIN` (required for domain-specific acceptance tests such as `dynu_domain` and `dynu_dns_records`)
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
TF_ACC=1 DYNU_API_KEY="your-dynu-api-key" DYNU_DOMAIN="www.example.com" ./scripts/testacc.sh
|
||||
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
|
||||
```
|
||||
|
||||
If `DYNU_DOMAIN` is omitted, domain-specific tests skip cleanly.
|
||||
If `DYNU_DOMAIN` is omitted, domain-specific live tests skip cleanly.
|
||||
|
||||
## CI
|
||||
|
||||
@@ -103,7 +125,7 @@ GitHub Actions CI runs on push and pull requests and executes:
|
||||
- `go vet ./...`
|
||||
- `go test ./...`
|
||||
|
||||
Acceptance tests are intentionally excluded from default CI.
|
||||
Live acceptance tests are intentionally excluded from default CI.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -117,5 +139,5 @@ Registry-style markdown docs are stored in `docs/`.
|
||||
|
||||
## Roadmap
|
||||
|
||||
Next planned milestone after this quality-hardening release:
|
||||
- first writable resource (`dynu_dns_record`) with careful CRUD behavior and acceptance coverage.
|
||||
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.
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
module github.com/dynu/terraform-provider-dynu
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.3
|
||||
|
||||
require (
|
||||
github.com/hashicorp/terraform-plugin-framework v1.14.1
|
||||
github.com/hashicorp/terraform-plugin-framework-validators v0.17.0
|
||||
github.com/hashicorp/terraform-plugin-framework v1.16.1
|
||||
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0
|
||||
github.com/hashicorp/terraform-plugin-go v0.29.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||
github.com/hashicorp/go-plugin v1.6.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||
github.com/hashicorp/go-plugin v1.7.0 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
|
||||
github.com/hashicorp/terraform-registry-address v0.2.4 // indirect
|
||||
github.com/hashicorp/terraform-plugin-log v0.10.0 // indirect
|
||||
github.com/hashicorp/terraform-registry-address v0.4.0 // indirect
|
||||
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/oklog/run v1.0.0 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
|
||||
google.golang.org/grpc v1.69.4 // indirect
|
||||
google.golang.org/protobuf v1.36.3 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/grpc v1.79.2 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
@@ -1,91 +1,101 @@
|
||||
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
|
||||
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog=
|
||||
github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
|
||||
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.14.1 h1:jaT1yvU/kEKEsxnbrn4ZHlgcxyIfjvZ41BLdlLk52fY=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.14.1/go.mod h1:xNUKmvTs6ldbwTuId5euAtg37dTxuyj3LHS3uj7BHQ4=
|
||||
github.com/hashicorp/terraform-plugin-framework-validators v0.17.0 h1:0uYQcqqgW3BMyyve07WJgpKorXST3zkpzvrOnf3mpbg=
|
||||
github.com/hashicorp/terraform-plugin-framework-validators v0.17.0/go.mod h1:VwdfgE/5Zxm43flraNa0VjcvKQOGVrcO4X8peIri0T0=
|
||||
github.com/hashicorp/terraform-plugin-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiypAfFQmw8aE65O2M=
|
||||
github.com/hashicorp/terraform-plugin-go v0.26.0/go.mod h1:+CXjuLDiFgqR+GcrM5a2E2Kal5t5q2jb0E3D57tTdNY=
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
|
||||
github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA=
|
||||
github.com/hashicorp/terraform-registry-address v0.2.4/go.mod h1:tUNYTVyCtU4OIGXXMDp7WNcJ+0W1B4nmstVDgHMjfAU=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.16.1 h1:1+zwFm3MEqd/0K3YBB2v9u9DtyYHyEuhVOfeIXbteWA=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.16.1/go.mod h1:0xFOxLy5lRzDTayc4dzK/FakIgBhNf/lC4499R9cV4Y=
|
||||
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0 h1:Zz3iGgzxe/1XBkooZCewS0nJAaCFPFPHdNJd8FgE4Ow=
|
||||
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0/go.mod h1:GBKTNGbGVJohU03dZ7U8wHqc2zYnMUawgCN+gC0itLc=
|
||||
github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU=
|
||||
github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM=
|
||||
github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g=
|
||||
github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0=
|
||||
github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk=
|
||||
github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE=
|
||||
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
|
||||
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
|
||||
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
|
||||
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
||||
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
|
||||
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
|
||||
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
|
||||
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
|
||||
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
|
||||
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
|
||||
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
package dynuclient
|
||||
package dynuclient_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"github.com/dynu/terraform-provider-dynu/internal/dynuclient"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dynu/terraform-provider-dynu/internal/testutil/fakedynu"
|
||||
)
|
||||
|
||||
func TestClientListDomainsSuccess(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/dns" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if got := r.Header.Get("API-Key"); got != "test-key" {
|
||||
t.Fatalf("unexpected api key header: %s", got)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"statusCode":200,"domains":[{"id":2,"name":"z.example.com"},{"id":1,"name":"a.example.com"}]}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
|
||||
client := New("test-key", WithBaseURL(ts.URL), WithHTTPClient(ts.Client()))
|
||||
client := dynuclient.New("test-key", dynuclient.WithBaseURL(fake.BaseURL()), dynuclient.WithHTTPClient(fake.Client()))
|
||||
domains, err := client.ListDomains(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("ListDomains() error = %v", err)
|
||||
@@ -32,13 +24,11 @@ func TestClientListDomainsSuccess(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientDoGetNon2xxStatus(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = w.Write([]byte(`{"message":"nope"}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
fake.SetRawResponse("/dns", 401, `{"message":"nope"}`)
|
||||
|
||||
client := New("test-key", WithBaseURL(ts.URL), WithHTTPClient(ts.Client()))
|
||||
client := dynuclient.New("test-key", dynuclient.WithBaseURL(fake.BaseURL()), dynuclient.WithHTTPClient(fake.Client()))
|
||||
_, err := client.ListDomains(context.Background())
|
||||
if err == nil || !strings.Contains(err.Error(), "status 401") {
|
||||
t.Fatalf("expected status error, got %v", err)
|
||||
@@ -46,13 +36,11 @@ func TestClientDoGetNon2xxStatus(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientDoGetAPIExceptionPayload(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"statusCode":200,"exception":{"statusCode":400,"type":"Validation Exception","message":"bad hostname"}}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
fake.SetAPIError("/dns", fakedynu.APIError{HTTPStatus: 400, StatusCode: 400, Type: "Validation Exception", Message: "bad hostname"})
|
||||
|
||||
client := New("test-key", WithBaseURL(ts.URL), WithHTTPClient(ts.Client()))
|
||||
client := dynuclient.New("test-key", dynuclient.WithBaseURL(fake.BaseURL()), dynuclient.WithHTTPClient(fake.Client()))
|
||||
_, err := client.ListDomains(context.Background())
|
||||
if err == nil || !strings.Contains(err.Error(), "Validation Exception") {
|
||||
t.Fatalf("expected API exception error, got %v", err)
|
||||
@@ -60,13 +48,11 @@ func TestClientDoGetAPIExceptionPayload(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientDoGetMalformedJSON(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"statusCode":200,"domains":[`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
fake.SetRawResponse("/dns", 200, `{"statusCode":200,"domains":[`)
|
||||
|
||||
client := New("test-key", WithBaseURL(ts.URL), WithHTTPClient(ts.Client()))
|
||||
client := dynuclient.New("test-key", dynuclient.WithBaseURL(fake.BaseURL()), dynuclient.WithHTTPClient(fake.Client()))
|
||||
_, err := client.ListDomains(context.Background())
|
||||
if err == nil || !strings.Contains(err.Error(), "failed to decode dynu API response") {
|
||||
t.Fatalf("expected decode error, got %v", err)
|
||||
@@ -74,31 +60,25 @@ func TestClientDoGetMalformedJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClientGetRootDomainIncompleteResponse(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"statusCode":200,"id":0,"domainName":""}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
fake.SetRawResponse("/dns/getroot/www.a.example.com", 200, `{"statusCode":200,"id":0,"domainName":""}`)
|
||||
|
||||
client := New("test-key", WithBaseURL(ts.URL), WithHTTPClient(ts.Client()))
|
||||
_, _, err := client.GetRootDomain(context.Background(), "www.example.com")
|
||||
client := dynuclient.New("test-key", dynuclient.WithBaseURL(fake.BaseURL()), dynuclient.WithHTTPClient(fake.Client()))
|
||||
_, _, err := client.GetRootDomain(context.Background(), "www.a.example.com")
|
||||
if err == nil || !strings.Contains(err.Error(), "incomplete root domain response") {
|
||||
t.Fatalf("expected incomplete response error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientGetRootDomainEscapesHostname(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.RequestURI, "spaces%20in%20name.example.com") {
|
||||
t.Fatalf("expected escaped hostname path, got %s", r.RequestURI)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"statusCode":200,"id":123,"domainName":"example.com"}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
fake.SetAPIError("/dns/getroot/spaces%20in%20name.example.com", fakedynu.APIError{HTTPStatus: 404, StatusCode: 404, Type: "Not Found", Message: "hostname not found"})
|
||||
|
||||
client := New("test-key", WithBaseURL(ts.URL), WithHTTPClient(ts.Client()))
|
||||
client := dynuclient.New("test-key", dynuclient.WithBaseURL(fake.BaseURL()), dynuclient.WithHTTPClient(fake.Client()))
|
||||
_, _, err := client.GetRootDomain(context.Background(), "spaces in name.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got %v", err)
|
||||
if err == nil || !strings.Contains(err.Error(), "hostname not found") {
|
||||
t.Fatalf("expected hostname not found error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,18 +100,20 @@ func (d *dnsRecordsDataSource) Configure(_ context.Context, req datasource.Confi
|
||||
}
|
||||
|
||||
func (d *dnsRecordsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||
var state dnsRecordsDataSourceModel
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
|
||||
var config struct {
|
||||
Hostname types.String `tfsdk:"hostname"`
|
||||
}
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if state.Hostname.IsUnknown() || state.Hostname.IsNull() {
|
||||
if config.Hostname.IsUnknown() || config.Hostname.IsNull() {
|
||||
resp.Diagnostics.AddAttributeError(path.Root("hostname"), "Invalid hostname", "The hostname must be known and non-null.")
|
||||
return
|
||||
}
|
||||
|
||||
domainID, domainName, err := d.clientProvider.client.GetRootDomain(ctx, state.Hostname.ValueString())
|
||||
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())
|
||||
return
|
||||
@@ -125,9 +127,12 @@ func (d *dnsRecordsDataSource) Read(ctx context.Context, req datasource.ReadRequ
|
||||
|
||||
sortDNSRecords(records)
|
||||
|
||||
state.DomainID = types.Int64Value(domainID)
|
||||
state.DomainName = types.StringValue(domainName)
|
||||
state.Records = make([]dnsRecordStateItem, 0, len(records))
|
||||
state := dnsRecordsDataSourceModel{
|
||||
Hostname: config.Hostname,
|
||||
DomainID: types.Int64Value(domainID),
|
||||
DomainName: types.StringValue(domainName),
|
||||
Records: make([]dnsRecordStateItem, 0, len(records)),
|
||||
}
|
||||
for _, record := range records {
|
||||
state.Records = append(state.Records, dnsRecordStateItem{
|
||||
ID: types.Int64Value(record.ID),
|
||||
|
||||
@@ -73,18 +73,20 @@ func (d *domainDataSource) Configure(_ context.Context, req datasource.Configure
|
||||
}
|
||||
|
||||
func (d *domainDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||
var state domainDataSourceModel
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
|
||||
var config struct {
|
||||
Hostname types.String `tfsdk:"hostname"`
|
||||
}
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if state.Hostname.IsUnknown() || state.Hostname.IsNull() {
|
||||
if config.Hostname.IsUnknown() || config.Hostname.IsNull() {
|
||||
resp.Diagnostics.AddAttributeError(path.Root("hostname"), "Invalid hostname", "The hostname must be known and non-null.")
|
||||
return
|
||||
}
|
||||
|
||||
domainID, _, err := d.clientProvider.client.GetRootDomain(ctx, state.Hostname.ValueString())
|
||||
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())
|
||||
return
|
||||
@@ -101,7 +103,10 @@ func (d *domainDataSource) Read(ctx context.Context, req datasource.ReadRequest,
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
state.Domain = domainObject
|
||||
state := domainDataSourceModel{
|
||||
Hostname: config.Hostname,
|
||||
Domain: domainObject,
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dynu/terraform-provider-dynu/internal/testutil/fakedynu"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-go/tftypes"
|
||||
)
|
||||
|
||||
func TestIntegrationDataSourceDomains(t *testing.T) {
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
|
||||
ds := NewDomainsDataSource().(*domainsDataSource)
|
||||
configureDataSource(t, ds, fake.BaseURL())
|
||||
|
||||
var schemaResp datasource.SchemaResponse
|
||||
ds.Schema(context.Background(), datasource.SchemaRequest{}, &schemaResp)
|
||||
|
||||
resp := datasource.ReadResponse{State: tfsdk.State{Schema: schemaResp.Schema}}
|
||||
ds.Read(context.Background(), datasource.ReadRequest{}, &resp)
|
||||
if resp.Diagnostics.HasError() {
|
||||
t.Fatalf("unexpected diagnostics: %v", resp.Diagnostics)
|
||||
}
|
||||
|
||||
var state domainsDataSourceModel
|
||||
diags := resp.State.Get(context.Background(), &state)
|
||||
if diags.HasError() {
|
||||
t.Fatalf("state get diagnostics: %v", diags)
|
||||
}
|
||||
|
||||
if len(state.Domains) != 2 {
|
||||
t.Fatalf("expected 2 domains, got %d", len(state.Domains))
|
||||
}
|
||||
if state.Domains[0].Name.ValueString() != "a.example.com" {
|
||||
t.Fatalf("expected sorted domain a.example.com first, got %q", state.Domains[0].Name.ValueString())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationDataSourceDomain(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, "www.a.example.com"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
resp := datasource.ReadResponse{State: tfsdk.State{Schema: schemaResp.Schema}}
|
||||
ds.Read(context.Background(), req, &resp)
|
||||
if resp.Diagnostics.HasError() {
|
||||
t.Fatalf("unexpected diagnostics: %v", resp.Diagnostics)
|
||||
}
|
||||
|
||||
var state domainDataSourceModel
|
||||
diags := resp.State.Get(context.Background(), &state)
|
||||
if diags.HasError() {
|
||||
t.Fatalf("state get diagnostics: %v", diags)
|
||||
}
|
||||
|
||||
var domainID int64
|
||||
diags = resp.State.GetAttribute(context.Background(), path.Root("domain").AtName("id"), &domainID)
|
||||
if diags.HasError() {
|
||||
t.Fatalf("domain.id decode diagnostics: %v", diags)
|
||||
}
|
||||
var domainName string
|
||||
diags = resp.State.GetAttribute(context.Background(), path.Root("domain").AtName("name"), &domainName)
|
||||
if diags.HasError() {
|
||||
t.Fatalf("domain.name decode diagnostics: %v", diags)
|
||||
}
|
||||
if domainID != 1001 || domainName != "a.example.com" {
|
||||
t.Fatalf("unexpected domain state: id=%d name=%q", domainID, domainName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationDataSourceDNSRecords(t *testing.T) {
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
|
||||
ds := NewDNSRecordsDataSource().(*dnsRecordsDataSource)
|
||||
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.Fatalf("unexpected diagnostics: %v", resp.Diagnostics)
|
||||
}
|
||||
|
||||
var state dnsRecordsDataSourceModel
|
||||
diags := resp.State.Get(context.Background(), &state)
|
||||
if diags.HasError() {
|
||||
t.Fatalf("state get diagnostics: %v", diags)
|
||||
}
|
||||
|
||||
if state.DomainID.ValueInt64() != 1001 || state.DomainName.ValueString() != "a.example.com" {
|
||||
t.Fatalf("unexpected domain resolution: domain_id=%d domain_name=%q", state.DomainID.ValueInt64(), state.DomainName.ValueString())
|
||||
}
|
||||
if len(state.Records) != 2 || state.Records[0].ID.ValueInt64() != 10 {
|
||||
t.Fatalf("unexpected records state: %#v", state.Records)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationDataSourceDiagnosticsFromAPIError(t *testing.T) {
|
||||
fake := fakedynu.NewServer()
|
||||
defer fake.Close()
|
||||
fake.SetAPIError("/dns/getroot/www.a.example.com", fakedynu.APIError{HTTPStatus: 400, StatusCode: 400, Type: "Validation Exception", Message: "bad hostname"})
|
||||
|
||||
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(), "Unable to resolve Dynu domain from hostname") {
|
||||
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{}
|
||||
ds.Configure(context.Background(), datasource.ConfigureRequest{
|
||||
ProviderData: &providerData{client: newDynuClient("dummy-local-key", types.StringValue(baseURL))},
|
||||
}, &resp)
|
||||
if resp.Diagnostics.HasError() {
|
||||
t.Fatalf("configure diagnostics: %v", resp.Diagnostics)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package fakedynu
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/dynu/terraform-provider-dynu/internal/dynuclient"
|
||||
)
|
||||
|
||||
type RootDomain struct {
|
||||
ID int64
|
||||
Hostname string
|
||||
DomainName string
|
||||
Node string
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
HTTPStatus int
|
||||
StatusCode int
|
||||
Type string
|
||||
Message string
|
||||
}
|
||||
|
||||
type Fixture struct {
|
||||
Domains []dynuclient.Domain
|
||||
RootsByHostname map[string]RootDomain
|
||||
RecordsByDomain map[int64][]dynuclient.DNSRecord
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
*httptest.Server
|
||||
|
||||
mu sync.RWMutex
|
||||
fixture Fixture
|
||||
errors map[string]APIError
|
||||
rawPayload map[string]rawResponse
|
||||
}
|
||||
|
||||
type rawResponse struct {
|
||||
httpStatus int
|
||||
body string
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
s := &Server{
|
||||
errors: map[string]APIError{},
|
||||
rawPayload: map[string]rawResponse{},
|
||||
fixture: Fixture{
|
||||
Domains: []dynuclient.Domain{
|
||||
{ID: 2002, Name: "z.example.com", UnicodeName: "z.example.com", TTL: 60, CreatedOn: "2024-01-03T00:00:00", UpdatedOn: "2024-01-04T00:00:00"},
|
||||
{ID: 1001, Name: "a.example.com", UnicodeName: "a.example.com", TTL: 120, CreatedOn: "2024-01-01T00:00:00", UpdatedOn: "2024-01-02T00:00:00"},
|
||||
},
|
||||
RootsByHostname: map[string]RootDomain{
|
||||
"www.a.example.com": {ID: 1001, Hostname: "www.a.example.com", DomainName: "a.example.com", Node: "www"},
|
||||
"api.a.example.com": {ID: 1001, Hostname: "api.a.example.com", DomainName: "a.example.com", Node: "api"},
|
||||
},
|
||||
RecordsByDomain: map[int64][]dynuclient.DNSRecord{
|
||||
1001: {
|
||||
{ID: 20, DomainID: 1001, DomainName: "a.example.com", NodeName: "www", Hostname: "www.a.example.com", RecordType: "TXT", TTL: 90, State: true, Content: "v=spf1", UpdatedOn: "2024-01-03T11:00:00"},
|
||||
{ID: 10, DomainID: 1001, DomainName: "a.example.com", NodeName: "www", Hostname: "www.a.example.com", RecordType: "A", TTL: 30, State: true, Content: "203.0.113.5", UpdatedOn: "2024-01-03T10:00:00"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s.Server = httptest.NewServer(http.HandlerFunc(s.serveHTTP))
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) BaseURL() string {
|
||||
return s.URL
|
||||
}
|
||||
|
||||
func (s *Server) SetFixture(fixture Fixture) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.fixture = fixture
|
||||
}
|
||||
|
||||
func (s *Server) SetAPIError(path string, apiErr APIError) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.errors[path] = apiErr
|
||||
}
|
||||
|
||||
func (s *Server) SetRawResponse(path string, httpStatus int, body string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.rawPayload[path] = rawResponse{httpStatus: httpStatus, body: body}
|
||||
}
|
||||
|
||||
func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if raw, ok := s.rawPayload[r.URL.Path]; ok {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(raw.httpStatus)
|
||||
_, _ = w.Write([]byte(raw.body))
|
||||
return
|
||||
}
|
||||
|
||||
if apiErr, ok := s.errors[r.URL.Path]; ok {
|
||||
s.writeAPIError(w, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case r.URL.Path == "/dns":
|
||||
s.writeJSON(w, http.StatusOK, map[string]any{"statusCode": 200, "domains": s.fixture.Domains})
|
||||
return
|
||||
case strings.HasPrefix(r.URL.Path, "/dns/getroot/"):
|
||||
hostname := strings.TrimPrefix(r.URL.Path, "/dns/getroot/")
|
||||
root, ok := s.fixture.RootsByHostname[hostname]
|
||||
if !ok {
|
||||
s.writeAPIError(w, APIError{HTTPStatus: http.StatusNotFound, StatusCode: 404, Type: "Not Found", Message: "hostname not found"})
|
||||
return
|
||||
}
|
||||
s.writeJSON(w, http.StatusOK, map[string]any{
|
||||
"statusCode": 200,
|
||||
"id": root.ID,
|
||||
"hostname": root.Hostname,
|
||||
"domainName": root.DomainName,
|
||||
"node": root.Node,
|
||||
})
|
||||
return
|
||||
case strings.HasSuffix(r.URL.Path, "/record"):
|
||||
domainID, err := domainIDFromPath(strings.TrimSuffix(r.URL.Path, "/record"))
|
||||
if err != nil {
|
||||
s.writeAPIError(w, APIError{HTTPStatus: http.StatusBadRequest, StatusCode: 400, Type: "Validation Exception", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
records, ok := s.fixture.RecordsByDomain[domainID]
|
||||
if !ok {
|
||||
records = []dynuclient.DNSRecord{}
|
||||
}
|
||||
s.writeJSON(w, http.StatusOK, map[string]any{"statusCode": 200, "dnsRecords": records})
|
||||
return
|
||||
default:
|
||||
domainID, err := domainIDFromPath(r.URL.Path)
|
||||
if err != nil {
|
||||
s.writeAPIError(w, APIError{HTTPStatus: http.StatusNotFound, StatusCode: 404, Type: "Not Found", Message: "endpoint not found"})
|
||||
return
|
||||
}
|
||||
for _, domain := range s.fixture.Domains {
|
||||
if domain.ID == domainID {
|
||||
payload := map[string]any{"statusCode": 200}
|
||||
b, _ := json.Marshal(domain)
|
||||
_ = json.Unmarshal(b, &payload)
|
||||
s.writeJSON(w, http.StatusOK, payload)
|
||||
return
|
||||
}
|
||||
}
|
||||
s.writeAPIError(w, APIError{HTTPStatus: http.StatusNotFound, StatusCode: 404, Type: "Not Found", Message: "domain not found"})
|
||||
}
|
||||
}
|
||||
|
||||
func domainIDFromPath(path string) (int64, error) {
|
||||
trimmed := strings.TrimPrefix(path, "/dns/")
|
||||
if trimmed == path {
|
||||
return 0, fmt.Errorf("invalid domain path: %s", path)
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(trimmed, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid domain id: %s", trimmed)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *Server) writeAPIError(w http.ResponseWriter, apiErr APIError) {
|
||||
if apiErr.HTTPStatus == 0 {
|
||||
apiErr.HTTPStatus = http.StatusBadRequest
|
||||
}
|
||||
if apiErr.StatusCode == 0 {
|
||||
apiErr.StatusCode = apiErr.HTTPStatus
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(apiErr.HTTPStatus)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"statusCode": apiErr.StatusCode,
|
||||
"exception": map[string]any{
|
||||
"statusCode": apiErr.StatusCode,
|
||||
"type": apiErr.Type,
|
||||
"message": apiErr.Message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) writeJSON(w http.ResponseWriter, status int, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "[test-integration] running mock-backed provider integration tests"
|
||||
go test ./internal/provider -run '^TestIntegration' -count=1 -v
|
||||
+13
-3
@@ -1,13 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
mode="mock"
|
||||
if [[ "${1:-}" == "--live" || "${LIVE:-}" == "1" ]]; then
|
||||
mode="live"
|
||||
fi
|
||||
|
||||
if [[ "${mode}" == "mock" ]]; then
|
||||
echo "[testacc] no --live flag detected; running local mock-backed integration tests"
|
||||
exec "$(dirname "$0")/test-integration.sh"
|
||||
fi
|
||||
|
||||
if [[ "${TF_ACC:-}" != "1" ]]; then
|
||||
echo "[testacc][error] TF_ACC must be set to 1" >&2
|
||||
echo "[testacc][error] live mode requires TF_ACC=1" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${DYNU_API_KEY:-}" ]]; then
|
||||
echo "[testacc][error] DYNU_API_KEY must be set" >&2
|
||||
echo "[testacc][error] live mode requires DYNU_API_KEY" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -15,5 +25,5 @@ if [[ -z "${DYNU_DOMAIN:-}" ]]; then
|
||||
echo "[testacc][warn] DYNU_DOMAIN not set; domain-specific acceptance tests will skip"
|
||||
fi
|
||||
|
||||
echo "[testacc] running acceptance tests"
|
||||
echo "[testacc] running live acceptance tests"
|
||||
go test ./internal/provider -run '^TestAcc' -count=1 -v
|
||||
|
||||
Reference in New Issue
Block a user