diff --git a/packages/Gotrue/Gotrue.Tests/Stateless/StatelessSignOutTests.cs b/packages/Gotrue/Gotrue.Tests/Stateless/StatelessSignOutTests.cs new file mode 100644 index 00000000..2c63bff6 --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Stateless/StatelessSignOutTests.cs @@ -0,0 +1,65 @@ +#region + +using System.Threading.Tasks; +using FluentAssertions; +using Gotrue.Tests.Support; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Supabase.Gotrue; +using Supabase.Gotrue.Exceptions; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using static Supabase.Gotrue.Constants; +using static Supabase.Gotrue.StatelessClient; + +#endregion + +namespace Gotrue.Tests.Stateless; + +/// +/// Stateless sign-out sends the requested scope to /logout, global by default, and throws when the request fails. +/// +[TestClass] +[TestCategory("Contract")] +public class StatelessSignOutTests +{ + private MockGotrueServer server = null!; + + [TestInitialize] + public void TestInitialize() => this.server = new MockGotrueServer(); + + [TestCleanup] + public void TestCleanup() => this.server.Dispose(); + + [TestMethod] + [DataRow(SignOutScope.Global, "global")] + [DataRow(SignOutScope.Local, "local")] + [DataRow(SignOutScope.Others, "others")] + public async Task SignOut_ShouldSendTheRequestedScope(SignOutScope scope, string expected) + { + this.StubLogout(200); + await new StatelessClient().SignOut("user-access-token", this.Options(), scope); + this.server.VerifySingleReceivedRequest().WithQueryParam("scope", expected); + } + + [TestMethod] + public async Task SignOut_ShouldSendGlobalScope_GivenNoScope() + { + this.StubLogout(200); + await new StatelessClient().SignOut("user-access-token", this.Options()); + this.server.VerifySingleReceivedRequest().WithQueryParam("scope", "global"); + } + + [TestMethod] + public async Task SignOut_ShouldThrow_GivenErrorResponse() + { + this.StubLogout(401); + var signOut = () => new StatelessClient().SignOut("user-access-token", this.Options(), SignOutScope.Local); + await signOut.Should().ThrowAsync(); + } + + private void StubLogout(int statusCode) => + this.server.Given(Request.Create().WithPath("/logout").UsingPost()) + .RespondWith(Response.Create().WithStatusCode(statusCode)); + + private StatelessClientOptions Options() => new() { Url = this.server.Url }; +} diff --git a/packages/Gotrue/Gotrue/Interfaces/IGotrueStatelessClient.cs b/packages/Gotrue/Gotrue/Interfaces/IGotrueStatelessClient.cs index db205f66..3cadca16 100644 --- a/packages/Gotrue/Gotrue/Interfaces/IGotrueStatelessClient.cs +++ b/packages/Gotrue/Gotrue/Interfaces/IGotrueStatelessClient.cs @@ -199,6 +199,12 @@ public interface IGotrueStatelessClient /// Task SignOut(string accessToken, StatelessClientOptions options); + /// + /// Signs out the user from the sessions in the given scope. + /// JWT tokens will still be valid for stateless auth until they expire. + /// + Task SignOut(string accessToken, StatelessClientOptions options, SignOutScope scope); + /// /// Signs up a user /// diff --git a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt index 67f6412b..4656ff26 100644 --- a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt +++ b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt @@ -116,3 +116,5 @@ Supabase.Gotrue.Interfaces.IGotrueApi.GetUserAsync(string! jwt, Supabase.Gotrue.Interfaces.IGotrueClient.GetClaimsAsync(string? jwt = null, Supabase.Gotrue.Claims.GetClaimsOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Supabase.Gotrue.SignUpOptions.CaptchaToken.get -> string? Supabase.Gotrue.SignUpOptions.CaptchaToken.set -> void +Supabase.Gotrue.Interfaces.IGotrueStatelessClient.SignOut(string! accessToken, Supabase.Gotrue.StatelessClient.StatelessClientOptions! options, Supabase.Gotrue.Constants.SignOutScope scope) -> System.Threading.Tasks.Task! +Supabase.Gotrue.StatelessClient.SignOut(string! accessToken, Supabase.Gotrue.StatelessClient.StatelessClientOptions! options, Supabase.Gotrue.Constants.SignOutScope scope) -> System.Threading.Tasks.Task! diff --git a/packages/Gotrue/Gotrue/StatelessClient.cs b/packages/Gotrue/Gotrue/StatelessClient.cs index 72c004b8..65edca12 100644 --- a/packages/Gotrue/Gotrue/StatelessClient.cs +++ b/packages/Gotrue/Gotrue/StatelessClient.cs @@ -178,11 +178,17 @@ public async Task SignIn(string email, StatelessClientOptions options, Sig /// public async Task SignOut(string accessToken, StatelessClientOptions options) { - var result = await this.GetApi(options).SignOut(accessToken); - result.ResponseMessage?.EnsureSuccessStatusCode(); + await this.SignOut(accessToken, options, SignOutScope.Global).ConfigureAwait(false); return true; } + /// + public async Task SignOut(string accessToken, StatelessClientOptions options, SignOutScope scope) + { + var result = await this.GetApi(options).SignOut(accessToken, scope).ConfigureAwait(false); + result.ResponseMessage?.EnsureSuccessStatusCode(); + } + /// public async Task VerifyOTP(string phone, string otpToken, StatelessClientOptions options, MobileOtpType type = MobileOtpType.SMS) {