Skip to content

Commit 0c1d732

Browse files
authored
Merge pull request #638 from labd/fix/mock-service
fix: fixed failing tests and commercetools-mock image
2 parents 825fdbe + 4c493ab commit 0c1d732

File tree

11 files changed

+233
-105
lines changed

11 files changed

+233
-105
lines changed

.github/workflows/tests.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99

1010
services:
1111
commercetools:
12-
image: labdigital/commercetools-mock-server
12+
image: labdigital/commercetools-mock-server:3.0.0-beta.1
1313
ports:
1414
- 8989:8989
1515

@@ -28,7 +28,7 @@ jobs:
2828
args: --issues-exit-code=0 --timeout=5m
2929

3030
- name: Run tests
31-
run: go test -race -coverprofile=coverage.out -covermode=atomic -coverpkg=./... -v ./...
31+
run: go test -race -coverprofile=coverage.out -covermode=atomic -coverpkg=./... ./...
3232
env:
3333
TF_ACC: 1
3434
CTP_CLIENT_ID: unittest

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ vendor/
2020
/.idea
2121
/.env
2222
/go.work*
23+
/.vscode

.vscode/launch.json

Lines changed: 0 additions & 40 deletions
This file was deleted.

.vscode/settings.json

Lines changed: 0 additions & 13 deletions
This file was deleted.

AGENTS.md

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# AGENTS.md — terraform-provider-commercetools
2+
3+
## Project Overview
4+
5+
Terraform provider for commercetools, written in Go 1.24. Uses a **dual-provider mux architecture**:
6+
- `commercetools/` — Older resources using `terraform-plugin-sdk/v2`
7+
- `internal/` — Newer resources using `terraform-plugin-framework`
8+
- `main.go` muxes both via `tf5muxserver`
9+
10+
New resources must use the `terraform-plugin-framework` pattern in `internal/resources/`.
11+
12+
## Build & Run Commands
13+
14+
This project uses **Taskfile** (go-task), not Make.
15+
16+
| Command | Description |
17+
|---|---|
18+
| `task test` | Run unit tests (`go test ./...`) |
19+
| `task testacc` | Start mock server, run all tests with `TF_ACC=true`, stop mock |
20+
| `task format` | Run `go fmt ./...` and `terraform fmt` on examples |
21+
| `task build-local` | Build and install to `~/.terraform.d/plugins/` (version 99.0.0) |
22+
| `task docs` | Generate docs via `tfplugindocs` (`go generate`) |
23+
| `task coverage` | Run tests with coverage, print function summary |
24+
25+
### Running a Single Test
26+
27+
```sh
28+
# Single test by name (unit or acceptance):
29+
go test -v -run TestAccState_createAndUpdateWithID ./internal/resources/state/
30+
31+
# Single test with acceptance mode (requires mock server running):
32+
TF_ACC=true go test -v -run TestAccState_createAndUpdateWithID ./internal/resources/state/
33+
34+
# Start mock server first if running acceptance tests:
35+
docker-compose up -d
36+
TF_ACC=true go test -run TestAccChannel_AllFields ./commercetools/
37+
docker-compose down
38+
```
39+
40+
### Linting
41+
42+
```sh
43+
golangci-lint run
44+
```
45+
46+
Config: `.golangci.yml` — uses disable-all with explicit enables. Key linters: `goimports`, `govet`, `errcheck`, `staticcheck`, `unused`, `cyclop`, `forcetypeassert`.
47+
48+
## Project Structure
49+
50+
```
51+
main.go # Entrypoint, provider mux
52+
commercetools/ # SDK-based resources (legacy)
53+
resource_<name>.go # Resource implementation
54+
resource_<name>_test.go # Tests
55+
internal/
56+
provider/ # Framework provider registration
57+
acctest/ # Shared acceptance test helpers
58+
customtypes/ # Custom TF types (LocalizedString)
59+
customvalidator/ # Custom TF validators
60+
models/ # Shared data models
61+
sharedtypes/ # Shared schema types (address, custom fields)
62+
utils/ # Utilities (errors, refs, HCL templates, mutex)
63+
resources/<name>/ # Framework-based resources
64+
resource.go # CRUD + schema
65+
model.go # Model struct, NewXFromNative(), draft(), updateActions()
66+
resource_test.go # Acceptance tests (external _test package)
67+
model_test.go # Unit tests (same package, optional)
68+
upgrade_v<N>.go # State migration (optional)
69+
datasource/<name>/ # Framework-based data sources
70+
```
71+
72+
## Code Style
73+
74+
### Imports
75+
76+
Three groups separated by blank lines, enforced by `goimports`:
77+
```go
78+
import (
79+
"context"
80+
"time"
81+
82+
"github.com/hashicorp/terraform-plugin-framework/resource"
83+
"github.com/labd/commercetools-go-sdk/platform"
84+
85+
"github.com/labd/terraform-provider-commercetools/internal/utils"
86+
)
87+
```
88+
1. Standard library
89+
2. Third-party packages
90+
3. Internal packages (full module path)
91+
92+
### Naming Conventions
93+
94+
- **Resource files (SDK):** `resource_<name>.go` — flat in `commercetools/`
95+
- **Resource packages (Framework):** `internal/resources/<snake_case_name>/` with `resource.go`, `model.go`
96+
- **Resource structs:** unexported camelCase — `stateResource`, `subscriptionResource`
97+
- **Model structs:** exported PascalCase — `State`, `Subscription`, `AssociateRole`
98+
- **Constructors:** `NewResource()` returns `resource.Resource`
99+
- **Model constructors:** `NewStateFromNative(n *platform.State) State`
100+
- **Model methods:** `draft()`, `updateActions(plan)`, `matchDefaults(state)`, `setDefaults()`
101+
- **SDK CRUD:** `resourceChannelCreate`, `resourceChannelRead`, etc. (unexported)
102+
- **Framework CRUD:** `Create()`, `Read()`, `Update()`, `Delete()` methods on resource struct
103+
- **Test configs:** `testAccStateConfig(...)` — unexported, prefixed with `testAcc`
104+
- **Destroy checks:** `testAccCheckStateDestroy`
105+
106+
### Resource Interface Checks
107+
108+
Every framework resource has compile-time interface satisfaction:
109+
```go
110+
var (
111+
_ resource.Resource = &stateResource{}
112+
_ resource.ResourceWithConfigure = &stateResource{}
113+
_ resource.ResourceWithImportState = &stateResource{}
114+
)
115+
```
116+
117+
### Error Handling
118+
119+
**Framework resources** — use diagnostics:
120+
```go
121+
diags := req.Plan.Get(ctx, &plan)
122+
resp.Diagnostics.Append(diags...)
123+
if resp.Diagnostics.HasError() {
124+
return
125+
}
126+
```
127+
128+
**API errors** — use `AddError` with summary "Error <verb>ing <resource>":
129+
```go
130+
resp.Diagnostics.AddError("Error creating state", err.Error())
131+
```
132+
133+
**404 handling in Read** — remove from state:
134+
```go
135+
if utils.IsResourceNotFoundError(err) {
136+
resp.State.RemoveResource(ctx)
137+
return
138+
}
139+
```
140+
141+
**All API calls** use retry with `utils.ProcessRemoteError()`:
142+
```go
143+
err := retry.RetryContext(ctx, 20*time.Second, func() *retry.RetryError {
144+
res, err = r.client.States().Post(draft).Execute(ctx)
145+
return utils.ProcessRemoteError(err)
146+
})
147+
```
148+
- Create: `20*time.Second` timeout
149+
- Update/Delete: `5*time.Second` timeout
150+
151+
### Schema Patterns
152+
153+
Every resource has `id` (Computed String) and `version` (Computed Int64). Use `tfsdk:"field_name"` struct tags (snake_case). Use `types.String`, `types.Int64`, `types.Bool` from the framework. For localized strings use `customtypes.LocalizedStringValue`. Slices use Go slices of type wrappers: `[]types.String`.
154+
155+
### Update Actions Pattern
156+
157+
Framework resources compare old state vs new plan directly using `reflect.DeepEqual` for complex types and `.Equal()` for simple types:
158+
```go
159+
func (s State) updateActions(plan State) platform.StateUpdate {
160+
result := platform.StateUpdate{Version: int(s.Version.ValueInt64())}
161+
if !reflect.DeepEqual(s.Name, plan.Name) {
162+
result.Actions = append(result.Actions, platform.StateSetNameAction{...})
163+
}
164+
return result
165+
}
166+
```
167+
168+
### Testing
169+
170+
- **Acceptance tests**: external test package (`_test` suffix), `resource.Test()` with `ProtoV5ProviderFactories` (framework) or `ProviderFactories` (SDK)
171+
- **Unit tests**: same package for unexported method access; table-driven with `t.Run()`
172+
- Use `github.com/stretchr/testify/assert` for assertions
173+
- Use `utils.HCLTemplate(tmpl, map[string]any{...})` for test HCL configs
174+
175+
### Utility Functions
176+
177+
- `utils.OptionalString` / `utils.FromOptionalString``types.String` <-> `*string`
178+
- `utils.Ref[T](T) *T` — generic pointer helper
179+
- `utils.IsResourceNotFoundError(err)` — checks for 404
180+
- `utils.ProcessRemoteError(err)` — classifies retry behavior
181+
182+
### Provider Data
183+
184+
Resources receive `*utils.ProviderData` via `Configure()`, providing `Client` (`*platform.ByProjectKeyRequestBuilder`) and `Mutex` (`*utils.MutexKV`).
185+
186+
## Environment Variables (Acceptance Tests)
187+
188+
`TF_ACC=true`, `CTP_CLIENT_ID`, `CTP_CLIENT_SECRET`, `CTP_PROJECT_KEY`, `CTP_SCOPES`, `CTP_API_URL`, `CTP_AUTH_URL`. For the mock server: API/Auth URL = `http://localhost:8989`.

Taskfile.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ tasks:
3030

3131
test:
3232
cmds:
33-
- go test -v ./...
33+
- go test ./...
3434

3535
docs:
3636
cmds:
@@ -56,6 +56,6 @@ tasks:
5656
CTP_API_URL: http://localhost:8989
5757
CTP_AUTH_URL: http://localhost:8989
5858
cmds:
59-
- docker-compose up -d
60-
- go test -v ./...
61-
- docker-compose down
59+
- docker compose up -d
60+
- go test ./...
61+
- docker compose down -v

commercetools/resource_channel_test.go

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -29,29 +29,21 @@ func TestAccChannel_AllFields(t *testing.T) {
2929
return err
3030
}
3131

32-
expected := &platform.Channel{
33-
Name: &platform.LocalizedString{
34-
"en": "Lab Digital",
35-
},
36-
Description: &platform.LocalizedString{
37-
"en": "Lab Digital Office",
38-
},
39-
Address: &platform.Address{
40-
Country: "NL",
41-
StreetName: stringRef("Reykjavikstraat"),
42-
PostalCode: stringRef("3543 KH"),
43-
},
44-
GeoLocation: platform.GeoJsonPoint{
45-
Coordinates: []float64{52.10014028522915, 5.064886641132926},
46-
},
47-
}
48-
4932
assert.NotNil(t, result)
33+
assert.EqualValues(t, &platform.LocalizedString{
34+
"en": "Lab Digital",
35+
}, result.Name)
36+
assert.EqualValues(t, &platform.LocalizedString{
37+
"en": "Lab Digital Office",
38+
}, result.Description)
5039
assert.NotNil(t, result.Address)
51-
assert.EqualValues(t, expected.Name, result.Name)
52-
assert.EqualValues(t, expected.Description, result.Description)
53-
assert.EqualValues(t, expected.Address, result.Address)
54-
assert.EqualValues(t, expected.GeoLocation, result.GeoLocation)
40+
assert.NotNil(t, result.Address.ID)
41+
assert.EqualValues(t, "NL", result.Address.Country)
42+
assert.EqualValues(t, stringRef("Reykjavikstraat"), result.Address.StreetName)
43+
assert.EqualValues(t, stringRef("3543 KH"), result.Address.PostalCode)
44+
assert.EqualValues(t, platform.GeoJsonPoint{
45+
Coordinates: []float64{52.10014028522915, 5.064886641132926},
46+
}, result.GeoLocation)
5547
return nil
5648
},
5749
),
@@ -85,29 +77,21 @@ func TestAccChannel_AllFields(t *testing.T) {
8577
return err
8678
}
8779

88-
expected := &platform.Channel{
89-
Name: &platform.LocalizedString{
90-
"en": "Lab Digital",
91-
},
92-
Description: &platform.LocalizedString{
93-
"en": "Lab Digital Office",
94-
},
95-
Address: &platform.Address{
96-
Country: "NL",
97-
StreetName: stringRef("Reykjavikstraat"),
98-
PostalCode: stringRef("3543 KH"),
99-
},
100-
GeoLocation: platform.GeoJsonPoint{
101-
Coordinates: []float64{52.10014028522915, 5.064886641132926},
102-
},
103-
}
104-
10580
assert.NotNil(t, result)
10681
assert.NotNil(t, result.Address)
107-
assert.EqualValues(t, expected.Name, result.Name)
108-
assert.EqualValues(t, expected.Description, result.Description)
109-
assert.EqualValues(t, expected.Address, result.Address)
110-
assert.EqualValues(t, expected.GeoLocation, result.GeoLocation)
82+
assert.EqualValues(t, &platform.LocalizedString{
83+
"en": "Lab Digital",
84+
}, result.Name)
85+
assert.EqualValues(t, &platform.LocalizedString{
86+
"en": "Lab Digital Office",
87+
}, result.Description)
88+
assert.NotNil(t, result.Address.ID)
89+
assert.EqualValues(t, "NL", result.Address.Country)
90+
assert.EqualValues(t, stringRef("Reykjavikstraat"), result.Address.StreetName)
91+
assert.EqualValues(t, stringRef("3543 KH"), result.Address.PostalCode)
92+
assert.EqualValues(t, platform.GeoJsonPoint{
93+
Coordinates: []float64{52.10014028522915, 5.064886641132926},
94+
}, result.GeoLocation)
11195
return nil
11296
},
11397
),

commercetools/resource_shipping_method.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ func resourceShippingMethodCreate(ctx context.Context, d *schema.ResourceData, m
108108
TaxCategory: taxCategory,
109109
Predicate: nilIfEmpty(stringRef(d.Get("predicate"))),
110110
Custom: custom,
111+
// Set default to empty array, otherwise the API will return an error if no shipping rates are added to the shipping method.
112+
// Actual shipping rates are added in the shipping zone resource, see resource_shipping_zone_rate.go
113+
ZoneRates: []platform.ZoneRateDraft{},
111114
}
112115

113116
key := stringRef(d.Get("key"))

docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
services:
22
commercetools:
3-
image: labdigital/commercetools-mock-server
3+
image: labdigital/commercetools-mock-server:3.0.0-beta.1
44
ports:
55
- 8989:8989

0 commit comments

Comments
 (0)