Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create email verification #80

Merged
merged 6 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ string emailAddressOrErrorMessage = userResponse.Result.Match(

## ⌛ Progress
### Server & Client
![14 / 288](https://progress-bar.dev/14/?scale=288&suffix=%20/%20288&width=500)
![15 / 288](https://progress-bar.dev/15/?scale=288&suffix=%20/%20288&width=500)
### Server Only
![2 / 195](https://progress-bar.dev/2/?scale=195&suffix=%20/%20195&width=300)
### Client Only
![12 / 93](https://progress-bar.dev/12/?scale=93&suffix=%20/%2093&width=300)
![13 / 93](https://progress-bar.dev/13/?scale=93&suffix=%20/%2093&width=300)

### 🔑 Key
| Icon | Definition |
Expand All @@ -153,7 +153,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match(
| ❌ | There is currently no intention to implement the endpoint for the given SDK type (client or server) |

### Account
![14 / 52](https://progress-bar.dev/14/?scale=52&suffix=%20/%2052&width=120)
![15 / 52](https://progress-bar.dev/15/?scale=52&suffix=%20/%2052&width=120)

| Endpoint | Client | Server |
|:-:|:-:|:-:|
Expand Down Expand Up @@ -200,7 +200,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match(
| [Create Magic URL Token](https://appwrite.io/docs/references/1.5.x/client-rest/account#createMagicURLToken) | ⬛ | ⬛ |
| [Create OAuth2 Token](https://appwrite.io/docs/references/1.5.x/client-rest/account#createOAuth2Token) | ⬛ | ⬛ |
| [Create Phone Token](https://appwrite.io/docs/references/1.5.x/client-rest/account#createPhoneToken) | ⬛ | ⬛ |
| [Create Email Verification](https://appwrite.io/docs/references/1.5.x/client-rest/account#createVerification) | | ❌ |
| [Create Email Verification](https://appwrite.io/docs/references/1.5.x/client-rest/account#createVerification) | | ❌ |
| [Create Email Verification (Confirmation)](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateVerification) | ⬛ | ❌ |
| [Create Phone Verification](https://appwrite.io/docs/references/1.5.x/client-rest/account#createPhoneVerification) | ⬛ | ❌ |
| [Create Phone Verification (Confirmation)](https://appwrite.io/docs/references/1.5.x/client-rest/account#updatePhoneVerification) | ⬛ | ❌ |
Expand Down
17 changes: 17 additions & 0 deletions src/PinguApps.Appwrite.Client/Clients/AccountClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,21 @@ public async Task<AppwriteResult<Session>> UpdateSession(string sessionId = "cur
return e.GetExceptionResponse<Session>();
}
}

/// <inheritdoc/>
public async Task<AppwriteResult<Token>> CreateEmailVerification(CreateEmailVerificationRequest request)
{
try
{
request.Validate(true);

var result = await _accountApi.CreateEmailVerification(Session, request);

return result.GetApiResponse();
}
catch (Exception e)
{
return e.GetExceptionResponse<Token>();
}
}
}
9 changes: 9 additions & 0 deletions src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,13 @@ public interface IAccountClient
/// <param name="sessionId">Session ID. Use the string 'current' to update the current device session.</param>
/// <returns>The session</returns>
Task<AppwriteResult<Session>> UpdateSession(string sessionId = "current");

/// <summary>
/// <para>Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the userId and secret arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the userId and secret parameters. Learn more about how to <see href="https://appwrite.io/docs/references/cloud/client-web/account#updateVerification">complete the verification process</see>. The verification link sent to the user's email address is valid for 7 days.</para>
/// <para>Please note that in order to avoid a <see href="https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md">Redirect Attack</see>, the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.</para>
/// <para><see href="https://appwrite.io/docs/references/1.5.x/client-rest/account#createVerification">Appwrite Docs</see></para>
/// </summary>
/// <param name="request">The request content</param>
/// <returns>The token</returns>
Task<AppwriteResult<Token>> CreateEmailVerification(CreateEmailVerificationRequest request);
}
3 changes: 3 additions & 0 deletions src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@ internal interface IAccountApi : IBaseApi

[Patch("/account/sessions/{sessionId}")]
Task<IApiResponse<Session>> UpdateSession([Header("x-appwrite-session")] string? session, string sessionId);

[Post(("/account/verification"))]
Task<IApiResponse<Token>> CreateEmailVerification([Header("x-appwrite-session")] string? session, CreateEmailVerificationRequest request);
}
27 changes: 6 additions & 21 deletions src/PinguApps.Appwrite.Playground/App.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Configuration;
using PinguApps.Appwrite.Client;
using PinguApps.Appwrite.Server.Servers;
using PinguApps.Appwrite.Shared.Requests;

namespace PinguApps.Appwrite.Playground;
internal class App
Expand All @@ -20,32 +21,16 @@ public async Task Run(string[] args)
{
_client.SetSession(_session);

Console.WriteLine("Getting Session...");
var request = new CreateEmailVerificationRequest
{
Url = "https://localhost:5001/abc123"
};

//var response = await _client.Account.CreateEmailToken(new CreateEmailTokenRequest
//{
// Email = "[email protected]",
// UserId = "664aac1a00113f82e620"
//});

//var response = await _client.Account.CreateSession(new CreateSessionRequest
//{
// UserId = "664aac1a00113f82e620",
// Secret = "623341"
//});

var response = await _client.Account.GetSession("66a810f2e55b1329e25b");

var response2 = await _client.Account.UpdateSession("66a810f2e55b1329e25b");
var response = await _client.Account.CreateEmailVerification(request);

Console.WriteLine(response.Result.Match(
account => account.ToString(),
appwriteError => appwriteError.Message,
internalERror => internalERror.Message));

Console.WriteLine(response2.Result.Match(
account => account.ToString(),
appwriteError => appwriteError.Message,
internalERror => internalERror.Message));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
using PinguApps.Appwrite.Shared.Requests.Validators;

namespace PinguApps.Appwrite.Shared.Requests;

/// <summary>
/// The request for creating an email verification (will email the user a link to verify their email)
/// </summary>
public class CreateEmailVerificationRequest : BaseRequest<CreateEmailVerificationRequest, CreateEmailVerificationRequestValidator>
{
/// <summary>
/// URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an <see href="https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html">open redirect</see> attack against your project API.
/// </summary>
[JsonPropertyName("url")]
public string Url { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using FluentValidation;

namespace PinguApps.Appwrite.Shared.Requests.Validators;
public class CreateEmailVerificationRequestValidator : AbstractValidator<CreateEmailVerificationRequest>
{
public CreateEmailVerificationRequestValidator()
{
RuleFor(x => x.Url).NotEmpty().Must(uri => Uri.TryCreate(uri, UriKind.Absolute, out _));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Net;
using PinguApps.Appwrite.Shared.Requests;
using PinguApps.Appwrite.Shared.Tests;
using RichardSzalay.MockHttp;

namespace PinguApps.Appwrite.Client.Tests.Clients.Account;
public partial class AccountClientTests
{
[Fact]
public async Task CreateEmailVerification_ShouldReturnSuccess_WhenApiCallSucceeds()
{
// Arrange
var request = new CreateEmailVerificationRequest()
{
Url = "https://localhost:5001/abc123"
};

_mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/verification")
.ExpectedHeaders(true)
.WithJsonContent(request)
.Respond(Constants.AppJson, Constants.TokenResponse);

_appwriteClient.SetSession(Constants.Session);

// Act
var result = await _appwriteClient.Account.CreateEmailVerification(request);

// Assert
Assert.True(result.Success);
}

[Fact]
public async Task CreateEmailVerification_ShouldHandleException_WhenApiCallFails()
{
// Arrange
var request = new CreateEmailVerificationRequest()
{
Url = "https://localhost:5001/abc123"
};

_mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/verification")
.ExpectedHeaders(true)
.WithJsonContent(request)
.Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError);

_appwriteClient.SetSession(Constants.Session);

// Act
var result = await _appwriteClient.Account.CreateEmailVerification(request);

// Assert
Assert.True(result.IsError);
Assert.True(result.IsAppwriteError);
}

[Fact]
public async Task CreateEmailVerification_ShouldReturnErrorResponse_WhenExceptionOccurs()
{
// Arrange
var request = new CreateEmailVerificationRequest()
{
Url = "https://localhost:5001/abc123"
};

_mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/verification")
.ExpectedHeaders(true)
.WithJsonContent(request)
.Throw(new HttpRequestException("An error occurred"));

_appwriteClient.SetSession(Constants.Session);

// Act
var result = await _appwriteClient.Account.CreateEmailVerification(request);

// Assert
Assert.False(result.Success);
Assert.True(result.IsInternalError);
Assert.Equal("An error occurred", result.Result.AsT2.Message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using FluentValidation;
using PinguApps.Appwrite.Shared.Requests;

namespace PinguApps.Appwrite.Shared.Tests.Requests;
public class CreateEmailVerificationRequestTests
{

Check notice on line 6 in tests/PinguApps.Appwrite.Shared.Tests/Requests/CreateEmailVerificationRequestTests.cs

View check run for this annotation

codefactor.io / CodeFactor

tests/PinguApps.Appwrite.Shared.Tests/Requests/CreateEmailVerificationRequestTests.cs#L6

An opening brace should not be followed by a blank line. (SA1505)

pingu2k4 marked this conversation as resolved.
Show resolved Hide resolved
[Fact]
public void Constructor_InitializesWithExpectedValues()
{
// Arrange & Act
var request = new CreateEmailVerificationRequest();

// Assert
Assert.NotNull(request.Url);
Assert.Equal(string.Empty, request.Url);
}

[Theory]
[InlineData("https://google.com")]
[InlineData("https://localhost:1234")]
public void Properties_CanBeSet(string url)
{
// Arrange
var request = new CreateEmailVerificationRequest();

// Act
request.Url = url;

// Assert
Assert.Equal(url, request.Url);
}

[Theory]
[InlineData("https://google.com")]
[InlineData("https://localhost:1234")]
public void IsValid_WithValidData_ReturnsTrue(string url)
{
// Arrange
var request = new CreateEmailVerificationRequest
{
Url = url
};

// Act
var isValid = request.IsValid();

// Assert
Assert.True(isValid);
}

[Theory]
[InlineData("")]
[InlineData("not a url")]
public void IsValid_WithInvalidData_ReturnsFalse(string url)
{
// Arrange
var request = new CreateEmailVerificationRequest
{
Url = url
};

// Act
var isValid = request.IsValid();

// Assert
Assert.False(isValid);
}

[Fact]
public void Validate_WithThrowOnFailuresTrue_ThrowsValidationExceptionOnFailure()
{
// Arrange
var request = new CreateEmailVerificationRequest
{
Url = ""
};

// Assert
Assert.Throws<ValidationException>(() => request.Validate(true));
}

[Fact]
public void Validate_WithThrowOnFailuresFalse_ReturnsInvalidResultOnFailure()
{
// Arrange
var request = new CreateEmailVerificationRequest
{
Url = ""
};

// Act
var result = request.Validate(false);

// Assert
Assert.False(result.IsValid);
}
}