Skip to content

Commit 8531ad3

Browse files
feat(errors): add JsonApiErrorCodes and JsonApiErrors factory methods (#60)
- Add JsonApiErrorCodes with 18 standard error codes for consistent error identification - Add JsonApiErrors factory class with 12 methods for creating well-structured exceptions - Factories include proper code, source, and meta fields automatically
1 parent 0863dee commit 8531ad3

3 files changed

Lines changed: 493 additions & 0 deletions

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
using JsonApiToolkit.Models.Errors;
2+
3+
namespace JsonApiToolkit.Tests.Models.Errors;
4+
5+
public class JsonApiErrorsTests
6+
{
7+
// ─────────────────────────────────────────────────────────────────────────
8+
// NotFound
9+
// ─────────────────────────────────────────────────────────────────────────
10+
11+
[Fact]
12+
public void NotFound_CreatesCorrectException()
13+
{
14+
var ex = JsonApiErrors.NotFound("books", 123);
15+
16+
Assert.IsType<JsonApiNotFoundException>(ex);
17+
Assert.Equal(404, ex.StatusCode);
18+
Assert.Equal(JsonApiErrorCodes.ResourceNotFound, ex.Code);
19+
Assert.Equal("Resource 'books' with id '123' not found.", ex.Message);
20+
Assert.NotNull(ex.Meta);
21+
Assert.Equal("books", ex.Meta["resourceType"]);
22+
Assert.Equal(123, ex.Meta["id"]);
23+
}
24+
25+
[Fact]
26+
public void RelatedNotFound_CreatesCorrectException()
27+
{
28+
var ex = JsonApiErrors.RelatedNotFound("books", 1, "author", 99);
29+
30+
Assert.IsType<JsonApiNotFoundException>(ex);
31+
Assert.Equal(404, ex.StatusCode);
32+
Assert.Equal(JsonApiErrorCodes.ResourceNotFound, ex.Code);
33+
Assert.Contains("author", ex.Message);
34+
Assert.Contains("99", ex.Message);
35+
Assert.NotNull(ex.Meta);
36+
Assert.Equal("author", ex.Meta["relationship"]);
37+
Assert.Equal(99, ex.Meta["relatedId"]);
38+
}
39+
40+
// ─────────────────────────────────────────────────────────────────────────
41+
// InvalidFilterValue
42+
// ─────────────────────────────────────────────────────────────────────────
43+
44+
[Fact]
45+
public void InvalidFilterValue_IncludesSourceParameter()
46+
{
47+
var ex = JsonApiErrors.InvalidFilterValue("age", "abc", typeof(int));
48+
49+
Assert.IsType<JsonApiBadRequestException>(ex);
50+
Assert.Equal(400, ex.StatusCode);
51+
Assert.Equal(JsonApiErrorCodes.InvalidFilterValue, ex.Code);
52+
Assert.NotNull(ex.ErrorSource);
53+
Assert.Equal("filter[age]", ex.ErrorSource.Parameter);
54+
Assert.NotNull(ex.Meta);
55+
Assert.Equal("age", ex.Meta["field"]);
56+
Assert.Equal("Int32", ex.Meta["expectedType"]);
57+
Assert.Equal("abc", ex.Meta["actualValue"]);
58+
}
59+
60+
// ─────────────────────────────────────────────────────────────────────────
61+
// InvalidFilterField
62+
// ─────────────────────────────────────────────────────────────────────────
63+
64+
[Fact]
65+
public void InvalidFilterField_WithAvailableFields_IncludesThemInMeta()
66+
{
67+
var availableFields = new[] { "name", "email", "age" };
68+
69+
var ex = JsonApiErrors.InvalidFilterField("foo", typeof(TestClass), availableFields);
70+
71+
Assert.IsType<JsonApiBadRequestException>(ex);
72+
Assert.Equal(JsonApiErrorCodes.InvalidFilterField, ex.Code);
73+
Assert.NotNull(ex.ErrorSource);
74+
Assert.Equal("filter[foo]", ex.ErrorSource.Parameter);
75+
Assert.NotNull(ex.Meta);
76+
Assert.Equal("TestClass", ex.Meta["entityType"]);
77+
Assert.Equal(availableFields.ToList(), ex.Meta["availableFields"]);
78+
}
79+
80+
[Fact]
81+
public void InvalidFilterField_WithoutAvailableFields_OmitsFromMeta()
82+
{
83+
var ex = JsonApiErrors.InvalidFilterField("foo", typeof(TestClass));
84+
85+
Assert.NotNull(ex.Meta);
86+
Assert.False(ex.Meta.ContainsKey("availableFields"));
87+
}
88+
89+
// ─────────────────────────────────────────────────────────────────────────
90+
// IncludeNotAllowed
91+
// ─────────────────────────────────────────────────────────────────────────
92+
93+
[Fact]
94+
public void IncludeNotAllowed_WithAllowedIncludes_IncludesThemInMeta()
95+
{
96+
var allowed = new[] { "author", "comments" };
97+
98+
var ex = JsonApiErrors.IncludeNotAllowed("secretData", allowed);
99+
100+
Assert.IsType<JsonApiForbiddenException>(ex);
101+
Assert.Equal(403, ex.StatusCode);
102+
Assert.Equal(JsonApiErrorCodes.IncludeNotAllowed, ex.Code);
103+
Assert.NotNull(ex.ErrorSource);
104+
Assert.Equal("include", ex.ErrorSource.Parameter);
105+
Assert.NotNull(ex.Meta);
106+
Assert.Equal("secretData", ex.Meta["requestedInclude"]);
107+
Assert.Equal(allowed.ToList(), ex.Meta["allowedIncludes"]);
108+
}
109+
110+
[Fact]
111+
public void IncludeNotAllowed_WithoutAllowedIncludes_OmitsFromMeta()
112+
{
113+
var ex = JsonApiErrors.IncludeNotAllowed("secretData");
114+
115+
Assert.NotNull(ex.Meta);
116+
Assert.False(ex.Meta.ContainsKey("allowedIncludes"));
117+
}
118+
119+
// ─────────────────────────────────────────────────────────────────────────
120+
// AlreadyExists
121+
// ─────────────────────────────────────────────────────────────────────────
122+
123+
[Fact]
124+
public void AlreadyExists_IncludesSourcePointer()
125+
{
126+
var ex = JsonApiErrors.AlreadyExists("users", "email", "test@example.com");
127+
128+
Assert.IsType<JsonApiConflictException>(ex);
129+
Assert.Equal(409, ex.StatusCode);
130+
Assert.Equal(JsonApiErrorCodes.ResourceAlreadyExists, ex.Code);
131+
Assert.NotNull(ex.ErrorSource);
132+
Assert.Equal("/data/attributes/email", ex.ErrorSource.Pointer);
133+
Assert.NotNull(ex.Meta);
134+
Assert.Equal("users", ex.Meta["resourceType"]);
135+
Assert.Equal("email", ex.Meta["field"]);
136+
Assert.Equal("test@example.com", ex.Meta["value"]);
137+
}
138+
139+
// ─────────────────────────────────────────────────────────────────────────
140+
// QueryTooComplex
141+
// ─────────────────────────────────────────────────────────────────────────
142+
143+
[Fact]
144+
public void QueryTooComplex_IncludesLimitDetails()
145+
{
146+
var ex = JsonApiErrors.QueryTooComplex("filters", 50, 75, "JsonApiOptions.MaxFilters");
147+
148+
Assert.IsType<JsonApiBadRequestException>(ex);
149+
Assert.Equal(JsonApiErrorCodes.QueryTooComplex, ex.Code);
150+
Assert.Contains("75", ex.Message);
151+
Assert.Contains("50", ex.Message);
152+
Assert.Contains("JsonApiOptions.MaxFilters", ex.Message);
153+
Assert.NotNull(ex.Meta);
154+
Assert.Equal(50, ex.Meta["limit"]);
155+
Assert.Equal(75, ex.Meta["actual"]);
156+
}
157+
158+
// ─────────────────────────────────────────────────────────────────────────
159+
// Validation helpers
160+
// ─────────────────────────────────────────────────────────────────────────
161+
162+
[Fact]
163+
public void ValidationFailed_CreatesCorrectException()
164+
{
165+
var ex = JsonApiErrors.ValidationFailed("email", "Invalid email format");
166+
167+
Assert.IsType<JsonApiBadRequestException>(ex);
168+
Assert.Equal(JsonApiErrorCodes.ValidationFailed, ex.Code);
169+
Assert.Equal("Invalid email format", ex.Message);
170+
Assert.NotNull(ex.ErrorSource);
171+
Assert.Equal("/data/attributes/email", ex.ErrorSource.Pointer);
172+
}
173+
174+
[Fact]
175+
public void RequiredFieldMissing_CreatesCorrectException()
176+
{
177+
var ex = JsonApiErrors.RequiredFieldMissing("title");
178+
179+
Assert.IsType<JsonApiBadRequestException>(ex);
180+
Assert.Equal(JsonApiErrorCodes.RequiredFieldMissing, ex.Code);
181+
Assert.Contains("title", ex.Message);
182+
Assert.NotNull(ex.ErrorSource);
183+
Assert.Equal("/data/attributes/title", ex.ErrorSource.Pointer);
184+
}
185+
186+
// ─────────────────────────────────────────────────────────────────────────
187+
// All factories produce valid exceptions
188+
// ─────────────────────────────────────────────────────────────────────────
189+
190+
[Fact]
191+
public void AllFactories_ProduceJsonApiExceptionSubclasses()
192+
{
193+
var exceptions = new JsonApiException[]
194+
{
195+
JsonApiErrors.NotFound("books", 1),
196+
JsonApiErrors.RelatedNotFound("books", 1, "author", 2),
197+
JsonApiErrors.InvalidFilterValue("age", "abc", typeof(int)),
198+
JsonApiErrors.InvalidFilterField("foo", typeof(TestClass)),
199+
JsonApiErrors.InvalidFilterOperator("bad"),
200+
JsonApiErrors.InvalidSortField("foo", typeof(TestClass)),
201+
JsonApiErrors.QueryTooComplex("filters", 50, 75, "config"),
202+
JsonApiErrors.IncludeNotAllowed("secret"),
203+
JsonApiErrors.FilterNotAllowed("secret.field"),
204+
JsonApiErrors.AlreadyExists("users", "email", "test@test.com"),
205+
JsonApiErrors.ValidationFailed("email", "Invalid"),
206+
JsonApiErrors.RequiredFieldMissing("title"),
207+
};
208+
209+
foreach (var ex in exceptions)
210+
{
211+
Assert.NotNull(ex.Code);
212+
Assert.NotNull(ex.Message);
213+
Assert.True(ex.StatusCode >= 400 && ex.StatusCode < 600);
214+
}
215+
}
216+
217+
private class TestClass
218+
{
219+
public string? Name { get; set; }
220+
}
221+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
namespace JsonApiToolkit.Models.Errors;
2+
3+
/// <summary>
4+
/// Standard error codes for JSON:API responses.
5+
/// Use these with JsonApiErrors factory methods for consistent error handling.
6+
/// </summary>
7+
#pragma warning disable CS1591 // Constants are self-documenting
8+
public static class JsonApiErrorCodes
9+
{
10+
// Resource errors
11+
public const string ResourceNotFound = "RESOURCE_NOT_FOUND";
12+
public const string ResourceAlreadyExists = "RESOURCE_ALREADY_EXISTS";
13+
14+
// Filter errors
15+
public const string InvalidFilterField = "INVALID_FILTER_FIELD";
16+
public const string InvalidFilterValue = "INVALID_FILTER_VALUE";
17+
public const string InvalidFilterOperator = "INVALID_FILTER_OPERATOR";
18+
public const string FilterNotAllowed = "FILTER_NOT_ALLOWED";
19+
20+
// Include errors
21+
public const string IncludeNotAllowed = "INCLUDE_NOT_ALLOWED";
22+
public const string IncludeDepthExceeded = "INCLUDE_DEPTH_EXCEEDED";
23+
24+
// Pagination errors
25+
public const string InvalidPageNumber = "INVALID_PAGE_NUMBER";
26+
public const string InvalidPageSize = "INVALID_PAGE_SIZE";
27+
public const string PageSizeExceeded = "PAGE_SIZE_EXCEEDED";
28+
29+
// Sort errors
30+
public const string InvalidSortField = "INVALID_SORT_FIELD";
31+
32+
// Query complexity
33+
public const string QueryTooComplex = "QUERY_TOO_COMPLEX";
34+
public const string TooManyFilters = "TOO_MANY_FILTERS";
35+
36+
// Validation
37+
public const string ValidationFailed = "VALIDATION_FAILED";
38+
public const string RequiredFieldMissing = "REQUIRED_FIELD_MISSING";
39+
40+
// Auth
41+
public const string AuthenticationRequired = "AUTHENTICATION_REQUIRED";
42+
public const string InsufficientPermissions = "INSUFFICIENT_PERMISSIONS";
43+
}
44+
#pragma warning restore CS1591

0 commit comments

Comments
 (0)