Add unit tests for RabbitMq and Udp transport servers and clients
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling. - Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options. - Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation. - Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios. - Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling. - Included tests for UdpTransportOptions to verify default values and modification capabilities. - Enhanced service registration tests for Udp transport services in the dependency injection container.
This commit is contained in:
503
docs/modules/router/openapi-aggregation.md
Normal file
503
docs/modules/router/openapi-aggregation.md
Normal file
@@ -0,0 +1,503 @@
|
||||
# OpenAPI Aggregation
|
||||
|
||||
This document describes how the StellaOps Gateway aggregates OpenAPI documentation from connected microservices into a unified specification.
|
||||
|
||||
## Overview
|
||||
|
||||
The Gateway automatically generates a single OpenAPI 3.1.0 document that aggregates all endpoints from connected microservices. This provides:
|
||||
|
||||
- **Unified API documentation**: All services documented in one place
|
||||
- **Dynamic updates**: Document regenerates when services connect/disconnect
|
||||
- **Standard compliance**: OpenAPI 3.1.0 with native JSON Schema draft 2020-12 support
|
||||
- **Multiple formats**: Available as JSON or YAML
|
||||
- **Efficient caching**: ETag-based caching with configurable TTL
|
||||
|
||||
### How It Works
|
||||
|
||||
```
|
||||
┌──────────────┐ HELLO ┌──────────────┐ GET /openapi.json ┌──────────────┐
|
||||
│ Billing │ ──────────► │ │ ◄───────────────────── │ Client │
|
||||
│ Service │ + schemas │ Gateway │ │ │
|
||||
└──────────────┘ │ │ OpenAPI 3.1.0 │ │
|
||||
│ │ ─────────────────────► │ │
|
||||
┌──────────────┐ HELLO │ │ unified document └──────────────┘
|
||||
│ Inventory │ ──────────► │ │
|
||||
│ Service │ + schemas └──────────────┘
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
1. Microservices send schemas and endpoint metadata via HELLO payload
|
||||
2. Gateway stores this information in routing state
|
||||
3. OpenAPI generator aggregates all connected services
|
||||
4. Document is cached and served via HTTP endpoints
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### OpenApiAggregationOptions
|
||||
|
||||
Configure OpenAPI aggregation in your Gateway configuration:
|
||||
|
||||
```yaml
|
||||
# router.yaml or appsettings.yaml
|
||||
OpenApi:
|
||||
Title: "My API Gateway"
|
||||
Description: "Unified API for all microservices"
|
||||
Version: "2.0.0"
|
||||
ServerUrl: "https://api.example.com"
|
||||
CacheTtlSeconds: 60
|
||||
Enabled: true
|
||||
LicenseName: "AGPL-3.0-or-later"
|
||||
ContactName: "API Team"
|
||||
ContactEmail: "api@example.com"
|
||||
TokenUrl: "/auth/token"
|
||||
```
|
||||
|
||||
### Configuration Reference
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `Title` | `string` | `"StellaOps Gateway API"` | API title in OpenAPI info section |
|
||||
| `Description` | `string` | `"Unified API aggregating all connected microservices."` | API description |
|
||||
| `Version` | `string` | `"1.0.0"` | API version number |
|
||||
| `ServerUrl` | `string` | `"/"` | Base server URL |
|
||||
| `CacheTtlSeconds` | `int` | `60` | Cache time-to-live in seconds |
|
||||
| `Enabled` | `bool` | `true` | Enable/disable OpenAPI aggregation |
|
||||
| `LicenseName` | `string` | `"AGPL-3.0-or-later"` | License name in OpenAPI info |
|
||||
| `ContactName` | `string?` | `null` | Contact name (optional) |
|
||||
| `ContactEmail` | `string?` | `null` | Contact email (optional) |
|
||||
| `TokenUrl` | `string` | `"/auth/token"` | OAuth2 token endpoint URL |
|
||||
|
||||
### Disabling OpenAPI
|
||||
|
||||
To disable OpenAPI aggregation entirely:
|
||||
|
||||
```yaml
|
||||
OpenApi:
|
||||
Enabled: false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Discovery Endpoint
|
||||
|
||||
```http
|
||||
GET /.well-known/openapi
|
||||
```
|
||||
|
||||
Returns metadata about the OpenAPI document:
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"openapi_json": "/openapi.json",
|
||||
"openapi_yaml": "/openapi.yaml",
|
||||
"etag": "\"5d41402abc4b2a76b9719d911017c592\"",
|
||||
"generated_at": "2025-01-15T10:30:00.0000000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAPI JSON
|
||||
|
||||
```http
|
||||
GET /openapi.json
|
||||
```
|
||||
|
||||
Returns the full OpenAPI 3.1.0 specification in JSON format.
|
||||
|
||||
**Headers:**
|
||||
- `Cache-Control: public, max-age=60`
|
||||
- `ETag: "<content-hash>"`
|
||||
- `Content-Type: application/json; charset=utf-8`
|
||||
|
||||
**Conditional Request:**
|
||||
```http
|
||||
GET /openapi.json
|
||||
If-None-Match: "5d41402abc4b2a76b9719d911017c592"
|
||||
```
|
||||
|
||||
Returns `304 Not Modified` if content unchanged.
|
||||
|
||||
### OpenAPI YAML
|
||||
|
||||
```http
|
||||
GET /openapi.yaml
|
||||
```
|
||||
|
||||
Returns the full OpenAPI 3.1.0 specification in YAML format.
|
||||
|
||||
**Headers:**
|
||||
- `Cache-Control: public, max-age=60`
|
||||
- `ETag: "<content-hash>"`
|
||||
- `Content-Type: application/yaml; charset=utf-8`
|
||||
|
||||
---
|
||||
|
||||
## Security Mapping
|
||||
|
||||
The Gateway automatically maps claim requirements to OpenAPI security schemes.
|
||||
|
||||
### Claim to Scope Mapping
|
||||
|
||||
When endpoints define `RequiringClaims`, these are converted to OAuth2 scopes:
|
||||
|
||||
```csharp
|
||||
// Endpoint with claim requirements
|
||||
[StellaEndpoint("POST", "/invoices")]
|
||||
[RequireClaim("billing:write")]
|
||||
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<...>
|
||||
```
|
||||
|
||||
Becomes in OpenAPI:
|
||||
|
||||
```json
|
||||
{
|
||||
"paths": {
|
||||
"/invoices": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": [],
|
||||
"OAuth2": ["billing:write"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Security Schemes
|
||||
|
||||
The Gateway generates two security schemes:
|
||||
|
||||
#### BearerAuth
|
||||
|
||||
HTTP Bearer token authentication (always present):
|
||||
|
||||
```json
|
||||
{
|
||||
"BearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT",
|
||||
"description": "JWT Bearer token authentication"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### OAuth2
|
||||
|
||||
Client credentials flow with collected scopes (only if endpoints have claims):
|
||||
|
||||
```json
|
||||
{
|
||||
"OAuth2": {
|
||||
"type": "oauth2",
|
||||
"flows": {
|
||||
"clientCredentials": {
|
||||
"tokenUrl": "/auth/token",
|
||||
"scopes": {
|
||||
"billing:write": "Access scope: billing:write",
|
||||
"billing:read": "Access scope: billing:read",
|
||||
"inventory:read": "Access scope: inventory:read"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scope Collection
|
||||
|
||||
Scopes are automatically collected from all connected services. If multiple endpoints require the same claim, it appears only once in the scopes list.
|
||||
|
||||
---
|
||||
|
||||
## Generated Document Structure
|
||||
|
||||
The aggregated OpenAPI document follows this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "StellaOps Gateway API",
|
||||
"version": "1.0.0",
|
||||
"description": "Unified API aggregating all connected microservices.",
|
||||
"license": {
|
||||
"name": "AGPL-3.0-or-later"
|
||||
},
|
||||
"contact": {
|
||||
"name": "API Team",
|
||||
"email": "api@example.com"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/invoices": {
|
||||
"post": {
|
||||
"operationId": "billing_invoices_POST",
|
||||
"tags": ["billing"],
|
||||
"summary": "Create invoice",
|
||||
"description": "Creates a new draft invoice",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": [],
|
||||
"OAuth2": ["billing:write"]
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/billing_CreateInvoiceRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/billing_CreateInvoiceResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": { "description": "Bad Request" },
|
||||
"401": { "description": "Unauthorized" },
|
||||
"404": { "description": "Not Found" },
|
||||
"422": { "description": "Validation Error" },
|
||||
"500": { "description": "Internal Server Error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/items": {
|
||||
"get": {
|
||||
"operationId": "inventory_items_GET",
|
||||
"tags": ["inventory"],
|
||||
"summary": "List items",
|
||||
"responses": {
|
||||
"200": { "description": "Success" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"billing_CreateInvoiceRequest": {
|
||||
"type": "object",
|
||||
"required": ["customerId", "amount"],
|
||||
"properties": {
|
||||
"customerId": { "type": "string" },
|
||||
"amount": { "type": "number" },
|
||||
"description": { "type": ["string", "null"] },
|
||||
"lineItems": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/billing_LineItem" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"billing_CreateInvoiceResponse": {
|
||||
"type": "object",
|
||||
"required": ["invoiceId", "createdAt", "status"],
|
||||
"properties": {
|
||||
"invoiceId": { "type": "string" },
|
||||
"createdAt": { "type": "string", "format": "date-time" },
|
||||
"status": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"billing_LineItem": {
|
||||
"type": "object",
|
||||
"required": ["description", "amount"],
|
||||
"properties": {
|
||||
"description": { "type": "string" },
|
||||
"amount": { "type": "number" },
|
||||
"quantity": { "type": "integer", "default": 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"BearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT",
|
||||
"description": "JWT Bearer token authentication"
|
||||
},
|
||||
"OAuth2": {
|
||||
"type": "oauth2",
|
||||
"flows": {
|
||||
"clientCredentials": {
|
||||
"tokenUrl": "/auth/token",
|
||||
"scopes": {
|
||||
"billing:write": "Access scope: billing:write"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "billing",
|
||||
"description": "billing microservice (v1.0.0)"
|
||||
},
|
||||
{
|
||||
"name": "inventory",
|
||||
"description": "inventory microservice (v2.0.0)"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Schema Prefixing
|
||||
|
||||
Schemas are prefixed with the service name to prevent naming conflicts:
|
||||
|
||||
| Service | Original Type | Prefixed Schema ID |
|
||||
|---------|--------------|-------------------|
|
||||
| `billing` | `CreateInvoiceRequest` | `billing_CreateInvoiceRequest` |
|
||||
| `inventory` | `GetItemResponse` | `inventory_GetItemResponse` |
|
||||
|
||||
### Tag Generation
|
||||
|
||||
Tags are automatically generated from connected services:
|
||||
|
||||
- Tag name: Service name (lowercase)
|
||||
- Tag description: Service description from `OpenApiInfo` or auto-generated
|
||||
|
||||
### Operation IDs
|
||||
|
||||
Operation IDs follow the pattern: `{serviceName}_{path}_{method}`
|
||||
|
||||
Example: `billing_invoices_POST`
|
||||
|
||||
---
|
||||
|
||||
## Cache Behavior
|
||||
|
||||
### TTL-Based Expiration
|
||||
|
||||
The document cache expires based on `CacheTtlSeconds` (default: 60 seconds):
|
||||
|
||||
```yaml
|
||||
OpenApi:
|
||||
CacheTtlSeconds: 30 # More frequent regeneration
|
||||
```
|
||||
|
||||
Setting `CacheTtlSeconds: 0` regenerates the document on every request (not recommended for production).
|
||||
|
||||
### Connection-Based Invalidation
|
||||
|
||||
The cache is automatically invalidated when:
|
||||
|
||||
1. A new microservice connects (HELLO received)
|
||||
2. A microservice disconnects (connection closed)
|
||||
|
||||
This ensures the OpenAPI document always reflects currently connected services.
|
||||
|
||||
### ETag Consistency
|
||||
|
||||
The ETag is computed from the document content hash (SHA256). This ensures:
|
||||
|
||||
- Same content = same ETag
|
||||
- Content changes = new ETag
|
||||
- Clients can use conditional requests to avoid re-downloading unchanged documents
|
||||
|
||||
### Recommended Client Strategy
|
||||
|
||||
```javascript
|
||||
// Store ETag from previous response
|
||||
let cachedETag = localStorage.getItem('openapi-etag');
|
||||
|
||||
const response = await fetch('/openapi.json', {
|
||||
headers: cachedETag ? { 'If-None-Match': cachedETag } : {}
|
||||
});
|
||||
|
||||
if (response.status === 304) {
|
||||
// Use cached document
|
||||
return getCachedDocument();
|
||||
}
|
||||
|
||||
// Store new ETag and document
|
||||
localStorage.setItem('openapi-etag', response.headers.get('ETag'));
|
||||
const document = await response.json();
|
||||
cacheDocument(document);
|
||||
return document;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service Registration
|
||||
|
||||
### Microservice Options
|
||||
|
||||
Configure service metadata that appears in OpenAPI:
|
||||
|
||||
```csharp
|
||||
services.AddStellaMicroservice(options =>
|
||||
{
|
||||
options.ServiceName = "billing";
|
||||
options.ServiceDescription = "Invoice and payment processing service";
|
||||
options.ContactInfo = "billing-team@example.com";
|
||||
});
|
||||
```
|
||||
|
||||
### Service OpenAPI Info
|
||||
|
||||
The `ServiceOpenApiInfo` is sent in the HELLO payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"instance": { ... },
|
||||
"endpoints": [ ... ],
|
||||
"schemas": { ... },
|
||||
"openApiInfo": {
|
||||
"title": "billing",
|
||||
"description": "Invoice and payment processing service"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This description appears in the tag entry for the service.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Document Not Updating
|
||||
|
||||
1. Check `CacheTtlSeconds` - may need to wait for TTL expiration
|
||||
2. Verify service connected successfully (check Gateway logs)
|
||||
3. Force refresh by restarting the Gateway
|
||||
|
||||
### Missing Schemas
|
||||
|
||||
1. Ensure `[ValidateSchema]` attribute is applied to endpoints
|
||||
2. Check for schema parsing errors in Gateway logs
|
||||
3. Verify endpoint implements `IStellaEndpoint<TRequest, TResponse>`
|
||||
|
||||
### Security Schemes Not Appearing
|
||||
|
||||
1. OAuth2 scheme only appears if endpoints have claim requirements
|
||||
2. Check `RequiringClaims` is populated on endpoint descriptors
|
||||
3. Verify claim types are being transmitted correctly
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Schema Validation](schema-validation.md) - JSON Schema validation reference
|
||||
- [API Overview](../../api/overview.md) - General API conventions
|
||||
- [Gateway OpenAPI](../gateway/openapi.md) - Gateway OpenAPI implementation details
|
||||
Reference in New Issue
Block a user