diff --git a/e2e/adapter/conformance_negative.go b/e2e/adapter/conformance_negative.go new file mode 100644 index 0000000..9c54ab6 --- /dev/null +++ b/e2e/adapter/conformance_negative.go @@ -0,0 +1,57 @@ +package adapter + +import ( + "context" + + "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" //nolint:staticcheck // dot import for test readability + + "github.com/openshift-hyperfleet/hyperfleet-e2e/pkg/api/openapi" + "github.com/openshift-hyperfleet/hyperfleet-e2e/pkg/client" + "github.com/openshift-hyperfleet/hyperfleet-e2e/pkg/helper" + "github.com/openshift-hyperfleet/hyperfleet-e2e/pkg/labels" +) + +const conditionTypeReady = "Ready" + +var _ = ginkgo.Describe("[Suite: adapter][negative] Legacy adapter behavior rejected by v1 API contract", + ginkgo.Label(labels.Tier0, labels.Negative), + func() { + ginkgo.It("should not have a resource-level Ready condition on a reconciled cluster", + func(ctx context.Context) { + h := helper.New() + + ginkgo.By("creating a cluster") + cluster, err := h.Client.CreateClusterFromPayload(ctx, h.TestDataPath("payloads/clusters/cluster-request.json")) + Expect(err).NotTo(HaveOccurred()) + Expect(cluster.Id).NotTo(BeNil()) + clusterID := *cluster.Id + + ginkgo.DeferCleanup(func(ctx context.Context) { + if err := h.CleanupTestCluster(ctx, clusterID); err != nil { + ginkgo.GinkgoWriter.Printf("Warning: failed to cleanup cluster %s: %v\n", clusterID, err) + } + }) + + ginkgo.By("waiting for Reconciled=True") + Eventually(h.PollCluster(ctx, clusterID), h.Cfg.Timeouts.Cluster.Reconciled, h.Cfg.Polling.Interval). + Should(helper.HaveResourceCondition(client.ConditionTypeReconciled, openapi.ResourceConditionStatusTrue)) + + ginkgo.By("verifying resource-level Ready condition does not exist") + reconciledCluster, err := h.Client.GetCluster(ctx, clusterID) + Expect(err).NotTo(HaveOccurred()) + Expect(reconciledCluster.Status).NotTo(BeNil()) + + for _, c := range reconciledCluster.Status.Conditions { + Expect(c.Type).NotTo(Equal(conditionTypeReady), + "v1.0.0 removed the Ready condition; no Ready condition should exist regardless of status") + } + + ginkgo.By("confirming Reconciled and LastKnownReconciled are the correct v1.0.0 conditions") + Expect(h.HasResourceCondition(reconciledCluster.Status.Conditions, + client.ConditionTypeReconciled, openapi.ResourceConditionStatusTrue)).To(BeTrue()) + Expect(h.HasResourceCondition(reconciledCluster.Status.Conditions, + client.ConditionTypeLastKnownReconciled, openapi.ResourceConditionStatusTrue)).To(BeTrue()) + }) + }, +) diff --git a/test-design/testcases/adapter-conformance-negative.md b/test-design/testcases/adapter-conformance-negative.md new file mode 100644 index 0000000..ed394d9 --- /dev/null +++ b/test-design/testcases/adapter-conformance-negative.md @@ -0,0 +1,156 @@ +# Feature: Adapter Contract Conformance (Negative) + +## Table of Contents + +1. [POST to statuses endpoint returns 405](#test-title-post-to-statuses-endpoint-returns-405) +2. [PUT with missing mandatory conditions returns 400](#test-title-put-with-missing-mandatory-conditions-returns-400) +3. [Ready condition absent on reconciled cluster](#test-title-ready-condition-absent-on-reconciled-cluster) + +--- + +## Test Title: POST to statuses endpoint returns 405 + +### Description + +Validates that the v1.0.0 API rejects the old v0.2.0 POST method for adapter status reporting. Adapters that have not migrated to PUT will receive a 405 Method Not Allowed response instead of silently succeeding. + +--- + +| **Field** | **Value** | +|-----------|-----------| +| **Pos/Neg** | Negative | +| **Priority** | Tier0 | +| **Status** | Draft | +| **Automation** | Automated | +| **Version** | v1.0.0 | +| **Created** | 2026-06-23 | +| **Updated** | 2026-06-23 | + +--- + +### Preconditions + +1. HyperFleet API is deployed with v1.0.0 adapter contract (PUT-only statuses endpoint) +2. HyperFleet Sentinel is deployed and running + +--- + +### Test Steps + +#### Step 1: Create a cluster + +**Action:** +- Create a cluster via the API using a standard cluster payload + +**Expected Result:** +- API returns the created cluster with an ID and generation + +#### Step 2: Send a POST status report + +**Action:** +- Send a POST request to `/clusters/{id}/statuses` with a valid `AdapterStatusCreateRequest` body containing all three mandatory conditions (Applied, Available, Health) + +**Expected Result:** +- API returns HTTP 405 Method Not Allowed +- The POST method was removed in v1.0.0; only PUT is accepted + +--- + +## Test Title: PUT with missing mandatory conditions returns 400 + +### Description + +Validates that the v1.0.0 API rejects adapter status reports that omit the three mandatory conditions (Available, Applied, Health). Sends a PUT with only a `Ready` condition (which was removed in v1.0.0) and no mandatory conditions. + +--- + +| **Field** | **Value** | +|-----------|-----------| +| **Pos/Neg** | Negative | +| **Priority** | Tier0 | +| **Status** | Draft | +| **Automation** | Automated | +| **Version** | v1.0.0 | +| **Created** | 2026-06-23 | +| **Updated** | 2026-06-23 | + +--- + +### Preconditions + +1. HyperFleet API is deployed with v1.0.0 adapter contract +2. HyperFleet Sentinel is deployed and running + +--- + +### Test Steps + +#### Step 1: Create a cluster + +**Action:** +- Create a cluster via the API using a standard cluster payload + +**Expected Result:** +- API returns the created cluster with an ID and generation + +#### Step 2: Send a PUT with only a Ready condition + +**Action:** +- Send a PUT request to `/clusters/{id}/statuses` with an `AdapterStatusCreateRequest` containing only `{Type: "Ready", Status: "True"}`, omitting the mandatory Available, Applied, and Health conditions + +**Expected Result:** +- API returns HTTP 400 Bad Request +- The three mandatory conditions (Available, Applied, Health) must be present in every adapter status report + +--- + +## Test Title: Ready condition absent on reconciled cluster + +### Description + +Validates that the v1.0.0 API does not produce a resource-level `Ready` condition on a fully reconciled cluster. Adapters that poll for `Ready=True` to determine readiness will hang forever. The correct v1.0.0 conditions are `Reconciled` and `LastKnownReconciled`. + +--- + +| **Field** | **Value** | +|-----------|-----------| +| **Pos/Neg** | Negative | +| **Priority** | Tier0 | +| **Status** | Draft | +| **Automation** | Automated | +| **Version** | v1.0.0 | +| **Created** | 2026-06-23 | +| **Updated** | 2026-06-23 | + +--- + +### Preconditions + +1. HyperFleet API is deployed with v1.0.0 adapter contract +2. HyperFleet Sentinel is deployed and running +3. At least one adapter is configured and running to drive reconciliation + +--- + +### Test Steps + +#### Step 1: Create a cluster and wait for reconciliation + +**Action:** +- Create a cluster via the API +- Poll until `Reconciled=True` + +**Expected Result:** +- Cluster reaches `Reconciled` condition with `status: "True"` + +#### Step 2: Verify Ready condition is absent + +**Action:** +- Fetch the reconciled cluster and inspect `status.conditions` + +**Expected Result:** +- No condition with `type: "Ready"` exists in the resource-level conditions +- `Reconciled` condition is present with `status: "True"` +- `LastKnownReconciled` condition is present with `status: "True"` + +---