Fix TelemetryPublisherFirestore.cs bugs not re-authenticating properly.
This commit is contained in:
parent
61bccfbdf1
commit
9ad323e755
5 changed files with 57 additions and 47 deletions
|
@ -5,8 +5,8 @@ using System.Text.Json;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using sNetHttp = System.Net.Http;
|
||||
using sNetHttpHeaders = System.Net.Http.Headers;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace NucuCar.Domain.Http
|
||||
{
|
||||
|
@ -14,7 +14,7 @@ namespace NucuCar.Domain.Http
|
|||
/// A simple HttpClient wrapper designed to make it easier to work with web requests with media type application/json.
|
||||
/// It implements a simple retry mechanism.
|
||||
/// </summary>
|
||||
public class HttpClient : IDisposable
|
||||
public class MinimalHttpClient : IDisposable
|
||||
{
|
||||
#region Fields
|
||||
|
||||
|
@ -54,26 +54,26 @@ namespace NucuCar.Domain.Http
|
|||
protected int timeout;
|
||||
// ReSharper restore InconsistentNaming
|
||||
|
||||
private readonly sNetHttp.HttpClient _httpClient;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public HttpClient()
|
||||
public MinimalHttpClient()
|
||||
{
|
||||
_httpClient = new sNetHttp.HttpClient();
|
||||
_httpClient = new HttpClient();
|
||||
maxRetries = 3;
|
||||
timeout = 10000;
|
||||
Logger = null;
|
||||
}
|
||||
|
||||
public HttpClient(string baseAddress) : this()
|
||||
public MinimalHttpClient(string baseAddress) : this()
|
||||
{
|
||||
_httpClient.BaseAddress = new Uri(baseAddress);
|
||||
}
|
||||
|
||||
public HttpClient(string baseAddress, int maxRetries) : this(baseAddress)
|
||||
public MinimalHttpClient(string baseAddress, int maxRetries) : this(baseAddress)
|
||||
{
|
||||
MaxRetries = maxRetries;
|
||||
}
|
||||
|
@ -82,10 +82,15 @@ namespace NucuCar.Domain.Http
|
|||
|
||||
#region Public Methods
|
||||
|
||||
public void ClearAuthorizationHeader()
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public void Authorization(string scheme, string token)
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new sNetHttpHeaders.AuthenticationHeaderValue(scheme, token);
|
||||
new AuthenticationHeaderValue(scheme, token);
|
||||
}
|
||||
|
||||
public void Authorization(string token)
|
||||
|
@ -93,29 +98,29 @@ namespace NucuCar.Domain.Http
|
|||
Authorization("Bearer", token);
|
||||
}
|
||||
|
||||
public async Task<sNetHttp.HttpResponseMessage> GetAsync(string path)
|
||||
public async Task<HttpResponseMessage> GetAsync(string path)
|
||||
{
|
||||
var request = _makeRequest(sNetHttp.HttpMethod.Get, path);
|
||||
var request = _makeRequest(HttpMethod.Get, path);
|
||||
return await SendAsync(request);
|
||||
}
|
||||
|
||||
public async Task<sNetHttp.HttpResponseMessage> PostAsync(string path, Dictionary<string, object> data)
|
||||
public async Task<HttpResponseMessage> PostAsync(string path, Dictionary<string, object> data)
|
||||
{
|
||||
var request = _makeRequest(sNetHttp.HttpMethod.Post, path);
|
||||
var request = _makeRequest(HttpMethod.Post, path);
|
||||
request.Content = _makeContent(data);
|
||||
return await SendAsync(request);
|
||||
}
|
||||
|
||||
public async Task<sNetHttp.HttpResponseMessage> PutAsync(string path, Dictionary<string, object> data)
|
||||
public async Task<HttpResponseMessage> PutAsync(string path, Dictionary<string, object> data)
|
||||
{
|
||||
var request = _makeRequest(sNetHttp.HttpMethod.Put, path);
|
||||
var request = _makeRequest(HttpMethod.Put, path);
|
||||
request.Content = _makeContent(data);
|
||||
return await SendAsync(request);
|
||||
}
|
||||
|
||||
public async Task<sNetHttp.HttpResponseMessage> DeleteAsync(string path, Dictionary<string, object> data)
|
||||
public async Task<HttpResponseMessage> DeleteAsync(string path, Dictionary<string, object> data)
|
||||
{
|
||||
var request = _makeRequest(sNetHttp.HttpMethod.Delete, path);
|
||||
var request = _makeRequest(HttpMethod.Delete, path);
|
||||
request.Content = _makeContent(data);
|
||||
return await SendAsync(request);
|
||||
}
|
||||
|
@ -125,23 +130,28 @@ namespace NucuCar.Domain.Http
|
|||
/// </summary>
|
||||
/// <param name="requestMessage">The request to make.</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<sNetHttp.HttpResponseMessage> SendAsync(sNetHttp.HttpRequestMessage requestMessage)
|
||||
public virtual async Task<HttpResponseMessage> SendAsync(HttpRequestMessage requestMessage)
|
||||
{
|
||||
var currentRetry = 0;
|
||||
sNetHttp.HttpResponseMessage responseMessage = null;
|
||||
HttpResponseMessage responseMessage = null;
|
||||
|
||||
while (currentRetry < maxRetries)
|
||||
{
|
||||
try
|
||||
{
|
||||
responseMessage = await _sendAsync(requestMessage);
|
||||
// We need a request copy because we can't send the same request multiple times.
|
||||
var requestCopy = new HttpRequestMessage(requestMessage.Method, requestMessage.RequestUri);
|
||||
requestCopy.Headers.Authorization = requestMessage.Headers.Authorization;
|
||||
requestCopy.Content = requestMessage.Content;
|
||||
|
||||
responseMessage = await _sendAsync(requestCopy);
|
||||
break;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
Logger?.LogError($"Request timeout for {requestMessage.RequestUri}!");
|
||||
}
|
||||
catch (sNetHttp.HttpRequestException e)
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
// The request failed due to an underlying issue such as network connectivity, DNS failure,
|
||||
// server certificate validation or timeout.
|
||||
|
@ -172,9 +182,9 @@ namespace NucuCar.Domain.Http
|
|||
/// </summary>
|
||||
/// <param name="data">A dictionary representing JSON data.</param>
|
||||
/// <returns></returns>
|
||||
private sNetHttp.StringContent _makeContent(Dictionary<string, object> data)
|
||||
private StringContent _makeContent(Dictionary<string, object> data)
|
||||
{
|
||||
return new sNetHttp.StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json");
|
||||
return new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -183,11 +193,11 @@ namespace NucuCar.Domain.Http
|
|||
/// <param name="method">The HttpMethod to use</param>
|
||||
/// <param name="path">The path, whether it is relative to the base or a new one.</param>
|
||||
/// <returns></returns>
|
||||
private sNetHttp.HttpRequestMessage _makeRequest(sNetHttp.HttpMethod method, string path)
|
||||
private HttpRequestMessage _makeRequest(HttpMethod method, string path)
|
||||
{
|
||||
var uri = _httpClient.BaseAddress == null ? new Uri(path) : new Uri(_httpClient.BaseAddress, path);
|
||||
|
||||
var requestMessage = new sNetHttp.HttpRequestMessage
|
||||
|
||||
var requestMessage = new HttpRequestMessage
|
||||
{
|
||||
Method = method,
|
||||
RequestUri = uri
|
||||
|
@ -202,17 +212,17 @@ namespace NucuCar.Domain.Http
|
|||
/// </summary>
|
||||
/// <param name="requestMessage"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<sNetHttp.HttpResponseMessage> _sendAsync(sNetHttp.HttpRequestMessage requestMessage)
|
||||
private async Task<HttpResponseMessage> _sendAsync(HttpRequestMessage requestMessage)
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
sNetHttp.HttpResponseMessage response;
|
||||
HttpResponseMessage response;
|
||||
|
||||
// Make sure we cancel after a certain timeout.
|
||||
cts.CancelAfter(timeout);
|
||||
try
|
||||
{
|
||||
response = await _httpClient.SendAsync(requestMessage,
|
||||
sNetHttp.HttpCompletionOption.ResponseContentRead,
|
||||
HttpCompletionOption.ResponseContentRead,
|
||||
cts.Token);
|
||||
}
|
||||
finally
|
||||
|
@ -242,9 +252,9 @@ namespace NucuCar.Domain.Http
|
|||
/// <summary>
|
||||
/// Extension used to deserialize the body of a HttpResponseMessage into Json.
|
||||
/// </summary>
|
||||
/// <param name="responseMessage">The HttpResponseMessage message. <see cref="sNetHttp.HttpResponseMessage"/></param>
|
||||
/// <param name="responseMessage">The HttpResponseMessage message. <see cref="HttpResponseMessage"/></param>
|
||||
/// <returns>A JsonElement. <see cref="JsonElement"/></returns>
|
||||
public static async Task<JsonElement> GetJson(this sNetHttp.HttpResponseMessage responseMessage)
|
||||
public static async Task<JsonElement> GetJson(this HttpResponseMessage responseMessage)
|
||||
{
|
||||
return JsonSerializer.Deserialize<JsonElement>(await responseMessage.Content.ReadAsStringAsync());
|
||||
}
|
|
@ -4,14 +4,14 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace NucuCar.Domain.Http
|
||||
{
|
||||
public class MockHttpClient : Domain.Http.HttpClient
|
||||
public class MockMinimalHttpClient : MinimalHttpClient
|
||||
{
|
||||
public List<HttpRequestMessage> SendAsyncArgCalls;
|
||||
public List<HttpResponseMessage> SendAsyncResponses;
|
||||
|
||||
private int _sendAsyncCallCounter;
|
||||
|
||||
public MockHttpClient(string baseAddress) : base(baseAddress)
|
||||
public MockMinimalHttpClient(string baseAddress) : base(baseAddress)
|
||||
{
|
||||
_sendAsyncCallCounter = 0;
|
||||
SendAsyncArgCalls = new List<HttpRequestMessage>();
|
|
@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging;
|
|||
using NucuCar.Domain.Http;
|
||||
using NucuCar.Domain.Utilities;
|
||||
using NucuCar.Telemetry.Abstractions;
|
||||
using HttpClient = NucuCar.Domain.Http.HttpClient;
|
||||
|
||||
namespace NucuCar.Telemetry.Publishers
|
||||
{
|
||||
|
@ -29,7 +28,7 @@ namespace NucuCar.Telemetry.Publishers
|
|||
/// </summary>
|
||||
public class TelemetryPublisherFirestore : TelemetryPublisher
|
||||
{
|
||||
protected HttpClient HttpClient;
|
||||
protected MinimalHttpClient HttpClient;
|
||||
|
||||
private string _idToken;
|
||||
private DateTime _authorizationExpiryTime;
|
||||
|
@ -67,13 +66,15 @@ namespace NucuCar.Telemetry.Publishers
|
|||
// Setup HttpClient
|
||||
var requestUrl = $"https://firestore.googleapis.com/v1/projects/{firestoreProjectId}/" +
|
||||
$"databases/(default)/documents/{firestoreCollection}/";
|
||||
HttpClient = new HttpClient(requestUrl) {Timeout = timeout, Logger = Logger};
|
||||
HttpClient = new MinimalHttpClient(requestUrl) {Timeout = timeout, Logger = Logger};
|
||||
Logger?.LogInformation($"Initialized {nameof(TelemetryPublisherFirestore)}");
|
||||
Logger?.LogInformation($"ProjectId: {firestoreProjectId}; CollectionName: {firestoreCollection}.");
|
||||
}
|
||||
|
||||
private async Task SetupAuthorization()
|
||||
{
|
||||
HttpClient.ClearAuthorizationHeader();
|
||||
|
||||
// https://cloud.google.com/identity-platform/docs/use-rest-api#section-sign-in-email-password
|
||||
var requestUrl = $"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={_webApiKey}";
|
||||
var data = new Dictionary<string, object>()
|
||||
|
@ -82,7 +83,7 @@ namespace NucuCar.Telemetry.Publishers
|
|||
["password"] = _webPassword,
|
||||
["returnSecureToken"] = true
|
||||
};
|
||||
|
||||
|
||||
var response = await HttpClient.PostAsync(requestUrl, data);
|
||||
|
||||
if (response?.StatusCode == HttpStatusCode.OK)
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace NucuCar.Telemetry
|
|||
/// Creates an instance of <see cref="TelemetryPublisher"/>. See <see cref="TelemetryPublisherType"/>
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the publisher. <see cref="TelemetryPublisherType"/> </param>
|
||||
/// <param name="connectionString">Device connection string for Microsoft Azure IoT hub device.</param>
|
||||
/// <param name="connectionString">Device connection string for the telemetry publisher.</param>
|
||||
/// <param name="telemetrySource">String that is used to identify the source of the telemetry data.</param>
|
||||
/// <param name="logger">An <see cref="ILogger"/> logger instance. </param>
|
||||
/// <returns>A <see cref="TelemetryPublisher"/> instance.</returns>
|
||||
|
@ -25,7 +25,7 @@ namespace NucuCar.Telemetry
|
|||
Guard.ArgumentNotNullOrWhiteSpace(nameof(connectionString), connectionString);
|
||||
Guard.ArgumentNotNullOrWhiteSpace(nameof(telemetrySource), telemetrySource);
|
||||
Guard.ArgumentNotNull(nameof(logger), logger);
|
||||
var opts = new TelemetryPublisherOptions()
|
||||
var opts = new TelemetryPublisherOptions
|
||||
{ConnectionString = connectionString, TelemetrySource = telemetrySource, Logger = logger};
|
||||
return SpawnPublisher(type, opts);
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ namespace NucuCar.Telemetry
|
|||
{
|
||||
Guard.ArgumentNotNullOrWhiteSpace(nameof(connectionString), connectionString);
|
||||
var opts = new TelemetryPublisherOptions()
|
||||
{ConnectionString = connectionString, TelemetrySource = "TelemetryPublisherAzure"};
|
||||
{ConnectionString = connectionString, TelemetrySource = "NucuCar.Sensors"};
|
||||
return SpawnPublisher(type, opts);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ using NucuCar.Domain.Http;
|
|||
using NucuCar.Telemetry;
|
||||
using NucuCar.Telemetry.Publishers;
|
||||
using Xunit;
|
||||
using HttpClient = NucuCar.Domain.Http.HttpClient;
|
||||
|
||||
namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
|
||||
{
|
||||
|
@ -25,7 +24,7 @@ namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
|
|||
_mockData = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
public void SetHttpClient(HttpClient client)
|
||||
public void SetHttpClient(MinimalHttpClient client)
|
||||
{
|
||||
HttpClient = client;
|
||||
}
|
||||
|
@ -57,7 +56,7 @@ namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
private void Test_Construct_BadCollectiontName()
|
||||
private void Test_Construct_BadCollectionName()
|
||||
{
|
||||
// Setup
|
||||
var opts = new TelemetryPublisherOptions()
|
||||
|
@ -78,7 +77,7 @@ namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
|
|||
ConnectionString = "ProjectId=test;CollectionName=test;WebApiKey=TAPIKEY;WebApiEmail=t@emai.com;WebApiPassword=tpass"
|
||||
};
|
||||
var publisher = new MockTelemetryPublisherFirestore(opts);
|
||||
var mockHttpClient = new MockHttpClient("http://testing.com");
|
||||
var mockHttpClient = new MockMinimalHttpClient("http://testing.com");
|
||||
var authResponse = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{Content = new StringContent("{\"idToken\": \"1\",\"expiresIn\": \"3600\"}")};
|
||||
mockHttpClient.SendAsyncResponses.Add(authResponse);
|
||||
|
@ -106,7 +105,7 @@ namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
|
|||
ConnectionString = "ProjectId=test;CollectionName=test;WebApiKey=TAPIKEY;WebApiEmail=t@emai.com;WebApiPassword=tpass"
|
||||
};
|
||||
var publisher = new MockTelemetryPublisherFirestore(opts);
|
||||
var mockHttpClient = new MockHttpClient("http://testing.com");
|
||||
var mockHttpClient = new MockMinimalHttpClient("http://testing.com");
|
||||
var authResponse = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{Content = new StringContent("{\"idToken\": \"1\",\"expiresIn\": \"3600\"}")};
|
||||
mockHttpClient.SendAsyncResponses.Add(authResponse);
|
||||
|
@ -130,7 +129,7 @@ namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
|
|||
ConnectionString = "ProjectId=test;CollectionName=test"
|
||||
};
|
||||
var publisher = new MockTelemetryPublisherFirestore(opts);
|
||||
var mockHttpClient = new MockHttpClient("http://testing.com");
|
||||
var mockHttpClient = new MockMinimalHttpClient("http://testing.com");
|
||||
mockHttpClient.SendAsyncResponses.Add(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
publisher.SetHttpClient(mockHttpClient);
|
||||
|
@ -155,7 +154,7 @@ namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
|
|||
"ProjectId=test;CollectionName=test;WebApiKey=TAPIKEY;WebApiEmail=t@emai.com;WebApiPassword=tpass"
|
||||
};
|
||||
var publisher = new MockTelemetryPublisherFirestore(opts);
|
||||
var mockHttpClient = new MockHttpClient("http://testing.com");
|
||||
var mockHttpClient = new MockMinimalHttpClient("http://testing.com");
|
||||
|
||||
mockHttpClient.SendAsyncResponses.Add(new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{Content = new StringContent("{\"idToken\": \"1\",\"expiresIn\": \"0\"}")});
|
||||
|
|
Loading…
Reference in a new issue