From e834b0b04f51ac5d2af21e71dde01acc08ab71c0 Mon Sep 17 00:00:00 2001 From: Pol Michel Date: Sat, 14 Mar 2026 15:02:51 +0100 Subject: [PATCH 1/6] updating internal-documentation --- dev/knowledge/backend/architecture.md | 3 ++- dev/knowledge/backend/async-tasks.md | 1 + dev/knowledge/backend/events.md | 1 + dev/knowledge/backend/webhooks.md | 20 ++++++++++++++++++-- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/dev/knowledge/backend/architecture.md b/dev/knowledge/backend/architecture.md index 97f9ef34e2..2de890e0e8 100644 --- a/dev/knowledge/backend/architecture.md +++ b/dev/knowledge/backend/architecture.md @@ -41,7 +41,8 @@ Similar to pull requests, proposed changes allow reviewing and approving data mo | Core | Business logic, domain models | `core/` | | Database | Query execution, connection mgmt | `database/` | | Workers | Async task processing | `workers/`, `task_manager/` | -| Events | Pub/sub, triggers, webhooks | `events/`, `message_bus/` | +| Events | Pub/sub, triggers | `events/`, `message_bus/` | +| Webhooks | HTTP notification delivery | `webhook/` | ## Entry Points diff --git a/dev/knowledge/backend/async-tasks.md b/dev/knowledge/backend/async-tasks.md index 2ab85e8599..85a4c6f3e0 100644 --- a/dev/knowledge/backend/async-tasks.md +++ b/dev/knowledge/backend/async-tasks.md @@ -199,4 +199,5 @@ Available dependencies: - [ADR-0003: Asynchronous Tasks](../../adr/0003-asynchronous-tasks.md) - Why we use Prefect - [Creating Workflows Guide](../../guides/backend/creating-async-tasks.md) - How to create a new workflow - [Events System](events.md) - Event-driven workflow triggers +- [Webhooks](webhooks.md) - Primary consumer of events and async tasks - [Backend Architecture](architecture.md) - Overall backend structure diff --git a/dev/knowledge/backend/events.md b/dev/knowledge/backend/events.md index 2a16ca5d4e..abbfb4a528 100644 --- a/dev/knowledge/backend/events.md +++ b/dev/knowledge/backend/events.md @@ -94,4 +94,5 @@ Events can be queried through: - [ADR-0002: Prefect Events System](../../adr/0002-events-system.md) - Why we use Prefect Events - [Creating Events Guide](../../guides/backend/creating-events.md) - How to create a new event +- [Webhooks](webhooks.md) - HTTP notification delivery triggered by events - [Backend Architecture](architecture.md) - Overall backend structure diff --git a/dev/knowledge/backend/webhooks.md b/dev/knowledge/backend/webhooks.md index f15c5ec2e7..06b72086fe 100644 --- a/dev/knowledge/backend/webhooks.md +++ b/dev/knowledge/backend/webhooks.md @@ -26,6 +26,14 @@ configure_webhook flow └─ WebhookAction.RECONCILE_ALL ──► _reconcile_all() └──► Full sync via setup_triggers_specific() +KeyValue CRUD (create/update/delete header) + │ + ▼ +Built-in Trigger (TRIGGER_KEYVALUE_WEBHOOK_INVALIDATE) + │ (empty parameters → forces RECONCILE_ALL) + ▼ +configure_webhook flow → _reconcile_all() + ┌─────────────────────┐ Application Event (e.g. node.created) ──► │ Prefect Automation │ │ (event matching) │ @@ -75,6 +83,10 @@ Bridges Infrahub webhooks to Prefect automations. Extends `TriggerDefinition` wi Normalized representation of the event extracted from Prefect's raw event payload. Contains `id`, `branch`, `account_id`, `occured_at`, and `event` type. Created via `from_event()` which parses the nested context structure from Prefect. +### `WebhookHeader` + +Pydantic model for a custom HTTP header: `key` (str), `value` (str), `kind` (Literal `"static"` | `"environment"`). The `resolve()` method returns the header value — for `"static"` it returns the value directly, for `"environment"` it looks up `os.environ.get(value)` and returns `None` if the variable is missing. + ### `Webhook` class hierarchy `Webhook` (base) → `StandardWebhook`, `CustomWebhook`, `TransformWebhook` @@ -82,10 +94,12 @@ Normalized representation of the event extracted from Prefect's raw event payloa The base class handles: - Payload preparation (`_prepare_payload`) -- Header assignment with optional HMAC signing (`_assign_headers`) +- Header assignment with custom headers and optional HMAC signing (`_assign_headers`) - HTTP delivery via `send()` - Cache serialization (`to_cache` / `from_cache`) +The `custom_headers: list[WebhookHeader]` field on the base `Webhook` class holds headers loaded from the `CoreWebhook.headers` relationship. During `_assign_headers()`, custom headers are applied after system defaults (Accept, Content-Type) but before HMAC signature headers. Static headers use the value directly; environment headers resolve from `os.environ` at send time (missing vars are skipped with a warning log). + ## Schema (GraphQL) Defined in `backend/infrahub/core/schema/definitions/core/webhook.py`: @@ -220,7 +234,9 @@ Two built-in triggers in `triggers.py` react to webhook-related node lifecycle e | Schema definitions | `backend/infrahub/core/schema/definitions/core/webhook.py` | | GraphQL mutations | `backend/infrahub/graphql/mutations/webhook.py` | | Workflow catalogue | `backend/infrahub/workflows/catalogue.py` | -| Unit tests | `backend/tests/unit/webhook/test_models.py` | +| KeyValue schema | `backend/infrahub/core/schema/definitions/core/key_value.py` | +| Unit tests (models) | `backend/tests/unit/webhook/test_models.py` | +| Unit tests (triggers) | `backend/tests/unit/webhook/test_triggers.py` | | Functional tests (configure) | `backend/tests/functional/webhook/test_configure.py` | | Functional tests (process) | `backend/tests/functional/webhook/test_process.py` | | Mutation tests | `backend/tests/component/graphql/mutations/test_webhook.py` | From db53db745f1b5f1ec693ebdb5c0674ec82781ba6 Mon Sep 17 00:00:00 2001 From: Pol Michel Date: Sat, 14 Mar 2026 15:03:13 +0100 Subject: [PATCH 2/6] update spec-kit specification after password corekeyvalue removal --- dev/specs/infp-445-webhook-headers/spec.md | 24 +++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/dev/specs/infp-445-webhook-headers/spec.md b/dev/specs/infp-445-webhook-headers/spec.md index 735006d0bb..3d1fbd8819 100644 --- a/dev/specs/infp-445-webhook-headers/spec.md +++ b/dev/specs/infp-445-webhook-headers/spec.md @@ -10,17 +10,16 @@ ### User Story 1 - Attach Authentication Headers to a Webhook (Priority: P1) -An infrastructure administrator configures a webhook in Infrahub to notify an external system (e.g., Ansible Automation Platform) about infrastructure changes. The external system requires an authentication header such as `Authorization: Bearer ` to accept incoming requests. The administrator creates a key-value pair containing the sensitive header credentials, associates it with the webhook, and when events fire, the custom header is automatically included in the HTTP request. +An infrastructure administrator configures a webhook in Infrahub to notify an external system (e.g., Ansible Automation Platform) about infrastructure changes. The external system requires an authentication header such as `Authorization: Bearer ` to accept incoming requests. The administrator creates a key-value pair containing the header credentials, associates it with the webhook, and when events fire, the custom header is automatically included in the HTTP request. **Why this priority**: This is the core use case driving the feature. Without it, customers cannot integrate Infrahub webhooks with any system requiring header-based authentication, which is the majority of modern APIs and automation platforms. -**Independent Test**: Can be fully tested by creating a key-value pair with a sensitive header value, linking it to a webhook, triggering an event, and verifying the target system receives the request with the correct authentication header. +**Independent Test**: Can be fully tested by creating a key-value pair with a header value, linking it to a webhook, triggering an event, and verifying the target system receives the request with the correct authentication header. **Acceptance Scenarios**: -1. **Given** a webhook configured to notify an external system and a key-value pair containing a sensitive authentication header, **When** the administrator links the key-value pair to the webhook and an event fires, **Then** the HTTP request to the external system includes the custom authentication header with the correct value. -2. **Given** a key-value pair with a sensitive header value (e.g., API key or bearer token), **When** the administrator views the key-value pair via the UI, **Then** the sensitive value is masked and not displayed in cleartext. -3. **Given** a webhook with a linked authentication header, **When** the administrator queries the webhook configuration, **Then** the header relationship is visible. +1. **Given** a webhook configured to notify an external system and a key-value pair containing an authentication header, **When** the administrator links the key-value pair to the webhook and an event fires, **Then** the HTTP request to the external system includes the custom authentication header with the correct value. +2. **Given** a webhook with a linked authentication header, **When** the administrator queries the webhook configuration, **Then** the header relationship is visible. --- @@ -56,18 +55,18 @@ An administrator has multiple webhooks that all target systems within the same o --- -### User Story 4 - Add Non-Sensitive Custom Headers to a Webhook (Priority: P3) +### User Story 4 - Add Plain-Text Custom Headers to a Webhook (Priority: P3) -An administrator needs to include non-sensitive identification or routing headers (e.g., `X-Source-System: infrahub`, `X-Tenant-Id: acme-corp`) in webhook requests. They create a plain-text key-value pair where the value is stored and displayed without masking, and associate it with their webhook. +An administrator needs to include identification or routing headers (e.g., `X-Source-System: infrahub`, `X-Tenant-Id: acme-corp`) in webhook requests. They create a plain-text key-value pair and associate it with their webhook. -**Why this priority**: Supports simpler use cases where headers carry non-sensitive metadata, providing flexibility without requiring secret management overhead. +**Why this priority**: Supports use cases where headers carry metadata like system identifiers or routing information. **Independent Test**: Can be fully tested by creating a plain-text key-value pair, linking it to a webhook, triggering an event, and verifying the header appears in the request. **Acceptance Scenarios**: 1. **Given** a plain-text key-value pair linked to a webhook, **When** a webhook event fires, **Then** the HTTP request includes the custom header with the literal value. -2. **Given** a plain-text key-value pair, **When** the administrator views it via the API or UI, **Then** the value is displayed in cleartext (not masked). +2. **Given** a plain-text key-value pair, **When** the administrator views it via the API or UI, **Then** the value is displayed as-is. --- @@ -85,12 +84,11 @@ An administrator needs to include non-sensitive identification or routing header ### Functional Requirements - **FR-001**: System MUST allow users to create key-value pairs representing custom HTTP headers with a human-friendly name (globally unique across all key-value pair types, consistent with standard generic behavior in Infrahub), a header name (the actual HTTP header field name), and a header value. -- **FR-002**: System MUST support three types of key-value pairs: plain-text (value stored and displayed as-is), password/sensitive (value uses a Password attribute kind and is masked in the UI), and environment-variable-based (value resolved from worker environment at send time). +- **FR-002**: System MUST support two types of key-value pairs: plain-text (value stored and displayed as-is) and environment-variable-based (value resolved from worker environment at send time). - **FR-003**: System MUST allow associating zero or more key-value pairs with any webhook (both Standard and Custom Webhooks). - **FR-004**: System MUST support many-to-many relationships between key-value pairs and webhooks, allowing one key-value pair to be referenced by multiple webhooks and one webhook to reference multiple key-value pairs. - **FR-005**: System MUST include all associated custom headers in webhook HTTP requests when events fire. - **FR-006**: System MUST merge custom headers with default system headers (Content-Type, Accept, HMAC signature headers) when sending webhook requests. In case of name conflicts, the user's custom header value MUST take precedence over the system default. -- **FR-007**: System MUST mask sensitive header values (password type) in the UI, consistent with how existing Password kind attributes are displayed. - **FR-008**: System MUST resolve environment-variable-based header values from the worker process environment at the time the webhook request is sent, not at configuration time. - **FR-009**: When an environment-variable-based header references a variable that does not exist in the worker environment, the system MUST skip that header, include all remaining resolvable headers in the request, and log a warning identifying the missing variable name. - **FR-010**: System MUST invalidate cached webhook data when associated key-value pairs are created, updated, deleted, or when the relationship between a key-value pair and a webhook changes. @@ -101,8 +99,7 @@ An administrator needs to include non-sensitive identification or routing header ### Key Entities - **Key-Value Pair (Generic)**: A reusable generic configuration object representing a key-value pair. Has a globally unique human-friendly name for identification (enforced across all key-value pair types, per standard Infrahub generic behavior), a key name (e.g., an HTTP header field name like "Authorization" or "X-Auth-Token"), and a value. Serves as the base generic entity with three specialized node types that inherit from it, differing only in how the value is stored and resolved. -- **Static Key-Value Pair (Node)**: A node type inheriting from the Key-Value Pair generic. The value is stored as plain text and displayed without masking. Used for non-sensitive data like system identifiers or routing metadata. -- **Password Key-Value Pair (Node)**: A node type inheriting from the Key-Value Pair generic. The value uses a Password attribute kind and is masked in the UI. Used for sensitive data like API keys and bearer tokens. +- **Static Key-Value Pair (Node)**: A node type inheriting from the Key-Value Pair generic. The value is stored as plain text and displayed as-is. Used for data like system identifiers, routing metadata, or static authentication tokens. - **Environment Variable Key-Value Pair (Node)**: A node type inheriting from the Key-Value Pair generic. The stored value is the name of an environment variable. The actual value is resolved from the worker process environment at the time of use. Used when secrets are managed externally (Kubernetes secrets, vault solutions). - **Webhook**: An existing generic entity (CoreWebhook) from which both Standard and Custom Webhook types inherit. A new optional `headers` relationship (cardinality=many) to the Key-Value Pair generic MUST be defined on this webhook generic, so that all webhook types automatically inherit the ability to reference zero or more key-value pairs. @@ -129,7 +126,6 @@ An administrator needs to include non-sensitive identification or routing header - **SC-001**: Users can successfully send webhook requests to external systems requiring custom authentication headers (e.g., Ansible Automation Platform) without any workarounds or intermediary proxies. - **SC-002**: Users can configure a webhook with custom headers and trigger a successful authenticated request to an external endpoint. This should be verified after the configuration has been updated and applied within Prefect. -- **SC-003**: Sensitive header values (password type) are masked in the UI and never appear in application logs. - **SC-004**: A single key-value pair update propagates to all linked webhooks on the next event trigger, with zero manual intervention required per webhook. - **SC-005**: Environment-variable-based headers resolve correctly at send time, and missing variables produce actionable error messages that identify the specific variable name. - **SC-006**: Custom header functionality works identically for both Standard and Custom Webhook types with no behavioral differences. From 61d38a5432fc10a4259424a7452b089ecb3676af Mon Sep 17 00:00:00 2001 From: Pol Michel Date: Sat, 14 Mar 2026 15:03:42 +0100 Subject: [PATCH 3/6] updating external documentation --- docs/docs/topics/webhooks.mdx | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/docs/topics/webhooks.mdx b/docs/docs/topics/webhooks.mdx index 0a04e9342f..7447588c96 100644 --- a/docs/docs/topics/webhooks.mdx +++ b/docs/docs/topics/webhooks.mdx @@ -236,6 +236,43 @@ Here are some example payloads of some events that may be sent. +## Custom headers + +Webhooks can include custom HTTP headers in every request they send. This is useful for authentication tokens, routing metadata, or any other headers the receiving system requires. + +### Key-value pair types + +Custom headers are defined as reusable **key-value pair** entities that can be shared across multiple webhooks: + +- **Static Key-Value** (`CoreStaticKeyValue`): The header value is stored as plain text and sent as-is. Use this for non-sensitive metadata like `X-Source-System: infrahub` or for authentication tokens like `Authorization: Bearer `. +- **Environment Variable Key-Value** (`CoreEnvironmentVariableKeyValue`): The stored value is the *name* of an environment variable. The actual value is resolved from the Prefect worker's environment at the time the webhook request is sent. Use this when secrets are managed externally (for example, Kubernetes secrets or HashiCorp Vault). + +### Creating and associating headers + +1. Create a key-value pair via the UI or GraphQL API, specifying a **name** (human-friendly identifier), a **key** (the HTTP header field name), and a **value**. +2. Associate the key-value pair with one or more webhooks using the **headers** relationship on the webhook. + +A single key-value pair can be linked to multiple webhooks (many-to-many relationship). When the key-value pair is updated, all linked webhooks automatically pick up the new value on their next trigger. + +### Header merge behavior + +When a webhook fires, headers are merged in this order: + +1. **System defaults**: `Accept: application/json` and `Content-Type: application/json` +2. **Custom headers**: Applied on top of defaults — a custom header with the same key as a system default will override it +3. **HMAC signature headers** (if a shared key is configured): `webhook-id`, `webhook-timestamp`, and `webhook-signature` are added last and cannot be overridden by custom headers + +### Environment variable resolution + +For environment-variable-based headers, the value is resolved from `os.environ` on the Prefect worker at send time: + +- If the environment variable exists, its value is used as the header value. +- If the environment variable is **not found**, the header is skipped, a warning is logged, and all remaining headers are still sent. + +:::note +Authentication header values must include the full value including any type prefix. For example, for Bearer token authentication, set the value to `Bearer eyJhbGci...` (not just the token). +::: + ## Custom webhook transformation The `data` object from the payloads above is passed to the `transform` method of your [Python transform](../guides/python-transform.mdx). From 284a292c8c38ac39c16d3c513bc798d05b61816d Mon Sep 17 00:00:00 2001 From: Pol Michel Date: Sat, 14 Mar 2026 15:05:49 +0100 Subject: [PATCH 4/6] towncrier --- changelog/+IFC-2272.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/+IFC-2272.added.md diff --git a/changelog/+IFC-2272.added.md b/changelog/+IFC-2272.added.md new file mode 100644 index 0000000000..a9271cc877 --- /dev/null +++ b/changelog/+IFC-2272.added.md @@ -0,0 +1 @@ +Added support for custom HTTP headers on webhooks. Users can create reusable key-value pairs (static or environment-variable-based) and associate them with webhooks. From aefdcd25c65b815d88df84a3e431048b7beb761d Mon Sep 17 00:00:00 2001 From: Pol Michel Date: Sat, 14 Mar 2026 17:03:56 +0100 Subject: [PATCH 5/6] E2E Playwright tests for webhook custom headers (IFC-2339) Add 3 serial E2E tests covering the full lifecycle of webhook custom headers: create a StaticKeyValue, associate it as a header on a webhook, then dissociate it. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/tests/e2e/webhook/webhook.spec.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/frontend/app/tests/e2e/webhook/webhook.spec.ts b/frontend/app/tests/e2e/webhook/webhook.spec.ts index 896cfdaf86..e950042921 100644 --- a/frontend/app/tests/e2e/webhook/webhook.spec.ts +++ b/frontend/app/tests/e2e/webhook/webhook.spec.ts @@ -72,6 +72,73 @@ test.describe("/objects/CoreWebhook", () => { }); }); + test("Create a static key-value header", async ({ page }) => { + await test.step("load key values", async () => { + await page.goto("/objects/CoreKeyValue"); + await expect(page.getByTestId("object-header")).toContainText("Key Value"); + }); + + await test.step("create a new static key value", async () => { + await page.getByTestId("create-object-button").click(); + + await page.getByLabel("Select an object type").click(); + await page.getByRole("option", { name: "Static Key Value Core" }).click(); + + await page.getByLabel("Name *").fill("eda-auth-header"); + await page.getByLabel("Key *").fill("Authorization"); + await page.getByLabel("Value *").fill("Bearer e2e-test-token"); + + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByText("StaticKeyValue created")).toBeVisible(); + }); + }); + + test("Add header to webhook", async ({ page }) => { + await test.step("navigate to webhook detail", async () => { + await page.goto("/objects/CoreWebhook"); + await page.getByRole("link", { name: "Ansible EDA" }).click(); + await expect( + page.getByTestId("object-header").getByText("Ansible EDA", { exact: true }) + ).toBeVisible(); + }); + + await test.step("add header relationship", async () => { + // Headers is an Attribute-kind relationship (no visible tab), navigate via URL param + await page.goto(`${page.url()}?tab=headers`); + await expect(page.getByText("No Key Value found")).toBeVisible(); + await page.getByTestId("open-relationship-form-button").click(); + await page.getByRole("combobox", { name: "Kind" }).click(); + await page.getByRole("option", { name: "Static Key Value Core" }).click(); + await page.getByLabel("Static Key Value", { exact: true }).click(); + await page.getByRole("option", { name: "eda-auth-header" }).click(); + + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByRole("link", { name: "eda-auth-header" })).toBeVisible(); + }); + }); + + test("Remove header from webhook", async ({ page }) => { + await test.step("navigate to webhook headers", async () => { + await page.goto("/objects/CoreWebhook"); + await page.getByRole("link", { name: "Ansible EDA" }).click(); + await expect( + page.getByTestId("object-header").getByText("Ansible EDA", { exact: true }) + ).toBeVisible(); + // Headers is an Attribute-kind relationship (no visible tab), navigate via URL param + await page.goto(`${page.url()}?tab=headers`); + }); + + await test.step("dissociate header", async () => { + await page.getByTestId("actions-cell-eda-auth-header").click(); + await page.getByRole("menuitem", { name: "Dissociate" }).click(); + await page.getByTestId("modal-delete-confirm").click(); + }); + + await test.step("verify dissociation", async () => { + await expect(page.getByRole("link", { name: "eda-auth-header" })).toBeHidden(); + }); + }); + test("Delete webhook", async ({ page }) => { await test.step("load webhooks", async () => { await page.goto("/objects/CoreWebhook"); From fdcfb29718feb170877938cb8bbfbc19a9b7e56f Mon Sep 17 00:00:00 2001 From: Pol Michel Date: Sat, 14 Mar 2026 17:21:59 +0100 Subject: [PATCH 6/6] updated internal documentations --- .../frontend/writing-component-tests.md | 2 +- dev/guides/frontend/writing-e2e-tests.md | 57 +++++++++++++++++++ dev/guides/frontend/writing-unit-tests.md | 2 +- .../frontend/object-detail-relationships.md | 55 ++++++++++++++++++ frontend/app/AGENTS.md | 1 + 5 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 dev/knowledge/frontend/object-detail-relationships.md diff --git a/dev/guides/frontend/writing-component-tests.md b/dev/guides/frontend/writing-component-tests.md index 03a7b91957..f7b5f4febf 100644 --- a/dev/guides/frontend/writing-component-tests.md +++ b/dev/guides/frontend/writing-component-tests.md @@ -1,6 +1,6 @@ # Writing Component Tests -> Part of: `dev/guides/frontend/` | Related: [TypeScript Standards](../../guidelines/frontend/typescript.md) +> Part of: `dev/guides/frontend/` | Related: [TypeScript Standards](../../guidelines/frontend/typescript.md), [E2E Tests](writing-e2e-tests.md), [Unit Tests](writing-unit-tests.md) Step-by-step guide for writing React component tests following the project's testing patterns and best practices. diff --git a/dev/guides/frontend/writing-e2e-tests.md b/dev/guides/frontend/writing-e2e-tests.md index 6fdddaeed2..c3c33e3c41 100644 --- a/dev/guides/frontend/writing-e2e-tests.md +++ b/dev/guides/frontend/writing-e2e-tests.md @@ -35,6 +35,7 @@ tests/e2e/ proposed-changes/ # Proposed change workflows ipam/ # IPAM-specific features form/ # Form interaction patterns + webhook/ # Webhook CRUD and header management utils/ # Shared helper functions auth.setup.ts # Authentication setup project ``` @@ -352,6 +353,39 @@ await page.getByTestId("side-panel-container").getByLabel("Status").click(); await page.getByRole("option", { name: "Maintenance" }).click(); ``` +### Relationship Management + +#### Adding a relationship (generic peer form) + +```typescript +// Navigate to a relationship tab (label + count badge, no space) +await page.getByText("Members0").click(); + +// For Attribute-kind relationships (no visible tab), navigate via URL param +await page.goto(`${page.url()}?tab=headers`); + +// Open the relationship form and select kind + peer +await page.getByTestId("open-relationship-form-button").click(); +await page.getByRole("combobox", { name: "Kind" }).click(); +await page.getByRole("option", { name: "Tag Builtin" }).click(); +// Note: the peer selector label matches the schema label of the selected kind +await page.getByLabel("Tag", { exact: true }).click(); +await page.getByRole("option", { name: "blue" }).click(); +await page.getByRole("button", { name: "Save" }).click(); +``` + +**Tab count badge selector**: Relationship tabs render label and count as separate DOM elements but Playwright merges child text, so `getByText("Members0")` works (no space). `getByText("Members 0")` (with space) will NOT match. + +**Non-tab relationships**: Only `Generic`, `Component`, `Hierarchy`, and `Template` relationship kinds get visible tabs. `Attribute`-kind (and `Parent`) relationships render inline in the details panel. Navigate via `?tab=` URL param — the `ObjectDetails` component renders the tab content whenever a `tab` query param is present, regardless of whether a visible tab exists. See `src/entities/nodes/object/utils/get-relationships-visible-in-tab.ts`. + +#### Dissociating a relationship + +```typescript +await page.getByTestId("actions-cell-").click(); +await page.getByRole("menuitem", { name: "Dissociate" }).click(); +await page.getByTestId("modal-delete-confirm").click(); +``` + ### 500 Error Guard Add this to catch unexpected server errors during tests: @@ -368,6 +402,29 @@ test.beforeEach(async function ({ page }) { ## Running Tests +### Running E2E Tests Locally + +E2E tests require a running Infrahub instance at `http://localhost:8080` (configurable via `INFRAHUB_ADDRESS` env var). + +`dev.start` runs the full stack in Docker using the locally-built image, matching the current branch. + +```bash +# 1. Build the local Docker image (required once, or after backend changes) +uv run invoke dev.build + +# 2. Start all services with clean state +uv run invoke dev.destroy +uv run invoke dev.start --wait + +# 3. Load test data +uv run invoke dev.load-infra-schema +uv run invoke dev.load-infra-data + +# 4. Run tests (see commands below) +``` + +### Test Commands + ```bash cd frontend/app diff --git a/dev/guides/frontend/writing-unit-tests.md b/dev/guides/frontend/writing-unit-tests.md index fc3d0c6538..54c81ce7b7 100644 --- a/dev/guides/frontend/writing-unit-tests.md +++ b/dev/guides/frontend/writing-unit-tests.md @@ -1,6 +1,6 @@ # Writing Unit Tests -> Part of: `dev/guides/frontend/` | Related: [TypeScript Standards](../../guidelines/frontend/typescript.md) +> Part of: `dev/guides/frontend/` | Related: [TypeScript Standards](../../guidelines/frontend/typescript.md), [E2E Tests](writing-e2e-tests.md), [Component Tests](writing-component-tests.md) Step-by-step guide for writing unit tests for TypeScript functions following the project's testing patterns and best practices. diff --git a/dev/knowledge/frontend/object-detail-relationships.md b/dev/knowledge/frontend/object-detail-relationships.md new file mode 100644 index 0000000000..3e8d10bc23 --- /dev/null +++ b/dev/knowledge/frontend/object-detail-relationships.md @@ -0,0 +1,55 @@ +# Relationship Display on Object Detail Pages + +> Part of: `dev/knowledge/frontend/` | Related: [Architecture](architecture.md), [E2E Tests Guide](../../guides/frontend/writing-e2e-tests.md) + +## Tab vs Inline Rendering + +Not all many-cardinality relationships get their own tab on object detail pages. The visibility is controlled by `src/entities/nodes/object/utils/get-relationships-visible-in-tab.ts`. + +| RelKind | Visible as tab? | +|-------------|----------------| +| Generic | Yes | +| Component | Yes | +| Hierarchy | Yes | +| Template | Yes | +| Attribute | No (inline) | +| Parent | No (inline) | + +Additionally, `cardinality=one` relationships are always rendered inline regardless of kind, and resource pool relationships are excluded. + +## Inline Relationships + +Relationships that don't get tabs are rendered as rows in the Details panel by `RelationshipManyRow` in `src/entities/nodes/object/ui/object-details/object-data-display/object-relationship-row.tsx`: + +- **Empty**: shows the label with a `-` value +- **Populated**: shows a list of peer links + +## Hidden Tab Navigation + +The `ObjectDetails` component (`src/entities/nodes/object/ui/object-details/object-details.tsx`) renders `ObjectDetailsTabContent` whenever a `tab` query string parameter is present, regardless of whether a visible tab exists for that relationship. This means any many-cardinality relationship can be managed via URL: + +``` +/objects/CoreWebhook/?tab=headers +``` + +This is the only way to reach the relationship management view (add/remove peers) for `Attribute`-kind relationships. + +## Tab Count Badge + +Relationship tabs render the label and count as separate DOM elements: + +```tsx + + {relationshipSchema.label} + {relationshipCount} + +``` + +In Playwright, `getByText("Members0")` works because Playwright merges child text content. `getByText("Members 0")` with a space will NOT match. + +## Key Files + +- `src/entities/nodes/object/utils/get-relationships-visible-in-tab.ts` — tab visibility rules +- `src/entities/nodes/object/ui/object-details/object-details.tsx` — tab routing via `?tab=` param +- `src/entities/nodes/object/ui/object-tabs.tsx` — `RelationshipTab` component +- `src/entities/nodes/object/ui/object-details/object-data-display/object-relationship-row.tsx` — inline relationship rendering diff --git a/frontend/app/AGENTS.md b/frontend/app/AGENTS.md index f05e0a8688..20b429534c 100644 --- a/frontend/app/AGENTS.md +++ b/frontend/app/AGENTS.md @@ -34,6 +34,7 @@ cd frontend/app && npm run codegen # Generate GraphQL types - `dev/knowledge/frontend/architecture.md` - Project organization - `dev/knowledge/frontend/entities-structure.md` - Entity layer pattern (api/domain/ui) - `dev/knowledge/frontend/file-components.md` - DataViewer and file handling components +- `dev/knowledge/frontend/object-detail-relationships.md` - Relationship tab vs inline rendering rules ### Guides (How to do X)