feat(api): implement endpoint for creating link tokens
This commit is contained in:
@@ -11,6 +11,7 @@ internal static class InstitutionsExtensions
|
||||
|
||||
institutionsGroup.MapConnectEndpoint();
|
||||
institutionsGroup.MapGetAvailableEndpoint();
|
||||
institutionsGroup.MapLinkEndpoint();
|
||||
|
||||
return institutionsGroup;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace FiscalOS.API.Institutions.Link;
|
||||
|
||||
internal static class Endpoint
|
||||
{
|
||||
private const string Route = "/link";
|
||||
|
||||
public static RouteHandlerBuilder MapLinkEndpoint(this RouteGroupBuilder groupBuilder)
|
||||
{
|
||||
return groupBuilder.MapPost(Route, HandleAsync);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] AppDbContext appDbContext,
|
||||
[FromServices] PlaidService plaidService
|
||||
)
|
||||
{
|
||||
var userId = httpContext.GetUserId();
|
||||
|
||||
var user = await appDbContext.Users.SingleOrDefaultAsync(u => u.Id == userId);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var linkToken = await plaidService.CreateLinkTokenAsync(user.Id.ToString());
|
||||
|
||||
return Results.Ok(Response.From(linkToken));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace FiscalOS.API.Institutions.Link;
|
||||
|
||||
public sealed record Response
|
||||
{
|
||||
public string LinkToken { get; init; } = string.Empty;
|
||||
|
||||
[JsonConstructor]
|
||||
private Response()
|
||||
{
|
||||
}
|
||||
|
||||
public static Response From(string linkToken)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
LinkToken = linkToken,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ global using System.ComponentModel.DataAnnotations;
|
||||
global using System.Security.Claims;
|
||||
global using System.Text.Json.Serialization;
|
||||
|
||||
global using FiscalOS.Core.Identity;
|
||||
global using FiscalOS.API.Accounts;
|
||||
global using FiscalOS.API.Accounts.Add;
|
||||
global using FiscalOS.API.Auth;
|
||||
@@ -12,8 +11,10 @@ global using FiscalOS.API.Http;
|
||||
global using FiscalOS.API.Institutions;
|
||||
global using FiscalOS.API.Institutions.Connect;
|
||||
global using FiscalOS.API.Institutions.GetAvailable;
|
||||
global using FiscalOS.API.Institutions.Link;
|
||||
global using FiscalOS.Core.Accounts;
|
||||
global using FiscalOS.Core.Authentication;
|
||||
global using FiscalOS.Core.Identity;
|
||||
global using FiscalOS.Core.Security;
|
||||
global using FiscalOS.Infra.Accounts.Plaid;
|
||||
global using FiscalOS.Infra.Authentication;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace FiscalOS.API.Tests.Integration.Institutions;
|
||||
|
||||
public class LinkTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
{
|
||||
private static readonly Uri LinkUri = new("/institutions/link", UriKind.Relative);
|
||||
|
||||
[Fact]
|
||||
public async Task Link_WhenCalledAndUnauthenticated_ItShouldReturn401WithProblemDetails()
|
||||
{
|
||||
using var request = HttpRequestBuilder.New()
|
||||
.Post(LinkUri)
|
||||
.Build();
|
||||
|
||||
var res = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
await res.Should().BeProblemDetails(HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Link_WhenCalledWithNonExistentUser_ItShouldReturn401WithProblemDetails()
|
||||
{
|
||||
using var request = HttpRequestBuilder.New()
|
||||
.Post(LinkUri)
|
||||
.WithUserId(Guid.NewGuid())
|
||||
.Build();
|
||||
|
||||
var res = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
await res.Should().BeProblemDetails(HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Link_WhenCalledWithUserWhoExists_ItShouldReturnLinkToken()
|
||||
{
|
||||
var user = await ExecuteAsync(static async (context, ct, sp) =>
|
||||
{
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
|
||||
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
||||
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
|
||||
|
||||
await context.AddAsync(user, ct);
|
||||
await context.SaveChangesAsync(ct);
|
||||
return user;
|
||||
}, TestContext.Current.CancellationToken);
|
||||
|
||||
using var request = HttpRequestBuilder.New()
|
||||
.Post(LinkUri)
|
||||
.WithUserId(user.Id)
|
||||
.Build();
|
||||
|
||||
var res = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
(await res.Should().BeJsonContentOfType<API.Institutions.Link.Response>(HttpStatusCode.OK))
|
||||
.Which.LinkToken.Should().NotBeEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user