Skip to content

Flpath 4720 4721 rfc9457 e2e contract tests - #47

Merged
vkolodny merged 4 commits into
dcm-project:mainfrom
vkolodny:flpath-4720-4721-rfc9457-e2e-contract-tests
Sep 3, 2026
Merged

Flpath 4720 4721 rfc9457 e2e contract tests#47
vkolodny merged 4 commits into
dcm-project:mainfrom
vkolodny:flpath-4720-4721-rfc9457-e2e-contract-tests

Conversation

@vkolodny

@vkolodny vkolodny commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

No description provided.

vkolodny and others added 2 commits September 1, 2026 12:21
Verify deployed service providers emit problem+json with project-controlled
type URIs, matching status fields, and container multi-error validation.

Signed-off-by: Vladislav Kolodny <vkolodny@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…0/4721

Update container and ACM cluster SP test plans with RFC 9457 references,
upstream PR links, and new TC-19/TC-20 and TC-14/TC-15 E2E cases.

Signed-off-by: Vladislav Kolodny <vkolodny@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@qodo-code-review

qodo-code-review Bot commented Sep 1, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Add RFC 9457 E2E contracts for container and ACM providers

🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Adds deployed RFC 9457 contract coverage for container and ACM cluster providers.
• Verifies project type URIs, numeric statuses, media types, and multi-error details.
• Documents new scenarios, prerequisites, and upstream implementation references.
Diagram

sequenceDiagram
    actor E2E as E2E Tests
    participant Container as Container SP
    participant ACM as ACM SP
    participant K8s as Kubernetes API
    participant Helper as RFC Helper
    E2E->>Container: POST invalid request
    Container-->>E2E: 400 problem+json
    E2E->>Helper: Verify RFC contract
    E2E->>K8s: Check HyperShift CRD
    K8s-->>E2E: CRD availability
    E2E->>ACM: POST invalid or GET missing
    ACM-->>E2E: 400 or 404 problem+json
    E2E->>Helper: Verify RFC contract
Loading
High-Level Assessment

The shared assertion helper plus provider-specific scenarios is the appropriate approach: it centralizes the common RFC 9457 wire contract while retaining domain-specific checks for container multi-errors and ACM prerequisites. A typed problem-details structure or JSON-schema validator was considered, but the map-based decoding better accommodates extension members such as errors without adding unnecessary test infrastructure.

Files changed (5) +162 / -11

Tests (3) +99 / -5
sp_acm_cluster_api_test.goAdd ACM RFC 9457 validation and not-found contracts +19/-5

Add ACM RFC 9457 validation and not-found contracts

• Replaces the limited RFC 7807 field check with shared RFC 9457 assertions for validation failures. Adds a HyperShift-gated not-found contract test for the project-controlled URI, title, status, detail, and media type.

tests/e2e/sp_acm_cluster_api_test.go

sp_container_api_test.goAdd container RFC 9457 and multi-error contracts +54/-0

Add container RFC 9457 and multi-error contracts

• Adds contract tests for invalid container requests and multi-field validation failures. The multi-error scenario requires at least two detailed entries covering both the invalid CPU range and reserved managed-by label.

tests/e2e/sp_container_api_test.go

sp_helpers_test.goCentralize RFC 9457 response assertions +26/-0

Centralize RFC 9457 response assertions

• Introduces a shared helper that parses the Content-Type media type and verifies the HTTP status, project-controlled type URI, title, detail, and numeric JSON status. It returns the decoded body for provider-specific extension checks.

tests/e2e/sp_helpers_test.go

Documentation (2) +63 / -6
FLPATH-3014-container-sp-api.mdDocument container RFC 9457 contract scenarios +29/-2

Document container RFC 9457 contract scenarios

• Adds FLPATH-4720 and upstream implementation references, updates RFC terminology, and defines TC-19 and TC-20. The plan records deployed validation and multi-error contract expectations and their added E2E value.

test-plans/FLPATH-3014-container-sp-api.md

FLPATH-3280-acm-cluster-sp-api.mdDocument ACM cluster RFC 9457 contract scenarios +34/-4

Document ACM cluster RFC 9457 contract scenarios

• Adds FLPATH-4721 references and defines TC-14 and TC-15 for validation and not-found responses. It also documents the HyperShift CRD prerequisite for reliable 404 behavior.

test-plans/FLPATH-3280-acm-cluster-sp-api.md

@qodo-code-review

qodo-code-review Bot commented Sep 1, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Malformed media type passes ✓ Resolved 🐞 Bug ≡ Correctness
Description
The shared contract helper uses substring matching for Content-Type, so invalid values such as
application/problem+json-seq or x-application/problem+json satisfy the assertion. Both test
plans require the response media type to be application/problem+json, allowing only valid
media-type parameters in addition to that exact type.
Code

tests/e2e/sp_helpers_test.go[R91-93]

+	ct := strings.ToLower(resp.Header.Get("Content-Type"))
+	Expect(ct).To(ContainSubstring("application/problem+json"),
+		"expected application/problem+json, got %q", resp.Header.Get("Content-Type"))
Evidence
The helper's substring matcher accepts supersets of the required media type, while both newly
documented contract cases specify the exact application/problem+json media type.

tests/e2e/sp_helpers_test.go[91-93]
test-plans/FLPATH-3014-container-sp-api.md[220-225]
test-plans/FLPATH-3280-acm-cluster-sp-api.md[213-218]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The RFC 9457 helper accepts any Content-Type containing `application/problem+json`, including invalid or different media types.

## Issue Context
These E2E tests are intended to verify the deployed services' exact RFC 9457 wire contract. Parse the header with `mime.ParseMediaType` and compare the resulting media type exactly to `application/problem+json`; valid parameters such as `charset` may remain allowed.

## Fix Focus Areas
- tests/e2e/sp_helpers_test.go[91-93]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Multi-error assertion contradicts contract ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new test requires exactly two errors even though TC-20 specifies at least two, so a valid
response containing both intended violations plus another validation error will fail. It also checks
only generic non-empty details, allowing two unrelated errors to pass without proving that CPU-range
and reserved-label validation both ran.
Code

tests/e2e/sp_container_api_test.go[R208-210]

+			errors, ok := problem["errors"].([]interface{})
+			Expect(ok).To(BeTrue(), "expected errors array in multi-field validation response, got %#v", problem["errors"])
+			Expect(errors).To(HaveLen(2))
Evidence
The request introduces two named invalid conditions, but the assertion enforces exact cardinality
and never checks their identities; the accompanying test plan explicitly says the array should
contain at least two entries.

tests/e2e/sp_container_api_test.go[188-210]
tests/e2e/sp_container_api_test.go[212-218]
test-plans/FLPATH-3014-container-sp-api.md[227-233]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The multi-field validation test requires exactly two generic errors, conflicting with the documented `>= 2` contract and failing to identify the intended violations.

## Issue Context
The request intentionally contains an inverted CPU range and the reserved `dcm.project/managed-by` label. Permit additional validation errors while asserting that entries corresponding to both intended violations are present and have non-empty details.

## Fix Focus Areas
- tests/e2e/sp_container_api_test.go[188-218]
- test-plans/FLPATH-3014-container-sp-api.md[227-233]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Fractional status value passes ✓ Resolved 🐞 Bug ≡ Correctness
Description
Converting the decoded status to int before comparison lets a malformed value such as 400.5 pass
as status 400. The contract helper therefore does not verify that the problem body's status value
exactly matches the expected HTTP status.
Code

tests/e2e/sp_helpers_test.go[R102-104]

+	status, ok := problem["status"].(float64)
+	Expect(ok).To(BeTrue(), "status should be a number, got %#v", problem["status"])
+	Expect(int(status)).To(Equal(wantStatus))
Evidence
The explicit int(status) conversion truncates fractional values, although the newly documented
contracts require status to equal 400 or 404 exactly.

tests/e2e/sp_helpers_test.go[102-104]
test-plans/FLPATH-3014-container-sp-api.md[222-225]
test-plans/FLPATH-3280-acm-cluster-sp-api.md[215-218]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The RFC 9457 helper truncates a JSON numeric status before comparison, allowing fractional values to pass.

## Issue Context
JSON numbers decode into `float64` in the current map representation. Compare the decoded number directly with `float64(wantStatus)` rather than converting it to `int`.

## Fix Focus Areas
- tests/e2e/sp_helpers_test.go[102-104]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 2 rules
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread tests/e2e/sp_helpers_test.go Outdated
Comment thread tests/e2e/sp_helpers_test.go Outdated
Comment thread tests/e2e/sp_container_api_test.go Outdated
Parse Content-Type with mime.ParseMediaType, compare status as float64,
require >=2 multi-errors with CPU and reserved-label coverage in details.

Signed-off-by: Vladislav Kolodny <vkolodny@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@vkolodny
vkolodny marked this pull request as ready for review September 1, 2026 16:49
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit b9f790f

@chadcrum chadcrum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@chadcrum

chadcrum commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

@vkolodny I just noticed the PR description was empty.

@testetson22
testetson22 self-requested a review September 2, 2026 19:09
})

It("returns errors array on multi-field validation failure", func() {
body := `{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why unstructured data? can't we use the go structure here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — addressed in 8ae2c94.

Added local typed structs in tests/e2e/problem_detail_test.go:

  • ProblemDetail — RFC 9457 core fields shared by all SPs
  • ContainerProblemDetail — adds errors[] for k8s-container SP only

They mirror the OpenAPI Error schema at the DCM boundary but are owned in dcm-utilities (no SP repo import). Contract tests now decode into these structs via expectRFC9457Problem() / readContainerProblemDetail() instead of map[string]interface{}.

Request bodies for the multi-error case stay as JSON strings for now since we're testing the deployed HTTP wire format; happy to type those too if you prefer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use a json in a string? this is very brittle and will break if there are changes to the json structure and you will have to manually refactor this. Why not use a go structure and marshal it? It will ensure it survives refactoring and reduce cost maintenance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid payloads already use struct→marshal (containerSpec()). The multi-error body intentionally sends invalid values (cpu.min > max, reserved label) that a typed struct would reject at compile time, so inline JSON is simpler for these negative-path contract tests.

}
}
}`
resp, err := doContainerSPRequest(http.MethodPost, "/containers", body)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious, we don't have a go client bindings available that we have to use pure http client?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. Today there is no published Go HTTP client for the container or ACM cluster SPs that dcm-utilities can import.

In the SP repos, oapi-codegen generates server stubs (ServerInterface, handlers) and shared types (api/v1alpha1/types.gen.go), but not a client package. By contrast, control-plane subsystem tests do use generated clients — but that is for the control-plane APIs, not the standalone SP HTTP endpoints we exercise here.

These E2E tests are intentionally black-box against the deployed SP ports (DCM_CONTAINER_SP_URL / DCM_ACM_CLUSTER_SP_URL, default :8082/:8083). The whole sp_* suite uses the same raw http.Client pattern (doContainerSPRequest, doAcmClusterSPRequest) so we validate the wire contract of whatever image compose publishes, without taking a compile-time dependency on a specific SP module version.

For the response side, we added local typed structs in problem_detail_test.go (ProblemDetail, ContainerProblemDetail) that mirror the OpenAPI Error schema at the DCM boundary — typed decoding without importing the SP repo.

A shared generated client could make sense later (e.g. SP SDK / UDLM alignment), but it is out of scope for this RFC 9457 contract PR. Happy to track a follow-up if we want client codegen for E2E.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not quite right — both repos do generate a client. k8s-container-service-provider and acm-cluster-service-provider each have pkg/client/client.gen.go (oapi-codegen, generate: client: true), wired into generate-api and enforced by check-generate-api in CI — same pattern as control-plane's policy/catalog/sp clients and osac-service-provider's own pkg/client.

Given that, hand-rolling doContainerSPRequest/doAcmClusterSPRequest against raw http.Client is reinventing what's already generated and CI-verified. Can we import the generated clients here instead? Rough sketch using acm-cluster-service-provider:

import (
    acmclient "github.com/dcm-project/acm-cluster-service-provider/pkg/client"
)

client, err := acmclient.NewClientWithResponses(acmClusterSPBaseURL)
Expect(err).NotTo(HaveOccurred())

resp, err := client.CreateClusterWithResponse(ctx, &acmclient.CreateClusterParams{}, acmclient.CreateClusterJSONRequestBody{ /* ... */ })
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode()).To(Equal(http.StatusCreated))

Same shape for k8s-container-service-provider's client. Would need adding both as go.mod deps in tests/e2e/.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, apologies — both SPs do have generated clients. For these contract tests raw HTTP is intentional (validating wire format independent of client abstractions), but migrating the broader E2E suite to generated clients makes sense as a follow-up PR.

Comment thread tests/e2e/sp_container_api_test.go Outdated
problem := expectRFC9457Problem(resp, http.StatusBadRequest, "invalid-argument", "Invalid argument")
errors, ok := problem["errors"].([]interface{})
Expect(ok).To(BeTrue(), "expected errors array in multi-field validation response, got %#v", problem["errors"])
Expect(len(errors)).To(BeNumerically(">=", 2))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you be more deterministic and account for the actual number of failures you expect (not < or > or similar, but equal to). This will make the test capture regressions in the future.
You can also validate which errors are happening, that's a stretch goal worth considering.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 8ae2c94.

  • Expect(problem.Errors).To(HaveLen(2)) — exact count, not >=
  • Validates which errors: index 0 is CPU range (#/spec/resources/cpu/min), index 1 is reserved label (#/spec/metadata/labels/dcm.project~1managed-by), matching SP unit test TC-U092 ordering
  • Detail strings use ContainSubstring for now; happy to tighten to full exact messages if you prefer

Comment thread tests/e2e/sp_container_api_test.go Outdated

var detailsBlob strings.Builder
for i, e := range errors {
entry, ok := e.(map[string]interface{})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't have an existing go structure for errors? Using unstructured data like you're doing here is brittle and increases maintenance burden because refactoring is not possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same fix as #1 — addressed in 8ae2c94.

The SP repos do generate Error / ErrorDetail in api/v1alpha1/types.gen.go, but we intentionally mirror them locally in tests/e2e/problem_detail_test.go rather than importing the SP module into dcm-utilities (keeps E2E decoupled from SP release/version).

Contract tests no longer decode errors into map[string]interface{} — they use ProblemDetail / ContainerProblemDetail with field-level assertions, so renames to JSON tags would fail at compile time in the test package.

Comment thread tests/e2e/sp_helpers_test.go Outdated
decodeJSON(resp, &problem)

Expect(problem).To(HaveKeyWithValue("type", problemTypeBaseURI+wantTypeSuffix))
Expect(problem).To(HaveKeyWithValue("title", wantTitle))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this works, but I'd be more throughout and actually expect the full message, rather than a contains. Is that possible?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — partially addressed in 8ae2c94.

Already exact match today:

  • titleExpect(problem.Title).To(Equal(want.Title))
  • type, status — exact
  • Multi-error top-level detail — exact containerMultiErrorDetail constant (matches SP httperror.InvalidArgumentMultiDetail)

problemDetailExpectation.Detail: when set, the helper asserts full exact detail; when empty (e.g. simple POST {} validation), we only require non-empty because the message can come from OpenAPI middleware vs handler and may vary slightly.

Per-entry errors[].detail: still ContainSubstring in the multi-error test — can tighten to the full strings from validation.go if you want maximum strictness:

  • cpu.min (10) must not exceed cpu.max (5)
  • label "dcm.project/managed-by" is reserved by DCM and cannot be set by the user

Happy to do that in a follow-up commit.

Comment thread tests/e2e/sp_helpers_test.go Outdated

Expect(problem).To(HaveKeyWithValue("type", problemTypeBaseURI+wantTypeSuffix))
Expect(problem).To(HaveKeyWithValue("title", wantTitle))
Expect(problem).To(HaveKey("detail"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you check the value of the detail key?.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially — when detail is stable we assert the exact value.

Exact today:

  • Multi-error test: top-level detail is Equal(containerMultiErrorDetail) via whole-struct comparison on ProblemDetail

Non-empty only today:

  • Simple POST {} validation (container + ACM) and ACM 404 — expectRFC9457Problem requires detail is present and non-empty when want.Detail is left empty

Why not exact for those cases:

  • Container SP has no single-error detail constant (only InvalidArgumentMultiDetail for multi-error)
  • SP integration tests (TC-U014 for POST {}) assert type/title/status but not exact detail — messages come from OpenAPI middleware and can vary with schema/codegen
  • ACM 404 similarly has no exported canonical detail string

The helper already supports tightening: set want.Detail when we have a shared constant or after a lab E2E capture pins the strings. Happy to do that in a follow-up if you want maximum strictness.

Comment thread tests/e2e/sp_helpers_test.go Outdated
}

// expectRFC9457Problem asserts an RFC 9457 problem+json response (FLPATH-4720/4721).
func expectRFC9457Problem(resp *http.Response, wantStatus int, wantTypeSuffix, wantTitle string) map[string]interface{} {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you considered passing a structure that contains the error and doing the comparison directly against the structure rather than each field at a time. The problem with the current approach is that if the structure is extended with new fields, you will not catch the drift using this approach.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-error test already uses whole-struct Equal on ProblemDetail (commit 8ae2c94). Note that Go's json.Unmarshal silently drops unknown fields, so struct comparison won't catch new SP fields either — drift protection comes from keeping the local types aligned with the OpenAPI schema. Could add MatchJSON on raw bytes for full-body comparison if needed.

Add DCM-boundary ProblemDetail types (RFC 9457 core plus container
errors[] extension), refactor helpers to decode into structs, and assert
exact multi-error pointers matching SP unit test TC-U092.

Signed-off-by: Vladislav Kolodny <vkolodny@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
})

It("returns errors array on multi-field validation failure", func() {
body := `{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use a json in a string? this is very brittle and will break if there are changes to the json structure and you will have to manually refactor this. Why not use a go structure and marshal it? It will ensure it survives refactoring and reduce cost maintenance.

requireKubectl()
_, err := runKubectl("get", "crd", "hostedclusters.hypershift.openshift.io")
if err != nil {
Skip("HyperShift CRDs required — without them GET returns 500 instead of 404")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not install the CRDs as part of the test fixtures and avoid this step?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HyperShift CRDs are installed by the HyperShift operator, not standalone manifests we can apply as fixtures. The ACM cluster SP requires a real HyperShift environment to return 404 vs 500. The Skip keeps the test safe to run on any cluster.

if want.Detail != "" {
Expect(problem.Detail).To(Equal(want.Detail))
} else {
Expect(problem.Detail).NotTo(BeEmpty())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not validate the contents of the field here if it's not empty?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same rationale as my reply on #6 above — no stable SP constant for single-error detail. The helper supports exact match via want.Detail when we have one; will tighten after a lab capture or if the SPs export the strings.

@vkolodny
vkolodny merged commit 7c2179b into dcm-project:main Sep 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants