Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add shared tests.
Browse files Browse the repository at this point in the history
dnwpark committed Jan 21, 2025
1 parent 6f3bbd2 commit c2a295e
Showing 1 changed file with 713 additions and 0 deletions.
713 changes: 713 additions & 0 deletions tests/EdgeDB.Tests.Unit/SharedClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,713 @@
using EdgeDB.Abstractions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace EdgeDB.Tests.Unit;

[TestClass]
public class SharedClientTests
{
[TestMethod]
public void TestConnectParams()
{
StreamReader reader = new("shared-client-testcases/connection_testcases.json");
List<TestCase>? testcases = JsonSerializer.Deserialize<List<TestCase>>(reader.ReadToEnd());
if (testcases is null)
{
throw new JsonException("Failed to read 'connection_testcases.json.\n"
+ "Is the 'shared-client-testcases' submodule initialised? "
+ "Try running 'git submodule update --init'.");
}

foreach ((int textIndex, TestCase testCase) in testcases.Select((x, i) => (i, x)))
{
Console.WriteLine(testCase.Name);

if (testCase.FileSystem is not null
&& (
!(testCase.Platform is null && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) ||
!(testCase.Platform == "windows" && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) ||
!(testCase.Platform == "macos" && RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
))
{
// skipping unsupported platform test
continue;
}

if ((testCase.Result is null) == (testCase.Error is null))
{
throw new Exception("invalid test case: either \"result\" or \"error\" key has to be specified");
}

EdgeDBConnection? expectedConnection = GetExpectedConnection(testCase);
Exception? expectedException = testCase.Error?.GetExpectedException();
Result result = ParseConnection(testCase);

if (expectedConnection is not null)
{
Expect(result, expectedConnection);
}
else if (expectedException is not null)
{
ExpectException(result, expectedException);
}
}

Assert.IsTrue(false);
}

private static void Expect(Result result, EdgeDBConnection expected)
{
if (result.Exception is not null)
{
Console.WriteLine($" {result.Exception.Message}");
}
Assert.IsNotNull(result.Connection);
var actual = result.Connection;

Assert.AreEqual(expected.Username, actual.Username);
Assert.AreEqual(expected.Password, actual.Password);
static string ResolveHostname(string hostname)
{
return hostname == "localhost" ? "127.0.0.1" : hostname;
}
Assert.AreEqual(ResolveHostname(expected.Hostname), ResolveHostname(actual.Hostname));
Assert.AreEqual(expected.Port, actual.Port);
Assert.AreEqual(expected.Database, actual.Database);
Assert.AreEqual(expected.TLSCertificateAuthority, actual.TLSCertificateAuthority);
Assert.AreEqual(expected.TLSServerName, actual.TLSServerName);
Assert.AreEqual(expected.TLSSecurity, actual.TLSSecurity);
CollectionAssert.AreEqual(expected.ServerSettings, actual.ServerSettings);
}

private static void ExpectException(Result result, Exception expected)
{
Assert.IsNotNull(result.Exception);
Assert.IsInstanceOfType(result.Exception, expected.GetType(),
$"Exception type {expected.GetType()} expected but got {result.Exception.GetType()}");
Assert.AreEqual(expected.Message, result.Exception.Message);
}

private static Result ParseConnection(TestCase testCase)
{
try
{
MockSystemProvider mockSystem = new(testCase);

EdgeDBConnection connection = EdgeDBConnection._Parse(
instance: null,
dsn: testCase?.Options?.Instance ?? testCase?.Options?.Dsn,
configure: x => {
TestCase.Credentials? credentials =
JsonSerializer.Deserialize<TestCase.Credentials>(
testCase?.Options?.CredentialsFile is not null ?
mockSystem.FileReadAllText(testCase.Options.CredentialsFile) :
testCase?.Options?.Credentials is not null ?
testCase.Options.Credentials :
"{}"
);

if (testCase?.Options?.Host is not null) x.Hostname = testCase.Options.Host;
if (testCase?.Options?.Port is not null) x.Port = int.Parse(testCase.Options.Port);
if (testCase?.Options?.Database is not null) x.Database = testCase.Options.Database;
if (testCase?.Options?.Branch is not null) x.Branch = testCase.Options.Branch;
if (testCase?.Options?.User is not null) x.Username = testCase.Options.User;
if (testCase?.Options?.Password is not null) x.Password = testCase.Options.Password;
if (testCase?.Options?.SecretKey is not null) x.SecretKey = testCase.Options.SecretKey;
if (testCase?.Options?.TlsCA is not null) x.TLSCertificateAuthority = testCase.Options.TlsCA;
if (testCase?.Options?.TlsCAFile is not null)
{
x.TLSCertificateAuthority = mockSystem.FileReadAllText(testCase.Options.TlsCAFile);
}
if (testCase?.Options?.TlsSecurity is not null) x.TLSSecurity = testCase.Options.TlsSecurity.Value;
if (testCase?.Options?.TlsServerName is not null) x.TLSServerName = testCase.Options.TlsServerName;

if (credentials?.Host is not null) x.Hostname = credentials.Host;
if (credentials?.Port is not null) x.Port = int.Parse(credentials.Port);
if (credentials?.Database is not null) x.Database = credentials.Database;
if (credentials?.Branch is not null) x.Branch = credentials.Branch;
if (credentials?.User is not null) x.Username = credentials.User;
if (credentials?.Password is not null) x.Password = credentials.Password;
if (credentials?.TlsCA is not null) x.TLSCertificateAuthority = credentials.TlsCA;
if (credentials?.TlsSecurity is not null) x.TLSSecurity = credentials.TlsSecurity.Value;
},
autoResolve: false,
platform: mockSystem);

return connection;
}
catch (Exception x)
{
return x;
}
}

private static EdgeDBConnection? GetExpectedConnection(TestCase testCase)
{
if (testCase.Result is null)
{
return null;
}
else
{
EdgeDBConnection connection = new();

if (testCase.Result.User is not null) connection.Username = testCase.Result.User;
if (testCase.Result.Password is not null) connection.Password = testCase.Result.Password;
if (testCase.Result.Address is not null) connection.Hostname = testCase.Result.Address[0];
if (testCase.Result.Address is not null) connection.Port = int.Parse(testCase.Result.Address[1]);
if (testCase.Result.Database is not null) connection.Database = testCase.Result.Database;
if (testCase.Result.TlsCAData is not null) connection.TLSCertificateAuthority = testCase.Result.TlsCAData;
if (testCase.Result.TlsServerName is not null) connection.TLSServerName = testCase.Result.TlsServerName;
if (testCase.Result.TlsSecurity is not null) connection.TLSSecurity = testCase.Result.TlsSecurity.Value;
if (testCase.Result.WaitUntilAvailable is not null)
{
connection.Timeout = EdgeDBConnection.ParseWaitUntilAvailable(testCase.Result.WaitUntilAvailable);
}
if (testCase.Result.ServerSettings is not null) connection.ServerSettings = testCase.Result.ServerSettings;

return connection;
}
}

private class Result
{
public EdgeDBConnection? Connection { get; init; }
public Exception? Exception { get; init; }

public static implicit operator Result(EdgeDBConnection c) => new() {Connection = c};
public static implicit operator Result(Exception x) => new() {Exception = x};
}

class MockSystemProvider : BaseDefaultSystemProvider
{
private readonly string? _homeDir;
private readonly string? _currentDir;
private readonly Dictionary<string, string> _envVars;
private Dictionary<string, string> _files;

public List<string> Warnings { get; } = new();

public MockSystemProvider(TestCase testCase)
{
_homeDir = testCase.FileSystem?.HomeDir;
_currentDir = testCase.FileSystem?.CurrentDir;
_envVars = testCase.EnvVars ?? new();
_files = CacheFiles(testCase?.FileSystem?.Files);
}

private Dictionary<string, string> CacheFiles(Dictionary<string, TestCase.File>? files)
{
return files?.SelectMany(
x => {
string path = x.Key;
TestCase.File file = x.Value;

if (file.Contents is not null)
{
return new List<(string, string)>(){(path, file.Contents)};
}
else
{
if (file.Fields is null)
{
throw new Exception("File must be either string or json object of fields");
}
if (!file.Fields.ContainsKey("project-path"))
{
throw new Exception("File as object must have \"project-path\" field");
}

List<(string,string)> subfiles = new();

string dir = path.Replace("${HASH}", ProjectPathHash(file.Fields["project-path"]));

foreach (KeyValuePair<string, string> field in file.Fields)
{
subfiles.Add((CombinePaths(new string[]{ dir, field.Key }), field.Value));
}

return subfiles;
}
})
.ToDictionary(x => x.Item1, x => x.Item2)
?? new();
}

private string ProjectPathHash(string path)
{
if (IsOSPlatform(OSPlatform.Windows) && !path.StartsWith("\\\\"))
{
path = "\\\\?\\" + path;
}

return Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(path)));
}

public override string GetHomeDir() => _homeDir ?? base.GetHomeDir();

public override string GetCurrentDirectory() => _currentDir ?? base.GetCurrentDirectory();

public override string? GetEnvVariable(string name)
=> _envVars.TryGetValue(name, out var val)
? val
: null;

public override bool FileExists(string path) => _files.ContainsKey(path);

public override string FileReadAllText(string path) => _files[path];

public override void WriteWarning(string message)
=> Warnings.Add(message);
}

class TestCase
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;

[JsonPropertyName("opts")]
public OptionsData? Options { get; init; }

[JsonPropertyName("env")]
public Dictionary<string, string>? EnvVars { get; init; }

[JsonPropertyName("platform")]
public string? Platform { get; init; }

[JsonPropertyName("fs")]
public FileSystemData? FileSystem { get; init; }

[JsonPropertyName("warnings")]
public List<string>? Warnings { get; init; }

[JsonPropertyName("result")]
public ResultData? Result { get; init; }

[JsonPropertyName("error")]
public ErrorData? Error { get; init; }

public class OptionsData
{
[JsonPropertyName("instance")]
public string? Instance { get; init; }

[JsonPropertyName("database")]
public string? Database { get; init; }

[JsonPropertyName("branch")]
public string? Branch { get; init; }

[JsonPropertyName("host")]
public string? Host { get; init; }

[JsonPropertyName("dsn")]
public string? Dsn { get; init; }

[JsonPropertyName("port")]
[JsonConverter(typeof(AsStringConverter))]
public string? Port { get; init; }

[JsonPropertyName("user")]
public string? User { get; init; }

[JsonPropertyName("password")]
public string? Password { get; init; }

[JsonPropertyName("secretKey")]
public string? SecretKey { get; init; }

[JsonPropertyName("credentials")]
public string? Credentials { get; init; }

[JsonPropertyName("credentialsFile")]
public string? CredentialsFile { get; init; }

[JsonPropertyName("tlsCA")]
public string? TlsCA { get; init; }

[JsonPropertyName("tlsCAFile")]
public string? TlsCAFile { get; init; }

[JsonPropertyName("tlsSecurity")]
[JsonConverter(typeof(TLSSecurityModeConverter))]
public TLSSecurityMode? TlsSecurity { get; init; }

[JsonPropertyName("tlsServerName")]
public string? TlsServerName { get; init; }

[JsonPropertyName("waitUntilAvailable")]
public string? WaitUntilAvailable { get; init; }

[JsonPropertyName("serverSettings")]
public Dictionary<string, string>? ServerSettings { get; init; }
}

public class Credentials
{
[JsonPropertyName("host")]
public string? Host { get; init; }

[JsonPropertyName("port")]
[JsonConverter(typeof(AsStringConverter))]
public string? Port { get; init; }

[JsonPropertyName("database")]
public string? Database { get; init; }

[JsonPropertyName("branch")]
public string? Branch { get; init; }

[JsonPropertyName("user")]
public string? User { get; init; }

[JsonPropertyName("password")]
public string? Password { get; init; }

[JsonPropertyName("tls_ca")]
public string? TlsCA { get; init; }

[JsonPropertyName("tls_security")]
[JsonConverter(typeof(TLSSecurityModeConverter))]
public TLSSecurityMode? TlsSecurity { get; init; }
}

public class FileSystemData
{
[JsonPropertyName("cwd")]
public string? CurrentDir { get; init; }

[JsonPropertyName("homedir")]
public string? HomeDir { get; init; }

[JsonPropertyName("files")]
public Dictionary<string, File>? Files { get; init; }
}

[JsonConverter(typeof(FileJsonConverter))]
public class File
{
// Has either string contents or has explicitly defined instance information

// string contents
public string? Contents { get; init; }

// instance information
public Dictionary<string, string>? Fields { get; init; }
}

public class ResultData
{
[JsonPropertyName("address")]
[JsonConverter(typeof(AsListStringConverter))]
public List<string> Address { get; init; } = new();

[JsonPropertyName("database")]
public string Database { get; init; } = string.Empty;

[JsonPropertyName("branch")]
public string Branch { get; init; } = string.Empty;

[JsonPropertyName("user")]
public string User { get; init; } = string.Empty;

[JsonPropertyName("password")]
public string? Password { get; init; }

[JsonPropertyName("secretKey")]
public string? SecretKey { get; init; }

[JsonPropertyName("tlsCAData")]
public string? TlsCAData { get; init; }

[JsonPropertyName("tlsServerName")]
public string? TlsServerName { get; init; }

[JsonPropertyName("tlsSecurity")]
[JsonConverter(typeof(TLSSecurityModeConverter))]
public TLSSecurityMode? TlsSecurity { get; init; }

[JsonPropertyName("waitUntilAvailable")]
public string? WaitUntilAvailable { get; init; }

[JsonPropertyName("serverSettings")]
public Dictionary<string, string>? ServerSettings { get; init; }
}

public class ErrorData
{
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;

public Exception? GetExpectedException()
{
(Type, string) error = _errorMapping[Type];

return (Exception?)Activator.CreateInstance(error.Item1, new object[]{error.Item2});
}

private static readonly Dictionary<string, (Type, string)> _errorMapping = new()
{
{"credentials_file_not_found", (
typeof(ConfigurationException),
"cannot read credentials")},
{"project_not_initialised", (
typeof(ConfigurationException),
"Found `\\w+.toml` but the project is not initialized")},
{"no_options_or_toml", (
typeof(ConfigurationException),
"no `gel.toml` found and no connection options specified")},
{"invalid_credentials_file", (
typeof(ConfigurationException),
"cannot read credentials")},
{"invalid_dsn_or_instance_name", (typeof(ConfigurationException), "invalid DSN or instance name")},
{"invalid_instance_name", (typeof(ConfigurationException), "invalid instance name")},
{"invalid_dsn", (typeof(ConfigurationException), "invalid DSN")},
{"unix_socket_unsupported", (typeof(ConfigurationException), "unix socket paths not supported")},
{"invalid_host", (typeof(ConfigurationException), "invalid host")},
{"invalid_port", (typeof(ConfigurationException), "invalid port")},
{"invalid_user", (typeof(ConfigurationException), "invalid user")},
{"invalid_database", (typeof(ConfigurationException), "invalid database")},
{"multiple_compound_env", (typeof(ConfigurationException), "Cannot have more than one of the following connection environment variables")},
{"multiple_compound_opts", (
typeof(ConfigurationException),
"Cannot have more than one of the following connection options")},
{"exclusive_options", (
typeof(ConfigurationException),
"are mutually exclusive")},
{"env_not_found", (
typeof(ConfigurationException),
"environment variable \".*\" doesn\"t exist")},
{"file_not_found", (
typeof(ConfigurationException),
"No such file or directory")},
{"invalid_tls_security", (
typeof(ConfigurationException),
"tls_security can only be one of `insecure`, |tls_security must be set to strict")},
{"invalid_secret_key", (
typeof(ConfigurationException),
"Invalid secret key")},
{"secret_key_not_found", (
typeof(ConfigurationException),
"Cannot connect to cloud instances without secret key")},
{"docker_tcp_port", (
typeof(ConfigurationException),
"EDGEDB_PORT in \"tcp://host:port\" format, so will be ignored")},
{"gel_and_edgedb", (
typeof(ConfigurationException),
"Both GEL_\\w+ and EDGEDB_\\w+ are set; EDGEDB_\\w+ will be ignored")}
};
}

private class AsStringConverter : JsonConverter<string>
{
// Always reads numbers as strings

public override string? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number)
{
if (reader.TryGetInt32(out int asInt))
{
return asInt.ToString();
}
else
{
return reader.GetDouble().ToString();
}
}
else if (reader.TokenType == JsonTokenType.String)
{
return reader.GetString();
}
else
{
throw new JsonException("Expected Number or String.");
}
}

public override void Write(
Utf8JsonWriter writer,
string value,
JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}

private class AsListStringConverter : JsonConverter<List<string>>
{
// Always reads numbers as strings

public override List<string>? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
List<string> result = new();

while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
break;
}

if (reader.TokenType == JsonTokenType.Number)
{
if (reader.TryGetInt32(out int asInt))
{
result.Add(asInt.ToString());
}
else
{
result.Add(reader.GetDouble().ToString());
}
}
else if (reader.TokenType == JsonTokenType.String)
{
string? text = reader.GetString();
if (text is null)
{
throw new JsonException();
}
result.Add(text);
}
else
{
throw new JsonException("Expected Number or String.");
}
}

return result;
}

public override void Write(
Utf8JsonWriter writer,
List<string> value,
JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}

private class TLSSecurityModeConverter : JsonConverter<TLSSecurityMode?>
{
// Always reads numbers as strings

public override TLSSecurityMode? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
string? text = reader.GetString();
if (text is not null)
{
if (!TLSSecurityModeParser.TryParse(text, true, out TLSSecurityMode? tlsSecurity))
{
throw new FormatException($"\"{text}\" must be a value of TLSSecurityMode");
}
return tlsSecurity ?? TLSSecurityMode.Default;
}
else
{
throw new JsonException("Expected String.");
}
}

public override void Write(
Utf8JsonWriter writer,
TLSSecurityMode? value,
JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}

private class FileJsonConverter : JsonConverter<File>
{
public override File? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
// string contents
return new()
{
Contents = reader.GetString(),
};
}
else if (reader.TokenType == JsonTokenType.StartObject)
{
// instance information
Dictionary<string, string> fields = new();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}

if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}

string? propertyName = reader.GetString();
if (propertyName is not null)
{
reader.Read();
if (reader.TokenType != JsonTokenType.String)
{
throw new JsonException($"Expected string for property \"{propertyName}\"");
}

string? value = reader.GetString();
if (value == null)
{
throw new JsonException();
}

fields[propertyName] = value;
}
else
{
throw new JsonException();
}
}

return new()
{
Fields = fields,
};
}
else
{
throw new JsonException("Could not read File object.");
}
}

public override void Write(
Utf8JsonWriter writer,
File value,
JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
}
}

0 comments on commit c2a295e

Please sign in to comment.