Skip to content

Commit e5b50be

Browse files
feat(errors): complete refactor Phase 1 with exception filter tests a… (#61)
…nd documentation - Add tests verifying JsonApiExceptionFilter serializes code, source, and meta fields - Complete JSON:API spec compliance audit (no deviations found) - Update upgrade-guide.md with v1.3.0 changes - Update enhanced-error-handling.md with factory method examples
1 parent 8531ad3 commit e5b50be

3 files changed

Lines changed: 162 additions & 7 deletions

File tree

JsonApiToolkit.Tests/Filters/JsonApiExceptionFilterTests.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,73 @@ string expectedTitle
238238
Assert.Equal(statusCode.ToString(), errorResponse.Errors[0].Status);
239239
Assert.Equal(expectedTitle, errorResponse.Errors[0].Title);
240240
}
241+
242+
[Fact]
243+
public void OnException_WithFullMetadata_SerializesAllFields()
244+
{
245+
var exception = new JsonApiBadRequestException(
246+
"Invalid filter value",
247+
code: "INVALID_FILTER_VALUE",
248+
errorSource: new ErrorSource { Parameter = "filter[age]" },
249+
meta: new Dictionary<string, object>
250+
{
251+
["field"] = "age",
252+
["expectedType"] = "Int32",
253+
["actualValue"] = "abc",
254+
}
255+
);
256+
var context = CreateExceptionContext(exception);
257+
258+
_filter.OnException(context);
259+
260+
var result = Assert.IsType<ObjectResult>(context.Result);
261+
var errorResponse = Assert.IsType<JsonApiErrorResponse>(result.Value);
262+
var error = errorResponse.Errors[0];
263+
264+
Assert.Equal("INVALID_FILTER_VALUE", error.Code);
265+
Assert.NotNull(error.Source);
266+
Assert.Equal("filter[age]", error.Source.Parameter);
267+
Assert.NotNull(error.Meta);
268+
Assert.Equal("age", error.Meta["field"]);
269+
Assert.Equal("Int32", error.Meta["expectedType"]);
270+
Assert.Equal("abc", error.Meta["actualValue"]);
271+
}
272+
273+
[Fact]
274+
public void OnException_WithFactoryException_SerializesCorrectly()
275+
{
276+
var exception = JsonApiErrors.NotFound("books", 123);
277+
var context = CreateExceptionContext(exception);
278+
279+
_filter.OnException(context);
280+
281+
var result = Assert.IsType<ObjectResult>(context.Result);
282+
Assert.Equal(404, result.StatusCode);
283+
284+
var errorResponse = Assert.IsType<JsonApiErrorResponse>(result.Value);
285+
var error = errorResponse.Errors[0];
286+
287+
Assert.Equal("RESOURCE_NOT_FOUND", error.Code);
288+
Assert.NotNull(error.Meta);
289+
Assert.Equal("books", error.Meta["resourceType"]);
290+
Assert.Equal(123, error.Meta["id"]);
291+
}
292+
293+
[Fact]
294+
public void OnException_WithSourcePointer_SerializesCorrectly()
295+
{
296+
var exception = JsonApiErrors.AlreadyExists("users", "email", "test@example.com");
297+
var context = CreateExceptionContext(exception);
298+
299+
_filter.OnException(context);
300+
301+
var result = Assert.IsType<ObjectResult>(context.Result);
302+
Assert.Equal(409, result.StatusCode);
303+
304+
var errorResponse = Assert.IsType<JsonApiErrorResponse>(result.Value);
305+
var error = errorResponse.Errors[0];
306+
307+
Assert.NotNull(error.Source);
308+
Assert.Equal("/data/attributes/email", error.Source.Pointer);
309+
}
241310
}

docs/docs/enhanced-error-handling.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,89 @@ The enhanced exception produces richer error responses:
139139

140140
> [!IMPORTANT]
141141
> When using these exceptions, ensure that the parent is not wrapped in a try-catch block that catches all exceptions. This will prevent the toolkit from handling the error correctly.
142+
143+
---
144+
145+
## Error Factory Methods (v1.3.0+)
146+
147+
For common error scenarios, use the `JsonApiErrors` factory class to create consistent, well-structured errors with proper codes, source information, and metadata:
148+
149+
### Available Factory Methods
150+
151+
| Factory | Status | Use Case |
152+
|---------|--------|----------|
153+
| `JsonApiErrors.NotFound(type, id)` | 404 | Resource not found |
154+
| `JsonApiErrors.RelatedNotFound(type, id, relationship, relatedId)` | 404 | Related resource not found |
155+
| `JsonApiErrors.InvalidFilterValue(field, value, expectedType)` | 400 | Type conversion failed |
156+
| `JsonApiErrors.InvalidFilterField(field, entityType)` | 400 | Field doesn't exist |
157+
| `JsonApiErrors.InvalidFilterOperator(op)` | 400 | Unknown filter operator |
158+
| `JsonApiErrors.InvalidSortField(field, entityType)` | 400 | Sort field doesn't exist |
159+
| `JsonApiErrors.IncludeNotAllowed(include)` | 403 | Include blocked by AllowedIncludes |
160+
| `JsonApiErrors.FilterNotAllowed(relationshipPath)` | 403 | Filter on disallowed relationship |
161+
| `JsonApiErrors.AlreadyExists(type, field, value)` | 409 | Duplicate key violation |
162+
| `JsonApiErrors.ValidationFailed(field, message)` | 400 | Generic validation error |
163+
| `JsonApiErrors.RequiredFieldMissing(field)` | 400 | Required field not provided |
164+
| `JsonApiErrors.QueryTooComplex(limitName, limit, actual, configKey)` | 400 | Query exceeds limits |
165+
166+
### Usage Examples
167+
168+
```csharp
169+
// Resource not found
170+
var book = await _db.Books.FindAsync(id)
171+
?? throw JsonApiErrors.NotFound("books", id);
172+
173+
// Invalid filter value
174+
if (!int.TryParse(filterValue, out _))
175+
throw JsonApiErrors.InvalidFilterValue("age", filterValue, typeof(int));
176+
177+
// Duplicate resource
178+
if (await _db.Users.AnyAsync(u => u.Email == email))
179+
throw JsonApiErrors.AlreadyExists("users", "email", email);
180+
181+
// Validation error
182+
if (string.IsNullOrWhiteSpace(request.Title))
183+
throw JsonApiErrors.RequiredFieldMissing("title");
184+
```
185+
186+
### Example Response
187+
188+
Using `JsonApiErrors.NotFound("books", 123)` produces:
189+
190+
```json
191+
{
192+
"errors": [{
193+
"status": "404",
194+
"code": "RESOURCE_NOT_FOUND",
195+
"title": "Not Found",
196+
"detail": "Resource 'books' with id '123' not found.",
197+
"meta": {
198+
"resourceType": "books",
199+
"id": 123
200+
}
201+
}]
202+
}
203+
```
204+
205+
### Standard Error Codes
206+
207+
All factory methods use codes from `JsonApiErrorCodes`:
208+
209+
```csharp
210+
public static class JsonApiErrorCodes
211+
{
212+
public const string ResourceNotFound = "RESOURCE_NOT_FOUND";
213+
public const string ResourceAlreadyExists = "RESOURCE_ALREADY_EXISTS";
214+
public const string InvalidFilterField = "INVALID_FILTER_FIELD";
215+
public const string InvalidFilterValue = "INVALID_FILTER_VALUE";
216+
public const string InvalidFilterOperator = "INVALID_FILTER_OPERATOR";
217+
public const string FilterNotAllowed = "FILTER_NOT_ALLOWED";
218+
public const string IncludeNotAllowed = "INCLUDE_NOT_ALLOWED";
219+
public const string InvalidSortField = "INVALID_SORT_FIELD";
220+
public const string QueryTooComplex = "QUERY_TOO_COMPLEX";
221+
public const string ValidationFailed = "VALIDATION_FAILED";
222+
public const string RequiredFieldMissing = "REQUIRED_FIELD_MISSING";
223+
// ... and more
224+
}
225+
```
226+
227+
Use these codes in your client applications to handle specific error types programmatically.

docs/docs/upgrade-guide.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -296,15 +296,15 @@ public async Task<IActionResult> GetUsers()
296296
**Release Date:** TBD
297297

298298
**Bug Fixes:**
299-
- [ ] Fixed exception swallowing in InclusionMapper (now properly logged)
300-
- [ ] Fixed unsafe string parsing in filter parser
301-
- [ ] Fixed potential division by zero in pagination
302-
- [ ] Added defensive checks for reflection method lookups
303-
- [ ] Removed dead code (`AddIncludedResourcesRecursive`)
299+
- [x] Fixed exception swallowing in InclusionMapper (dead code removed)
300+
- [x] Fixed unsafe string parsing in filter parser
301+
- [x] Fixed potential division by zero in pagination
302+
- [x] Added defensive checks for reflection method lookups
303+
- [x] Removed dead code (`AddIncludedResourcesRecursive`)
304304

305305
**New Features:**
306-
- [ ] `JsonApiErrorCodes` - Standard error codes for consistent error identification
307-
- [ ] `JsonApiErrors` - Factory methods for creating rich, well-structured errors
306+
- [x] `JsonApiErrorCodes` - Standard error codes for consistent error identification
307+
- [x] `JsonApiErrors` - Factory methods for creating rich, well-structured errors
308308

309309
**Usage:**
310310
```csharp

0 commit comments

Comments
 (0)