Skip to content

Commit f9b673c

Browse files
committed
CalendarService: Support timeMin, timeMax, timeZone
1 parent d42fe44 commit f9b673c

File tree

6 files changed

+220
-14
lines changed

6 files changed

+220
-14
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.Net;
2+
using GoogleApis.Blazor.Extensions;
3+
using Moq.Contrib.HttpClient;
4+
5+
namespace GoogleApis.Blazor.Test.Extensions;
6+
7+
public class HttpClientExtensionsTests
8+
{
9+
private static Mock<HttpMessageHandler> GetMockHttpMessageHandler()
10+
{
11+
var handler = new Mock<HttpMessageHandler>();
12+
handler.SetupRequest(HttpMethod.Get, "https://www.helloworld.com")
13+
.ReturnsResponse(HttpStatusCode.OK, "");
14+
return handler;
15+
}
16+
17+
[Fact]
18+
public async Task GetWithQueryStringsAsync__WhenQueryStringsParam_Null__Returns_ArgumentNullException()
19+
{
20+
var handler = GetMockHttpMessageHandler();
21+
22+
var client = handler.CreateClient();
23+
24+
await Assert.ThrowsAsync<ArgumentNullException>(() => client.GetWithQueryStringsAsync("https://www.helloworld.com"));
25+
}
26+
27+
[Fact]
28+
public async Task GetWithQueryStringsAsync__WhenQueryStringsParam_Empty__Returns_ArgumentNullException()
29+
{
30+
var handler = GetMockHttpMessageHandler();
31+
32+
var client = handler.CreateClient();
33+
34+
await Assert.ThrowsAsync<ArgumentNullException>(() => client.GetWithQueryStringsAsync("https://www.helloworld.com", Array.Empty<string>()));
35+
}
36+
37+
[Fact]
38+
public async Task GetWithQueryStringsAsync__WhenQueryStringsParam_NotEven__Returns_ArgumentOutOfRangeException()
39+
{
40+
var handler = GetMockHttpMessageHandler();
41+
42+
var client = handler.CreateClient();
43+
44+
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => client.GetWithQueryStringsAsync("https://www.helloworld.com", new []{ "", "", ""}));
45+
}
46+
47+
[Fact]
48+
public async Task GetWithQueryStringsAsync__WhenQueryStringsParam_HasNullKey__Returns_ArgumentException()
49+
{
50+
var handler = GetMockHttpMessageHandler();
51+
52+
var client = handler.CreateClient();
53+
54+
await Assert.ThrowsAsync<ArgumentException>(() => client.GetWithQueryStringsAsync("https://www.helloworld.com", new []{ null, "something"}));
55+
}
56+
57+
[Fact]
58+
public async Task GetWithQueryStringsAsync__ReturnsExpectedResult()
59+
{
60+
var handler = new Mock<HttpMessageHandler>();
61+
handler.SetupRequest(HttpMethod.Get, "https://www.helloworld.com?fruit=apple&size=large")
62+
.ReturnsResponse(HttpStatusCode.OK, "hello_world");
63+
var client = new HttpClient(handler.Object);
64+
65+
var response = await client.GetWithQueryStringsAsync("https://www.helloworld.com", new[]
66+
{
67+
"fruit", "apple",
68+
"size", "large",
69+
});
70+
71+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
72+
var body = await response.Content.ReadAsStringAsync();
73+
Assert.Equal("hello_world", body);
74+
}
75+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
13+
<PackageReference Include="Moq.Contrib.HttpClient" Version="1.3.0" />
14+
<PackageReference Include="xunit" Version="2.4.1" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
16+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17+
<PrivateAssets>all</PrivateAssets>
18+
</PackageReference>
19+
<PackageReference Include="coverlet.collector" Version="3.1.2">
20+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
21+
<PrivateAssets>all</PrivateAssets>
22+
</PackageReference>
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<ProjectReference Include="..\GoogleApis.Blazor\GoogleApis.Blazor.csproj" />
27+
</ItemGroup>
28+
29+
</Project>

GoogleApis.Blazor.Test/Usings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
global using Xunit;
2+
global using Moq;

GoogleApis.Blazor.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.1.32421.90
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoogleApis.Blazor", "GoogleApis.Blazor\GoogleApis.Blazor.csproj", "{7460C3F8-290E-4920-AE79-857E37DA1BBD}"
77
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoogleApis.Blazor.Test", "GoogleApis.Blazor.Test\GoogleApis.Blazor.Test.csproj", "{584B3888-D256-47A3-AFE8-EAA8BA341D3A}"
9+
EndProject
810
Global
911
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1012
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
1517
{7460C3F8-290E-4920-AE79-857E37DA1BBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
1618
{7460C3F8-290E-4920-AE79-857E37DA1BBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
1719
{7460C3F8-290E-4920-AE79-857E37DA1BBD}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{584B3888-D256-47A3-AFE8-EAA8BA341D3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{584B3888-D256-47A3-AFE8-EAA8BA341D3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{584B3888-D256-47A3-AFE8-EAA8BA341D3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{584B3888-D256-47A3-AFE8-EAA8BA341D3A}.Release|Any CPU.Build.0 = Release|Any CPU
1824
EndGlobalSection
1925
GlobalSection(SolutionProperties) = preSolution
2026
HideSolutionNode = FALSE

GoogleApis.Blazor/Calendar/CalendarService.cs

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using GoogleApis.Blazor.Auth;
1+
using GoogleApis.Blazor.Auth;
22
using GoogleApis.Blazor.Extensions;
33
using GoogleApis.Blazor.Models;
44
using Microsoft.AspNetCore.Components;
@@ -94,7 +94,7 @@ public async Task<GoogleCalendarListRoot> GetCalendars(int maxResults = 250, boo
9494
return model;
9595
}
9696

97-
AccessToken = AuthService.RefreshAccessToken(_refreshToken);
97+
AccessToken = await AuthService.RefreshAccessToken(_refreshToken);
9898
return await GetCalendars();
9999
}
100100

@@ -117,7 +117,7 @@ public async Task<GoogleCalendarListModel> GetCalendarById(string calendarId, bo
117117
return model;
118118
}
119119

120-
AccessToken = AuthService.RefreshAccessToken(_refreshToken);
120+
AccessToken = await AuthService.RefreshAccessToken(_refreshToken);
121121
return await GetCalendarById(calendarId);
122122
}
123123

@@ -179,7 +179,7 @@ public async Task<GoogleCalendarModel> AddCalendar(GoogleCalendarListModel googl
179179
return model;
180180
}
181181

182-
AccessToken = AuthService.RefreshAccessToken(_refreshToken);
182+
AccessToken = await AuthService.RefreshAccessToken(_refreshToken);
183183
return await AddCalendar(googleCalendarListModel);
184184
}
185185

@@ -209,7 +209,7 @@ public async Task<GoogleCalendarModel> UpdateCalendar(string calendarId, GoogleC
209209
return model;
210210
}
211211

212-
AccessToken = AuthService.RefreshAccessToken(_refreshToken);
212+
AccessToken = await AuthService.RefreshAccessToken(_refreshToken);
213213
return await UpdateCalendar(calendarId, googleCalendarListModel);
214214
}
215215

@@ -234,7 +234,7 @@ public async Task DeleteCalendar(string calendarId, bool forceAccessToken = fals
234234
return;
235235
}
236236

237-
AccessToken = AuthService.RefreshAccessToken(_refreshToken);
237+
AccessToken = await AuthService.RefreshAccessToken(_refreshToken);
238238
await DeleteCalendar(calendarId);
239239
}
240240

@@ -260,7 +260,7 @@ public async Task ClearCalendar(string calendarId, bool forceAccessToken = false
260260
return;
261261
}
262262

263-
AccessToken = AuthService.RefreshAccessToken(_refreshToken);
263+
AccessToken = await AuthService.RefreshAccessToken(_refreshToken);
264264
await ClearCalendar(calendarId);
265265
}
266266

@@ -276,12 +276,19 @@ public async Task ClearCalendar(string calendarId, bool forceAccessToken = false
276276
/// <param name="timeMin"></param>
277277
/// <param name="timeMax"></param>
278278
/// <param name="maxResults">Select how many items to return. Max is 2500.</param>
279+
/// <param name="timeZone">Time zone used in the response. Optional. The default is the time zone of the calendar.</param>
279280
/// <param name="forceAccessToken">If true and access token expired, it automatically calls for new access token with refresh token.</param>
280281
/// <returns></returns>
281-
public async Task<GoogleCalendarEventRoot> GetEvents(DateTime timeMin, DateTime timeMax, string calendarId, int maxResults = 2500, bool forceAccessToken = false)
282+
public async Task<GoogleCalendarEventRoot> GetEvents(DateTime timeMin, DateTime timeMax, string calendarId, int maxResults = 2500, string timeZone = null, bool forceAccessToken = false)
282283
{
283284
var client = HttpClientFactory.CreateClient();
284-
var result = await client.GetAsync($"https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events?access_token={_accessToken}&maxResults={maxResults.ToString()}");
285+
var uri = $"https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events?access_token={_accessToken}&maxResults={maxResults.ToString()}";
286+
287+
var result = await client.GetWithQueryStringsAsync(uri, new[] {
288+
"timeMin", GetProperDateTimeFormat(timeMin),
289+
"timeMax", GetProperDateTimeFormat(timeMax),
290+
"timeZone", timeZone
291+
});
285292

286293
string contentResult = await result.Content.ReadAsStringAsync();
287294
var model = JsonSerializer.Deserialize<GoogleCalendarEventRoot>(contentResult);
@@ -291,7 +298,7 @@ public async Task<GoogleCalendarEventRoot> GetEvents(DateTime timeMin, DateTime
291298
return model;
292299
}
293300

294-
AccessToken = AuthService.RefreshAccessToken(_refreshToken);
301+
AccessToken = await AuthService.RefreshAccessToken(_refreshToken);
295302
return await GetEvents(timeMin, timeMax, calendarId, maxResults);
296303
}
297304

@@ -315,7 +322,7 @@ public async Task<GoogleCalendarEventModel> GetEventById(string eventId, string
315322
return json;
316323
}
317324

318-
AccessToken = AuthService.RefreshAccessToken(_refreshToken);
325+
AccessToken = await AuthService.RefreshAccessToken(_refreshToken);
319326
return await GetEventById(eventId, calendarId);
320327
}
321328

@@ -343,7 +350,7 @@ public async Task<string> AddEvent(GoogleCalendarEventModel calendarEvent, strin
343350
return contentResult;
344351
}
345352

346-
AccessToken = AuthService.RefreshAccessToken(_refreshToken);
353+
AccessToken = await AuthService.RefreshAccessToken(_refreshToken);
347354
await AddEvent(calendarEvent, calendarId);
348355
return "";
349356
}
@@ -385,7 +392,7 @@ public async Task<string> UpdateEvent(GoogleCalendarEventModel newCalendarEvent,
385392
return contentResult;
386393
}
387394

388-
AccessToken = AuthService.RefreshAccessToken(_refreshToken);
395+
AccessToken = await AuthService.RefreshAccessToken(_refreshToken);
389396
await UpdateEvent(newCalendarEvent, eventId, calendarId);
390397
return "";
391398
}
@@ -553,7 +560,7 @@ public async Task DeleteEvent(string eventId, string calendarId, bool forceAcces
553560
return;
554561
}
555562

556-
AccessToken = AuthService.RefreshAccessToken(_refreshToken);
563+
AccessToken = await AuthService.RefreshAccessToken(_refreshToken);
557564
await DeleteEvent(eventId, calendarId);
558565
}
559566

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#pragma warning disable CS1591
2+
using System.ComponentModel;
3+
4+
namespace GoogleApis.Blazor.Extensions
5+
{
6+
public static class HttpClientExtensions
7+
{
8+
/// <summary>
9+
/// Send a GET request to the specified Uri as an asynchronous operation,
10+
/// using the specified key/value string pairs (required) as query strings.
11+
/// <example>
12+
/// For example:
13+
/// <code>
14+
/// var result = await httpClient.GetWithQueryStringsAsync(uri, new[] {
15+
/// "timeMin", GetProperDateTimeFormat(timeMin),
16+
/// "timeMax", GetProperDateTimeFormat(timeMax),
17+
/// "timeZone", timeZone // might be a null value
18+
/// });
19+
/// </code>
20+
/// results in <c>p</c>'s having the value (2,8).
21+
/// </example>
22+
/// </summary>
23+
/// <param name="client"></param>
24+
/// <param name="requestUri">The Uri the request is sent to.</param>
25+
/// <param name="queryParameters">
26+
/// String array representing a set of key/value pairs to append the
27+
/// <paramref>requestUri</paramref> as query strings elements.
28+
///
29+
/// For each pair, the first element is a "key" and second is its "value".
30+
/// Null keys are not permitted and will result in a runtime
31+
/// <typeparamref>ArgumentNullException</typeparamref>. Null values are
32+
/// permitted, and the pair as a whole will be ignored.
33+
/// </param>
34+
/// <returns>The task object representing the asynchronous operation.</returns>
35+
/// <exception cref="System.ArgumentNullException">
36+
/// The <paramref>requestUri</paramref> parameter is required.
37+
/// </exception>
38+
/// <exception cref="System.ArgumentOutOfRangeException">
39+
/// The <paramref>requestUri</paramref> parameter is assumed to be an <c>String</c>
40+
/// array representing key/value pairs, and therefore must be an even number in total.
41+
/// Null values are prohibited with the key, but allowed with the value.
42+
/// </exception>
43+
/// <exception cref="System.ArgumentException">
44+
/// The <paramref>requestUri</paramref> parameter is assumed to be an <c>String</c>
45+
/// array representing key/value pairs, and therefore must be an even number in total.
46+
/// Null values are prohibited with the key, but allowed with the value.
47+
/// </exception>
48+
/// <exception cref="System.InvalidOperationException">
49+
/// The requestUri must be an absolute URI or System.Net.Http.HttpClient.BaseAddress
50+
/// must be set.
51+
/// </exception>
52+
/// <exception cref="System.Net.Http.HttpRequestException">
53+
/// The request failed due to an underlying issue such as network connectivity, DNS
54+
/// failure, server certificate validation or timeout.
55+
/// </exception>
56+
/// <exception cref="System.Threading.Tasks.TaskCanceledException">
57+
/// .NET Core and .NET 5.0 and later only: The request failed due to timeout.
58+
/// </exception>
59+
public static async Task<HttpResponseMessage> GetWithQueryStringsAsync(this HttpClient client, string requestUri, params string[] queryParameters)
60+
{
61+
if (queryParameters == null || queryParameters.Length == 0) {
62+
throw new ArgumentNullException(nameof(queryParameters));
63+
}
64+
if (queryParameters.Length % 2 != 0) { // Isn't even?
65+
throw new ArgumentOutOfRangeException(nameof(queryParameters), "queryParameters is expected to be a set of key/value string pairs, and therefore should be an even number of strings.");
66+
}
67+
68+
var uri = requestUri;
69+
string Separator() => uri.Contains('?') ? "&" : "?";
70+
71+
var numberPairs = queryParameters.Length / 2;
72+
for (var i = 0; i < numberPairs; i++)
73+
{
74+
var key = queryParameters[2 * i];
75+
var value = queryParameters[2 * i + 1];
76+
77+
if (key == null)
78+
throw new ArgumentException("For each pair taken from the queryParameters string array, the first item is treated as a \"key\" and is therefore required.", nameof(queryParameters));
79+
if (value == null) continue;
80+
81+
uri += $"{Separator()}{key}={Uri.EscapeDataString(value)}";
82+
}
83+
84+
return await client.GetAsync(uri);
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)