Skip to content

Commit

Permalink
Add mock connection test
Browse files Browse the repository at this point in the history
  • Loading branch information
rschili committed Dec 20, 2024
1 parent 99a795f commit 5a1b6f7
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 7 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"targetArchitecture": "x86_64",
},
"cSpell.words": [
"homeserver",
"Localpart"
]
}
170 changes: 170 additions & 0 deletions src/MatrixTextClient.Tests/LoginTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using MatrixTextClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Moq.Protected;
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Threading.Tasks;
using TUnit.Core;

namespace MatrixTextClient.Tests;

public class LoginTests
{
/// <summary>
/// Goes through the process of a full password login
/// </summary>
[Test]
public async Task TestSuccessfulPasswordLogin()
{
// TODO: We do not actually verify the server request contents. This test flow is tedious enough as it is. May add later.
// arrange
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);

HttpResponseMessage wellKnownResponse = new(HttpStatusCode.OK)
{
Content = new StringContent("""
{
"m.homeserver": {
"base_url": "https://matrix.example.com"
},
"m.identity_server": {
"base_url": "https://identity.example.com"
},
"org.example.custom.property": {
"app_url": "https://custom.app.example.org"
}
}
""", Encoding.UTF8, "application/json")
};

var protectedMock = handlerMock.Protected();
protectedMock
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(r => r.RequestUri == new Uri("https://example.org/.well-known/matrix/client")),
ItExpr.IsAny<CancellationToken>())
.Returns(Task.FromResult(wellKnownResponse))
.Verifiable(Times.Once, "Expected well known request to be made once");

HttpResponseMessage versionsResponse = new(HttpStatusCode.OK)
{
Content = new StringContent("""
{
"unstable_features": {
"org.example.my_feature": true
},
"versions": [
"r0.0.1",
"v1.1"
]
}
""", Encoding.UTF8, "application/json")
};

protectedMock
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(r => r.RequestUri == new Uri("https://matrix.example.com/_matrix/client/versions")),
ItExpr.IsAny<CancellationToken>())
.Returns(Task.FromResult(versionsResponse))
.Verifiable(Times.Once, "Expected versions request to be made once");

HttpResponseMessage supportedAuthFlowsResponse = new(HttpStatusCode.OK)
{
Content = new StringContent("""
{
"flows": [
{
"type": "m.login.password"
},
{
"type": "m.login.token"
}
]
}
""", Encoding.UTF8, "application/json")
};

protectedMock
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(r => r.RequestUri == new Uri("https://matrix.example.com/_matrix/client/v3/login") && r.Method == HttpMethod.Get),
ItExpr.IsAny<CancellationToken>())
.Returns(Task.FromResult(supportedAuthFlowsResponse))
.Verifiable(Times.Once, "Expected auth flows request to be made once");

HttpResponseMessage loginResponse = new(HttpStatusCode.OK)
{
Content = new StringContent("""
{
"access_token": "abc123",
"device_id": "GHTYAJCE",
"user_id": "@cheeky_monkey:matrix.org"
}
""", Encoding.UTF8, "application/json")
};

protectedMock
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(r => r.RequestUri == new Uri("https://matrix.example.com/_matrix/client/v3/login") && r.Method == HttpMethod.Post),
ItExpr.IsAny<CancellationToken>())
.Returns(Task.FromResult(loginResponse))
.Verifiable(Times.Once, "Expected login request to be made once");

HttpResponseMessage capabilitiesResponse = new(HttpStatusCode.OK)
{
Content = new StringContent("""
{
"capabilities": {
"com.example.custom.ratelimit": {
"max_requests_per_hour": 600
},
"m.change_password": {
"enabled": false
},
"m.room_versions": {
"available": {
"1": "stable",
"2": "stable",
"3": "unstable",
"test-version": "unstable"
},
"default": "1"
}
}
}
""", Encoding.UTF8, "application/json")
};

protectedMock
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(r => r.RequestUri == new Uri("https://matrix.example.com/_matrix/client/v3/capabilities") &&
r.Headers.Authorization!.Parameter == "abc123"),
ItExpr.IsAny<CancellationToken>())
.Returns(Task.FromResult(capabilitiesResponse))
.Verifiable(Times.Once, "Expected capabilities request to be made once");


var mockHttpClientFactory = new Mock<IHttpClientFactory>();

mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(() => new HttpClient(handlerMock.Object, false));

// act
var result = await MatrixClient.ConnectAsync("@nobody:example.org", "password", "deviceId", mockHttpClientFactory.Object, CancellationToken.None, NullLogger.Instance);

// assert
await Assert.That(result).IsNotNull();
handlerMock.VerifyAll();
await Assert.That(result.UserId.FullId).IsEqualTo("@nobody:example.org");
await Assert.That(result.HttpClientParameters.BearerToken).IsEqualTo("abc123");
await Assert.That(result.ServerCapabilities?.ChangePassword?.Enabled).IsFalse();
await Assert.That(result.SupportedSpecVersions).Contains(new SpecVersion(1, 1, null, null));
}
}
3 changes: 2 additions & 1 deletion src/MatrixTextClient/MatrixClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ public static async Task<MatrixClient> ConnectAsync(string userId, string passwo
CurrentSpecVersion.VersionString, string.Join(',', parsedVersions));

var authFlows = await MatrixHelper.FetchSupportedAuthFlowsAsync(httpClientParameters).ConfigureAwait(false);
if(!authFlows.Contains(Constants.PASSWORD_LOGIN_FLOW))
var authFlowsList = authFlows.Flows.Select(f => f.Type).ToList();
if(!authFlowsList.Contains(Constants.PASSWORD_LOGIN_FLOW))
{
logger.LogError("The server does not support password based authentication. Supported types: {Types}", string.Join(", ", authFlows));
throw new InvalidOperationException("The server does not support password based authentication.");
Expand Down
8 changes: 2 additions & 6 deletions src/MatrixTextClient/MatrixHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,10 @@ public static async Task<SpecVersionsResponse> FetchSupportedSpecVersionsAsync(H
return await HttpClientHelper.SendAsync<SpecVersionsResponse>(parameters, "/_matrix/client/versions").ConfigureAwait(false);
}

public static async Task<List<string>> FetchSupportedAuthFlowsAsync(HttpClientParameters parameters)
public static async Task<AuthFlowsResponse> FetchSupportedAuthFlowsAsync(HttpClientParameters parameters)
{
ArgumentNullException.ThrowIfNull(parameters);
var response = await HttpClientHelper.SendAsync<AuthFlowsResponse>(parameters, "/_matrix/client/v3/login").ConfigureAwait(false);
if (response != null)
return response.Flows.Select(f => f.Type).ToList();

return new List<string>();
return await HttpClientHelper.SendAsync<AuthFlowsResponse>(parameters, "/_matrix/client/v3/login").ConfigureAwait(false);
}

public static async Task<LoginResponse> PasswordLoginAsync(HttpClientParameters parameters, string userId, string password, string deviceId)
Expand Down
3 changes: 3 additions & 0 deletions src/MatrixTextClient/Responses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public class AuthFlowsResponse
{
[JsonPropertyName("flows")]
public required List<AuthFlow> Flows { get; set; }

[JsonPropertyName("session")]
public string? Session { get; set; }
}

public class AuthFlow
Expand Down

0 comments on commit 5a1b6f7

Please sign in to comment.