Skip to content

Commit

Permalink
Research push notifications (#95)
Browse files Browse the repository at this point in the history
A temporary solution, it should be rethought in terms of architecture
and improved for the next alert task.
  • Loading branch information
jakubzehner authored Dec 7, 2023
1 parent 49c72fc commit 7344aee
Show file tree
Hide file tree
Showing 18 changed files with 973 additions and 1 deletion.
104 changes: 104 additions & 0 deletions SensoBackend.UnitTests/Application/Modules/Account/AddDeviceToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using FluentValidation;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Time.Testing;
using SensoBackend.Application.Modules.Accounts.Contracts;
using SensoBackend.Application.Modules.Accounts.CreateAccount;
using SensoBackend.Domain.Enums;
using SensoBackend.Domain.Exceptions;
using SensoBackend.Infrastructure.Data;
using SensoBackend.UnitTests.Utils;

namespace SensoBackend.Tests.Application.Modules.Accounts.CreateAccount;

public sealed class AddDeviceTokenHandlerTests : IDisposable
{
private static readonly DateTimeOffset AddedAt = DateTimeOffset.UtcNow;

private readonly AppDbContext _context = Database.CreateFixture();

private static readonly string _deviceTypeName = "Android";
private static readonly DeviceType _deviceType = DeviceType.Android;
private static readonly string _deviceToken = "J3b4cDr4p4l3";

private readonly AddDeviceTokenHandler _sut;

public AddDeviceTokenHandlerTests() =>
_sut = new AddDeviceTokenHandler(_context, new FakeTimeProvider(AddedAt));

public void Dispose() => _context.Dispose();

[Fact]
public async Task Handle_ShouldAddDeviceToken()
{
var account = await _context.SetUpAccount();
var dto = new AddDeviceTokenDto
{
DeviceToken = _deviceToken,
DeviceType = _deviceTypeName,
};

var request = new AddDeviceTokenRequest { AccountId = account.Id, Dto = dto };
await _sut.Handle(request, CancellationToken.None);

var device = await _context.Devices.FirstOrDefaultAsync(d => d.AccountId == account.Id);

device.Should().NotBeNull();
if (device == null)
return;

device.Token.Should().Be(_deviceToken);
device.Type.Should().Be(_deviceType);
device.AddedAt.Should().Be(AddedAt);
}

[Fact]
public async Task Handle_ShouldUpdateDeviceToken()
{
var account = await _context.SetUpAccount();
await _context.SetUpDevice(account.Id, "oldToken", _deviceType);
var dto = new AddDeviceTokenDto
{
DeviceToken = _deviceToken,
DeviceType = _deviceTypeName,
};

var request = new AddDeviceTokenRequest { AccountId = account.Id, Dto = dto };
await _sut.Handle(request, CancellationToken.None);

var deviceInDb = await _context.Devices.FirstOrDefaultAsync(d => d.AccountId == account.Id);

deviceInDb.Should().NotBeNull();
if (deviceInDb == null)
return;

deviceInDb.Token.Should().Be(_deviceToken);
deviceInDb.Type.Should().Be(_deviceType);
deviceInDb.AddedAt.Should().Be(AddedAt);
}

[Fact]
public async Task Handle_ShouldNotUpdateDeviceToken_WhenDeviceTokenIsTheSame()
{
var account = await _context.SetUpAccount();
var device = await _context.SetUpDevice(account.Id, _deviceToken, _deviceType);
var dto = new AddDeviceTokenDto
{
DeviceToken = _deviceToken,
DeviceType = _deviceTypeName,
};

var request = new AddDeviceTokenRequest { AccountId = account.Id, Dto = dto };
await _sut.Handle(request, CancellationToken.None);

var deviceInDb = await _context.Devices.FirstOrDefaultAsync(d => d.AccountId == account.Id);

deviceInDb.Should().NotBeNull();
if (deviceInDb == null)
return;

deviceInDb.Token.Should().Be(_deviceToken);
deviceInDb.Type.Should().Be(_deviceType);
deviceInDb.AddedAt.Should().Be(device.AddedAt);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using SensoBackend.Application.Modules.Accounts.Utils;
using SensoBackend.Application.Modules.Alerts.Utils;
using SensoBackend.Domain.Enums;
using SensoBackend.Domain.Exceptions;

namespace SensoBackend.UnitTests.Application.Modules.Alerts.Utils;

public sealed class GetDeviceTypeFromNameTests
{
[Theory]
[InlineData("Android", DeviceType.Android)]
[InlineData("Ios", DeviceType.Ios)]
public void GetDeviceTypeFromName_ShouldReturnDeviceType_WhenDeviceTypeNameIsValid(
string deviceTypeName,
DeviceType expectedDeviceType
)
{
var deviceType = GetDeviceType.FromName(deviceTypeName);

deviceType.Should().Be(expectedDeviceType);
}

[Fact]
public void GetDeviceTypeFromName_ShouldThrowIncorrectDeviceTypeNameException_WhenDeviceTypeNameIsInvalid()
{
var deviceTypeName = "invalidDeviceTypeName";

var action = () => GetDeviceType.FromName(deviceTypeName);

action
.Should()
.Throw<IncorrectDeviceTypeNameException>()
.WithMessage($"Device type {deviceTypeName} is incorrect");
}
}
21 changes: 21 additions & 0 deletions SensoBackend.UnitTests/Utils/AppDbContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Bogus;
using Microsoft.EntityFrameworkCore;
using Npgsql.Replication;
using SensoBackend.Domain.Entities;
using SensoBackend.Domain.Enums;
using SensoBackend.Infrastructure.Data;
Expand All @@ -16,6 +17,26 @@ public static async Task<Account> SetUpAccount(this AppDbContext context)
return account;
}

public static async Task<Device> SetUpDevice(
this AppDbContext context,
int accountId,
string token,
DeviceType deviceType
)
{
var device = new Device
{
Id = default,
AccountId = accountId,
Token = token,
Type = deviceType,
AddedAt = new Bogus.DataSets.Date().RecentOffset(),
};
await context.Devices.AddAsync(device);
await context.SaveChangesAsync();
return device;
}

public static async Task<Profile> SetUpSeniorProfile(this AppDbContext context, Account account)
{
var profile = new Profile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public async Task Invoke_ShouldSetInternalErrorStatusCode_WhenUnknownExceptionOc
}

[Theory]
[InlineData(400, typeof(IncorrectDeviceTypeNameException))]
[InlineData(400, typeof(ValidationException))]
[InlineData(401, typeof(InvalidCredentialException))]
[InlineData(403, typeof(NoteAccessDeniedException))]
Expand Down
66 changes: 66 additions & 0 deletions SensoBackend/Application/Modules/Accounts/AddDeviceToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using FluentValidation;
using JetBrains.Annotations;
using MediatR;
using Microsoft.EntityFrameworkCore;
using SensoBackend.Application.Modules.Accounts.Contracts;
using SensoBackend.Application.Modules.Accounts.Utils;
using SensoBackend.Domain.Entities;
using SensoBackend.Infrastructure.Data;

namespace SensoBackend.Application.Modules.Accounts.CreateAccount;

public sealed record AddDeviceTokenRequest : IRequest
{
public required int AccountId;
public required AddDeviceTokenDto Dto;
}

[UsedImplicitly]
public sealed class AddDeviceTokenValidator : AbstractValidator<AddDeviceTokenRequest>
{
public AddDeviceTokenValidator()
{
RuleFor(r => r.Dto.DeviceToken).NotEmpty().WithMessage("Device token is required");
RuleFor(r => r.Dto.DeviceType).NotEmpty().WithMessage("Device type is required");
}
}

[UsedImplicitly]
public sealed class AddDeviceTokenHandler(AppDbContext context, TimeProvider timeProvider)
: IRequestHandler<AddDeviceTokenRequest>
{
public async Task Handle(AddDeviceTokenRequest request, CancellationToken ct)
{
var deviceType = GetDeviceType.FromName(request.Dto.DeviceType);

var deviceInDb = await context
.Devices
.Where(d => d.AccountId == request.AccountId && d.Type == deviceType)
.FirstOrDefaultAsync(ct);

if (deviceInDb is null)
{
var device = new Device
{
Id = default,
AccountId = request.AccountId,
Token = request.Dto.DeviceToken,
Type = deviceType,
AddedAt = timeProvider.GetUtcNow(),
};

context.Devices.Add(device);
await context.SaveChangesAsync(ct);
return;
}

if (deviceInDb.Token == request.Dto.DeviceToken)
{
return;
}

deviceInDb.Token = request.Dto.DeviceToken;
deviceInDb.AddedAt = timeProvider.GetUtcNow();
await context.SaveChangesAsync(ct);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;

namespace SensoBackend.Application.Modules.Accounts.Contracts;

public sealed record AddDeviceTokenDto
{
[Required]
public required string DeviceToken { get; init; }

[Required]
public required string DeviceType { get; init; }
}
16 changes: 16 additions & 0 deletions SensoBackend/Application/Modules/Accounts/Utils/GetDeviceType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using SensoBackend.Domain.Enums;
using SensoBackend.Domain.Exceptions;

namespace SensoBackend.Application.Modules.Accounts.Utils;

public static class GetDeviceType
{
public static DeviceType FromName(string deviceTypeName)
{
if (Enum.TryParse(deviceTypeName, true, out DeviceType result))
{
return result;
}
throw new IncorrectDeviceTypeNameException(deviceTypeName);
}
}
Loading

0 comments on commit 7344aee

Please sign in to comment.