Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/+IFC-2272.added.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion dev/guides/frontend/writing-component-tests.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
57 changes: 57 additions & 0 deletions dev/guides/frontend/writing-e2e-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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=<relationship_name>` 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-<peer-name>").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:
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion dev/guides/frontend/writing-unit-tests.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
3 changes: 2 additions & 1 deletion dev/knowledge/backend/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions dev/knowledge/backend/async-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions dev/knowledge/backend/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 18 additions & 2 deletions dev/knowledge/backend/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) │
Expand Down Expand Up @@ -75,17 +83,23 @@ 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`

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`:
Expand Down Expand Up @@ -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` |
Expand Down
55 changes: 55 additions & 0 deletions dev/knowledge/frontend/object-detail-relationships.md
Original file line number Diff line number Diff line change
@@ -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/<id>?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
<ObjectDetailsTab>
{relationshipSchema.label} <!-- e.g. "Headers" -->
<Badge>{relationshipCount}</Badge> <!-- e.g. "0" -->
</ObjectDetailsTab>
```

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
Loading
Loading