Skip to content

Commit

Permalink
Merge branch 'dev' into issue/OSOE-663
Browse files Browse the repository at this point in the history
  • Loading branch information
milosh-96 committed Dec 29, 2024
2 parents a108ab4 + bbb6f90 commit 78ee42c
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 33 deletions.
84 changes: 51 additions & 33 deletions Lombiq.Tests.UI/Helpers/CloudflareHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Refit;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
Expand All @@ -15,26 +16,41 @@ namespace Lombiq.Tests.UI.Helpers;
internal static class CloudflareHelper
{
private static readonly SemaphoreSlim _semaphore = new(1, 1);
private static int _referenceCount;

private static string _currentIp;
private static string _ipAccessRuleId;
private static readonly ConcurrentDictionary<string, int> _referenceCounts = new();
private static readonly ConcurrentDictionary<string, string> _ipAccessRuleIds = new();
private static ICloudflareApi _cloudflareApi;

/// <summary>
/// Executes a while maintaining a Cloudflare IP Access Rule allowing the current IP address to access the site
/// without getting an anti-bot challenge.
/// </summary>
/// <remarks>
/// <para>
/// Maintaining references is necessary because a) we want to create only the minimally necessary Rules (i.e. no
/// separate rule per test, to keep API communication to a minimum) and b) we can't know when the whole test suite
/// starts and ends, only when individual tests do.
/// </para>
/// </remarks>
public static async Task ExecuteWrappedInIpAccessRuleManagementAsync(
Func<Task> testAsync,
string cloudflareAccountId,
string cloudflareApiToken,
ITestOutputHelper testOutputHelper)
{
var currentIp = await GetPublicIpAsync();

testOutputHelper.WriteLineTimestampedAndDebug(
"Current Cloudflare IP Access Rule reference count before entering semaphore: {0}.", _referenceCount);
"Current Cloudflare IP Access Rule reference count for IP {0} before entering semaphore: {1}.",
currentIp,
_referenceCounts.GetOrAdd(currentIp, 0));

await _semaphore.WaitAsync();
Interlocked.Increment(ref _referenceCount);
_referenceCounts.AddOrUpdate(currentIp, 1, (_, count) => count + 1);

testOutputHelper.WriteLineTimestampedAndDebug(
"Current Cloudflare IP Access Rule reference count after entering semaphore: {0}.", _referenceCount);
"Current Cloudflare IP Access Rule reference count for IP {0} after entering semaphore: {1}.",
currentIp,
_referenceCounts[currentIp]);

try
{
Expand All @@ -43,18 +59,16 @@ public static async Task ExecuteWrappedInIpAccessRuleManagementAsync(
AuthorizationHeaderValueGetter = (_, _) => Task.FromResult(cloudflareApiToken),
});

_currentIp ??= _cloudflareApi != null ? await GetPublicIpAsync() : string.Empty;

if (_ipAccessRuleId == null)
if (!_ipAccessRuleIds.ContainsKey(currentIp))
{
testOutputHelper.WriteLineTimestampedAndDebug("Creating a Cloudflare IP Access Rule for the IP {0}.", _currentIp);
testOutputHelper.WriteLineTimestampedAndDebug("Creating a Cloudflare IP Access Rule for the IP {0}.", currentIp);

// Delete any pre-existing rules for the current IP first.
string preexistingRuleId = null;
await ReliabilityHelper.DoWithRetriesAndCatchesAsync(
async () =>
{
var rulesResponse = await _cloudflareApi.GetIpAccessRulesAsync(cloudflareAccountId, _currentIp);
var rulesResponse = await _cloudflareApi.GetIpAccessRulesAsync(cloudflareAccountId, currentIp);
preexistingRuleId = rulesResponse.Result?.FirstOrDefault()?.Id;
return rulesResponse.Success;
});
Expand All @@ -74,29 +88,29 @@ await ReliabilityHelper.DoWithRetriesAndCatchesAsync(
var createResponse = await _cloudflareApi.CreateIpAccessRuleAsync(cloudflareAccountId, new IpAccessRuleRequest
{
Mode = "whitelist",
Configuration = new IpAccessRuleConfiguration { Target = "ip", Value = _currentIp },
Configuration = new IpAccessRuleConfiguration { Target = "ip", Value = currentIp },
Notes = "Temporarily allow a remote UI test from GitHub Actions.",
});

_ipAccessRuleId = createResponse.Result?.Id;
_ipAccessRuleIds[currentIp] = createResponse.Result?.Id;

return createResponse.Success && _ipAccessRuleId != null;
return createResponse.Success && _ipAccessRuleIds[currentIp] != null;
});

ThrowIfNotSuccess(createResponseResult, _currentIp, "didn't save properly");
ThrowIfNotSuccess(createResponseResult, currentIp, "didn't save properly");

// Wait for the rule to appear, to make sure that it's active.
var ruleCheckRequestResult = await ReliabilityHelper.DoWithRetriesAndCatchesAsync(
async () =>
{
var rulesResponse = await _cloudflareApi.GetIpAccessRulesAsync(cloudflareAccountId);
return rulesResponse.Success && rulesResponse.Result.Exists(rule => rule.Id == _ipAccessRuleId);
return rulesResponse.Success && rulesResponse.Result.Exists(rule => rule.Id == _ipAccessRuleIds[currentIp]);
});

ThrowIfNotSuccess(ruleCheckRequestResult, _currentIp, "didn't get activated");
ThrowIfNotSuccess(ruleCheckRequestResult, currentIp, "didn't get activated");

testOutputHelper.WriteLineTimestampedAndDebug(
"Created a Cloudflare IP Access Rule for the IP {0} (Rule ID: {1}).", _currentIp, _ipAccessRuleId);
"Created a Cloudflare IP Access Rule for the IP {0} (Rule ID: {1}).", currentIp, _ipAccessRuleIds[currentIp]);
}
}
finally
Expand All @@ -111,35 +125,36 @@ await ReliabilityHelper.DoWithRetriesAndCatchesAsync(
finally
{
testOutputHelper.WriteLineTimestampedAndDebug(
"Current Cloudflare IP Access Rule reference count after the test (including this test): {0}.", _referenceCount);
"Current Cloudflare IP Access Rule reference count for IP {0} after the test (including this test): {1}.",
currentIp,
_referenceCounts[currentIp]);

// Clean up the IP access rule.
if (_ipAccessRuleId != null && Interlocked.Decrement(ref _referenceCount) == 0)
if (_ipAccessRuleIds.TryGetValue(currentIp, out string oldIpAccessRuleId) &&
_referenceCounts.AddOrUpdate(currentIp, 0, (_, count) => count - 1) == 0)
{
testOutputHelper.WriteLineTimestampedAndDebug(
"Removing the Cloudflare IP Access Rule for the IP {0} (Rule ID: {1}) since this test has the last reference to it.",
_currentIp,
_ipAccessRuleId);

var oldIpAccessRuleId = _ipAccessRuleId;
currentIp,
oldIpAccessRuleId);

var deleteSucceededResult = await DeleteIpAccessRuleWithRetriesAsync(cloudflareAccountId, _ipAccessRuleId);
var deleteSucceededResult = await DeleteIpAccessRuleWithRetriesAsync(cloudflareAccountId, oldIpAccessRuleId);

if (deleteSucceededResult.IsSuccess) _ipAccessRuleId = null;
if (deleteSucceededResult.IsSuccess) _ipAccessRuleIds.TryRemove(currentIp, out _);

ThrowIfNotSuccess(deleteSucceededResult, _currentIp, "couldn't be deleted");
ThrowIfNotSuccess(deleteSucceededResult, currentIp, "couldn't be deleted");

testOutputHelper.WriteLineTimestampedAndDebug(
"Removed the Cloudflare IP Access Rule for the IP {0} (Rule ID: {1}) since this test had the last reference to it.",
_currentIp,
currentIp,
oldIpAccessRuleId);
}
else
{
testOutputHelper.WriteLineTimestampedAndDebug(
"Not removing the Cloudflare IP Access Rule for the IP {0} (Rule ID: {1}) since the current reference count is NOT 0.",
_currentIp,
_ipAccessRuleId);
currentIp,
_ipAccessRuleIds[currentIp]);
}
}
}
Expand All @@ -152,8 +167,11 @@ private static async Task<string> GetPublicIpAsync()
var ipRequestResult = await ReliabilityHelper.DoWithRetriesAndCatchesAsync(
async () =>
{
ip = await client.GetStringAsync("https://api.ipify.org");
return true;
var response = await client.GetStringAsync("https://cloudflare.com/cdn-cgi/trace");
var lines = response.Split('\n');
var ipLine = Array.Find(lines, line => line.StartsWithOrdinalIgnoreCase("ip="));
ip = ipLine?.Split('=')[1];
return !string.IsNullOrEmpty(ip);
});

if (!ipRequestResult.IsSuccess)
Expand Down
28 changes: 28 additions & 0 deletions renovate.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
'$schema': 'https://docs.renovatebot.com/renovate-schema.json',
'extends': ['github>Lombiq/renovate-config:default-orchard-core-submodule.json5'],
packageRules: [
{
groupName: 'Atata',
matchPackageNames: [
'Atata*',
],
},
{
groupName: 'Deque.AxeCore',
matchPackageNames: [
'Deque.AxeCore.*',
],
},
{
groupName: 'Microsoft.Extensions',
matchPackageNames: [
'Microsoft.Extensions.*',
],
},
{
groupName: 'All packages',
matchUpdateTypes: ['*'],
},
],
}

0 comments on commit 78ee42c

Please sign in to comment.