Skip to content

Commit

Permalink
Merge pull request #193 from AdrianJSClark/186-implementing-savecooki…
Browse files Browse the repository at this point in the history
…es-and-restorecookies-does-not-prevent-unnecessary-logins

Consider Client Logged In Cookies are Restored
  • Loading branch information
AdrianJSClark authored Nov 26, 2023
2 parents 902ae2d + 8574c53 commit ced98dc
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public abstract class BaseIntegrationFixture<TClient> : IDisposable

internal TClient Client { get; set; } = default!;

protected iRacingDataClientOptions BaseSetUp()
protected virtual iRacingDataClientOptions BaseSetUp()
{
Configuration = new ConfigurationBuilder()
.SetBasePath(TestContext.CurrentContext.TestDirectory)
Expand Down
39 changes: 39 additions & 0 deletions src/Aydsko.iRacingData.IntegrationTests/Logins/LoginTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// © 2023 Adrian Clark
// This file is licensed to you under the MIT license.

using System.Net;
using Aydsko.iRacingData.Exceptions;

namespace Aydsko.iRacingData.IntegrationTests.Logins;

internal sealed class LoginTests : BaseIntegrationFixture<DataClient>
{
private CookieCollection? _cookiesToRestore;
private CookieCollection? _cookiesFromSave;

[OneTimeSetUp]
public void OneTimeSetUp()
{
var options = BaseSetUp();
options.RestoreCookies = () => _cookiesToRestore ?? [];
options.SaveCookies = (cookies) => _cookiesFromSave = cookies;

Client = new DataClient(HttpClient, new TestLogger<DataClient>(), options, CookieContainer);
}

[Test]
public async Task TestFailedLoginFromBadCookieRestoreAsync()

Check warning on line 25 in src/Aydsko.iRacingData.IntegrationTests/Logins/LoginTests.cs

View workflow job for this annotation

GitHub Actions / Build & Test / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
_cookiesToRestore = [new Cookie("test", "test", "/", ".iracing.com")];
_cookiesFromSave = [new Cookie("test", "test", "/", "localhost")];

Assert.Multiple(() =>
{
Assert.That(async () => await Client.GetClubHistoryLookupsAsync(2022, 1).ConfigureAwait(false),
Throws.Exception.InstanceOf<iRacingUnauthorizedResponseException>());

Assert.That(_cookiesFromSave, Is.Empty);
Assert.That(Client.IsLoggedIn, Is.False);
});
}
}
44 changes: 44 additions & 0 deletions src/Aydsko.iRacingData.UnitTests/LoginViaOptionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// © 2023 Adrian Clark
// This file is licensed to you under the MIT license.

using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;

Expand Down Expand Up @@ -123,6 +124,49 @@ public async Task ValidateLoginRequestViaMethod(string username, string password
Assert.That(lookups.Data, Is.Not.Null.Or.Empty);
}

[TestCaseSource(nameof(GetTestCasesWithUnencodedPasswords))]
public async Task LoginIsNotCalledIfCookiesAreSuccessfullyRestored(string username, string password, string expectedEncodedPassword)
{
var restoreCookiesWasCalled = false;
var saveCookiesWasCalled = false;

Check warning on line 131 in src/Aydsko.iRacingData.UnitTests/LoginViaOptionsTests.cs

View workflow job for this annotation

GitHub Actions / Build & Test / build

The variable 'saveCookiesWasCalled' is assigned but its value is never used

Check warning on line 131 in src/Aydsko.iRacingData.UnitTests/LoginViaOptionsTests.cs

View workflow job for this annotation

GitHub Actions / Build & Test / build

The variable 'saveCookiesWasCalled' is assigned but its value is never used

var cookieContainer = new CookieContainer();
cookieContainer.Add(new Cookie("authtoken_members", "%7B%22authtoken%22%3A%7B%22authcode%22%3A%22AbC123%22%2C%22email%22%3A%22test.user%40example.com%22%7D%7D", "/", ".iracing.com"));
cookieContainer.Add(new Cookie("irsso_members", "ABC123DEF456", "/", ".iracing.com"));
cookieContainer.Add(new Cookie("r_members", "", "/", ".iracing.com"));

var cookieCollection = cookieContainer.GetCookies(new Uri("https://members-ng.iracing.com"));

var options = new iRacingDataClientOptions
{
RestoreCookies = () =>
{
restoreCookiesWasCalled = true;
return cookieCollection;
},
SaveCookies = (newCollection) =>
{
cookieCollection = newCollection;
saveCookiesWasCalled = true;
},
};

await MessageHandler.QueueResponsesAsync(nameof(CapturedResponseValidationTests.GetLookupsSuccessfulAsync), false).ConfigureAwait(false);

var sut = new DataClient(HttpClient,
new TestLogger<DataClient>(),
options,
CookieContainer);

sut.UseUsernameAndPassword(username, password);

var lookups = await sut.GetLookupsAsync(CancellationToken.None).ConfigureAwait(false);

Assert.That(restoreCookiesWasCalled, Is.True);
Assert.That(MessageHandler.RequestContent.Count, Is.EqualTo(2));
}


#pragma warning disable CA1024 // Use properties where appropriate - NUnit's API requires these to be methods.
public static IEnumerable<TestCaseData> GetTestCases()
{
Expand Down
5 changes: 4 additions & 1 deletion src/Aydsko.iRacingData/Common/ErrorResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@ public class ErrorResponse
public string? ErrorCode { get; set; }

[JsonPropertyName("note")]
public string? ErrorDescription { get; set; }
public string? Note { get; set; }

[JsonPropertyName("message")]
public string? Message { get; set; }
}
42 changes: 42 additions & 0 deletions src/Aydsko.iRacingData/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@
<Left>lib/netstandard2.0/Aydsko.iRacingData.dll</Left>
<Right>lib/net6.0/Aydsko.iRacingData.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Aydsko.iRacingData.Common.ErrorResponse.get_ErrorDescription</Target>
<Left>lib/net6.0/Aydsko.iRacingData.dll</Left>
<Right>lib/net6.0/Aydsko.iRacingData.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Aydsko.iRacingData.Common.ErrorResponse.set_ErrorDescription(System.String)</Target>
<Left>lib/net6.0/Aydsko.iRacingData.dll</Left>
<Right>lib/net6.0/Aydsko.iRacingData.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Aydsko.iRacingData.Exceptions.iRacingUnauthorizedResponseException.Create</Target>
<Left>lib/net6.0/Aydsko.iRacingData.dll</Left>
<Right>lib/net6.0/Aydsko.iRacingData.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Aydsko.iRacingData.Member.MemberChartDataPoint.get_Day</Target>
Expand Down Expand Up @@ -73,6 +94,27 @@
<Left>lib/netstandard2.0/Aydsko.iRacingData.dll</Left>
<Right>lib/net6.0/Aydsko.iRacingData.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Aydsko.iRacingData.Common.ErrorResponse.get_ErrorDescription</Target>
<Left>lib/netstandard2.0/Aydsko.iRacingData.dll</Left>
<Right>lib/netstandard2.0/Aydsko.iRacingData.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Aydsko.iRacingData.Common.ErrorResponse.set_ErrorDescription(System.String)</Target>
<Left>lib/netstandard2.0/Aydsko.iRacingData.dll</Left>
<Right>lib/netstandard2.0/Aydsko.iRacingData.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Aydsko.iRacingData.Exceptions.iRacingUnauthorizedResponseException.Create</Target>
<Left>lib/netstandard2.0/Aydsko.iRacingData.dll</Left>
<Right>lib/netstandard2.0/Aydsko.iRacingData.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Aydsko.iRacingData.Common.EventType</Target>
Expand Down
17 changes: 14 additions & 3 deletions src/Aydsko.iRacingData/DataClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2013,6 +2013,14 @@ private async Task LoginInternalAsync(CancellationToken cancellationToken)
&& options.RestoreCookies() is CookieCollection savedCookies)
{
cookieContainer.Add(savedCookies);

// Assume we're logged in if we have cookies for our target domain
if (cookieContainer.GetCookies(new Uri("https://members-ng.iracing.com")).Count > 0)
{
IsLoggedIn = true;
logger.LoginCookiesRestored(options.Username!);
return;
}
}

string? encodedHash = null;
Expand Down Expand Up @@ -2172,13 +2180,13 @@ private void HandleUnsuccessfulResponse(HttpResponseMessage httpResponse, string
else
{
var errorResponse = JsonSerializer.Deserialize<ErrorResponse>(content);
errorDescription = errorResponse?.ErrorDescription;
errorDescription = errorResponse?.Note ?? errorResponse?.Message ?? "An error occurred.";

exception = errorResponse switch
{
{ ErrorCode: "Site Maintenance" } => new iRacingInMaintenancePeriodException(errorResponse.ErrorDescription ?? "iRacing services are down for maintenance."),
{ ErrorCode: "Site Maintenance" } => new iRacingInMaintenancePeriodException(errorResponse.Note ?? "iRacing services are down for maintenance."),
{ ErrorCode: "Forbidden" } => iRacingForbiddenResponseException.Create(),
{ ErrorCode: "Unauthorized" } => iRacingUnauthorizedResponseException.Create(),
{ ErrorCode: "Unauthorized" } => iRacingUnauthorizedResponseException.Create(errorResponse.Message),
_ => null
};
}
Expand All @@ -2194,6 +2202,9 @@ private void HandleUnsuccessfulResponse(HttpResponseMessage httpResponse, string
{
// Unauthorized might just be our session expired
IsLoggedIn = false;

// Clear any saved cookies
options.SaveCookies?.Invoke([]);
}

logger.ErrorResponse(errorDescription, exception);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ namespace Aydsko.iRacingData.Exceptions;
[Serializable]
public class iRacingUnauthorizedResponseException : iRacingDataClientException
{
public static iRacingUnauthorizedResponseException Create()
public static iRacingUnauthorizedResponseException Create(string? message)
{
return new("The iRacing API returned an \"Unauthorized\" response code.");
return new($"The iRacing API returned an \"Unauthorized\" response code{(string.IsNullOrEmpty(message)?"":" with message \"" + message + "\"")}.");
}

public iRacingUnauthorizedResponseException()
Expand Down
11 changes: 10 additions & 1 deletion src/Aydsko.iRacingData/LoggingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public static class LoggingExtensions
new EventId(EventIdLoginSuccessful, nameof(LoginSuccessful)),
"Authenticated successfully as {UserEmail}");

private static readonly Action<ILogger, string, Exception?> loginCookiesRestored = LoggerMessage.Define<string>(LogLevel.Information,
new EventId(EventIdLoginSuccessful, nameof(LoginSuccessful)),
"Authenticated successfully as {UserEmail} using restored cookies");

private static readonly Action<ILogger, int?, int?, DateTimeOffset?, Exception?> rateLimitsUpdated = LoggerMessage.Define<int?, int?, DateTimeOffset?>(LogLevel.Debug,
new EventId(EventIdRateLimitsUpdated, nameof(RateLimitsUpdated)),
"Currently have {RateLimitRemaining} calls left from {RateLimitTotal} resetting at {RateLimitResetInstant}");
Expand All @@ -28,7 +32,7 @@ public static class LoggingExtensions

private static readonly Action<ILogger, string?, Exception?> errorResponse = LoggerMessage.Define<string?>(LogLevel.Trace,
new EventId(EventIdErrorResponseTrace, nameof(ErrorResponse)),
"Error response from iRacing API: {ErrorDescription}");
"Error response from iRacing API: {Note}");

private static readonly Action<ILogger, int?, int?, HttpStatusCode?, string?, Exception?> failedToRetrieveChunkError = LoggerMessage.Define<int?, int?, HttpStatusCode?, string?>(LogLevel.Error,
new EventId(EventIdFailedToRetrieveChunkError, nameof(FailedToRetrieveChunkError)),
Expand All @@ -41,6 +45,11 @@ public static void LoginSuccessful(this ILogger logger, string userEmail)
loginSuccessful(logger, userEmail, null);
}

public static void LoginCookiesRestored(this ILogger logger, string userEmail)
{
loginCookiesRestored(logger, userEmail, null);
}

public static void RateLimitsUpdated(this ILogger logger, int? rateLimitRemaining, int? rateLimit, DateTimeOffset? rateLimitResetInstant)
{
rateLimitsUpdated(logger, rateLimitRemaining, rateLimit, rateLimitResetInstant, null);
Expand Down
5 changes: 5 additions & 0 deletions src/Aydsko.iRacingData/Package Release Notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@

- Fix "Interval" property on "SubsessionChartLap" so it doesn't throw an exception when driver has been lapped, which is a valid situation.

- Fix "Implementing SaveCookies and RestoreCookies does not prevent unnecessary logins" (Issue: #186)
- If you implement the "SaveCookies" and "RestoreCookies" methods and the cookies are for the correct domain, then the library will not attempt to login again.



Contributions:

- From Tobias Zürcher (https://github.com/tobiaszuercher):
Expand Down

0 comments on commit ced98dc

Please sign in to comment.