Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Common.Tests/Common.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>OpenShock.Common.Tests</AssemblyName>
<RootNamespace>OpenShock.Common.Tests</RootNamespace>
<Company>OpenShock</Company>
<Product>OpenShock.Common.Tests</Product>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="TUnit" Version="0.1.877" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>

</Project>
159 changes: 159 additions & 0 deletions Common.Tests/Geo/Alpha2CountryCodeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using OpenShock.Common.Geo;

namespace OpenShock.Common.Tests.Geo;

public class Alpha2CountryCodeTests
{
[Test]
[Arguments("US", 'U', 'S')]
[Arguments("DE", 'D', 'E')]
public async Task ValidCode_ShouldParse(string str, char char1, char char2)
{
// Act
Alpha2CountryCode c = str;

// Assert
await Assert.That(c.Char1).IsEqualTo(char1);
await Assert.That(c.Char2).IsEqualTo(char2);
}

[Test]
[Arguments("E")]
[Arguments("INVALID")]
public async Task InvalidCharCount_ShouldThrow_InvalidLength(string str)
{
// Act
var ex = await Assert.ThrowsAsync<ArgumentException>(() =>
{
Alpha2CountryCode c = str;
return Task.CompletedTask;
});

// Assert
await Assert.That(ex.Message).IsEqualTo("Country code must be exactly 2 characters long (Parameter 'str')");
}

[Test]
[Arguments("us")]
[Arguments("Us")]
[Arguments("uS")]
[Arguments("12")]
[Arguments("U1")]
[Arguments("1U")]
[Arguments("ÆØ")]
[Arguments(":D")]
public async Task InvalidCharTypes_ShouldThrow(string str)
{
// Act
var ex = await Assert.ThrowsAsync<ArgumentException>(() => { Alpha2CountryCode c = str; return Task.CompletedTask; });

// Assert
await Assert.That(ex.Message).IsEqualTo("Country code must be uppercase ASCII characters only (Parameter 'str')");
}

[Test]
[Arguments("US", 'U', 'S')]
[Arguments("DE", 'D', 'E')]
public async Task TryParseAndValidate_ValidCode_ShouldReturnTrue(string str, char char1, char char2)
{
// Act
var result = Alpha2CountryCode.TryParseAndValidate(str, out var c);

// Assert
await Assert.That(result).IsTrue();
await Assert.That(c.Char1).IsEqualTo(char1);
await Assert.That(c.Char2).IsEqualTo(char2);
}

[Test]
[Arguments("E")]
[Arguments("INVALID")]
[Arguments("us")]
[Arguments("Us")]
[Arguments("uS")]
[Arguments("12")]
[Arguments("U1")]
[Arguments("1U")]
[Arguments("ÆØ")]
[Arguments(":D")]
public async Task TryParseAndValidate_InvalidCode_ShouldReturnFalse(string str)
{
// Act
var result = Alpha2CountryCode.TryParseAndValidate(str, out var c);

// Assert
await Assert.That(result).IsFalse();
await Assert.That(c).IsEqualTo(Alpha2CountryCode.UnknownCountry);
}

[Test]
[Arguments("US", "US", 0x5553_5553)]
[Arguments("US", "DE", 0x4445_5553)]
[Arguments("DE", "US", 0x4445_5553)]
public async Task GetCombinedHashCode_ShouldReturnCombined(string str1, string str2, int expected)
{
// Act
var result = Alpha2CountryCode.GetCombinedHashCode(str1, str2);

// Assert
await Assert.That(result).IsEqualTo(expected);
}

[Test]
[Arguments("US", 0x5553)]
[Arguments("DE", 0x4445)]
[Arguments("NO", 0x4E4F)]
public async Task GetHashcode_ShouldReturnHash(string str, int expected)
{
// Arrange
Alpha2CountryCode code = str;

// Act
var result = code.GetHashCode();

// Assert
await Assert.That(result).IsEqualTo(expected); // "US"
}

[Test]
public async Task IsUnknown_ShouldReturnTrue()
{
// Arrange
Alpha2CountryCode code = Alpha2CountryCode.UnknownCountry;

// Act
var result = code.IsUnknown();

// Assert
await Assert.That(result).IsTrue();
}

[Test]
public async Task UnknownCountry_IsEqualTo_XX()
{
// Arrange
Alpha2CountryCode code = "XX";

// Act
var result = code == Alpha2CountryCode.UnknownCountry;

// Assert
await Assert.That(result).IsTrue();
}

[Test]
[Arguments("US")]
[Arguments("DE")]
[Arguments("NO")]
public async Task IsUnknown_Known_ShouldReturnFalse(string str)
{
// Arrange
Alpha2CountryCode code = str;

// Act
var result = code.IsUnknown();

// Assert
await Assert.That(result).IsFalse();
}
}
35 changes: 35 additions & 0 deletions Common.Tests/Geo/DistanceLookupTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using OpenShock.Common.Geo;
using TUnit.Assertions.Extensions.Numbers;

namespace OpenShock.Common.Tests.Geo;

public class DistanceLookupTests
{
[Test]
[Arguments("US", "US", 0f)]
[Arguments("US", "DE", 7861.5f)]
public async Task TryGetDistanceBetween_ValidCountries(string str1, string str2, float expectedDistance)
{
// Act
var result = DistanceLookup.TryGetDistanceBetween(str1, str2, out var distance);

// Assert
await Assert.That(result).IsTrue();
await Assert.That(distance).IsEqualToWithTolerance(expectedDistance, 0.1f);
}

[Test]
[Arguments("US", "XX")]
[Arguments("XX", "US")]
[Arguments("XX", "XX")]
[Arguments("EZ", "PZ")]
public async Task TryGetDistanceBetween_UnknownCountry(string str1, string str2)
{
// Act
var result = DistanceLookup.TryGetDistanceBetween(str1, str2, out var distance);

// Assert
await Assert.That(result).IsFalse();
await Assert.That(distance).IsEqualTo(0f);
}
}
17 changes: 17 additions & 0 deletions Common.Tests/Utils/HashingUtilsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using OpenShock.Common.Utils;

namespace OpenShock.Common.Tests.Utils;

public class HashingUtilsTests
{
[Test]
[Arguments("test", "9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08")]
public async Task HashSha256(string str, string expectedHash)
{
// Act
var result = HashingUtils.HashSha256(str);

// Assert
await Assert.That(result).IsEqualTo(expectedHash);
}
}
2 changes: 1 addition & 1 deletion Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ public static class Constants

public static readonly TimeSpan PasswordResetRequestLifetime = TimeSpan.FromHours(1);

public const double DistanceToAndromedaGalaxyInKm = 2.401E19;
public const float DistanceToAndromedaGalaxyInKm = 2.401E19f;
}
19 changes: 16 additions & 3 deletions Common/Geo/Alpha2CountryCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public static bool TryParseAndValidate(string str, [MaybeNullWhen(false)] out Al
{
if (str.Length != 2 || !char.IsAsciiLetterUpper(str[0]) || !char.IsAsciiLetterUpper(str[1]))
{
code = default;
code = UnknownCountry;
return false;
}

Expand All @@ -22,15 +22,28 @@ public static bool TryParseAndValidate(string str, [MaybeNullWhen(false)] out Al
public static implicit operator Alpha2CountryCode(string str)
{
if (str.Length != 2)
throw new ArgumentOutOfRangeException(nameof(str), "String input must be exactly 2 chars");
throw new ArgumentOutOfRangeException(nameof(str), "Country code must be exactly 2 characters long");

if (!char.IsAsciiLetterUpper(str[0]) || !char.IsAsciiLetterUpper(str[1]))
throw new ArgumentOutOfRangeException(nameof(str), "String input must be upper characters only");
throw new ArgumentOutOfRangeException(nameof(str), "Country code must be uppercase ASCII characters only");

return new Alpha2CountryCode(str[0], str[1]);
}

public static int GetCombinedHashCode(Alpha2CountryCode code1, Alpha2CountryCode code2)
{
int a = code1.GetHashCode();
int b = code2.GetHashCode();

int v = (a << 16) | b;

if (a > b) v = int.RotateLeft(v, 16);

return v;
}

public bool IsUnknown() => this == UnknownCountry;

public override int GetHashCode() => (Char1 << 8) | Char2;
public override string ToString() => new([Char1, Char2]);
}
43 changes: 21 additions & 22 deletions Common/Geo/DistanceLookup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,6 @@ namespace OpenShock.Common.Geo;

public static class DistanceLookup
{
/// <summary>
/// Generates a unique ID for a pair of countries, regardless of order
/// </summary>
/// <param name="code1"></param>
/// <param name="code2"></param>
/// <returns></returns>
private static int CreateId(Alpha2CountryCode code1, Alpha2CountryCode code2)
{
int a = (code1.Char1 << 8) | code1.Char2;
int b = (code2.Char1 << 8) | code2.Char2;

int v = (a << 16) | b;

if (a > b) v = int.RotateLeft(v, 16);

return v;
}

/// <summary>
/// Generates all distances between all countries in the world, along with their unique ID
///
Expand All @@ -35,11 +17,14 @@ private static IEnumerable<KeyValuePair<int, float>> GetAllDistances()
{
var first = CountryInfo.Countries[i];

for (int j = i; j < CountryInfo.Countries.Length; j++)
// Same country, no need to calculate distance
yield return new KeyValuePair<int, float>(Alpha2CountryCode.GetCombinedHashCode(first.CountryCode, first.CountryCode), 0f);

for (int j = i + 1; j < CountryInfo.Countries.Length; j++)
{
var second = CountryInfo.Countries[j];

var id = CreateId(first.CountryCode, second.CountryCode);
var id = Alpha2CountryCode.GetCombinedHashCode(first.CountryCode, second.CountryCode);
var dist = MathUtils.CalculateHaversineDistance(first.Latitude, first.Longitude, second.Latitude, second.Longitude);

yield return new KeyValuePair<int, float>(id, dist);
Expand All @@ -52,6 +37,20 @@ private static IEnumerable<KeyValuePair<int, float>> GetAllDistances()
/// </summary>
private static readonly FrozenDictionary<int, float> Distances = GetAllDistances().ToFrozenDictionary(); // Create a frozen dictionary for fast lookups

public static bool TryGetDistanceBetween(Alpha2CountryCode alpha2CountryA, Alpha2CountryCode alpha2CountryB, out float distance) =>
Distances.TryGetValue(CreateId(alpha2CountryA, alpha2CountryB), out distance);
public static bool TryGetDistanceBetween(Alpha2CountryCode alpha2CountryA, Alpha2CountryCode alpha2CountryB, out float distance)
{
if (alpha2CountryA.IsUnknown() || alpha2CountryB.IsUnknown())
{
distance = 0f;
return false;
}

if (!Distances.TryGetValue(Alpha2CountryCode.GetCombinedHashCode(alpha2CountryA, alpha2CountryB), out distance))
{
distance = 0f;
return false;
}

return true;
}
}
20 changes: 16 additions & 4 deletions OpenShockBackend.sln
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{185193B9-9A3D-4DDA-9750-A3CE83717FD3}"
# Visual Studio Version 17
VisualStudioVersion = 17.11.35208.52
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "API", "API\API.csproj", "{185193B9-9A3D-4DDA-9750-A3CE83717FD3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{61A32805-02D5-44BA-A0E5-0B46C99896DD}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csproj", "{61A32805-02D5-44BA-A0E5-0B46C99896DD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveControlGateway", "LiveControlGateway\LiveControlGateway.csproj", "{83BB2593-B344-42DB-8C61-D1A331054279}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveControlGateway", "LiveControlGateway\LiveControlGateway.csproj", "{83BB2593-B344-42DB-8C61-D1A331054279}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cron", "Cron\Cron.csproj", "{1A5D3B84-AF07-4884-B317-50BC600F33A8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MigrationHelper", "MigrationHelper\MigrationHelper.csproj", "{7396B01D-5463-4794-B2ED-4A5AE3B89320}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MigrationHelper", "MigrationHelper\MigrationHelper.csproj", "{7396B01D-5463-4794-B2ED-4A5AE3B89320}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Tests", "Common.Tests\Common.Tests.csproj", "{7AED9D47-F0B0-4644-A154-EE386A81C85D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -36,5 +41,12 @@ Global
{7396B01D-5463-4794-B2ED-4A5AE3B89320}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7396B01D-5463-4794-B2ED-4A5AE3B89320}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7396B01D-5463-4794-B2ED-4A5AE3B89320}.Release|Any CPU.Build.0 = Release|Any CPU
{7AED9D47-F0B0-4644-A154-EE386A81C85D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7AED9D47-F0B0-4644-A154-EE386A81C85D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7AED9D47-F0B0-4644-A154-EE386A81C85D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7AED9D47-F0B0-4644-A154-EE386A81C85D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal