A .NET 9 service that ingests events from a CMS via webhook, processes them, stores them in a database, and exposes them via a secure REST API.
CmsEventProcessor/
├── src/
│ ├── CmsEventProcessor.Api/ # Web API, Controllers, Authentication
│ ├── CmsEventProcessor.Application/ # Business Logic, DTOs, Validators
│ ├── CmsEventProcessor.Domain/ # Entities, Interfaces
│ └── CmsEventProcessor.Infrastructure/ # EF Core, Repositories
├── tests/
│ └── CmsEventProcessor.Tests/ # Unit and Integration Tests
git clone https://github.com/jdmadriz/InterviewAssessment.git
cd CmsEventProcessordotnet restoredotnet builddotnet run --project src/CmsEventProcessor.ApiThe API will start at http://localhost:5000
Open your browser and navigate to: http://localhost:5000/swagger
The API uses Basic Authentication with two separate schemes:
| Field | Value |
|---|---|
| Username | webhook |
| Password | 550e8400-e29b-41d4-a716-446655440000 |
| Base64 Token | d2ViaG9vazo1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDA= |
| User | Username | Password | Role | Base64 Token |
|---|---|---|---|---|
| Admin | admin |
7c9e6679-7425-40de-944b-e07fc1f90ae7 |
Admin | YWRtaW46N2M5ZTY2NzktNzQyNS00MGRlLTk0NGItZTA3ZmMxZjkwYWU3 |
| User | user |
a3bb189e-8bf9-3888-9912-ace4e6543002 |
User | dXNlcjphM2JiMTg5ZS04YmY5LTM4ODgtOTkxMi1hY2U0ZTY1NDMwMDI= |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /cms/events |
CMS User | Receive batch events from CMS |
| GET | /api/entities |
API Users | List all entities |
| GET | /api/entities/{id} |
API Users | Get entity by ID |
| POST | /api/entities/{id}/disable |
Admin only | Disable entity (admin override) |
| POST | /api/entities/{id}/enable |
Admin only | Enable entity (admin override) |
Make sure the API is running (dotnet run --project src/CmsEventProcessor.Api)
curl -X POST http://localhost:5000/cms/events \
-H "Content-Type: application/json" \
-H "Authorization: Basic d2ViaG9vazo1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDA=" \
-d '[
{"type": "publish", "id": "entity-1", "payload": {"title": "First Article", "content": "Content here"}, "version": 1, "timestamp": "2024-01-01T00:00:00Z"},
{"type": "publish", "id": "entity-2", "payload": {"title": "Second Article", "content": "More content"}, "version": 1, "timestamp": "2024-01-01T00:00:00Z"},
{"type": "publish", "id": "entity-3", "payload": {"title": "Third Article", "content": "Third content"}, "version": 1, "timestamp": "2024-01-01T00:00:00Z"}
]'Expected Response: {"message":"Processed 3 events"}
curl -X POST http://localhost:5000/cms/events \
-H "Content-Type: application/json" \
-H "Authorization: Basic d2ViaG9vazo1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDA=" \
-d '[
{"type": "publish", "id": "entity-1", "payload": {"title": "First Article UPDATED", "content": "Updated content"}, "version": 2, "timestamp": "2024-01-02T00:00:00Z"}
]'curl -X POST http://localhost:5000/cms/events \
-H "Content-Type: application/json" \
-H "Authorization: Basic d2ViaG9vazo1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDA=" \
-d '[
{"type": "publish", "id": "entity-1", "payload": {"title": "OLD VERSION - SHOULD BE IGNORED"}, "version": 1, "timestamp": "2024-01-01T00:00:00Z"}
]'curl -X POST http://localhost:5000/cms/events \
-H "Content-Type: application/json" \
-H "Authorization: Basic d2ViaG9vazo1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDA=" \
-d '[
{"type": "unpublish", "id": "entity-2", "payload": {"title": "Second Article"}, "version": 1, "timestamp": "2024-01-03T00:00:00Z"}
]'curl -X POST http://localhost:5000/cms/events \
-H "Content-Type: application/json" \
-H "Authorization: Basic d2ViaG9vazo1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDA=" \
-d '[
{"type": "delete", "id": "entity-3", "timestamp": "2024-01-04T00:00:00Z"}
]'curl -X POST http://localhost:5000/cms/events \
-H "Content-Type: application/json" \
-H "Authorization: Basic d2ViaG9vazo1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDA=" \
-d '[
{"type": "unpublish", "id": "never-existed", "payload": {"title": "Never Published"}, "version": 5, "timestamp": "2024-01-05T00:00:00Z"}
]'curl -X GET http://localhost:5000/api/entities \
-H "Authorization: Basic dXNlcjphM2JiMTg5ZS04YmY5LTM4ODgtOTkxMi1hY2U0ZTY1NDMwMDI="Note: Normal users only see published and non-disabled entities.
curl -X GET http://localhost:5000/api/entities \
-H "Authorization: Basic YWRtaW46N2M5ZTY2NzktNzQyNS00MGRlLTk0NGItZTA3ZmMxZjkwYWU3"Note: Admin users see all entities including unpublished and disabled ones.
curl -X GET http://localhost:5000/api/entities/entity-1 \
-H "Authorization: Basic YWRtaW46N2M5ZTY2NzktNzQyNS00MGRlLTk0NGItZTA3ZmMxZjkwYWU3"curl -X POST http://localhost:5000/api/entities/entity-1/disable \
-H "Authorization: Basic YWRtaW46N2M5ZTY2NzktNzQyNS00MGRlLTk0NGItZTA3ZmMxZjkwYWU3"Expected Response: {"message":"Entity entity-1 disabled"}
curl -X GET http://localhost:5000/api/entities \
-H "Authorization: Basic dXNlcjphM2JiMTg5ZS04YmY5LTM4ODgtOTkxMi1hY2U0ZTY1NDMwMDI="Expected Result: Empty list (entity-1 was disabled by admin).
curl -X POST http://localhost:5000/api/entities/entity-1/enable \
-H "Authorization: Basic YWRtaW46N2M5ZTY2NzktNzQyNS00MGRlLTk0NGItZTA3ZmMxZjkwYWU3"Expected Response: {"message":"Entity entity-1 enabled"}
curl -X POST http://localhost:5000/api/entities/entity-1/disable \
-H "Authorization: Basic dXNlcjphM2JiMTg5ZS04YmY5LTM4ODgtOTkxMi1hY2U0ZTY1NDMwMDI="curl -X GET http://localhost:5000/api/entities \
-H "Authorization: Basic aW52YWxpZDppbnZhbGlk"curl -X GET http://localhost:5000/api/entitiescurl -X POST http://localhost:5000/cms/events \
-H "Content-Type: application/json" \
-H "Authorization: Basic YWRtaW46N2M5ZTY2NzktNzQyNS00MGRlLTk0NGItZTA3ZmMxZjkwYWU3" \
-d '[{"type": "publish", "id": "test", "payload": {"title": "Test"}, "version": 1, "timestamp": "2024-01-01T00:00:00Z"}]'dotnet testdotnet test tests/CmsEventProcessor.TestsThe test suite includes:
Unit Tests (Unit/EventProcessorServiceTests.cs):
- Publish new entity
- Publish existing entity (version update)
- Publish with lower version (should ignore)
- Unpublish existing entity
- Unpublish non-existing entity (corner case)
- Delete entity
- Invalid event type validation
- Missing required fields validation
Integration Tests (Integration/AuthenticationTests.cs):
- CMS webhook with valid credentials
- CMS webhook with invalid credentials
- CMS webhook with no credentials
- API with valid admin credentials
- API with valid user credentials
- API with invalid credentials
- User trying admin action (forbidden)
- API user trying CMS endpoint (unauthorized)
| Type | Action | Payload Required | Version Required |
|---|---|---|---|
publish |
Create or update entity | Yes | Yes |
unpublish |
Mark entity as unpublished (soft-delete) | Yes | Yes |
delete |
Remove entity permanently (hard-delete) | No | No |
- Events with a version lower than or equal to the current version are ignored
- Unpublish events for non-existing entities are ignored (corner case)
| User Type | Sees |
|---|---|
| Normal User | Published entities that are not disabled by admin |
| Admin User | All entities (published, unpublished, disabled) |