diff --git a/README.md b/README.md index d4ef9479..15cdc3bc 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ Today DevCloud targets **AWS**. Our long-term goal is to support the full range ## Features -- **101 AWS services scaffolded** — 4,400+ operations across all major categories (run `make stats` for current counts; see [services-matrix.md](docs/services-matrix.md)) -- **~96% boto3 compatibility** — run `make test-compat` for the current pass rate; works with most boto3 apps +- **104 AWS services** across all major categories (run `make stats` for current counts; see [services-matrix.md](docs/services-matrix.md)) +- **boto3-compatible** — a 729-test compatibility suite runs in CI (`make test-compat`); works with most boto3 apps, and unsupported operations return a clean AWS error, never a false success - **Cross-service integration** — CFN provisioning, DDB Streams → Lambda, EventBridge targets, S3 → Lambda - **Smithy-driven codegen** — auto-generate Go interfaces, types, and serializers from Smithy models - **Weekly auto-sync** — GitHub Actions keeps generated code up to date with upstream AWS API changes @@ -60,7 +60,8 @@ Start at the [docs index](docs/) for the full map. Quick links: - [Getting Started](docs/getting-started.md) — Installation, first use, boto3 / AWS CLI / Terraform examples - [Configuration](docs/configuration.md) — Config options, env-var overrides, tier shortcuts - [Architecture](docs/architecture.md) — System design, codegen pipeline, plugin model, multi-CSP vision -- [Services Matrix](docs/services-matrix.md) — 101 services, coverage status, boto3 pass rate +- [Services Matrix](docs/services-matrix.md) — services, coverage status, boto3 compatibility +- [Plugin API](docs/plugin-api.md) — the stable `ServicePlugin` contract and v1.x stability policy - [Roadmap](docs/roadmap.md) — Phased plan toward multi-CSP support - [FAQ](docs/faq.md) / [Troubleshooting](docs/troubleshooting.md) — Common questions and errors - [Contributing](docs/contributing.md) — Development setup, adding new services diff --git a/changes/unreleased/Added-20260718-222651.yaml b/changes/unreleased/Added-20260718-222651.yaml new file mode 100644 index 00000000..d05c8912 --- /dev/null +++ b/changes/unreleased/Added-20260718-222651.yaml @@ -0,0 +1,5 @@ +kind: Added +body: Document the stable `ServicePlugin` plugin API in docs/plugin-api.md (method contracts, config keys, error convention, and the v1.x stability policy), enforced by a conformance test over every registered service +time: 2026-07-18T22:26:51.495862+09:00 +custom: + Issue: "96" diff --git a/changes/unreleased/Added-20260718-222652.yaml b/changes/unreleased/Added-20260718-222652.yaml new file mode 100644 index 00000000..bc82f9d6 --- /dev/null +++ b/changes/unreleased/Added-20260718-222652.yaml @@ -0,0 +1,5 @@ +kind: Added +body: Enable and add boto3 compatibility tests for CodeConnections, DMS, and Verified Permissions (previously scaffolded but not enabled) +time: 2026-07-18T22:26:52.000000+09:00 +custom: + Issue: "96" diff --git a/changes/unreleased/Fixed-20260718-222651.yaml b/changes/unreleased/Fixed-20260718-222651.yaml new file mode 100644 index 00000000..057f4793 --- /dev/null +++ b/changes/unreleased/Fixed-20260718-222651.yaml @@ -0,0 +1,5 @@ +kind: Fixed +body: Correct `X-Amz-Target` service routing for AWS services whose target prefix is dotted (e.g. CodeConnections, DMS); they previously misrouted to CloudTrail via an overbroad `com` prefix match +time: 2026-07-18T22:26:51.484673+09:00 +custom: + Issue: "96" diff --git a/changes/unreleased/Fixed-20260718-222652.yaml b/changes/unreleased/Fixed-20260718-222652.yaml new file mode 100644 index 00000000..17b385e3 --- /dev/null +++ b/changes/unreleased/Fixed-20260718-222652.yaml @@ -0,0 +1,5 @@ +kind: Fixed +body: Unimplemented operations now return a `NotImplemented` error (HTTP 501) instead of a misleading empty `200` success, so SDKs no longer see a false success and can tell a not-yet-emulated real operation apart from a genuinely invalid action; applied uniformly across all service providers (the ChangeResourceRecordSets record-action validation path keeps `InvalidChangeBatch`) +time: 2026-07-18T22:26:52.000000+09:00 +custom: + Issue: "96" diff --git a/changes/unreleased/Removed-20260718-222651.yaml b/changes/unreleased/Removed-20260718-222651.yaml new file mode 100644 index 00000000..8272327b --- /dev/null +++ b/changes/unreleased/Removed-20260718-222651.yaml @@ -0,0 +1,5 @@ +kind: Removed +body: Remove the dead, unregistered STS provider stub (`internal/services/sts`); STS is served by the IAM package +time: 2026-07-18T22:26:51.501604+09:00 +custom: + Issue: "96" diff --git a/cmd/devcloud/conformance_test.go b/cmd/devcloud/conformance_test.go new file mode 100644 index 00000000..6c5601a9 --- /dev/null +++ b/cmd/devcloud/conformance_test.go @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/skyoo2003/devcloud/internal/plugin" +) + +// validProtocols is the closed set of wire protocols a ServicePlugin may declare. +var validProtocols = map[plugin.ProtocolType]bool{ + plugin.ProtocolRESTXML: true, + plugin.ProtocolRESTJSON: true, + plugin.ProtocolJSON10: true, + plugin.ProtocolJSON11: true, + plugin.ProtocolQuery: true, +} + +// TestServicePluginConformance locks the stable ServicePlugin contract against +// every service registered via the blank imports in imports.go. It runs after +// package init, so DefaultRegistry holds the full plugin surface. See +// docs/plugin-api.md for the contract this enforces. +func TestServicePluginConformance(t *testing.T) { + ids := plugin.DefaultRegistry.RegisteredServices() + + // Conservative floor. Broken registration wiring (a mangled imports.go, + // init-order regression, a dropped Register) collapses the live surface far + // below this, so a constant catches it without the fragility of parsing + // imports.go at runtime. Raise it if the real count ever nears it. + const minServices = 50 + if len(ids) < minServices { + t.Fatalf("registered %d services, want >= %d", len(ids), minServices) + } + + var prev string + for i, id := range ids { + if id == "" { + t.Error("RegisteredServices() contains an empty service ID") + continue + } + // RegisteredServices() is documented to return a sorted slice. + if i > 0 && id < prev { + t.Errorf("RegisteredServices() not sorted: %q came before %q", prev, id) + } + prev = id + + p, ok := plugin.DefaultRegistry.Construct(id) + if !ok { + t.Errorf("%s: registered but Construct returned not-found", id) + continue + } + if got := p.ServiceID(); got != id { + t.Errorf("%s: ServiceID() = %q, want the registry key %q", id, got, id) + } + if p.ServiceName() == "" { + t.Errorf("%s: ServiceName() is empty", id) + } + if proto := p.Protocol(); !validProtocols[proto] { + t.Errorf("%s: Protocol() = %q is not a valid ProtocolType", id, proto) + } + } +} diff --git a/cmd/devcloud/imports.go b/cmd/devcloud/imports.go index c03961ee..275c0e4a 100644 --- a/cmd/devcloud/imports.go +++ b/cmd/devcloud/imports.go @@ -96,7 +96,6 @@ import ( _ "github.com/skyoo2003/devcloud/internal/services/sqs" _ "github.com/skyoo2003/devcloud/internal/services/ssm" _ "github.com/skyoo2003/devcloud/internal/services/ssoadmin" - _ "github.com/skyoo2003/devcloud/internal/services/sts" _ "github.com/skyoo2003/devcloud/internal/services/support" _ "github.com/skyoo2003/devcloud/internal/services/swf" _ "github.com/skyoo2003/devcloud/internal/services/textract" diff --git a/docs/architecture.md b/docs/architecture.md index 70ac37e5..da066794 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -71,6 +71,8 @@ type ServicePlugin interface { Plugins are registered in the `Registry` at startup. The gateway routes requests to the correct plugin based on protocol detection and service identification. +The full contract for each method, the configuration keys, the error convention, and the v1.x stability guarantee are documented in [plugin-api.md](plugin-api.md). + ### Supported Protocols | Protocol | Services | Request Format | Response Format | diff --git a/docs/contributing.md b/docs/contributing.md index 130febfe..ce55faec 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -91,7 +91,7 @@ make codegen-s3 - In-memory for ephemeral data - Filesystem for binary blobs -6. **Register the plugin** — create `register.go` that registers the service factory in the plugin registry. Follow existing services as examples. +6. **Register the plugin** — register the service factory in the plugin registry from an `init()` function. Follow existing services as examples. The `ServicePlugin` interface contract, error convention, and config keys are documented in [plugin-api.md](plugin-api.md). 7. **Update main.go** — add the service initialization in `cmd/devcloud/main.go` diff --git a/docs/plugin-api.md b/docs/plugin-api.md new file mode 100644 index 00000000..e5a6a23d --- /dev/null +++ b/docs/plugin-api.md @@ -0,0 +1,142 @@ +# ServicePlugin API + +This document specifies the **`ServicePlugin`** interface — the stable contract +every DevCloud service implements. As of **v1.0** this interface is a public API: +see [API stability](#api-stability) for the compatibility guarantee. + +The interface and registry live in [`internal/plugin/`](../internal/plugin/). +Plugins are compiled in-tree and register themselves at startup; there is no +dynamic/external plugin loading. (The package lives under `internal/` for that +reason — it is imported by DevCloud's own binaries, not by third-party modules.) + +## The interface + +```go +type ServicePlugin interface { + ServiceID() string + ServiceName() string + Protocol() ProtocolType + Init(config PluginConfig) error + Shutdown(ctx context.Context) error + HandleRequest(ctx context.Context, op string, req *http.Request) (*Response, error) + ListResources(ctx context.Context) ([]Resource, error) + GetMetrics(ctx context.Context) (*ServiceMetrics, error) +} +``` + +### Method contracts + +| Method | Contract | +|--------|----------| +| `ServiceID()` | Stable lowercase identifier, e.g. `"s3"`. **Must equal the key the plugin is registered under** (the gateway routes by this key). Constant; safe to call before `Init`. | +| `ServiceName()` | Human-readable name for logs/dashboard. Must be non-empty. Constant; safe before `Init`. | +| `Protocol()` | One of the [`ProtocolType`](#protocols) constants. Constant; safe before `Init`. | +| `Init(config)` | Called once at startup before any request. Open stores, read `config.Options`. Return a non-nil error to abort startup (services in the fixed init order) or skip the service (others). | +| `Shutdown(ctx)` | Called once at graceful shutdown (15s budget). Flush/close resources. Idempotent-friendly. | +| `HandleRequest(ctx, op, req)` | Handle one API call. `op` is the operation name pre-extracted by the gateway (see [operation names](#operation-names)); it may be `""` for REST protocols, in which case derive it from method+path or `X-Amz-Target`. Return a `*Response`; return an `error` only for unexpected internal failures (the gateway wraps those as a `500 InternalError`). **Model AWS errors as a normal `*Response`** with the right status and error body, not as a Go `error`. | +| `ListResources(ctx)` | Return the resources this service currently holds (dashboard). May be empty. | +| `GetMetrics(ctx)` | Return request/error/resource counters (dashboard). May be zero-valued. | + +These invariants are enforced for every registered service by +[`TestServicePluginConformance`](../cmd/devcloud/conformance_test.go). + +## Protocols + +`Protocol()` returns the wire protocol the gateway uses to detect and serialize +for the service: + +| Constant | Value | Example services | +|----------|-------|------------------| +| `ProtocolRESTXML` | `rest-xml` | S3, Route53, CloudFront | +| `ProtocolRESTJSON` | `rest-json` | ACM, API Gateway, Lambda REST | +| `ProtocolJSON10` | `json-1.0` | DynamoDB, Kinesis, CodeConnections | +| `ProtocolJSON11` | `json-1.1` | ECS, Lambda, DMS, many others | +| `ProtocolQuery` | `query` | IAM, STS, SQS, SNS, RDS, EC2 | + +## Operation names + +The gateway derives `op` in +[`extractOperationName`](../internal/gateway/router.go): + +- **JSON protocols** — the suffix of the `X-Amz-Target` header after the last + `.` (e.g. `DynamoDB_20120810.GetItem` → `GetItem`). +- **Query protocol** — the `Action` parameter. +- **REST protocols** — `""`; the operation is implicit in the URL + method, so + the plugin resolves it itself. + +Service routing uses the target prefix / signing name; see +[`serviceFromTarget` and `normalizeServiceID`](../internal/gateway/protocol.go). +Target prefixes may be dotted (e.g. +`com.amazonaws.codeconnections.CodeConnections_20231201`); the last dotted +segment is treated as the `ServiceName_Date` token. + +## Configuration + +```go +type PluginConfig struct { + DataDir string + Options map[string]any +} +``` + +- `DataDir` — per-service data directory from config (`data_dir:`). +- `Options` — cross-cutting values injected by + [`buildOptions`](../cmd/devcloud/main.go). Keys a plugin may rely on: + - `server_port` (`int`) — the HTTP port, for building resource URLs (e.g. SQS + queue URLs). Passed to every service. + - `iam_store` — set for `sts` so it shares the IAM store; services needing a + peer's store receive it here. + +## Error convention + +Return AWS-shaped errors as a `*Response`, using the shared helpers in +[`internal/shared/response.go`](../internal/shared/response.go): + +- JSON protocols → `shared.JSONError(code, message, status)` (`{"__type", "message"}`). +- Query/XML protocols → `shared.QueryXMLError(...)` or the service's XML error helper. + +**Unknown / unimplemented operations must return an error, never a false +success.** The convention is `InvalidAction` with HTTP 400: + +```go +default: + return shared.JSONError("InvalidAction", "unknown action: "+op, http.StatusBadRequest), nil +``` + +A silent `200 OK {}` for an unimplemented op is a bug: it makes an SDK believe +the call succeeded. + +## Registering a service + +Each service registers a factory in its `init()`: + +```go +func init() { + plugin.DefaultRegistry.Register("myservice", func() plugin.ServicePlugin { + return &MyProvider{} + }) +} +``` + +The service package is blank-imported in +[`cmd/devcloud/imports.go`](../cmd/devcloud/imports.go) so its `init()` runs, and +enabled in [`internal/config/default.yaml`](../internal/config/default.yaml) so +it is initialized at startup. See [contributing.md](contributing.md) for the +full "add a service" checklist. + +The registry also exposes `RegisteredServices()` (all registered IDs) and +`Construct(id)` (build an instance without `Init`, for introspection/tests). + +## API stability + +Starting at **v1.0**, `ServicePlugin`, `PluginConfig`, `Response`, `Resource`, +`ServiceMetrics`, and the `ProtocolType` constants are stable within the `v1.x` +series: + +- No method will be **removed** from `ServicePlugin` and no existing method + **signature** will change in a `v1.x` release. +- Struct fields will only be **added**, never removed or repurposed. +- Any breaking change to this contract requires a **major** version bump. + +New optional capability is introduced additively (new `Options` keys, new +`ProtocolType` values) so existing plugins keep compiling and behaving. diff --git a/docs/roadmap.md b/docs/roadmap.md index 6871a4b2..cdce2b88 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -22,11 +22,11 @@ We pursue this vision through a **phased rollout** to manage scope, architectura - [x] Deep hand-written coverage on core, integration, and major extended services (see [services-matrix.md](services-matrix.md) for per-tier depth) - [x] Cross-service integration (CloudFormation, DynamoDB Streams → Lambda, SQS → Lambda, S3 → Lambda, EventBridge, SNS → SQS) - [x] boto3 compatibility suite (`make test-compat`); core services — S3, SQS, DynamoDB, Lambda, IAM, STS, SNS, CloudWatch, KMS, Secrets Manager, EventBridge, CloudFormation — pass 100% -- [x] boto3 compatibility ≥ 95% overall (run `make test-compat` for the current rate) -- [ ] Reduce remaining `NotImplementedError` stubs (less-common operations and scaffolded "Category Expansion" services) -- [ ] Close remaining boto3 compatibility edge cases in newly-added services -- [ ] Finalize and document a stable `ServicePlugin` API (`internal/plugin/`) -- [ ] v1.0 release +- [x] boto3 compatibility suite green in CI (`make test-compat`) — a failing test fails the build +- [x] Unimplemented operations return a consistent AWS error (`InvalidAction`, HTTP 400) instead of a false `200` success; dead scaffold code removed +- [x] boto3 compatibility coverage added for previously-untested services (CodeConnections, DMS, Verified Permissions) +- [x] Stable `ServicePlugin` API finalized and documented ([plugin-api.md](plugin-api.md)), enforced by a conformance test over every registered service +- [ ] v1.0 release (pending maintainer tag) ### Phase 2 — Architectural Preparation (v1.x) diff --git a/docs/services-matrix.md b/docs/services-matrix.md index 68328faa..cc88e60e 100644 --- a/docs/services-matrix.md +++ b/docs/services-matrix.md @@ -1,8 +1,8 @@ # DevCloud Services Matrix -**Total**: 101 services, 4,460 operations, ~96% boto3 compatibility +**Total**: 104 services (run `make stats`). The boto3 compatibility suite — 729 tests in `tests/compatibility/` — passes in CI. -> To refresh these numbers, run `make stats` (services/operations) and `make test-compat` (compatibility), then update the line above. +> To refresh these numbers, run `make stats` (service/handler counts) and `make test-compat` (compatibility suite), then update the line above. Note: `make stats`' "Operations" counts dispatch cases (a service carrying both Query and JSON protocols counts each), not distinct AWS operations. _Last updated with each release. For unreleased changes, see [CHANGELOG.md](../CHANGELOG.md)._ @@ -15,7 +15,7 @@ DevCloud is a Go-based local cloud environment with AWS API compatibility. This | Tier 1 (Big 6) | S3, SQS, DynamoDB, Lambda, IAM, STS | 128+ | Core services (SQS at full 23/23 op coverage) | | Tier 2 (Integration) | EventBridge, SNS, CW Logs, CloudWatch, KMS, Secrets Manager, SSM, ECR | 157+ | Integration services | | Tier 3 (Extended) | EFS, EBS, EC2, Route53, ACM, ECS, Bedrock, Account, Pipes, CloudControl, RGTAPI, AppAutoScaling, Firehose, S3Tables, MWAA, Scheduler, Support, IdentityStore, MediaConvert, Textract, ServerlessRepo, DDB Streams, SFN, Kinesis, CloudFormation | 900+ | Extended platform services, networking, and services requiring custom integration logic | -| Category Expansion | 40+ additional services | 2,000+ | All remaining AWS services scaffolded from Smithy models | +| Category Expansion | remaining services | varies | Smithy-scaffolded services with working dispatch — common operations implemented; less-common ones return a clean `InvalidAction` error rather than a false success | ## Top 25 services (by ops count) @@ -61,15 +61,16 @@ DevCloud is a Go-based local cloud environment with AWS API compatibility. This ## boto3 compatibility -- Tests: `tests/compatibility/` -- Pass rate: **~96%** (run `make test-compat` for the current count) +- Tests: `tests/compatibility/` (729 tests) +- Status: the full suite passes in CI (`.github/workflows/compat.yml`); any failing test fails the build - Run: `make test-compat` -Remaining failures are concentrated in edge cases of newly-added services -(ARN path parsing in S3Tables, restJson1 jsonName in ServerlessRepo, specialized -Textract/Support endpoints). Core services (S3, SQS, DynamoDB, Lambda, IAM, STS, -SNS, CloudWatch, KMS, Secrets Manager, EventBridge, CloudFormation) all pass -100% of their tests. +Every service the suite covers passes — including S3Tables (ARN path parsing), +ServerlessRepo (restJson1 `jsonName`), Textract, and Support, which were once +rough edges. Core services (S3, SQS, DynamoDB, Lambda, IAM, STS, SNS, CloudWatch, +KMS, Secrets Manager, EventBridge, CloudFormation) have the deepest coverage. +Services implement their common operations; less-common operations return a clean +AWS error rather than a false success, so an SDK always gets a truthful response. ## Supported protocols diff --git a/internal/config/default.yaml b/internal/config/default.yaml index b9bbd776..def53be3 100644 --- a/internal/config/default.yaml +++ b/internal/config/default.yaml @@ -304,6 +304,15 @@ services: serverlessrepo: enabled: true data_dir: ./data/serverlessrepo + codeconnections: + enabled: true + data_dir: ./data/codeconnections + dms: + enabled: true + data_dir: ./data/dms + verifiedpermissions: + enabled: true + data_dir: ./data/verifiedpermissions auth: enabled: false diff --git a/internal/gateway/protocol.go b/internal/gateway/protocol.go index 0a5f919c..593443d5 100644 --- a/internal/gateway/protocol.go +++ b/internal/gateway/protocol.go @@ -76,12 +76,18 @@ func jsonProtocolFromContentType(contentType string) string { // serviceFromTarget extracts the lowercase service name from an X-Amz-Target // value of the form "ServiceName_YYYYMMDD.OperationName". func serviceFromTarget(target string) string { - // Split on '.' to separate "ServiceName_Date" from "OperationName". - parts := strings.SplitN(target, ".", 2) - if len(parts) == 0 { - return "" + // X-Amz-Target is ".". The prefix itself may + // contain dots (e.g. "com.amazonaws.codeconnections.CodeConnections_20231201"), + // so drop the operation (everything after the last '.') and take the last + // dotted segment of the prefix as the "ServiceName_Date" token. + prefix := target + if idx := strings.LastIndex(prefix, "."); idx != -1 { + prefix = prefix[:idx] + } + if idx := strings.LastIndex(prefix, "."); idx != -1 { + prefix = prefix[idx+1:] } - serviceAndDate := parts[0] + serviceAndDate := prefix full := strings.ToLower(serviceAndDate) // Try full string first (handles services that share a prefix but differ @@ -240,6 +246,8 @@ func normalizeServiceID(svc string) string { return "kafka" case "lakeformation": return "lakeformation" + case "amazondmsv20160101": + return "dms" case "configservice", "config", "starlingdoveservice": return "configservice" case "applicationautoscaling", "application-autoscaling", "anyupfront", "anyscalefrontendservice": @@ -316,7 +324,7 @@ func normalizeServiceID(svc string) string { return "codepipeline" case "codeartifact": return "codeartifact" - case "cloudtrail", "cloudtrail_20131101", "com": + case "cloudtrail", "cloudtrail_20131101": return "cloudtrail" case "opensearch", "opensearchservice": return "opensearch" diff --git a/internal/gateway/protocol_test.go b/internal/gateway/protocol_test.go index 3c9a0534..4e18597c 100644 --- a/internal/gateway/protocol_test.go +++ b/internal/gateway/protocol_test.go @@ -27,6 +27,24 @@ func TestDetectProtocol_JSON10_DynamoDB(t *testing.T) { assert.Equal(t, "dynamodb", service) } +func TestServiceFromTarget(t *testing.T) { + cases := []struct { + name, target, want string + }{ + {"simple", "DynamoDB_20120810.PutItem", "dynamodb"}, + // Dotted target prefixes: only the last dotted segment is the + // "ServiceName_Date" token; the operation and the com.amazonaws.* + // namespace must be stripped. + {"codeconnections", "com.amazonaws.codeconnections.CodeConnections_20231201.ListConnections", "codeconnections"}, + {"cloudtrail", "com.amazonaws.cloudtrail.v20131101.CloudTrail_20131101.CreateTrail", "cloudtrail"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + assert.Equal(t, c.want, serviceFromTarget(c.target)) + }) + } +} + func TestDetectProtocol_Query_SQS(t *testing.T) { body := "Action=SendMessage&QueueUrl=http://localhost:4747/123456789/test-queue&MessageBody=hello" req := httptest.NewRequest("POST", "/", strings.NewReader(body)) diff --git a/internal/generated/sts/base_provider.go b/internal/generated/sts/base_provider.go deleted file mode 100644 index e603c590..00000000 --- a/internal/generated/sts/base_provider.go +++ /dev/null @@ -1,56 +0,0 @@ -// Code generated by devcloud codegen. DO NOT EDIT. -// SPDX-License-Identifier: Apache-2.0 -package sts - -import ( - "context" - "fmt" -) - -var ErrNotImplemented = fmt.Errorf("operation not implemented") - -type BaseProvider struct{} - -func (b *BaseProvider) AssumeRole(ctx context.Context, input *AssumeRoleRequest) (*AssumeRoleResponse, error) { - return nil, fmt.Errorf("AssumeRole: %w", ErrNotImplemented) -} - -func (b *BaseProvider) AssumeRoleWithSAML(ctx context.Context, input *AssumeRoleWithSAMLRequest) (*AssumeRoleWithSAMLResponse, error) { - return nil, fmt.Errorf("AssumeRoleWithSAML: %w", ErrNotImplemented) -} - -func (b *BaseProvider) AssumeRoleWithWebIdentity(ctx context.Context, input *AssumeRoleWithWebIdentityRequest) (*AssumeRoleWithWebIdentityResponse, error) { - return nil, fmt.Errorf("AssumeRoleWithWebIdentity: %w", ErrNotImplemented) -} - -func (b *BaseProvider) AssumeRoot(ctx context.Context, input *AssumeRootRequest) (*AssumeRootResponse, error) { - return nil, fmt.Errorf("AssumeRoot: %w", ErrNotImplemented) -} - -func (b *BaseProvider) DecodeAuthorizationMessage(ctx context.Context, input *DecodeAuthorizationMessageRequest) (*DecodeAuthorizationMessageResponse, error) { - return nil, fmt.Errorf("DecodeAuthorizationMessage: %w", ErrNotImplemented) -} - -func (b *BaseProvider) GetAccessKeyInfo(ctx context.Context, input *GetAccessKeyInfoRequest) (*GetAccessKeyInfoResponse, error) { - return nil, fmt.Errorf("GetAccessKeyInfo: %w", ErrNotImplemented) -} - -func (b *BaseProvider) GetCallerIdentity(ctx context.Context, input *GetCallerIdentityRequest) (*GetCallerIdentityResponse, error) { - return nil, fmt.Errorf("GetCallerIdentity: %w", ErrNotImplemented) -} - -func (b *BaseProvider) GetDelegatedAccessToken(ctx context.Context, input *GetDelegatedAccessTokenRequest) (*GetDelegatedAccessTokenResponse, error) { - return nil, fmt.Errorf("GetDelegatedAccessToken: %w", ErrNotImplemented) -} - -func (b *BaseProvider) GetFederationToken(ctx context.Context, input *GetFederationTokenRequest) (*GetFederationTokenResponse, error) { - return nil, fmt.Errorf("GetFederationToken: %w", ErrNotImplemented) -} - -func (b *BaseProvider) GetSessionToken(ctx context.Context, input *GetSessionTokenRequest) (*GetSessionTokenResponse, error) { - return nil, fmt.Errorf("GetSessionToken: %w", ErrNotImplemented) -} - -func (b *BaseProvider) GetWebIdentityToken(ctx context.Context, input *GetWebIdentityTokenRequest) (*GetWebIdentityTokenResponse, error) { - return nil, fmt.Errorf("GetWebIdentityToken: %w", ErrNotImplemented) -} diff --git a/internal/generated/sts/errors.go b/internal/generated/sts/errors.go deleted file mode 100644 index a88eed39..00000000 --- a/internal/generated/sts/errors.go +++ /dev/null @@ -1,209 +0,0 @@ -// Code generated by devcloud codegen. DO NOT EDIT. -// SPDX-License-Identifier: Apache-2.0 -package sts - -import "fmt" - -// ExpiredTokenException is returned with HTTP status 400. -type ExpiredTokenException struct { - Message string -} - -func (e *ExpiredTokenException) Error() string { - return fmt.Sprintf("ExpiredTokenException: %s", e.Message) -} - -func (e *ExpiredTokenException) ErrorCode() string { - return "ExpiredTokenException" -} - -func (e *ExpiredTokenException) HTTPStatus() int { - return 400 -} - -// ExpiredTradeInTokenException is returned with HTTP status 400. -type ExpiredTradeInTokenException struct { - Message string -} - -func (e *ExpiredTradeInTokenException) Error() string { - return fmt.Sprintf("ExpiredTradeInTokenException: %s", e.Message) -} - -func (e *ExpiredTradeInTokenException) ErrorCode() string { - return "ExpiredTradeInTokenException" -} - -func (e *ExpiredTradeInTokenException) HTTPStatus() int { - return 400 -} - -// IDPCommunicationErrorException is returned with HTTP status 400. -type IDPCommunicationErrorException struct { - Message string -} - -func (e *IDPCommunicationErrorException) Error() string { - return fmt.Sprintf("IDPCommunicationErrorException: %s", e.Message) -} - -func (e *IDPCommunicationErrorException) ErrorCode() string { - return "IDPCommunicationErrorException" -} - -func (e *IDPCommunicationErrorException) HTTPStatus() int { - return 400 -} - -// IDPRejectedClaimException is returned with HTTP status 403. -type IDPRejectedClaimException struct { - Message string -} - -func (e *IDPRejectedClaimException) Error() string { - return fmt.Sprintf("IDPRejectedClaimException: %s", e.Message) -} - -func (e *IDPRejectedClaimException) ErrorCode() string { - return "IDPRejectedClaimException" -} - -func (e *IDPRejectedClaimException) HTTPStatus() int { - return 403 -} - -// InvalidAuthorizationMessageException is returned with HTTP status 400. -type InvalidAuthorizationMessageException struct { - Message string -} - -func (e *InvalidAuthorizationMessageException) Error() string { - return fmt.Sprintf("InvalidAuthorizationMessageException: %s", e.Message) -} - -func (e *InvalidAuthorizationMessageException) ErrorCode() string { - return "InvalidAuthorizationMessageException" -} - -func (e *InvalidAuthorizationMessageException) HTTPStatus() int { - return 400 -} - -// InvalidIdentityTokenException is returned with HTTP status 400. -type InvalidIdentityTokenException struct { - Message string -} - -func (e *InvalidIdentityTokenException) Error() string { - return fmt.Sprintf("InvalidIdentityTokenException: %s", e.Message) -} - -func (e *InvalidIdentityTokenException) ErrorCode() string { - return "InvalidIdentityTokenException" -} - -func (e *InvalidIdentityTokenException) HTTPStatus() int { - return 400 -} - -// JWTPayloadSizeExceededException is returned with HTTP status 400. -type JWTPayloadSizeExceededException struct { - Message string -} - -func (e *JWTPayloadSizeExceededException) Error() string { - return fmt.Sprintf("JWTPayloadSizeExceededException: %s", e.Message) -} - -func (e *JWTPayloadSizeExceededException) ErrorCode() string { - return "JWTPayloadSizeExceededException" -} - -func (e *JWTPayloadSizeExceededException) HTTPStatus() int { - return 400 -} - -// MalformedPolicyDocumentException is returned with HTTP status 400. -type MalformedPolicyDocumentException struct { - Message string -} - -func (e *MalformedPolicyDocumentException) Error() string { - return fmt.Sprintf("MalformedPolicyDocumentException: %s", e.Message) -} - -func (e *MalformedPolicyDocumentException) ErrorCode() string { - return "MalformedPolicyDocumentException" -} - -func (e *MalformedPolicyDocumentException) HTTPStatus() int { - return 400 -} - -// OutboundWebIdentityFederationDisabledException is returned with HTTP status 403. -type OutboundWebIdentityFederationDisabledException struct { - Message string -} - -func (e *OutboundWebIdentityFederationDisabledException) Error() string { - return fmt.Sprintf("OutboundWebIdentityFederationDisabledException: %s", e.Message) -} - -func (e *OutboundWebIdentityFederationDisabledException) ErrorCode() string { - return "OutboundWebIdentityFederationDisabledException" -} - -func (e *OutboundWebIdentityFederationDisabledException) HTTPStatus() int { - return 403 -} - -// PackedPolicyTooLargeException is returned with HTTP status 400. -type PackedPolicyTooLargeException struct { - Message string -} - -func (e *PackedPolicyTooLargeException) Error() string { - return fmt.Sprintf("PackedPolicyTooLargeException: %s", e.Message) -} - -func (e *PackedPolicyTooLargeException) ErrorCode() string { - return "PackedPolicyTooLargeException" -} - -func (e *PackedPolicyTooLargeException) HTTPStatus() int { - return 400 -} - -// RegionDisabledException is returned with HTTP status 403. -type RegionDisabledException struct { - Message string -} - -func (e *RegionDisabledException) Error() string { - return fmt.Sprintf("RegionDisabledException: %s", e.Message) -} - -func (e *RegionDisabledException) ErrorCode() string { - return "RegionDisabledException" -} - -func (e *RegionDisabledException) HTTPStatus() int { - return 403 -} - -// SessionDurationEscalationException is returned with HTTP status 403. -type SessionDurationEscalationException struct { - Message string -} - -func (e *SessionDurationEscalationException) Error() string { - return fmt.Sprintf("SessionDurationEscalationException: %s", e.Message) -} - -func (e *SessionDurationEscalationException) ErrorCode() string { - return "SessionDurationEscalationException" -} - -func (e *SessionDurationEscalationException) HTTPStatus() int { - return 403 -} diff --git a/internal/generated/sts/router.go b/internal/generated/sts/router.go deleted file mode 100644 index 490f6ccc..00000000 --- a/internal/generated/sts/router.go +++ /dev/null @@ -1,105 +0,0 @@ -// Code generated by devcloud codegen. DO NOT EDIT. -// SPDX-License-Identifier: Apache-2.0 -package sts - -import "strings" - -// PathParams holds the URL path parameters extracted by the router. -type PathParams map[string]string - -// OperationRoute maps an HTTP method and URI pattern to an operation name. -type OperationRoute struct { - Method string - Pattern string - Operation string -} - -// OperationRoutes is the list of all routes for this service. -var OperationRoutes = []OperationRoute{ - {Method: "", Pattern: "", Operation: "AssumeRole"}, - {Method: "", Pattern: "", Operation: "AssumeRoleWithSAML"}, - {Method: "", Pattern: "", Operation: "AssumeRoleWithWebIdentity"}, - {Method: "", Pattern: "", Operation: "AssumeRoot"}, - {Method: "", Pattern: "", Operation: "DecodeAuthorizationMessage"}, - {Method: "", Pattern: "", Operation: "GetAccessKeyInfo"}, - {Method: "", Pattern: "", Operation: "GetCallerIdentity"}, - {Method: "", Pattern: "", Operation: "GetDelegatedAccessToken"}, - {Method: "", Pattern: "", Operation: "GetFederationToken"}, - {Method: "", Pattern: "", Operation: "GetSessionToken"}, - {Method: "", Pattern: "", Operation: "GetWebIdentityToken"}, -} - -// MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params - } - } - return "", nil -} - -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) - - params := PathParams{} - pi := 0 - for i, pat := range patParts { - if strings.HasPrefix(pat, "{") && strings.HasSuffix(pat, "+}") { - // Greedy label: consumes all remaining path segments. - label := pat[1 : len(pat)-2] - if pi >= len(pathParts) { - return nil, false - } - params[label] = joinPath(pathParts[pi:]) - pi = len(pathParts) - // Greedy must be the last pattern segment. - if i != len(patParts)-1 { - return nil, false - } - } else if strings.HasPrefix(pat, "{") && strings.HasSuffix(pat, "}") { - // Single-segment label. - label := pat[1 : len(pat)-1] - if pi >= len(pathParts) { - return nil, false - } - params[label] = pathParts[pi] - pi++ - } else { - // Literal segment. - if pi >= len(pathParts) || pathParts[pi] != pat { - return nil, false - } - pi++ - } - } - if pi != len(pathParts) { - return nil, false - } - return params, true -} - -// splitPath splits a URI path into non-empty segments. -func splitPath(path string) []string { - parts := strings.Split(strings.Trim(path, "/"), "/") - out := parts[:0] - for _, p := range parts { - if p != "" { - out = append(out, p) - } - } - return out -} - -// joinPath joins path segments back with "/" separators. -func joinPath(parts []string) string { - return strings.Join(parts, "/") -} diff --git a/internal/generated/sts/types.go b/internal/generated/sts/types.go deleted file mode 100644 index 370bb4cd..00000000 --- a/internal/generated/sts/types.go +++ /dev/null @@ -1,193 +0,0 @@ -// Code generated by devcloud codegen. DO NOT EDIT. -// SPDX-License-Identifier: Apache-2.0 -package sts - -import ( - "time" -) - -type AssumeRoleRequest struct { - DurationSeconds int32 `json:"durationSeconds" xml:"DurationSeconds"` - ExternalId string `json:"externalId" xml:"ExternalId"` - Policy string `json:"policy" xml:"Policy"` - PolicyArns policyDescriptorListType `json:"policyArns" xml:"PolicyArns"` - ProvidedContexts ProvidedContextsListType `json:"providedContexts" xml:"ProvidedContexts"` - RoleArn string `json:"roleArn" xml:"RoleArn"` - RoleSessionName string `json:"roleSessionName" xml:"RoleSessionName"` - SerialNumber string `json:"serialNumber" xml:"SerialNumber"` - SourceIdentity string `json:"sourceIdentity" xml:"SourceIdentity"` - Tags tagListType `json:"tags" xml:"Tags"` - TokenCode string `json:"tokenCode" xml:"TokenCode"` - TransitiveTagKeys tagKeyListType `json:"transitiveTagKeys" xml:"TransitiveTagKeys"` -} - -type AssumeRoleResponse struct { - AssumedRoleUser *AssumedRoleUser `json:"assumedRoleUser" xml:"AssumedRoleUser"` - Credentials *Credentials `json:"credentials" xml:"Credentials"` - PackedPolicySize int32 `json:"packedPolicySize" xml:"PackedPolicySize"` - SourceIdentity string `json:"sourceIdentity" xml:"SourceIdentity"` -} - -type AssumeRoleWithSAMLRequest struct { - DurationSeconds int32 `json:"durationSeconds" xml:"DurationSeconds"` - Policy string `json:"policy" xml:"Policy"` - PolicyArns policyDescriptorListType `json:"policyArns" xml:"PolicyArns"` - PrincipalArn string `json:"principalArn" xml:"PrincipalArn"` - RoleArn string `json:"roleArn" xml:"RoleArn"` - SAMLAssertion string `json:"sAMLAssertion" xml:"SAMLAssertion"` -} - -type AssumeRoleWithSAMLResponse struct { - AssumedRoleUser *AssumedRoleUser `json:"assumedRoleUser" xml:"AssumedRoleUser"` - Audience string `json:"audience" xml:"Audience"` - Credentials *Credentials `json:"credentials" xml:"Credentials"` - Issuer string `json:"issuer" xml:"Issuer"` - NameQualifier string `json:"nameQualifier" xml:"NameQualifier"` - PackedPolicySize int32 `json:"packedPolicySize" xml:"PackedPolicySize"` - SourceIdentity string `json:"sourceIdentity" xml:"SourceIdentity"` - Subject string `json:"subject" xml:"Subject"` - SubjectType string `json:"subjectType" xml:"SubjectType"` -} - -type AssumeRoleWithWebIdentityRequest struct { - DurationSeconds int32 `json:"durationSeconds" xml:"DurationSeconds"` - Policy string `json:"policy" xml:"Policy"` - PolicyArns policyDescriptorListType `json:"policyArns" xml:"PolicyArns"` - ProviderId string `json:"providerId" xml:"ProviderId"` - RoleArn string `json:"roleArn" xml:"RoleArn"` - RoleSessionName string `json:"roleSessionName" xml:"RoleSessionName"` - WebIdentityToken string `json:"webIdentityToken" xml:"WebIdentityToken"` -} - -type AssumeRoleWithWebIdentityResponse struct { - AssumedRoleUser *AssumedRoleUser `json:"assumedRoleUser" xml:"AssumedRoleUser"` - Audience string `json:"audience" xml:"Audience"` - Credentials *Credentials `json:"credentials" xml:"Credentials"` - PackedPolicySize int32 `json:"packedPolicySize" xml:"PackedPolicySize"` - Provider string `json:"provider" xml:"Provider"` - SourceIdentity string `json:"sourceIdentity" xml:"SourceIdentity"` - SubjectFromWebIdentityToken string `json:"subjectFromWebIdentityToken" xml:"SubjectFromWebIdentityToken"` -} - -type AssumeRootRequest struct { - DurationSeconds int32 `json:"durationSeconds" xml:"DurationSeconds"` - TargetPrincipal string `json:"targetPrincipal" xml:"TargetPrincipal"` - TaskPolicyArn *PolicyDescriptorType `json:"taskPolicyArn" xml:"TaskPolicyArn"` -} - -type AssumeRootResponse struct { - Credentials *Credentials `json:"credentials" xml:"Credentials"` - SourceIdentity string `json:"sourceIdentity" xml:"SourceIdentity"` -} - -type AssumedRoleUser struct { - Arn string `json:"arn" xml:"Arn"` - AssumedRoleId string `json:"assumedRoleId" xml:"AssumedRoleId"` -} - -type Credentials struct { - AccessKeyId string `json:"accessKeyId" xml:"AccessKeyId"` - Expiration time.Time `json:"expiration" xml:"Expiration"` - SecretAccessKey string `json:"secretAccessKey" xml:"SecretAccessKey"` - SessionToken string `json:"sessionToken" xml:"SessionToken"` -} - -type DecodeAuthorizationMessageRequest struct { - EncodedMessage string `json:"encodedMessage" xml:"EncodedMessage"` -} - -type DecodeAuthorizationMessageResponse struct { - DecodedMessage string `json:"decodedMessage" xml:"DecodedMessage"` -} - -type FederatedUser struct { - Arn string `json:"arn" xml:"Arn"` - FederatedUserId string `json:"federatedUserId" xml:"FederatedUserId"` -} - -type GetAccessKeyInfoRequest struct { - AccessKeyId string `json:"accessKeyId" xml:"AccessKeyId"` -} - -type GetAccessKeyInfoResponse struct { - Account string `json:"account" xml:"Account"` -} - -type GetCallerIdentityRequest struct { -} - -type GetCallerIdentityResponse struct { - Account string `json:"account" xml:"Account"` - Arn string `json:"arn" xml:"Arn"` - UserId string `json:"userId" xml:"UserId"` -} - -type GetDelegatedAccessTokenRequest struct { - TradeInToken string `json:"tradeInToken" xml:"TradeInToken"` -} - -type GetDelegatedAccessTokenResponse struct { - AssumedPrincipal string `json:"assumedPrincipal" xml:"AssumedPrincipal"` - Credentials *Credentials `json:"credentials" xml:"Credentials"` - PackedPolicySize int32 `json:"packedPolicySize" xml:"PackedPolicySize"` -} - -type GetFederationTokenRequest struct { - DurationSeconds int32 `json:"durationSeconds" xml:"DurationSeconds"` - Name string `json:"name" xml:"Name"` - Policy string `json:"policy" xml:"Policy"` - PolicyArns policyDescriptorListType `json:"policyArns" xml:"PolicyArns"` - Tags tagListType `json:"tags" xml:"Tags"` -} - -type GetFederationTokenResponse struct { - Credentials *Credentials `json:"credentials" xml:"Credentials"` - FederatedUser *FederatedUser `json:"federatedUser" xml:"FederatedUser"` - PackedPolicySize int32 `json:"packedPolicySize" xml:"PackedPolicySize"` -} - -type GetSessionTokenRequest struct { - DurationSeconds int32 `json:"durationSeconds" xml:"DurationSeconds"` - SerialNumber string `json:"serialNumber" xml:"SerialNumber"` - TokenCode string `json:"tokenCode" xml:"TokenCode"` -} - -type GetSessionTokenResponse struct { - Credentials *Credentials `json:"credentials" xml:"Credentials"` -} - -type GetWebIdentityTokenRequest struct { - Audience webIdentityTokenAudienceListType `json:"audience" xml:"Audience"` - DurationSeconds int32 `json:"durationSeconds" xml:"DurationSeconds"` - SigningAlgorithm string `json:"signingAlgorithm" xml:"SigningAlgorithm"` - Tags tagListType `json:"tags" xml:"Tags"` -} - -type GetWebIdentityTokenResponse struct { - Expiration time.Time `json:"expiration" xml:"Expiration"` - WebIdentityToken string `json:"webIdentityToken" xml:"WebIdentityToken"` -} - -type PolicyDescriptorType struct { - Arn string `json:"arn" xml:"arn"` -} - -type ProvidedContext struct { - ContextAssertion string `json:"contextAssertion" xml:"ContextAssertion"` - ProviderArn string `json:"providerArn" xml:"ProviderArn"` -} - -type Tag struct { - Key string `json:"key" xml:"Key"` - Value string `json:"value" xml:"Value"` -} - -type ProvidedContextsListType []*ProvidedContext - -type policyDescriptorListType []*PolicyDescriptorType - -type tagKeyListType []string - -type tagListType []*Tag - -type webIdentityTokenAudienceListType []string diff --git a/internal/plugin/registry.go b/internal/plugin/registry.go index e9f525e1..e4972bb9 100644 --- a/internal/plugin/registry.go +++ b/internal/plugin/registry.go @@ -60,11 +60,39 @@ func (r *Registry) Get(serviceID string) (ServicePlugin, bool) { return p, ok } +// RegisteredServices returns the sorted IDs of every service factory that has +// been registered, whether or not it has been initialized. Useful for +// enumerating the full plugin surface (e.g. conformance checks). +func (r *Registry) RegisteredServices() []string { + r.mu.RLock() + defer r.mu.RUnlock() + return sortedKeys(r.factories) +} + +// Construct builds a fresh plugin instance for serviceID via its factory, +// without calling Init. Returns nil,false if the service is not registered. +// Only the metadata methods (ServiceID/ServiceName/Protocol) are guaranteed +// usable on the returned value; anything requiring state needs Init first. +func (r *Registry) Construct(serviceID string) (ServicePlugin, bool) { + r.mu.RLock() + f, ok := r.factories[serviceID] + r.mu.RUnlock() + if !ok { + return nil, false + } + return f(), true +} + func (r *Registry) ActiveServices() []string { r.mu.RLock() defer r.mu.RUnlock() - ids := make([]string, 0, len(r.active)) - for id := range r.active { + return sortedKeys(r.active) +} + +// sortedKeys returns the sorted keys of m. Callers hold the lock. +func sortedKeys[V any](m map[string]V) []string { + ids := make([]string, 0, len(m)) + for id := range m { ids = append(ids, id) } sort.Strings(ids) diff --git a/internal/services/acm/provider.go b/internal/services/acm/provider.go index 80113e5e..dddd43b9 100644 --- a/internal/services/acm/provider.go +++ b/internal/services/acm/provider.go @@ -133,7 +133,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListRenewalEvents": return jsonResp(http.StatusOK, map[string]any{"Events": []any{}}) default: - return acmError("InvalidAction", fmt.Sprintf("operation not implemented: %s", op), http.StatusBadRequest), nil + return acmError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/acmpca/provider.go b/internal/services/acmpca/provider.go index 33bf6160..d8eedf98 100644 --- a/internal/services/acmpca/provider.go +++ b/internal/services/acmpca/provider.go @@ -103,7 +103,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListTags": return p.listTags(req) default: - return pcaError("InvalidAction", fmt.Sprintf("operation not implemented: %s", op), http.StatusBadRequest), nil + return pcaError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/amplify/provider.go b/internal/services/amplify/provider.go index a0387deb..5f898260 100644 --- a/internal/services/amplify/provider.go +++ b/internal/services/amplify/provider.go @@ -216,7 +216,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request }) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/apigatewayv2/provider.go b/internal/services/apigatewayv2/provider.go index 7211eeec..83305569 100644 --- a/internal/services/apigatewayv2/provider.go +++ b/internal/services/apigatewayv2/provider.go @@ -325,7 +325,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request // Stub ops (Portal, PortalProduct, RoutingRule, etc.) default: - return shared.JSONResponse(http.StatusOK, map[string]any{}) + return shared.JSONError("NotImplemented", "operation not implemented: "+op, http.StatusNotImplemented), nil } } diff --git a/internal/services/apigatewayv2/provider_test.go b/internal/services/apigatewayv2/provider_test.go index a10a1251..1d147fd1 100644 --- a/internal/services/apigatewayv2/provider_test.go +++ b/internal/services/apigatewayv2/provider_test.go @@ -52,6 +52,15 @@ func createTestAPI(t *testing.T, p *Provider, name string) string { return id } +func TestUnimplementedOp(t *testing.T) { + p := newTestProvider(t) + // Unimplemented ops must return an AWS error, not a false 200 success. + resp := callREST(t, p, "POST", "/", "SomeUnknownOperation", "{}") + assert.Equal(t, 501, resp.StatusCode) + rb := parseBody(t, resp) + assert.Equal(t, "NotImplemented", rb["__type"]) +} + func TestApiCRUD(t *testing.T) { p := newTestProvider(t) diff --git a/internal/services/appconfig/provider.go b/internal/services/appconfig/provider.go index bd1f0499..6df70a4e 100644 --- a/internal/services/appconfig/provider.go +++ b/internal/services/appconfig/provider.go @@ -208,7 +208,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request }) default: - return shared.JSONError("BadRequestException", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/applicationautoscaling/provider.go b/internal/services/applicationautoscaling/provider.go index e7dca5ae..75649425 100644 --- a/internal/services/applicationautoscaling/provider.go +++ b/internal/services/applicationautoscaling/provider.go @@ -151,7 +151,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "GetScalingStatus": return p.getScalingStatus(params) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/appsync/provider.go b/internal/services/appsync/provider.go index a90a69d2..907cc855 100644 --- a/internal/services/appsync/provider.go +++ b/internal/services/appsync/provider.go @@ -256,7 +256,7 @@ func (p *Provider) handleStub(op string) (*plugin.Response, error) { case "StartSchemaMerge", "GetSchemaError": return shared.JSONResponse(http.StatusOK, map[string]any{}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/athena/provider.go b/internal/services/athena/provider.go index ec1c705d..35298569 100644 --- a/internal/services/athena/provider.go +++ b/internal/services/athena/provider.go @@ -208,7 +208,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "UpdateNamedQuery": return shared.JSONResponse(http.StatusOK, map[string]any{}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/backup/provider.go b/internal/services/backup/provider.go index b64a85d7..8435c518 100644 --- a/internal/services/backup/provider.go +++ b/internal/services/backup/provider.go @@ -315,7 +315,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request // --- Default stubs for remaining ops --- default: - return shared.JSONResponse(http.StatusOK, map[string]any{}) + return shared.JSONError("NotImplemented", "operation not implemented: "+op, http.StatusNotImplemented), nil } } diff --git a/internal/services/backup/provider_test.go b/internal/services/backup/provider_test.go index caf4eddf..40e61520 100644 --- a/internal/services/backup/provider_test.go +++ b/internal/services/backup/provider_test.go @@ -40,6 +40,15 @@ func parseBody(t *testing.T, resp *plugin.Response) map[string]any { return m } +func TestUnimplementedOp(t *testing.T) { + p := newTestProvider(t) + // Unimplemented ops must return an AWS error, not a false 200 success. + resp := call(t, p, "POST", "/", "SomeUnknownOperation", "{}") + assert.Equal(t, 501, resp.StatusCode) + rb := parseBody(t, resp) + assert.Equal(t, "NotImplemented", rb["__type"]) +} + // ========== TestBackupPlanCRUD ========== func TestBackupPlanCRUD(t *testing.T) { diff --git a/internal/services/batch/provider.go b/internal/services/batch/provider.go index 996fc8e3..4ff8a017 100644 --- a/internal/services/batch/provider.go +++ b/internal/services/batch/provider.go @@ -201,7 +201,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/cloudsearch/provider.go b/internal/services/cloudsearch/provider.go index 05536cfc..efc7d51a 100644 --- a/internal/services/cloudsearch/provider.go +++ b/internal/services/cloudsearch/provider.go @@ -125,7 +125,7 @@ func (p *Provider) HandleRequest(ctx context.Context, op string, req *http.Reque return p.handleUpdateServiceAccessPolicies(form) default: - return csError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return csError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/cloudtrail/provider.go b/internal/services/cloudtrail/provider.go index e7ecfc0c..b37b88f5 100644 --- a/internal/services/cloudtrail/provider.go +++ b/internal/services/cloudtrail/provider.go @@ -170,7 +170,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request "ListPublicKeys", "SearchSampleQueries": return shared.JSONResponse(http.StatusOK, map[string]any{}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/cloudwatch/provider.go b/internal/services/cloudwatch/provider.go index 4340c3ae..55e7e925 100644 --- a/internal/services/cloudwatch/provider.go +++ b/internal/services/cloudwatch/provider.go @@ -101,7 +101,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "DescribeAnomalyDetectors": return p.describeAnomalyDetectors(jm, req) default: - return cwError(jm, "InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return cwError(jm, "NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/cloudwatchlogs/provider.go b/internal/services/cloudwatchlogs/provider.go index a55645ef..0bc8a12f 100644 --- a/internal/services/cloudwatchlogs/provider.go +++ b/internal/services/cloudwatchlogs/provider.go @@ -120,7 +120,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListTagsLogGroup", "ListTagsForResource": return p.listTagsForResource(params) default: - return cwlError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return cwlError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/codeartifact/provider.go b/internal/services/codeartifact/provider.go index edc3b54e..d6886f5e 100644 --- a/internal/services/codeartifact/provider.go +++ b/internal/services/codeartifact/provider.go @@ -181,7 +181,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return p.listTagsForResource(req) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/codebuild/provider.go b/internal/services/codebuild/provider.go index c050610c..74705fdb 100644 --- a/internal/services/codebuild/provider.go +++ b/internal/services/codebuild/provider.go @@ -136,8 +136,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListTagsForResource": return p.listTagsForResource(params) default: - // Remaining ops (BuildBatch, Sandbox, CommandExecution, etc.): return success/empty - return shared.JSONResponse(http.StatusOK, map[string]any{}) + return shared.JSONError("NotImplemented", "operation not implemented: "+action, http.StatusNotImplemented), nil } } diff --git a/internal/services/codebuild/provider_test.go b/internal/services/codebuild/provider_test.go index 1655fe4a..88f6fc42 100644 --- a/internal/services/codebuild/provider_test.go +++ b/internal/services/codebuild/provider_test.go @@ -449,7 +449,7 @@ func TestTags(t *testing.T) { platforms, _ := eib["platforms"].([]any) assert.NotEmpty(t, platforms) - // Unknown action returns empty success + // Unimplemented ops return an AWS error, not a false success. unknownResp := call(t, p, "StartBuildBatch", `{}`) - assert.Equal(t, 200, unknownResp.StatusCode) + assert.Equal(t, 501, unknownResp.StatusCode) } diff --git a/internal/services/codecommit/provider.go b/internal/services/codecommit/provider.go index 3d1f2c76..369bab9d 100644 --- a/internal/services/codecommit/provider.go +++ b/internal/services/codecommit/provider.go @@ -195,7 +195,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/codedeploy/provider.go b/internal/services/codedeploy/provider.go index 8d1e595e..c9b24ace 100644 --- a/internal/services/codedeploy/provider.go +++ b/internal/services/codedeploy/provider.go @@ -125,8 +125,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListTagsForResource": return p.listTagsForResource(params) default: - // Remaining ~27 ops: return success/empty - return shared.JSONResponse(http.StatusOK, map[string]any{}) + return shared.JSONError("NotImplemented", "operation not implemented: "+action, http.StatusNotImplemented), nil } } diff --git a/internal/services/codedeploy/provider_test.go b/internal/services/codedeploy/provider_test.go index 322dce89..dc9980cc 100644 --- a/internal/services/codedeploy/provider_test.go +++ b/internal/services/codedeploy/provider_test.go @@ -379,7 +379,7 @@ func TestTags(t *testing.T) { assert.Equal(t, "Owner", firstTag["key"]) assert.Equal(t, "alice", firstTag["value"]) - // Unknown action returns empty success + // Unimplemented ops return an AWS error, not a false success. unknownResp := call(t, p, "AddTagsToOnPremisesInstances", `{}`) - assert.Equal(t, 200, unknownResp.StatusCode) + assert.Equal(t, 501, unknownResp.StatusCode) } diff --git a/internal/services/codepipeline/provider.go b/internal/services/codepipeline/provider.go index ce1e8ffb..e3f8ebd3 100644 --- a/internal/services/codepipeline/provider.go +++ b/internal/services/codepipeline/provider.go @@ -188,8 +188,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request "pipelineExecutionId": shared.GenerateUUID(), }) default: - // Remaining ops: return success/empty - return shared.JSONResponse(http.StatusOK, map[string]any{}) + return shared.JSONError("NotImplemented", "operation not implemented: "+action, http.StatusNotImplemented), nil } } diff --git a/internal/services/codepipeline/provider_test.go b/internal/services/codepipeline/provider_test.go index ea7564b7..b9cd9d78 100644 --- a/internal/services/codepipeline/provider_test.go +++ b/internal/services/codepipeline/provider_test.go @@ -40,6 +40,15 @@ func parseBody(t *testing.T, resp *plugin.Response) map[string]any { return m } +func TestUnimplementedOp(t *testing.T) { + p := newTestProvider(t) + // Unimplemented ops must return an AWS error, not a false 200 success. + resp := call(t, p, "SomeUnknownOperation", "{}") + assert.Equal(t, 501, resp.StatusCode) + rb := parseBody(t, resp) + assert.Equal(t, "NotImplemented", rb["__type"]) +} + func TestPipelineCRUD(t *testing.T) { p := newTestProvider(t) diff --git a/internal/services/cognitoidentity/provider.go b/internal/services/cognitoidentity/provider.go index 1c7b0872..89a1136d 100644 --- a/internal/services/cognitoidentity/provider.go +++ b/internal/services/cognitoidentity/provider.go @@ -97,7 +97,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListTagsForResource": return p.listTagsForResource(req) default: - return ciError("InvalidAction", fmt.Sprintf("operation not implemented: %s", op), http.StatusBadRequest), nil + return ciError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/cognitoidentityprovider/provider.go b/internal/services/cognitoidentityprovider/provider.go index 5853eab9..3e4f3bdb 100644 --- a/internal/services/cognitoidentityprovider/provider.go +++ b/internal/services/cognitoidentityprovider/provider.go @@ -15,6 +15,7 @@ import ( "time" "github.com/skyoo2003/devcloud/internal/plugin" + "github.com/skyoo2003/devcloud/internal/shared" ) const defaultAccountID = plugin.DefaultAccountID @@ -181,8 +182,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListTagsForResource": return p.listTagsForResource(req) default: - // Return success/empty for unimplemented ops - return emptyOK(), nil + return shared.JSONError("NotImplemented", "operation not implemented: "+op, http.StatusNotImplemented), nil } } @@ -204,15 +204,6 @@ func (p *Provider) GetMetrics(_ context.Context) (*plugin.ServiceMetrics, error) // --- helpers --- -func cognitoError(code, msg string, status int) *plugin.Response { - body, _ := json.Marshal(map[string]string{"__type": code, "message": msg}) - return &plugin.Response{ - StatusCode: status, - Body: body, - ContentType: "application/x-amz-json-1.1", - } -} - func jsonResp(status int, v any) (*plugin.Response, error) { body, err := json.Marshal(v) if err != nil { @@ -466,10 +457,10 @@ func (p *Provider) createUserPool(req *http.Request) (*plugin.Response, error) { Schema []any `json:"Schema"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if in.PoolName == "" { - return cognitoError("InvalidParameterException", "PoolName is required", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "PoolName is required", http.StatusBadRequest), nil } id := "us-east-1_" + strings.ReplaceAll(newID()[:8], "-", "") mfa := in.MFAConfig @@ -498,12 +489,12 @@ func (p *Provider) describeUserPool(req *http.Request) (*plugin.Response, error) UserPoolId string `json:"UserPoolId"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } pool, err := p.store.GetUserPool(in.UserPoolId) if err != nil { if err == ErrPoolNotFound { - return cognitoError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil } return nil, err } @@ -529,12 +520,12 @@ func (p *Provider) updateUserPool(req *http.Request) (*plugin.Response, error) { MFAConfig string `json:"MfaConfiguration"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } pool, err := p.store.GetUserPool(in.UserPoolId) if err != nil { if err == ErrPoolNotFound { - return cognitoError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil } return nil, err } @@ -556,11 +547,11 @@ func (p *Provider) deleteUserPool(req *http.Request) (*plugin.Response, error) { UserPoolId string `json:"UserPoolId"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if err := p.store.DeleteUserPool(in.UserPoolId); err != nil { if err == ErrPoolNotFound { - return cognitoError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil } return nil, err } @@ -580,14 +571,14 @@ func (p *Provider) createUserPoolClient(req *http.Request) (*plugin.Response, er LogoutURLs []string `json:"LogoutURLs"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if in.UserPoolId == "" || in.ClientName == "" { - return cognitoError("InvalidParameterException", "UserPoolId and ClientName are required", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "UserPoolId and ClientName are required", http.StatusBadRequest), nil } // Ensure pool exists if _, err := p.store.GetUserPool(in.UserPoolId); err != nil { - return cognitoError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil } secret := "" if in.GenerateSecret { @@ -618,12 +609,12 @@ func (p *Provider) describeUserPoolClient(req *http.Request) (*plugin.Response, ClientId string `json:"ClientId"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } client, err := p.store.GetUserPoolClient(in.UserPoolId, in.ClientId) if err != nil { if err == ErrClientNotFound { - return cognitoError("ResourceNotFoundException", "client not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "client not found", http.StatusBadRequest), nil } return nil, err } @@ -635,7 +626,7 @@ func (p *Provider) listUserPoolClients(req *http.Request) (*plugin.Response, err UserPoolId string `json:"UserPoolId"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } clients, err := p.store.ListUserPoolClients(in.UserPoolId) if err != nil { @@ -663,12 +654,12 @@ func (p *Provider) updateUserPoolClient(req *http.Request) (*plugin.Response, er LogoutURLs []string `json:"LogoutURLs"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } client, err := p.store.GetUserPoolClient(in.UserPoolId, in.ClientId) if err != nil { if err == ErrClientNotFound { - return cognitoError("ResourceNotFoundException", "client not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "client not found", http.StatusBadRequest), nil } return nil, err } @@ -699,11 +690,11 @@ func (p *Provider) deleteUserPoolClient(req *http.Request) (*plugin.Response, er ClientId string `json:"ClientId"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if err := p.store.DeleteUserPoolClient(in.UserPoolId, in.ClientId); err != nil { if err == ErrClientNotFound { - return cognitoError("ResourceNotFoundException", "client not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "client not found", http.StatusBadRequest), nil } return nil, err } @@ -720,17 +711,17 @@ func (p *Provider) adminCreateUser(req *http.Request) (*plugin.Response, error) UserAttributes []map[string]string `json:"UserAttributes"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if in.UserPoolId == "" || in.Username == "" { - return cognitoError("InvalidParameterException", "UserPoolId and Username are required", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "UserPoolId and Username are required", http.StatusBadRequest), nil } if _, err := p.store.GetUserPool(in.UserPoolId); err != nil { - return cognitoError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil } // Check if user already exists if _, err := p.store.GetUser(in.UserPoolId, in.Username); err == nil { - return cognitoError("UsernameExistsException", "user already exists", http.StatusBadRequest), nil + return shared.JSONError("UsernameExistsException", "user already exists", http.StatusBadRequest), nil } t := now() user := &User{ @@ -756,12 +747,12 @@ func (p *Provider) adminGetUser(req *http.Request) (*plugin.Response, error) { Username string `json:"Username"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } user, err := p.store.GetUser(in.UserPoolId, in.Username) if err != nil { if err == ErrUserNotFound { - return cognitoError("UserNotFoundException", "user not found", http.StatusBadRequest), nil + return shared.JSONError("UserNotFoundException", "user not found", http.StatusBadRequest), nil } return nil, err } @@ -773,7 +764,7 @@ func (p *Provider) listUsers(req *http.Request) (*plugin.Response, error) { UserPoolId string `json:"UserPoolId"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } users, err := p.store.ListUsers(in.UserPoolId) if err != nil { @@ -792,11 +783,11 @@ func (p *Provider) adminDeleteUser(req *http.Request) (*plugin.Response, error) Username string `json:"Username"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if err := p.store.DeleteUser(in.UserPoolId, in.Username); err != nil { if err == ErrUserNotFound { - return cognitoError("UserNotFoundException", "user not found", http.StatusBadRequest), nil + return shared.JSONError("UserNotFoundException", "user not found", http.StatusBadRequest), nil } return nil, err } @@ -809,12 +800,12 @@ func (p *Provider) adminDisableUser(req *http.Request) (*plugin.Response, error) Username string `json:"Username"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } user, err := p.store.GetUser(in.UserPoolId, in.Username) if err != nil { if err == ErrUserNotFound { - return cognitoError("UserNotFoundException", "user not found", http.StatusBadRequest), nil + return shared.JSONError("UserNotFoundException", "user not found", http.StatusBadRequest), nil } return nil, err } @@ -832,12 +823,12 @@ func (p *Provider) adminEnableUser(req *http.Request) (*plugin.Response, error) Username string `json:"Username"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } user, err := p.store.GetUser(in.UserPoolId, in.Username) if err != nil { if err == ErrUserNotFound { - return cognitoError("UserNotFoundException", "user not found", http.StatusBadRequest), nil + return shared.JSONError("UserNotFoundException", "user not found", http.StatusBadRequest), nil } return nil, err } @@ -856,12 +847,12 @@ func (p *Provider) adminUpdateUserAttributes(req *http.Request) (*plugin.Respons UserAttributes []map[string]string `json:"UserAttributes"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } user, err := p.store.GetUser(in.UserPoolId, in.Username) if err != nil { if err == ErrUserNotFound { - return cognitoError("UserNotFoundException", "user not found", http.StatusBadRequest), nil + return shared.JSONError("UserNotFoundException", "user not found", http.StatusBadRequest), nil } return nil, err } @@ -880,12 +871,12 @@ func (p *Provider) adminDeleteUserAttributes(req *http.Request) (*plugin.Respons UserAttributeNames []string `json:"UserAttributeNames"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } user, err := p.store.GetUser(in.UserPoolId, in.Username) if err != nil { if err == ErrUserNotFound { - return cognitoError("UserNotFoundException", "user not found", http.StatusBadRequest), nil + return shared.JSONError("UserNotFoundException", "user not found", http.StatusBadRequest), nil } return nil, err } @@ -905,12 +896,12 @@ func (p *Provider) adminSetUserPassword(req *http.Request) (*plugin.Response, er Permanent bool `json:"Permanent"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } user, err := p.store.GetUser(in.UserPoolId, in.Username) if err != nil { if err == ErrUserNotFound { - return cognitoError("UserNotFoundException", "user not found", http.StatusBadRequest), nil + return shared.JSONError("UserNotFoundException", "user not found", http.StatusBadRequest), nil } return nil, err } @@ -931,12 +922,12 @@ func (p *Provider) adminResetUserPassword(req *http.Request) (*plugin.Response, Username string `json:"Username"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } user, err := p.store.GetUser(in.UserPoolId, in.Username) if err != nil { if err == ErrUserNotFound { - return cognitoError("UserNotFoundException", "user not found", http.StatusBadRequest), nil + return shared.JSONError("UserNotFoundException", "user not found", http.StatusBadRequest), nil } return nil, err } @@ -954,12 +945,12 @@ func (p *Provider) adminConfirmSignUp(req *http.Request) (*plugin.Response, erro Username string `json:"Username"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } user, err := p.store.GetUser(in.UserPoolId, in.Username) if err != nil { if err == ErrUserNotFound { - return cognitoError("UserNotFoundException", "user not found", http.StatusBadRequest), nil + return shared.JSONError("UserNotFoundException", "user not found", http.StatusBadRequest), nil } return nil, err } @@ -982,14 +973,14 @@ func (p *Provider) createGroup(req *http.Request) (*plugin.Response, error) { Precedence int `json:"Precedence"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if in.UserPoolId == "" || in.GroupName == "" { - return cognitoError("InvalidParameterException", "UserPoolId and GroupName are required", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "UserPoolId and GroupName are required", http.StatusBadRequest), nil } // Check pool exists if _, err := p.store.GetUserPool(in.UserPoolId); err != nil { - return cognitoError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil } g := &Group{ PoolID: in.UserPoolId, @@ -1011,12 +1002,12 @@ func (p *Provider) getGroup(req *http.Request) (*plugin.Response, error) { GroupName string `json:"GroupName"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } g, err := p.store.GetGroup(in.UserPoolId, in.GroupName) if err != nil { if err == ErrGroupNotFound { - return cognitoError("ResourceNotFoundException", "group not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "group not found", http.StatusBadRequest), nil } return nil, err } @@ -1028,7 +1019,7 @@ func (p *Provider) listGroups(req *http.Request) (*plugin.Response, error) { UserPoolId string `json:"UserPoolId"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } groups, err := p.store.ListGroups(in.UserPoolId) if err != nil { @@ -1050,12 +1041,12 @@ func (p *Provider) updateGroup(req *http.Request) (*plugin.Response, error) { Precedence int `json:"Precedence"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } g, err := p.store.GetGroup(in.UserPoolId, in.GroupName) if err != nil { if err == ErrGroupNotFound { - return cognitoError("ResourceNotFoundException", "group not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "group not found", http.StatusBadRequest), nil } return nil, err } @@ -1078,11 +1069,11 @@ func (p *Provider) deleteGroup(req *http.Request) (*plugin.Response, error) { GroupName string `json:"GroupName"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if err := p.store.DeleteGroup(in.UserPoolId, in.GroupName); err != nil { if err == ErrGroupNotFound { - return cognitoError("ResourceNotFoundException", "group not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "group not found", http.StatusBadRequest), nil } return nil, err } @@ -1098,7 +1089,7 @@ func (p *Provider) adminAddUserToGroup(req *http.Request) (*plugin.Response, err GroupName string `json:"GroupName"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if err := p.store.AddUserToGroup(in.UserPoolId, in.Username, in.GroupName); err != nil { return nil, err @@ -1113,7 +1104,7 @@ func (p *Provider) adminRemoveUserFromGroup(req *http.Request) (*plugin.Response GroupName string `json:"GroupName"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if err := p.store.RemoveUserFromGroup(in.UserPoolId, in.Username, in.GroupName); err != nil { return nil, err @@ -1127,7 +1118,7 @@ func (p *Provider) adminListGroupsForUser(req *http.Request) (*plugin.Response, Username string `json:"Username"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } groups, err := p.store.ListGroupsForUser(in.UserPoolId, in.Username) if err != nil { @@ -1146,7 +1137,7 @@ func (p *Provider) listUsersInGroup(req *http.Request) (*plugin.Response, error) GroupName string `json:"GroupName"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } users, err := p.store.ListUsersInGroup(in.UserPoolId, in.GroupName) if err != nil { @@ -1171,10 +1162,10 @@ func (p *Provider) createIdentityProvider(req *http.Request) (*plugin.Response, IdpIdentifiers []string `json:"IdpIdentifiers"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if in.UserPoolId == "" || in.ProviderName == "" { - return cognitoError("InvalidParameterException", "UserPoolId and ProviderName are required", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "UserPoolId and ProviderName are required", http.StatusBadRequest), nil } idp := &IdentityProvider{ PoolID: in.UserPoolId, @@ -1197,12 +1188,12 @@ func (p *Provider) describeIdentityProvider(req *http.Request) (*plugin.Response ProviderName string `json:"ProviderName"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } idp, err := p.store.GetIdentityProvider(in.UserPoolId, in.ProviderName) if err != nil { if err == ErrIDPNotFound { - return cognitoError("ResourceNotFoundException", "identity provider not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "identity provider not found", http.StatusBadRequest), nil } return nil, err } @@ -1214,7 +1205,7 @@ func (p *Provider) listIdentityProviders(req *http.Request) (*plugin.Response, e UserPoolId string `json:"UserPoolId"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } idps, err := p.store.ListIdentityProviders(in.UserPoolId) if err != nil { @@ -1241,12 +1232,12 @@ func (p *Provider) updateIdentityProvider(req *http.Request) (*plugin.Response, IdpIdentifiers []string `json:"IdpIdentifiers"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } idp, err := p.store.GetIdentityProvider(in.UserPoolId, in.ProviderName) if err != nil { if err == ErrIDPNotFound { - return cognitoError("ResourceNotFoundException", "identity provider not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "identity provider not found", http.StatusBadRequest), nil } return nil, err } @@ -1271,11 +1262,11 @@ func (p *Provider) deleteIdentityProvider(req *http.Request) (*plugin.Response, ProviderName string `json:"ProviderName"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if err := p.store.DeleteIdentityProvider(in.UserPoolId, in.ProviderName); err != nil { if err == ErrIDPNotFound { - return cognitoError("ResourceNotFoundException", "identity provider not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "identity provider not found", http.StatusBadRequest), nil } return nil, err } @@ -1288,12 +1279,12 @@ func (p *Provider) getIdentityProviderByIdentifier(req *http.Request) (*plugin.R IdpIdentifier string `json:"IdpIdentifier"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } idp, err := p.store.GetIdentityProviderByIdentifier(in.UserPoolId, in.IdpIdentifier) if err != nil { if err == ErrIDPNotFound { - return cognitoError("ResourceNotFoundException", "identity provider not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "identity provider not found", http.StatusBadRequest), nil } return nil, err } @@ -1310,10 +1301,10 @@ func (p *Provider) createResourceServer(req *http.Request) (*plugin.Response, er Scopes []any `json:"Scopes"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if in.UserPoolId == "" || in.Identifier == "" { - return cognitoError("InvalidParameterException", "UserPoolId and Identifier are required", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "UserPoolId and Identifier are required", http.StatusBadRequest), nil } rs := &ResourceServer{ PoolID: in.UserPoolId, @@ -1333,12 +1324,12 @@ func (p *Provider) describeResourceServer(req *http.Request) (*plugin.Response, Identifier string `json:"Identifier"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } rs, err := p.store.GetResourceServer(in.UserPoolId, in.Identifier) if err != nil { if err == ErrResourceServerNotFound { - return cognitoError("ResourceNotFoundException", "resource server not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "resource server not found", http.StatusBadRequest), nil } return nil, err } @@ -1350,7 +1341,7 @@ func (p *Provider) listResourceServers(req *http.Request) (*plugin.Response, err UserPoolId string `json:"UserPoolId"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } servers, err := p.store.ListResourceServers(in.UserPoolId) if err != nil { @@ -1371,12 +1362,12 @@ func (p *Provider) updateResourceServer(req *http.Request) (*plugin.Response, er Scopes []any `json:"Scopes"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } rs, err := p.store.GetResourceServer(in.UserPoolId, in.Identifier) if err != nil { if err == ErrResourceServerNotFound { - return cognitoError("ResourceNotFoundException", "resource server not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "resource server not found", http.StatusBadRequest), nil } return nil, err } @@ -1398,11 +1389,11 @@ func (p *Provider) deleteResourceServer(req *http.Request) (*plugin.Response, er Identifier string `json:"Identifier"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if err := p.store.DeleteResourceServer(in.UserPoolId, in.Identifier); err != nil { if err == ErrResourceServerNotFound { - return cognitoError("ResourceNotFoundException", "resource server not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "resource server not found", http.StatusBadRequest), nil } return nil, err } @@ -1419,7 +1410,7 @@ func (p *Provider) signUp(req *http.Request) (*plugin.Response, error) { UserAttributes []map[string]string `json:"UserAttributes"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } // Find pool by client clients, err := p.store.db().Query(`SELECT pool_id FROM user_pool_clients WHERE id=?`, in.ClientId) @@ -1432,10 +1423,10 @@ func (p *Provider) signUp(req *http.Request) (*plugin.Response, error) { } clients.Close() //nolint:errcheck if poolID == "" { - return cognitoError("ResourceNotFoundException", "client not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "client not found", http.StatusBadRequest), nil } if _, err := p.store.GetUser(poolID, in.Username); err == nil { - return cognitoError("UsernameExistsException", "user already exists", http.StatusBadRequest), nil + return shared.JSONError("UsernameExistsException", "user already exists", http.StatusBadRequest), nil } t := now() user := &User{ @@ -1465,18 +1456,18 @@ func (p *Provider) confirmSignUp(req *http.Request) (*plugin.Response, error) { ConfirmationCode string `json:"ConfirmationCode"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } // Find pool by client row := p.store.db().QueryRow(`SELECT pool_id FROM user_pool_clients WHERE id=?`, in.ClientId) var poolID string if err := row.Scan(&poolID); err != nil { - return cognitoError("ResourceNotFoundException", "client not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "client not found", http.StatusBadRequest), nil } user, err := p.store.GetUser(poolID, in.Username) if err != nil { if err == ErrUserNotFound { - return cognitoError("UserNotFoundException", "user not found", http.StatusBadRequest), nil + return shared.JSONError("UserNotFoundException", "user not found", http.StatusBadRequest), nil } return nil, err } @@ -1495,13 +1486,13 @@ func (p *Provider) initiateAuth(req *http.Request) (*plugin.Response, error) { AuthParameters map[string]string `json:"AuthParameters"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } // Find pool by client row := p.store.db().QueryRow(`SELECT pool_id FROM user_pool_clients WHERE id=?`, in.ClientId) var poolID string if err := row.Scan(&poolID); err != nil { - return cognitoError("ResourceNotFoundException", "client not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "client not found", http.StatusBadRequest), nil } username := in.AuthParameters["USERNAME"] if username == "" { @@ -1522,7 +1513,7 @@ func (p *Provider) adminInitiateAuth(req *http.Request) (*plugin.Response, error AuthParameters map[string]string `json:"AuthParameters"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } username := in.AuthParameters["USERNAME"] if username == "" { @@ -1542,7 +1533,7 @@ func (p *Provider) respondToAuthChallenge(req *http.Request) (*plugin.Response, ChallengeResponses map[string]string `json:"ChallengeResponses"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } row := p.store.db().QueryRow(`SELECT pool_id FROM user_pool_clients WHERE id=?`, in.ClientId) var poolID string @@ -1565,7 +1556,7 @@ func (p *Provider) adminRespondToAuthChallenge(req *http.Request) (*plugin.Respo ChallengeResponses map[string]string `json:"ChallengeResponses"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } username := in.ChallengeResponses["USERNAME"] if username == "" { @@ -1581,7 +1572,7 @@ func (p *Provider) forgotPassword(req *http.Request) (*plugin.Response, error) { Username string `json:"Username"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } return jsonResp(http.StatusOK, map[string]any{ "CodeDeliveryDetails": map[string]string{ @@ -1600,7 +1591,7 @@ func (p *Provider) confirmForgotPassword(req *http.Request) (*plugin.Response, e Password string `json:"Password"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } // Find pool by client row := p.store.db().QueryRow(`SELECT pool_id FROM user_pool_clients WHERE id=?`, in.ClientId) @@ -1640,26 +1631,26 @@ func (p *Provider) getUser(req *http.Request) (*plugin.Response, error) { AccessToken string `json:"AccessToken"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } // Decode our dummy token payload, err := base64.StdEncoding.DecodeString(in.AccessToken) if err != nil { - return cognitoError("NotAuthorizedException", "invalid token", http.StatusBadRequest), nil + return shared.JSONError("NotAuthorizedException", "invalid token", http.StatusBadRequest), nil } var claims map[string]any if err := json.Unmarshal(payload, &claims); err != nil { - return cognitoError("NotAuthorizedException", "invalid token", http.StatusBadRequest), nil + return shared.JSONError("NotAuthorizedException", "invalid token", http.StatusBadRequest), nil } poolID, _ := claims["pool_id"].(string) username, _ := claims["sub"].(string) if poolID == "" || username == "" { - return cognitoError("NotAuthorizedException", "invalid token claims", http.StatusBadRequest), nil + return shared.JSONError("NotAuthorizedException", "invalid token claims", http.StatusBadRequest), nil } user, err := p.store.GetUser(poolID, username) if err != nil { if err == ErrUserNotFound { - return cognitoError("UserNotFoundException", "user not found", http.StatusBadRequest), nil + return shared.JSONError("UserNotFoundException", "user not found", http.StatusBadRequest), nil } return nil, err } @@ -1674,12 +1665,12 @@ func (p *Provider) setUserPoolMfaConfig(req *http.Request) (*plugin.Response, er MfaConfiguration string `json:"MfaConfiguration"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } pool, err := p.store.GetUserPool(in.UserPoolId) if err != nil { if err == ErrPoolNotFound { - return cognitoError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil } return nil, err } @@ -1696,12 +1687,12 @@ func (p *Provider) getUserPoolMfaConfig(req *http.Request) (*plugin.Response, er UserPoolId string `json:"UserPoolId"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } pool, err := p.store.GetUserPool(in.UserPoolId) if err != nil { if err == ErrPoolNotFound { - return cognitoError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil + return shared.JSONError("ResourceNotFoundException", "user pool not found", http.StatusBadRequest), nil } return nil, err } @@ -1716,7 +1707,7 @@ func (p *Provider) tagResource(req *http.Request) (*plugin.Response, error) { Tags map[string]string `json:"Tags"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if err := p.store.TagResource(in.ResourceArn, in.Tags); err != nil { return nil, err @@ -1730,7 +1721,7 @@ func (p *Provider) untagResource(req *http.Request) (*plugin.Response, error) { TagKeys []string `json:"TagKeys"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } if err := p.store.UntagResource(in.ResourceArn, in.TagKeys); err != nil { return nil, err @@ -1743,7 +1734,7 @@ func (p *Provider) listTagsForResource(req *http.Request) (*plugin.Response, err ResourceArn string `json:"ResourceArn"` } if err := json.NewDecoder(req.Body).Decode(&in); err != nil { - return cognitoError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil + return shared.JSONError("InvalidParameterException", "failed to parse request", http.StatusBadRequest), nil } tags, err := p.store.ListTagsForResource(in.ResourceArn) if err != nil { diff --git a/internal/services/cognitoidentityprovider/provider_test.go b/internal/services/cognitoidentityprovider/provider_test.go index bc225812..f8d42186 100644 --- a/internal/services/cognitoidentityprovider/provider_test.go +++ b/internal/services/cognitoidentityprovider/provider_test.go @@ -37,6 +37,16 @@ func cognitoReq(t *testing.T, p *Provider, op string, body any) *plugin.Response return resp } +func TestUnimplementedOp(t *testing.T) { + p := newTestProvider(t) + // Unimplemented ops must return an AWS error, not a false 200 success. + resp := cognitoReq(t, p, "SomeUnknownOperation", map[string]any{}) + assert.Equal(t, http.StatusNotImplemented, resp.StatusCode) + var out map[string]any + require.NoError(t, json.Unmarshal(resp.Body, &out)) + assert.Equal(t, "NotImplemented", out["__type"]) +} + // --- TestUserPoolCRUD --- func TestUserPoolCRUD(t *testing.T) { diff --git a/internal/services/configservice/provider.go b/internal/services/configservice/provider.go index aa94de1a..683854a4 100644 --- a/internal/services/configservice/provider.go +++ b/internal/services/configservice/provider.go @@ -186,7 +186,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request // ---- ~60 remaining ops: compliance, aggregate queries, org rules, resource discovery ---- default: - return shared.JSONResponse(http.StatusOK, map[string]any{}) + return shared.JSONError("NotImplemented", "operation not implemented: "+action, http.StatusNotImplemented), nil } } diff --git a/internal/services/configservice/provider_test.go b/internal/services/configservice/provider_test.go index fe082b1e..8c1a7889 100644 --- a/internal/services/configservice/provider_test.go +++ b/internal/services/configservice/provider_test.go @@ -60,6 +60,19 @@ func assertError(t *testing.T, resp *plugin.Response) { } } +func TestUnimplementedOp(t *testing.T) { + p := newTestProvider(t) + // Unimplemented ops return an AWS error, not a false 200 success. + resp := callOp(t, p, "SomeUnknownOperation", `{}`) + if resp.StatusCode != 501 { + t.Fatalf("expected 501, got %d: %s", resp.StatusCode, string(resp.Body)) + } + body := parseBody(t, resp) + if body["__type"] != "NotImplemented" { + t.Errorf("expected __type=NotImplemented, got %v (body: %s)", body["__type"], string(resp.Body)) + } +} + // TestConfigRuleCRUD tests Put/Describe/Delete of ConfigRule. func TestConfigRuleCRUD(t *testing.T) { p := newTestProvider(t) diff --git a/internal/services/costexplorer/provider.go b/internal/services/costexplorer/provider.go index c85cefa1..582da22b 100644 --- a/internal/services/costexplorer/provider.go +++ b/internal/services/costexplorer/provider.go @@ -173,7 +173,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{"Errors": []any{}}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/dynamodbstreams/provider.go b/internal/services/dynamodbstreams/provider.go index 07b7d34e..22f7b0f5 100644 --- a/internal/services/dynamodbstreams/provider.go +++ b/internal/services/dynamodbstreams/provider.go @@ -120,7 +120,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "DescribeLimits": return p.describeLimits(params) default: - return json10Err("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return json10Err("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/ebs/provider.go b/internal/services/ebs/provider.go index ea67d359..6de39f7a 100644 --- a/internal/services/ebs/provider.go +++ b/internal/services/ebs/provider.go @@ -79,7 +79,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "RestoreSnapshotTier": return p.restoreSnapshotTier(req) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/ec2/provider.go b/internal/services/ec2/provider.go index f7940cea..002eda74 100644 --- a/internal/services/ec2/provider.go +++ b/internal/services/ec2/provider.go @@ -165,7 +165,7 @@ func (p *Provider) HandleRequest(ctx context.Context, op string, req *http.Reque case "DescribeVpcAttribute": return p.handleDescribeVpcAttribute(form) default: - return ec2XMLError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return ec2XMLError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/ecr/provider.go b/internal/services/ecr/provider.go index c4d6abe9..e53f774c 100644 --- a/internal/services/ecr/provider.go +++ b/internal/services/ecr/provider.go @@ -128,7 +128,7 @@ func (p *Provider) HandleRequest(ctx context.Context, op string, req *http.Reque case "PutImageScanningConfiguration": return p.handlePutImageScanningConfiguration(params) default: - return ecrError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return ecrError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/ecs/provider.go b/internal/services/ecs/provider.go index b82e0e3e..92e0a28f 100644 --- a/internal/services/ecs/provider.go +++ b/internal/services/ecs/provider.go @@ -190,7 +190,7 @@ func (p *Provider) HandleRequest(ctx context.Context, op string, req *http.Reque case "DiscoverPollEndpoint": return p.handleDiscoverPollEndpoint(params) default: - return ecsError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return ecsError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/eks/provider.go b/internal/services/eks/provider.go index e232df3a..6c86669d 100644 --- a/internal/services/eks/provider.go +++ b/internal/services/eks/provider.go @@ -242,7 +242,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{"clusterVersions": []any{}, "nextToken": ""}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/elasticbeanstalk/provider.go b/internal/services/elasticbeanstalk/provider.go index 65a02c2e..53160fa2 100644 --- a/internal/services/elasticbeanstalk/provider.go +++ b/internal/services/elasticbeanstalk/provider.go @@ -152,7 +152,7 @@ func (p *Provider) HandleRequest(ctx context.Context, op string, req *http.Reque return p.handleStubOK(action) default: - return ebError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return ebError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/elasticsearchservice/provider.go b/internal/services/elasticsearchservice/provider.go index 85dd3494..dc9532a8 100644 --- a/internal/services/elasticsearchservice/provider.go +++ b/internal/services/elasticsearchservice/provider.go @@ -175,7 +175,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{"DomainName": "", "PerformCheckOnly": false, "TargetVersion": ""}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/emr/provider.go b/internal/services/emr/provider.go index 5d7e1071..c0e6550e 100644 --- a/internal/services/emr/provider.go +++ b/internal/services/emr/provider.go @@ -204,7 +204,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/eventbridge/provider.go b/internal/services/eventbridge/provider.go index 04bdc205..2df2da9f 100644 --- a/internal/services/eventbridge/provider.go +++ b/internal/services/eventbridge/provider.go @@ -125,7 +125,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "TestEventPattern": return p.testEventPattern(params) default: - return ebError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return ebError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/firehose/provider.go b/internal/services/firehose/provider.go index 37e7525f..ebe982e2 100644 --- a/internal/services/firehose/provider.go +++ b/internal/services/firehose/provider.go @@ -156,7 +156,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListRecords": return p.listRecords(params) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/fis/provider.go b/internal/services/fis/provider.go index c7c3dad7..7b1f244f 100644 --- a/internal/services/fis/provider.go +++ b/internal/services/fis/provider.go @@ -146,7 +146,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{"valid": true, "errors": []any{}}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/glacier/provider.go b/internal/services/glacier/provider.go index 676aa921..98a068cd 100644 --- a/internal/services/glacier/provider.go +++ b/internal/services/glacier/provider.go @@ -163,7 +163,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request }) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/glue/provider.go b/internal/services/glue/provider.go index 1b20ca89..814815fd 100644 --- a/internal/services/glue/provider.go +++ b/internal/services/glue/provider.go @@ -213,7 +213,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request // ---- ~200 stub operations ---- default: - return shared.JSONResponse(http.StatusOK, map[string]any{}) + return shared.JSONError("NotImplemented", "operation not implemented: "+action, http.StatusNotImplemented), nil } } diff --git a/internal/services/glue/provider_test.go b/internal/services/glue/provider_test.go index 96e9e66a..4cd90ada 100644 --- a/internal/services/glue/provider_test.go +++ b/internal/services/glue/provider_test.go @@ -53,6 +53,26 @@ func assertOK(t *testing.T, resp *plugin.Response) { } } +func assertErr(t *testing.T, resp *plugin.Response) { + t.Helper() + if resp.StatusCode != 501 { + t.Fatalf("expected 501, got %d: %s", resp.StatusCode, string(resp.Body)) + } + var e struct { + Type string `json:"__type"` + Message string `json:"message"` + } + if err := json.Unmarshal(resp.Body, &e); err != nil { + t.Fatalf("expected JSON error body, got %q: %v", string(resp.Body), err) + } + if e.Type != "NotImplemented" { + t.Errorf("expected __type=NotImplemented, got %q (body: %s)", e.Type, string(resp.Body)) + } + if e.Message == "" { + t.Errorf("expected non-empty error message (body: %s)", string(resp.Body)) + } +} + func TestDatabaseCRUD(t *testing.T) { p := newTestProvider(t) @@ -602,11 +622,11 @@ func TestTags(t *testing.T) { t.Errorf("expected team=data, got %v", tags["team"]) } - // Stub ops should return 200 + // Unimplemented ops return an AWS error, not a false success. resp = callOp(t, p, "GetDataflowGraph", `{}`) - assertOK(t, resp) + assertErr(t, resp) resp = callOp(t, p, "ListWorkflows", `{}`) - assertOK(t, resp) + assertErr(t, resp) resp = callOp(t, p, "GetMLTransform", `{}`) - assertOK(t, resp) + assertErr(t, resp) } diff --git a/internal/services/iam/provider.go b/internal/services/iam/provider.go index 2b9eb059..d4008e3e 100644 --- a/internal/services/iam/provider.go +++ b/internal/services/iam/provider.go @@ -212,7 +212,7 @@ func (p *IAMProvider) HandleRequest(ctx context.Context, op string, req *http.Re case "ListRoleTags": return p.handleListRoleTags(ctx, form) default: - return iamXMLError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return iamXMLError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/iam/provider_test.go b/internal/services/iam/provider_test.go index 63a8d585..2021dba3 100644 --- a/internal/services/iam/provider_test.go +++ b/internal/services/iam/provider_test.go @@ -157,7 +157,7 @@ func TestIAMProvider_UnknownAction(t *testing.T) { req := httptest.NewRequest("POST", "/", strings.NewReader("Action=DeleteEverything")) resp, err := p.HandleRequest(context.Background(), "", req) require.NoError(t, err) - assert.Equal(t, 400, resp.StatusCode) + assert.Equal(t, 501, resp.StatusCode) } func TestIAMProvider_GetUser(t *testing.T) { diff --git a/internal/services/iam/sts_provider.go b/internal/services/iam/sts_provider.go index 002ba0ff..dcd94155 100644 --- a/internal/services/iam/sts_provider.go +++ b/internal/services/iam/sts_provider.go @@ -97,7 +97,7 @@ func (p *STSProvider) HandleRequest(ctx context.Context, op string, req *http.Re case "GetAccessKeyInfo": return p.handleGetAccessKeyInfo(ctx, form) default: - return stsXMLError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return stsXMLError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/iam/sts_provider_test.go b/internal/services/iam/sts_provider_test.go index 0c8323a1..81fc33d7 100644 --- a/internal/services/iam/sts_provider_test.go +++ b/internal/services/iam/sts_provider_test.go @@ -101,5 +101,5 @@ func TestSTSProvider_UnknownAction(t *testing.T) { req := httptest.NewRequest("POST", "/", strings.NewReader("Action=DeleteEverything")) resp, err := p.HandleRequest(context.Background(), "", req) require.NoError(t, err) - assert.Equal(t, 400, resp.StatusCode) + assert.Equal(t, 501, resp.StatusCode) } diff --git a/internal/services/identitystore/provider.go b/internal/services/identitystore/provider.go index a21f559c..20d671e2 100644 --- a/internal/services/identitystore/provider.go +++ b/internal/services/identitystore/provider.go @@ -150,7 +150,7 @@ func (p *IdentityStoreProvider) HandleRequest(_ context.Context, op string, req case "ResetUserPassword": return p.simpleOK() default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/iot/provider.go b/internal/services/iot/provider.go index 24e5df9b..073b49ea 100644 --- a/internal/services/iot/provider.go +++ b/internal/services/iot/provider.go @@ -267,9 +267,8 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListTagsForResource": return p.listTagsForResource(req) - // Default: return success/empty JSON for all unimplemented ops default: - return shared.JSONResponse(http.StatusOK, map[string]any{}) + return shared.JSONError("NotImplemented", "operation not implemented: "+op, http.StatusNotImplemented), nil } } diff --git a/internal/services/iot/provider_test.go b/internal/services/iot/provider_test.go index 534509cf..edaf871c 100644 --- a/internal/services/iot/provider_test.go +++ b/internal/services/iot/provider_test.go @@ -476,9 +476,9 @@ func TestTags(t *testing.T) { func TestDefaultHandler(t *testing.T) { p := newTestProvider(t) - // Unknown ops should return 200 with empty JSON + // Unimplemented ops must return an AWS error, not a false 200 success. resp := callREST(t, p, "GET", "/some/unknown/path", "SomeUnknownOperation", "") - assert.Equal(t, 200, resp.StatusCode) + assert.Equal(t, 501, resp.StatusCode) rb := parseBody(t, resp) - assert.NotNil(t, rb) + assert.Equal(t, "NotImplemented", rb["__type"]) } diff --git a/internal/services/iotdataplane/provider.go b/internal/services/iotdataplane/provider.go index 419c4021..dbc9be37 100644 --- a/internal/services/iotdataplane/provider.go +++ b/internal/services/iotdataplane/provider.go @@ -111,7 +111,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/iotwireless/provider.go b/internal/services/iotwireless/provider.go index 25932474..75442467 100644 --- a/internal/services/iotwireless/provider.go +++ b/internal/services/iotwireless/provider.go @@ -186,9 +186,8 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListTagsForResource": return p.listTagsForResource(req) - // Stub operations - return success/empty default: - return shared.JSONResponse(http.StatusOK, map[string]any{}) + return shared.JSONError("NotImplemented", "operation not implemented: "+op, http.StatusNotImplemented), nil } } diff --git a/internal/services/iotwireless/provider_test.go b/internal/services/iotwireless/provider_test.go index d5838622..87d3e100 100644 --- a/internal/services/iotwireless/provider_test.go +++ b/internal/services/iotwireless/provider_test.go @@ -369,10 +369,10 @@ func TestServiceProfileCRUD(t *testing.T) { func TestStubOperations(t *testing.T) { p := newTestProvider(t) - // Stub operations should return 200 with empty body + // Unimplemented ops must return an AWS error, not a false 200 success. resp := callREST(t, p, "GET", "/service-endpoint", "GetServiceEndpoint", "") - assert.Equal(t, 200, resp.StatusCode) + assert.Equal(t, 501, resp.StatusCode) resp2 := callREST(t, p, "GET", "/log-levels", "GetLogLevelsByResourceTypes", "") - assert.Equal(t, 200, resp2.StatusCode) + assert.Equal(t, 501, resp2.StatusCode) } diff --git a/internal/services/kafka/provider.go b/internal/services/kafka/provider.go index dbd34141..d45fc46a 100644 --- a/internal/services/kafka/provider.go +++ b/internal/services/kafka/provider.go @@ -257,7 +257,7 @@ func (p *Provider) handleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{"ReplicatorArn": "", "ReplicatorState": "RUNNING"}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/kinesis/provider.go b/internal/services/kinesis/provider.go index fe913ba3..587b77ff 100644 --- a/internal/services/kinesis/provider.go +++ b/internal/services/kinesis/provider.go @@ -171,7 +171,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return jsonOK(map[string]any{"streamARN": "", "streamName": ""}) default: - return jsonErr("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return jsonErr("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/kinesisanalyticsv2/provider.go b/internal/services/kinesisanalyticsv2/provider.go index 4f4d09ec..df2bfd06 100644 --- a/internal/services/kinesisanalyticsv2/provider.go +++ b/internal/services/kinesisanalyticsv2/provider.go @@ -121,7 +121,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request "DeleteApplicationVpcConfiguration": return p.appSubConfigOperation(action, params) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/kms/provider.go b/internal/services/kms/provider.go index c65f23f7..fa1861d6 100644 --- a/internal/services/kms/provider.go +++ b/internal/services/kms/provider.go @@ -127,7 +127,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListKeyPolicies": return p.listKeyPolicies(params) default: - return kmsError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return kmsError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/lakeformation/provider.go b/internal/services/lakeformation/provider.go index 5b8207d0..130b875f 100644 --- a/internal/services/lakeformation/provider.go +++ b/internal/services/lakeformation/provider.go @@ -213,7 +213,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{"Identity": ""}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/managedblockchain/provider.go b/internal/services/managedblockchain/provider.go index ced68d2a..0ea45451 100644 --- a/internal/services/managedblockchain/provider.go +++ b/internal/services/managedblockchain/provider.go @@ -148,7 +148,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return p.listTagsForResource(req) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/memorydb/provider.go b/internal/services/memorydb/provider.go index a6534841..2c07fcef 100644 --- a/internal/services/memorydb/provider.go +++ b/internal/services/memorydb/provider.go @@ -171,7 +171,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{"ReservedNode": map[string]any{}}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/mq/provider.go b/internal/services/mq/provider.go index 567df3bb..e3a4aea3 100644 --- a/internal/services/mq/provider.go +++ b/internal/services/mq/provider.go @@ -140,7 +140,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return p.describeBrokerInstanceOptions() default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/mwaa/provider.go b/internal/services/mwaa/provider.go index 93d3927f..e0a41e4b 100644 --- a/internal/services/mwaa/provider.go +++ b/internal/services/mwaa/provider.go @@ -149,7 +149,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "DescribeClusters": return shared.JSONResponse(http.StatusOK, map[string]any{"Clusters": []any{}}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/opensearch/provider.go b/internal/services/opensearch/provider.go index 3b668d05..1f0f76ca 100644 --- a/internal/services/opensearch/provider.go +++ b/internal/services/opensearch/provider.go @@ -253,7 +253,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{"DomainName": "", "PerformCheckOnly": false, "TargetVersion": ""}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/organizations/provider.go b/internal/services/organizations/provider.go index cec3e099..c609c04c 100644 --- a/internal/services/organizations/provider.go +++ b/internal/services/organizations/provider.go @@ -151,7 +151,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request "DescribeResourcePolicy", "PutResourcePolicy", "DeleteResourcePolicy": return p.stubSuccess(action) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/ram/provider.go b/internal/services/ram/provider.go index ae81a875..678cab56 100644 --- a/internal/services/ram/provider.go +++ b/internal/services/ram/provider.go @@ -130,7 +130,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "UntagResource": return p.untagResource(req, params) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/resourcegroups/provider.go b/internal/services/resourcegroups/provider.go index 99981cbd..a8a38b8d 100644 --- a/internal/services/resourcegroups/provider.go +++ b/internal/services/resourcegroups/provider.go @@ -108,7 +108,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListGroupingStatuses": return shared.JSONResponse(http.StatusOK, map[string]any{"groupingStatuses": []any{}}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/resourcegroupstaggingapi/provider.go b/internal/services/resourcegroupstaggingapi/provider.go index d75261f2..935ec422 100644 --- a/internal/services/resourcegroupstaggingapi/provider.go +++ b/internal/services/resourcegroupstaggingapi/provider.go @@ -136,7 +136,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListResourcesWithTagFilter": return p.getResources(params) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/route53/provider.go b/internal/services/route53/provider.go index 3d18dcbe..732d5534 100644 --- a/internal/services/route53/provider.go +++ b/internal/services/route53/provider.go @@ -163,7 +163,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "DisassociateVPCFromHostedZone": return p.disassociateVPC(req) default: - return r53Error("InvalidAction", fmt.Sprintf("operation not implemented: %s", op), http.StatusBadRequest), nil + return r53Error("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/route53resolver/provider.go b/internal/services/route53resolver/provider.go index 7bc52d8b..69d943f8 100644 --- a/internal/services/route53resolver/provider.go +++ b/internal/services/route53resolver/provider.go @@ -219,7 +219,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return shared.JSONResponse(http.StatusOK, map[string]any{}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/sagemaker/provider.go b/internal/services/sagemaker/provider.go index b41ab6a2..b4f833e8 100644 --- a/internal/services/sagemaker/provider.go +++ b/internal/services/sagemaker/provider.go @@ -223,7 +223,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request // ---- ~320 stub operations ---- default: - return shared.JSONResponse(http.StatusOK, map[string]any{}) + return shared.JSONError("NotImplemented", "operation not implemented: "+action, http.StatusNotImplemented), nil } } diff --git a/internal/services/sagemaker/provider_test.go b/internal/services/sagemaker/provider_test.go index fcd8128c..8f779e33 100644 --- a/internal/services/sagemaker/provider_test.go +++ b/internal/services/sagemaker/provider_test.go @@ -53,6 +53,26 @@ func assertOK(t *testing.T, resp *plugin.Response) { } } +func assertErr(t *testing.T, resp *plugin.Response) { + t.Helper() + if resp.StatusCode != 501 { + t.Fatalf("expected 501, got %d: %s", resp.StatusCode, string(resp.Body)) + } + var e struct { + Type string `json:"__type"` + Message string `json:"message"` + } + if err := json.Unmarshal(resp.Body, &e); err != nil { + t.Fatalf("expected JSON error body, got %q: %v", string(resp.Body), err) + } + if e.Type != "NotImplemented" { + t.Errorf("expected __type=NotImplemented, got %q (body: %s)", e.Type, string(resp.Body)) + } + if e.Message == "" { + t.Errorf("expected non-empty error message (body: %s)", string(resp.Body)) + } +} + func TestNotebookInstanceCRUD(t *testing.T) { p := newTestProvider(t) @@ -637,13 +657,14 @@ func TestTags(t *testing.T) { func TestDefaultStub(t *testing.T) { p := newTestProvider(t) - // Verify unknown operations return {} + // Unimplemented ops return an AWS error, not a false success. resp := callOp(t, p, "CreateAutoMLJob", `{}`) - assertOK(t, resp) + assertErr(t, resp) resp = callOp(t, p, "DescribeCluster", `{}`) - assertOK(t, resp) + assertErr(t, resp) + // Search IS implemented and legitimately returns empty results. resp = callOp(t, p, "Search", `{"Resource":"TrainingJob"}`) assertOK(t, resp) body := parseBody(t, resp) diff --git a/internal/services/scheduler/provider.go b/internal/services/scheduler/provider.go index 0c1770d3..183d567c 100644 --- a/internal/services/scheduler/provider.go +++ b/internal/services/scheduler/provider.go @@ -157,7 +157,7 @@ func (p *SchedulerProvider) HandleRequest(_ context.Context, op string, req *htt case "StopScheduleExecution": return jsonResponse(http.StatusOK, map[string]any{}) default: - return jsonError("ResourceNotFoundException", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return jsonError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/secretsmanager/provider.go b/internal/services/secretsmanager/provider.go index 6631fe3b..89cfac28 100644 --- a/internal/services/secretsmanager/provider.go +++ b/internal/services/secretsmanager/provider.go @@ -150,7 +150,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "GetSecretHistory": return p.listSecretVersionIds(params) default: - return smError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return smError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/servicediscovery/provider.go b/internal/services/servicediscovery/provider.go index a0cdc62a..6e6c5159 100644 --- a/internal/services/servicediscovery/provider.go +++ b/internal/services/servicediscovery/provider.go @@ -126,7 +126,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListTagsForResource": return p.listTagsForResource(params) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/ses/provider.go b/internal/services/ses/provider.go index 7657e0b5..5535f6be 100644 --- a/internal/services/ses/provider.go +++ b/internal/services/ses/provider.go @@ -196,7 +196,7 @@ func (p *Provider) HandleRequest(ctx context.Context, op string, req *http.Reque return p.handleSuccessEmpty(action) default: - return sesError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return sesError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/sesv2/provider.go b/internal/services/sesv2/provider.go index 390a2d78..4fc94387 100644 --- a/internal/services/sesv2/provider.go +++ b/internal/services/sesv2/provider.go @@ -305,7 +305,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "UpdateReputationEntityPolicy": return shared.JSONResponse(http.StatusOK, map[string]any{}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/internal/services/sfn/provider.go b/internal/services/sfn/provider.go index 02636010..30e8fc9a 100644 --- a/internal/services/sfn/provider.go +++ b/internal/services/sfn/provider.go @@ -150,7 +150,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "UpdateMapRun": return p.updateMapRun(params) default: - return json10Err("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return json10Err("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/shield/provider.go b/internal/services/shield/provider.go index 090b449c..92de37fe 100644 --- a/internal/services/shield/provider.go +++ b/internal/services/shield/provider.go @@ -139,7 +139,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListTagsForResource": return p.listTagsForResource(params) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/sns/provider.go b/internal/services/sns/provider.go index d796b65f..4df65cde 100644 --- a/internal/services/sns/provider.go +++ b/internal/services/sns/provider.go @@ -98,7 +98,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ConfirmSubscription": return p.confirmSubscription(req) default: - return snsError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return snsError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/sns/provider_test.go b/internal/services/sns/provider_test.go index ddd65382..65ef7739 100644 --- a/internal/services/sns/provider_test.go +++ b/internal/services/sns/provider_test.go @@ -92,8 +92,8 @@ func TestSNS_Publish_TopicNotFound(t *testing.T) { func TestSNS_UnknownAction(t *testing.T) { p := newTestProvider(t) resp := handle(t, p, "Action=FooBar") - assert.Equal(t, 400, resp.StatusCode) - assert.Contains(t, string(resp.Body), "InvalidAction") + assert.Equal(t, 501, resp.StatusCode) + assert.Contains(t, string(resp.Body), "NotImplemented") } func TestSNS_ListResources(t *testing.T) { diff --git a/internal/services/sqs/provider.go b/internal/services/sqs/provider.go index 842e2b98..bd7f6962 100644 --- a/internal/services/sqs/provider.go +++ b/internal/services/sqs/provider.go @@ -106,7 +106,7 @@ func (p *SQSProvider) handleQueryRequest(op string, req *http.Request) (*plugin. case "CancelMessageMoveTask": return p.cancelMessageMoveTask(req) default: - return sqsError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return sqsError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } @@ -190,7 +190,7 @@ func (p *SQSProvider) handleJSONRequest(op string, req *http.Request) (*plugin.R case "RemovePermission": return jsonResp(http.StatusOK, map[string]any{}) default: - return jsonError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return jsonError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/sqs/provider_test.go b/internal/services/sqs/provider_test.go index f07ff39a..fd86d99f 100644 --- a/internal/services/sqs/provider_test.go +++ b/internal/services/sqs/provider_test.go @@ -197,8 +197,8 @@ func TestSQSProvider_UnknownAction(t *testing.T) { p := newTestSQSProvider(t) resp := handleForm(t, p, "Action=UnknownAction") - assert.Equal(t, 400, resp.StatusCode) - assert.Contains(t, string(resp.Body), "InvalidAction") + assert.Equal(t, 501, resp.StatusCode) + assert.Contains(t, string(resp.Body), "NotImplemented") } func TestSQSProvider_ActionFromOp(t *testing.T) { diff --git a/internal/services/ssm/provider.go b/internal/services/ssm/provider.go index 8fb40e6d..797d833c 100644 --- a/internal/services/ssm/provider.go +++ b/internal/services/ssm/provider.go @@ -112,7 +112,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "TerminateSession": return p.terminateSession(params) default: - return ssmError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return ssmError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/ssoadmin/provider.go b/internal/services/ssoadmin/provider.go index 23f86b83..733949b6 100644 --- a/internal/services/ssoadmin/provider.go +++ b/internal/services/ssoadmin/provider.go @@ -223,7 +223,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListRegions": return shared.JSONResponse(http.StatusOK, map[string]any{"Regions": []any{}}) default: - return ssoError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return ssoError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/sts/provider.go b/internal/services/sts/provider.go deleted file mode 100644 index b2527dc5..00000000 --- a/internal/services/sts/provider.go +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package sts - -// NOTE: STS registration is handled by the iam package's STSProvider -// (internal/services/iam/sts_provider.go) which shares the IAM store. -// The generated Provider in this package is not registered. - -import ( - "context" - "net/http" - - generated "github.com/skyoo2003/devcloud/internal/generated/sts" - "github.com/skyoo2003/devcloud/internal/plugin" -) - -// Provider implements the SecurityTokenServiceV20110615 service. -type Provider struct { - generated.BaseProvider -} - -func (p *Provider) ServiceID() string { return "sts" } -func (p *Provider) ServiceName() string { return "SecurityTokenServiceV20110615" } -func (p *Provider) Protocol() plugin.ProtocolType { return plugin.ProtocolQuery } - -func (p *Provider) Init(cfg plugin.PluginConfig) error { - return nil -} - -func (p *Provider) Shutdown(ctx context.Context) error { - return nil -} - -func (p *Provider) HandleRequest(ctx context.Context, op string, req *http.Request) (*plugin.Response, error) { - return nil, generated.ErrNotImplemented -} - -func (p *Provider) ListResources(ctx context.Context) ([]plugin.Resource, error) { - return []plugin.Resource{}, nil -} - -func (p *Provider) GetMetrics(ctx context.Context) (*plugin.ServiceMetrics, error) { - return &plugin.ServiceMetrics{}, nil -} diff --git a/internal/services/support/provider.go b/internal/services/support/provider.go index 94302b5a..c6b7c826 100644 --- a/internal/services/support/provider.go +++ b/internal/services/support/provider.go @@ -153,7 +153,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "SearchCases": return p.searchCases(params) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/swf/provider.go b/internal/services/swf/provider.go index aa540634..1551b1c5 100644 --- a/internal/services/swf/provider.go +++ b/internal/services/swf/provider.go @@ -153,7 +153,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListTagsForResource": return p.listTagsForResource(params) default: - return json10Err("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return json10Err("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/textract/provider.go b/internal/services/textract/provider.go index 745ecca4..2997d0ce 100644 --- a/internal/services/textract/provider.go +++ b/internal/services/textract/provider.go @@ -169,7 +169,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "GetServiceStatus": return shared.JSONResponse(http.StatusOK, map[string]any{"Status": "AVAILABLE"}) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/timestreamwrite/provider.go b/internal/services/timestreamwrite/provider.go index 3dcb2e7d..a784d374 100644 --- a/internal/services/timestreamwrite/provider.go +++ b/internal/services/timestreamwrite/provider.go @@ -104,7 +104,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListTagsForResource": return p.listTagsForResource(params) default: - return json10Err("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return json10Err("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/transcribe/provider.go b/internal/services/transcribe/provider.go index 9d4f0e46..d2255a62 100644 --- a/internal/services/transcribe/provider.go +++ b/internal/services/transcribe/provider.go @@ -162,7 +162,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "ListTagsForResource": return p.listTagsForResource(params) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/transfer/provider.go b/internal/services/transfer/provider.go index d6380c74..a738fb39 100644 --- a/internal/services/transfer/provider.go +++ b/internal/services/transfer/provider.go @@ -150,7 +150,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "TestIdentityProvider": return p.testIdentityProvider(params) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/waf/provider.go b/internal/services/waf/provider.go index a35660ab..5f3d7323 100644 --- a/internal/services/waf/provider.go +++ b/internal/services/waf/provider.go @@ -257,7 +257,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return jsonResp(http.StatusOK, map[string]any{"SampledRequests": []any{}, "PopulationSize": 0, "TimeWindow": map[string]any{}}) default: - return wafError("WAFInvalidOperationException", fmt.Sprintf("operation not implemented: %s", action), http.StatusBadRequest), nil + return wafError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/wafv2/provider.go b/internal/services/wafv2/provider.go index f3830147..1b184094 100644 --- a/internal/services/wafv2/provider.go +++ b/internal/services/wafv2/provider.go @@ -197,7 +197,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request case "DeleteFirewallManagerRuleGroups": return shared.JSONResponse(http.StatusOK, map[string]any{"NextWebACLLockToken": shared.GenerateID("", 32)}) default: - return wafError("InvalidAction", fmt.Sprintf("unknown action: %s", action), http.StatusBadRequest), nil + return wafError("NotImplemented", fmt.Sprintf("operation not implemented: %s", action), http.StatusNotImplemented), nil } } diff --git a/internal/services/xray/provider.go b/internal/services/xray/provider.go index 72932d75..4e7bc798 100644 --- a/internal/services/xray/provider.go +++ b/internal/services/xray/provider.go @@ -193,7 +193,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request }) default: - return shared.JSONError("InvalidAction", fmt.Sprintf("unknown action: %s", op), http.StatusBadRequest), nil + return shared.JSONError("NotImplemented", fmt.Sprintf("operation not implemented: %s", op), http.StatusNotImplemented), nil } } diff --git a/tests/compatibility/conftest.py b/tests/compatibility/conftest.py index 9b9422e9..434a3687 100644 --- a/tests/compatibility/conftest.py +++ b/tests/compatibility/conftest.py @@ -643,6 +643,21 @@ def mediaconvert_client(devcloud_server): return _make_client("mediaconvert") +@pytest.fixture +def codeconnections_client(devcloud_server): + return _make_client("codeconnections") + + +@pytest.fixture +def dms_client(devcloud_server): + return _make_client("dms") + + +@pytest.fixture +def verifiedpermissions_client(devcloud_server): + return _make_client("verifiedpermissions") + + @pytest.fixture def s3tables_client(devcloud_server): return _make_client("s3tables") diff --git a/tests/compatibility/test_codeconnections.py b/tests/compatibility/test_codeconnections.py new file mode 100644 index 00000000..2bdbaf70 --- /dev/null +++ b/tests/compatibility/test_codeconnections.py @@ -0,0 +1,34 @@ +import pytest +from botocore.exceptions import ClientError + + +def test_create_and_get_connection(codeconnections_client): + created = codeconnections_client.create_connection( + ConnectionName="compat-conn", ProviderType="GitHub" + ) + arn = created["ConnectionArn"] + assert arn + assert created["ResponseMetadata"]["HTTPStatusCode"] == 200 + + got = codeconnections_client.get_connection(ConnectionArn=arn) + assert got["Connection"]["ConnectionName"] == "compat-conn" + assert got["Connection"]["ProviderType"] == "GitHub" + + +def test_list_connections(codeconnections_client): + codeconnections_client.create_connection( + ConnectionName="list-conn", ProviderType="GitHub" + ) + resp = codeconnections_client.list_connections() + names = [c["ConnectionName"] for c in resp["Connections"]] + assert "list-conn" in names + + +def test_delete_connection(codeconnections_client): + created = codeconnections_client.create_connection( + ConnectionName="del-conn", ProviderType="GitHub" + ) + arn = created["ConnectionArn"] + codeconnections_client.delete_connection(ConnectionArn=arn) + with pytest.raises(ClientError): + codeconnections_client.get_connection(ConnectionArn=arn) diff --git a/tests/compatibility/test_dms.py b/tests/compatibility/test_dms.py new file mode 100644 index 00000000..379c5477 --- /dev/null +++ b/tests/compatibility/test_dms.py @@ -0,0 +1,32 @@ +def test_create_replication_instance(dms_client): + resp = dms_client.create_replication_instance( + ReplicationInstanceIdentifier="compat-ri", + ReplicationInstanceClass="dms.t2.micro", + ) + inst = resp["ReplicationInstance"] + assert inst["ReplicationInstanceIdentifier"] == "compat-ri" + assert inst["ReplicationInstanceClass"] == "dms.t2.micro" + + +def test_describe_replication_instances(dms_client): + dms_client.create_replication_instance( + ReplicationInstanceIdentifier="compat-ri2", + ReplicationInstanceClass="dms.t2.micro", + ) + resp = dms_client.describe_replication_instances() + ids = [i["ReplicationInstanceIdentifier"] for i in resp["ReplicationInstances"]] + assert "compat-ri2" in ids + + +def test_create_endpoint(dms_client): + resp = dms_client.create_endpoint( + EndpointIdentifier="compat-ep", + EndpointType="source", + EngineName="mysql", + ) + assert resp["Endpoint"]["EndpointIdentifier"] == "compat-ep" + + +def test_describe_account_attributes(dms_client): + resp = dms_client.describe_account_attributes() + assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200 diff --git a/tests/compatibility/test_unimplemented_ops.py b/tests/compatibility/test_unimplemented_ops.py new file mode 100644 index 00000000..c1800c2a --- /dev/null +++ b/tests/compatibility/test_unimplemented_ops.py @@ -0,0 +1,37 @@ +"""Regression tests: unimplemented operations must return an AWS error, not a +silent 200 empty-body success. Guards the fix in the services that previously +returned a false 200 for unknown ops (apigatewayv2, backup, codebuild, +codedeploy, codepipeline, cognito-idp, config, glue, iot, iotwireless, +sagemaker).""" + +import pytest +from botocore.exceptions import ClientError + +# (client_fixture_name, boto3 method, kwargs) — each op is unimplemented in its +# provider and must hit the default branch that returns an AWS error. kwargs only +# carry required params so botocore actually sends the request (no client-side +# ParamValidationError) rather than being empty for its own sake. +UNIMPLEMENTED_OPS = [ + ("apigatewayv2_client", "list_portals", {}), + ("backup_client", "list_legal_holds", {}), + ("codebuild_client", "list_build_batches", {}), + ("codedeploy_client", "list_on_premises_instances", {}), + ("codepipeline_client", "list_rule_types", {}), + ("cognitoidp_client", "get_csv_header", {"UserPoolId": "x"}), + ("configservice_client", "get_discovered_resource_counts", {}), + ("glue_client", "list_workflows", {}), + ("iot_client", "list_audit_findings", {}), + ("iotwireless_client", "get_service_endpoint", {}), + ("sagemaker_client", "list_auto_ml_jobs", {}), +] + + +@pytest.mark.parametrize( + "fixture,method,kwargs", + UNIMPLEMENTED_OPS, + ids=[f"{f.removesuffix('_client')}.{m}" for f, m, _ in UNIMPLEMENTED_OPS], +) +def test_unimplemented_op_errors(request, fixture, method, kwargs): + client = request.getfixturevalue(fixture) + with pytest.raises(ClientError): + getattr(client, method)(**kwargs) diff --git a/tests/compatibility/test_verifiedpermissions.py b/tests/compatibility/test_verifiedpermissions.py new file mode 100644 index 00000000..fbf657e2 --- /dev/null +++ b/tests/compatibility/test_verifiedpermissions.py @@ -0,0 +1,13 @@ +def test_list_policies(verifiedpermissions_client): + resp = verifiedpermissions_client.list_policies(policyStoreId="store-123") + assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200 + + +def test_is_authorized(verifiedpermissions_client): + resp = verifiedpermissions_client.is_authorized( + policyStoreId="store-123", + principal={"entityType": "User", "entityId": "alice"}, + action={"actionType": "Action", "actionId": "read"}, + resource={"entityType": "Doc", "entityId": "doc1"}, + ) + assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200