Skip to content

Commit b8887ce

Browse files
authored
Merge pull request #137 from cronofy/include-error-response-bodies
Include error response bodies on API call failures
2 parents 6c4756e + a07a019 commit b8887ce

File tree

8 files changed

+143
-72
lines changed

8 files changed

+143
-72
lines changed

src/Cronofy/CronofyAccountClient.cs

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,7 @@ public void DeleteAllEvents()
311311
var requestBody = new { delete_all = true };
312312
request.SetJsonBody(requestBody);
313313

314-
var response = this.HttpClient.GetResponse(request);
315-
316-
if (response.Code != 202)
317-
{
318-
// TODO More useful exceptions for validation errors
319-
throw new CronofyException("Request failed");
320-
}
314+
this.HttpClient.GetValidResponse(request);
321315
}
322316

323317
/// <inheritdoc/>
@@ -334,13 +328,7 @@ public void DeleteAllEventsForCalendars(params string[] calendarIds)
334328
var requestBody = new { calendar_ids = calendarIds };
335329
request.SetJsonBody(requestBody);
336330

337-
var response = this.HttpClient.GetResponse(request);
338-
339-
if (response.Code != 202)
340-
{
341-
// TODO More useful exceptions for validation errors
342-
throw new CronofyException("Request failed");
343-
}
331+
this.HttpClient.GetValidResponse(request);
344332
}
345333

346334
/// <inheritdoc/>
@@ -358,12 +346,7 @@ public void DeleteExternalEvent(string calendarId, string eventUid)
358346
var requestBody = new DeleteExternalEventRequest { EventUid = eventUid };
359347
request.SetJsonBody(requestBody);
360348

361-
var response = this.HttpClient.GetResponse(request);
362-
363-
if (response.Code != 202)
364-
{
365-
throw new CronofyException("Request failed");
366-
}
349+
this.HttpClient.GetValidResponse(request);
367350
}
368351

369352
/// <inheritdoc/>
@@ -381,12 +364,7 @@ public void ChangeParticipationStatus(string calendarId, string eventUid, Partic
381364
var requestBody = new { status = status.ToString().ToLower() };
382365
request.SetJsonBody(requestBody);
383366

384-
var response = this.HttpClient.GetResponse(request);
385-
386-
if (response.Code != 202)
387-
{
388-
throw new CronofyException("Request failed");
389-
}
367+
this.HttpClient.GetValidResponse(request);
390368
}
391369

392370
/// <inheritdoc/>

src/Cronofy/CronofyOAuthClient.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,7 @@ public void RevokeAuthorization(RevokeAuthorizationOptions options)
179179

180180
request.SetJsonBody(requestBody);
181181

182-
var response = this.HttpClient.GetResponse(request);
183-
184-
if (response.Code != 200)
185-
{
186-
throw new CronofyResponseException("Request failed", response);
187-
}
182+
this.HttpClient.GetValidResponse(request);
188183
}
189184

190185
/// <inheritdoc/>

src/Cronofy/ICronofyAccountClient.cs

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,6 @@ public interface ICronofyAccountClient : ICronofyUserInfoClient
241241
/// <exception cref="CronofyException">
242242
/// Thrown if an error is encountered whilst making the request.
243243
/// </exception>
244-
/// <remarks>
245-
/// TODO Validation exceptions.
246-
/// </remarks>
247244
void UpsertEvent(string calendarId, IBuilder<UpsertEventRequest> eventBuilder);
248245

249246
/// <summary>
@@ -263,9 +260,6 @@ public interface ICronofyAccountClient : ICronofyUserInfoClient
263260
/// <exception cref="CronofyException">
264261
/// Thrown if an error is encountered whilst making the request.
265262
/// </exception>
266-
/// <remarks>
267-
/// TODO Validation exceptions.
268-
/// </remarks>
269263
void UpsertEvent(string calendarId, UpsertEventRequest eventRequest);
270264

271265
/// <summary>
@@ -285,9 +279,6 @@ public interface ICronofyAccountClient : ICronofyUserInfoClient
285279
/// <exception cref="CronofyException">
286280
/// Thrown if an error is encountered whilst making the request.
287281
/// </exception>
288-
/// <remarks>
289-
/// TODO Validation exceptions.
290-
/// </remarks>
291282
void DeleteEvent(string calendarId, string eventId);
292283

293284
/// <summary>
@@ -296,9 +287,6 @@ public interface ICronofyAccountClient : ICronofyUserInfoClient
296287
/// <exception cref="CronofyException">
297288
/// Thrown if an error is encountered whilst making the request.
298289
/// </exception>
299-
/// <remarks>
300-
/// TODO Validation exceptions.
301-
/// </remarks>
302290
void DeleteAllEvents();
303291

304292
/// <summary>
@@ -313,9 +301,6 @@ public interface ICronofyAccountClient : ICronofyUserInfoClient
313301
/// <exception cref="CronofyException">
314302
/// Thrown if an error is encountered whilst making the request.
315303
/// </exception>
316-
/// <remarks>
317-
/// TODO Validation exceptions.
318-
/// </remarks>
319304
void DeleteAllEventsForCalendars(params string[] calendarIds);
320305

321306
/// <summary>
@@ -335,9 +320,6 @@ public interface ICronofyAccountClient : ICronofyUserInfoClient
335320
/// <exception cref="CronofyException">
336321
/// Thrown if an error is encountered whilst making the request.
337322
/// </exception>
338-
/// <remarks>
339-
/// TODO Validation exceptions.
340-
/// </remarks>
341323
void DeleteExternalEvent(string calendarId, string eventUid);
342324

343325
/// <summary>
@@ -360,9 +342,6 @@ public interface ICronofyAccountClient : ICronofyUserInfoClient
360342
/// <exception cref="CronofyException">
361343
/// Thrown if an error is encountered whilst making the request.
362344
/// </exception>
363-
/// <remarks>
364-
/// TODO Validation exceptions.
365-
/// </remarks>
366345
void ChangeParticipationStatus(string calendarId, string eventUid, ParticipationStatus status);
367346

368347
/// <summary>
@@ -380,9 +359,6 @@ public interface ICronofyAccountClient : ICronofyUserInfoClient
380359
/// <exception cref="CronofyException">
381360
/// Thrown if an error is encountered whilst making the request.
382361
/// </exception>
383-
/// <remarks>
384-
/// TODO Validation exceptions.
385-
/// </remarks>
386362
Channel CreateChannel(string callbackUrl);
387363

388364
/// <summary>
@@ -401,9 +377,6 @@ public interface ICronofyAccountClient : ICronofyUserInfoClient
401377
/// <exception cref="CronofyException">
402378
/// Thrown if an error is encountered whilst making the request.
403379
/// </exception>
404-
/// <remarks>
405-
/// TODO Validation exceptions.
406-
/// </remarks>
407380
Channel CreateChannel(IBuilder<CreateChannelRequest> channelBuilder);
408381

409382
/// <summary>
@@ -421,9 +394,6 @@ public interface ICronofyAccountClient : ICronofyUserInfoClient
421394
/// <exception cref="CronofyException">
422395
/// Thrown if an error is encountered whilst making the request.
423396
/// </exception>
424-
/// <remarks>
425-
/// TODO Validation exceptions.
426-
/// </remarks>
427397
Channel CreateChannel(CreateChannelRequest channelRequest);
428398

429399
/// <summary>

src/Cronofy/ICronofyOAuthClient.cs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ public interface ICronofyOAuthClient
3131
/// <exception cref="CronofyException">
3232
/// Thrown if an error is encountered whilst making the request.
3333
/// </exception>
34-
/// <remarks>
35-
/// TODO Validation exceptions.
36-
/// </remarks>
3734
OAuthToken GetTokenFromCode(string code, string redirectUri);
3835

3936
/// <summary>
@@ -53,9 +50,6 @@ public interface ICronofyOAuthClient
5350
/// <exception cref="CronofyException">
5451
/// Thrown if an error is encountered whilst making the request.
5552
/// </exception>
56-
/// <remarks>
57-
/// TODO Validation exceptions.
58-
/// </remarks>
5953
OAuthToken GetTokenFromRefreshToken(string refreshToken);
6054

6155
/// <summary>
@@ -74,9 +68,6 @@ public interface ICronofyOAuthClient
7468
/// <exception cref="CronofyException">
7569
/// Thrown if an error is encountered whilst making the request.
7670
/// </exception>
77-
/// <remarks>
78-
/// TODO Validation exceptions.
79-
/// </remarks>
8071
OAuthToken ApplicationCalendar(string applicationCalendarId);
8172

8273
/// <summary>

test/Cronofy.Test/CronofyAccountClientTests/BulkDelete.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,54 @@ public void CanDeleteAllForCalendars()
3434

3535
this.Client.DeleteAllEventsForCalendars(calendar1, calendar2);
3636
}
37+
38+
[Test]
39+
public void DeleteAllThrowsCronofyResponseExceptionOnFailure()
40+
{
41+
// Realistically this method should not result in validation errors as there are no user-provided inputs.
42+
// But we can simulate error case handling here anyway to ensure this method is handling validation failures.
43+
this.Http.Stub(
44+
HttpDelete
45+
.Url("https://api.cronofy.com/v1/events")
46+
.RequestHeader("Authorization", "Bearer " + AccessToken)
47+
.RequestHeader("Content-Type", "application/json; charset=utf-8")
48+
.RequestBody("{\"delete_all\":true}")
49+
.ResponseCode(422)
50+
.ResponseBody(
51+
"{\"errors:\":{\"delete_all\":[{\"key\":\"errors.must_be_boolean\",\"description\":\"must be a boolean value, either true or false\"}}"));
52+
53+
var exception = Assert.Throws<CronofyResponseException>(() => this.Client.DeleteAllEvents());
54+
55+
Assert.AreEqual(exception.Message, "Validation failed");
56+
Assert.AreEqual(exception.Response.Code, 422);
57+
Assert.AreEqual(
58+
exception.Response.Body,
59+
"{\"errors:\":{\"delete_all\":[{\"key\":\"errors.must_be_boolean\",\"description\":\"must be a boolean value, either true or false\"}}");
60+
}
61+
62+
[Test]
63+
public void DeleteAllForCalendarsThrowsCronofyResponseExceptionOnFailure()
64+
{
65+
const string calendar1 = "cal_1234_5678";
66+
const string calendar2 = "cal_8765_4321";
67+
68+
this.Http.Stub(
69+
HttpDelete
70+
.Url("https://api.cronofy.com/v1/events")
71+
.RequestHeader("Authorization", "Bearer " + AccessToken)
72+
.RequestHeader("Content-Type", "application/json; charset=utf-8")
73+
.RequestBodyFormat("{{\"calendar_ids\":[\"{0}\",\"{1}\"]}}", calendar1, calendar2)
74+
.ResponseCode(422)
75+
.ResponseBody(
76+
"{\"errors:\":{\"calendar_ids\":[{\"key\":\"errors.invalid_calendar_ids\",\"description\":\"One or more of the calendar IDs provided was invalid\"}}"));
77+
78+
var exception = Assert.Throws<CronofyResponseException>(() => this.Client.DeleteAllEventsForCalendars(calendar1, calendar2));
79+
80+
Assert.AreEqual(exception.Message, "Validation failed");
81+
Assert.AreEqual(exception.Response.Code, 422);
82+
Assert.AreEqual(
83+
exception.Response.Body,
84+
"{\"errors:\":{\"calendar_ids\":[{\"key\":\"errors.invalid_calendar_ids\",\"description\":\"One or more of the calendar IDs provided was invalid\"}}");
85+
}
3786
}
3887
}

test/Cronofy.Test/CronofyAccountClientTests/ChangeParticipationStatus.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ internal sealed class ChangeParticipationStatus : Base
1111
[TestCase(ParticipationStatus.Accepted, "accepted")]
1212
[TestCase(ParticipationStatus.Tentative, "tentative")]
1313
[TestCase(ParticipationStatus.Declined, "declined")]
14-
public void CanDeleteEvent(ParticipationStatus status, string expectedStatus)
14+
public void CanChangeParticipationStatus(ParticipationStatus status, string expectedStatus)
1515
{
1616
this.Http.Stub(
1717
HttpPost
@@ -23,5 +23,22 @@ public void CanDeleteEvent(ParticipationStatus status, string expectedStatus)
2323

2424
this.Client.ChangeParticipationStatus(CalendarId, EventId, status);
2525
}
26+
27+
[Test]
28+
public void ChangeParticipationStatusThrowsCronofyResponseExceptionOnFailure()
29+
{
30+
this.Http.Stub(
31+
HttpPost
32+
.Url("https://api.cronofy.com/v1/calendars/" + CalendarId + "/events/" + EventId + "/participation_status")
33+
.RequestHeader("Authorization", "Bearer " + AccessToken)
34+
.RequestHeader("Content-Type", "application/json; charset=utf-8")
35+
.RequestBody("{\"status\":\"declined\"}")
36+
.ResponseCode(404));
37+
38+
var exception = Assert.Throws<CronofyResponseException>(() => this.Client.ChangeParticipationStatus(CalendarId, EventId, ParticipationStatus.Declined));
39+
40+
Assert.AreEqual(exception.Message, "Not found");
41+
Assert.AreEqual(exception.Response.Code, 404);
42+
}
2643
}
2744
}

test/Cronofy.Test/CronofyAccountClientTests/DeleteEvent.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,49 @@ public void CanDeleteExternalEvent()
3737

3838
this.Client.DeleteExternalEvent(CalendarId, eventUid);
3939
}
40+
41+
[Test]
42+
public void DeleteEventThrowsCronofyResponseExceptionOnFailure()
43+
{
44+
const string eventId = "qTtZdczOccgaPncGJaCiLg";
45+
46+
// Assume the calendar could not be found
47+
this.Http.Stub(
48+
HttpDelete
49+
.Url("https://api.cronofy.com/v1/calendars/" + CalendarId + "/events")
50+
.RequestHeader("Authorization", "Bearer " + AccessToken)
51+
.RequestHeader("Content-Type", "application/json; charset=utf-8")
52+
.RequestBodyFormat(@"{{""event_id"":""{0}""}}", eventId)
53+
.ResponseCode(404));
54+
55+
var exception = Assert.Throws<CronofyResponseException>(() => this.Client.DeleteEvent(CalendarId, eventId));
56+
57+
Assert.AreEqual(exception.Message, "Not found");
58+
Assert.AreEqual(exception.Response.Code, 404);
59+
}
60+
61+
[Test]
62+
public void DeleteExternalEventThrowsCronofyResponseExceptionOnFailure()
63+
{
64+
const string eventUid = "external_event_id";
65+
66+
this.Http.Stub(
67+
HttpDelete
68+
.Url("https://api.cronofy.com/v1/calendars/" + CalendarId + "/events")
69+
.RequestHeader("Authorization", "Bearer " + AccessToken)
70+
.RequestHeader("Content-Type", "application/json; charset=utf-8")
71+
.RequestBodyFormat(@"{{""event_uid"":""{0}""}}", eventUid)
72+
.ResponseCode(422)
73+
.ResponseBody(
74+
"{\"errors\":{\"event_uid\":[{\"key\":\"errors.must_be_external_event_uid\",\"description\":\"event uid must be a valid external event uid\"}]}"));
75+
76+
var exception = Assert.Throws<CronofyResponseException>(() => this.Client.DeleteExternalEvent(CalendarId, eventUid));
77+
78+
Assert.AreEqual(exception.Message, "Validation failed");
79+
Assert.AreEqual(exception.Response.Code, 422);
80+
Assert.AreEqual(
81+
exception.Response.Body,
82+
"{\"errors\":{\"event_uid\":[{\"key\":\"errors.must_be_external_event_uid\",\"description\":\"event uid must be a valid external event uid\"}]}");
83+
}
4084
}
4185
}

test/Cronofy.Test/CronofyOAuthClientTests/RevokeToken.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,32 @@ public void CanRevokeWithRemovePiiRequest()
7676
RequestPiiErasure = true,
7777
});
7878
}
79+
80+
[Test]
81+
public void RevokeAuthorizationThrowsCronofyResponseExceptionOnFailure()
82+
{
83+
const string sub = "acc_1234567890";
84+
85+
this.http.Stub(
86+
HttpPost
87+
.Url("https://app.cronofy.com/oauth/token/revoke")
88+
.RequestHeader("Content-Type", "application/json; charset=utf-8")
89+
.RequestBodyFormat(
90+
"{{\"client_id\":\"{0}\",\"client_secret\":\"{1}\",\"sub\":\"{2}\",\"request_pii_erasure\":{3}}}",
91+
ClientId, ClientSecret, sub, "true") // Need to provide bool as string to avoid incorrect serialization
92+
.ResponseCode(400)
93+
.ResponseBody("{\"error\":\"invalid_client\"}"));
94+
95+
var exception = Assert.Throws<CronofyResponseException>(() =>
96+
this.client.RevokeAuthorization(new RevokeAuthorizationOptions
97+
{
98+
Sub = sub,
99+
RequestPiiErasure = true,
100+
}));
101+
102+
Assert.AreEqual(exception.Message, "Request failed - code=400");
103+
Assert.AreEqual(exception.Response.Code, 400);
104+
Assert.AreEqual(exception.Response.Body, "{\"error\":\"invalid_client\"}");
105+
}
79106
}
80107
}

0 commit comments

Comments
 (0)