feat(api): implement endpoint for creating link tokens
This commit is contained in:
@@ -11,6 +11,7 @@ internal static class InstitutionsExtensions
|
|||||||
|
|
||||||
institutionsGroup.MapConnectEndpoint();
|
institutionsGroup.MapConnectEndpoint();
|
||||||
institutionsGroup.MapGetAvailableEndpoint();
|
institutionsGroup.MapGetAvailableEndpoint();
|
||||||
|
institutionsGroup.MapLinkEndpoint();
|
||||||
|
|
||||||
return institutionsGroup;
|
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.Security.Claims;
|
||||||
global using System.Text.Json.Serialization;
|
global using System.Text.Json.Serialization;
|
||||||
|
|
||||||
global using FiscalOS.Core.Identity;
|
|
||||||
global using FiscalOS.API.Accounts;
|
global using FiscalOS.API.Accounts;
|
||||||
global using FiscalOS.API.Accounts.Add;
|
global using FiscalOS.API.Accounts.Add;
|
||||||
global using FiscalOS.API.Auth;
|
global using FiscalOS.API.Auth;
|
||||||
@@ -12,8 +11,10 @@ global using FiscalOS.API.Http;
|
|||||||
global using FiscalOS.API.Institutions;
|
global using FiscalOS.API.Institutions;
|
||||||
global using FiscalOS.API.Institutions.Connect;
|
global using FiscalOS.API.Institutions.Connect;
|
||||||
global using FiscalOS.API.Institutions.GetAvailable;
|
global using FiscalOS.API.Institutions.GetAvailable;
|
||||||
|
global using FiscalOS.API.Institutions.Link;
|
||||||
global using FiscalOS.Core.Accounts;
|
global using FiscalOS.Core.Accounts;
|
||||||
global using FiscalOS.Core.Authentication;
|
global using FiscalOS.Core.Authentication;
|
||||||
|
global using FiscalOS.Core.Identity;
|
||||||
global using FiscalOS.Core.Security;
|
global using FiscalOS.Core.Security;
|
||||||
global using FiscalOS.Infra.Accounts.Plaid;
|
global using FiscalOS.Infra.Accounts.Plaid;
|
||||||
global using FiscalOS.Infra.Authentication;
|
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