diff --git a/Runtime/AsobiClient.cs b/Runtime/AsobiClient.cs index 1fbadb6..2909eab 100644 --- a/Runtime/AsobiClient.cs +++ b/Runtime/AsobiClient.cs @@ -23,7 +23,7 @@ public class AsobiClient : IDisposable public AsobiDirectMessages DirectMessages { get; } public AsobiRealtime Realtime { get; } - internal HttpClient Http { get; } + internal IHttpClient Http { get; } const string RefreshTokenKey = "asobi_refresh_token"; @@ -59,9 +59,12 @@ public AsobiClient(string host, int port = 8084, bool useSsl = false) : this(new AsobiConfig(host, port, useSsl)) { } public AsobiClient(AsobiConfig config) + : this(config, new HttpClient(config.BaseUrl)) { } + + internal AsobiClient(AsobiConfig config, IHttpClient http) { Config = config; - Http = new HttpClient(config.BaseUrl); + Http = http; Auth = new AsobiAuth(this); Players = new AsobiPlayers(this); diff --git a/Runtime/HttpClient.cs b/Runtime/HttpClient.cs index 7e40d5a..b61742d 100644 --- a/Runtime/HttpClient.cs +++ b/Runtime/HttpClient.cs @@ -7,7 +7,7 @@ namespace Asobi { - internal class HttpClient + internal class HttpClient : IHttpClient { readonly string _baseUrl; public string AccessToken { get; set; } diff --git a/Runtime/IHttpClient.cs b/Runtime/IHttpClient.cs new file mode 100644 index 0000000..9c1f0a9 --- /dev/null +++ b/Runtime/IHttpClient.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Asobi +{ + internal interface IHttpClient + { + string AccessToken { get; set; } + + Task Get(string path, Dictionary query = null); + Task Post(string path, object body = null); + Task PostJson(string path, string json); + Task Put(string path, object body = null); + Task PutJson(string path, string json); + Task GetRaw(string path, Dictionary query = null); + Task PutRaw(string path, string json); + Task Delete(string path, object body = null, Dictionary query = null); + } +} diff --git a/Runtime/IHttpClient.cs.meta b/Runtime/IHttpClient.cs.meta new file mode 100644 index 0000000..6e97430 --- /dev/null +++ b/Runtime/IHttpClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f73519b9e95a4c9a970e2553a284d2a3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/AsobiAuthTests.cs b/Tests/Runtime/AsobiAuthTests.cs new file mode 100644 index 0000000..24dec6c --- /dev/null +++ b/Tests/Runtime/AsobiAuthTests.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using NUnit.Framework; +using UnityEngine; + +namespace Asobi.Tests +{ + /// + /// Unit tests for AsobiAuth over a fake IHttpClient. No backend, no + /// network: they pin the request each method sends and the token-storage + /// side effects, and that an error status aborts without storing tokens. + /// + public class AsobiAuthTests + { + FakeHttpClient _http; + AsobiClient _client; + + [SetUp] + public void SetUp() + { + PlayerPrefs.DeleteKey("asobi_refresh_token"); + _http = new FakeHttpClient(); + _client = new AsobiClient(new AsobiConfig("localhost"), _http); + } + + [Test] + public async Task LoginSendsCredentialsAndStoresTokens() + { + _http.NextResponse = new AuthResponse + { + player_id = "pid", + access_token = "at", + refresh_token = "rt", + username = "bob" + }; + + var resp = await _client.Auth.LoginAsync("bob", "pw"); + + Assert.That(_http.LastPath, Is.EqualTo("/api/v1/auth/login")); + var body = (AuthRequest)_http.LastBody; + Assert.That(body.username, Is.EqualTo("bob")); + Assert.That(body.password, Is.EqualTo("pw")); + + Assert.That(resp.access_token, Is.EqualTo("at")); + Assert.That(_client.AccessToken, Is.EqualTo("at")); + Assert.That(_client.RefreshToken, Is.EqualTo("rt")); + Assert.That(_client.PlayerId, Is.EqualTo("pid")); + Assert.That(_http.AccessToken, Is.EqualTo("at"), + "access token must propagate to the HTTP layer for authenticated calls"); + } + + [Test] + public async Task RegisterDefaultsDisplayNameToUsername() + { + _http.NextResponse = new AuthResponse { access_token = "at", refresh_token = "rt" }; + + await _client.Auth.RegisterAsync("bob", "pw"); + + Assert.That(_http.LastPath, Is.EqualTo("/api/v1/auth/register")); + var body = (AuthRequest)_http.LastBody; + Assert.That(body.display_name, Is.EqualTo("bob")); + } + + [Test] + public async Task OAuthStoresTokens() + { + _http.NextResponse = new OAuthResponse + { + player_id = "pid", + access_token = "at", + refresh_token = "rt", + created = true + }; + + var resp = await _client.Auth.OAuthAsync("google", "id-token"); + + Assert.That(_http.LastPath, Is.EqualTo("/api/v1/auth/oauth")); + var body = (OAuthRequest)_http.LastBody; + Assert.That(body.provider, Is.EqualTo("google")); + Assert.That(body.token, Is.EqualTo("id-token")); + Assert.That(resp.created, Is.True); + Assert.That(_client.AccessToken, Is.EqualTo("at")); + } + + [Test] + public void LoginErrorDoesNotStoreTokens() + { + _http.NextError = new AsobiException(401, "invalid_credentials", + new AsobiError { error = "invalid_credentials" }); + + var ex = Assert.ThrowsAsync( + async () => await _client.Auth.LoginAsync("bob", "wrong")); + + Assert.That(ex.StatusCode, Is.EqualTo(401)); + Assert.That(ex.Error.error, Is.EqualTo("invalid_credentials")); + Assert.That(_client.AccessToken, Is.Null); + } + + class FakeHttpClient : IHttpClient + { + public string AccessToken { get; set; } + public string LastPath; + public object LastBody; + public object NextResponse; + public AsobiException NextError; + + public Task Post(string path, object body = null) + { + LastPath = path; + LastBody = body; + if (NextError != null) throw NextError; + return Task.FromResult((T)NextResponse); + } + + public Task Get(string path, Dictionary query = null) => throw new System.NotImplementedException(); + public Task PostJson(string path, string json) => throw new System.NotImplementedException(); + public Task Put(string path, object body = null) => throw new System.NotImplementedException(); + public Task PutJson(string path, string json) => throw new System.NotImplementedException(); + public Task GetRaw(string path, Dictionary query = null) => throw new System.NotImplementedException(); + public Task PutRaw(string path, string json) => throw new System.NotImplementedException(); + public Task Delete(string path, object body = null, Dictionary query = null) => throw new System.NotImplementedException(); + } + } +} diff --git a/Tests/Runtime/AsobiAuthTests.cs.meta b/Tests/Runtime/AsobiAuthTests.cs.meta new file mode 100644 index 0000000..567bad4 --- /dev/null +++ b/Tests/Runtime/AsobiAuthTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: efb5c1f0c37b4ddfae2b10d8d9770b3e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: