Compare commits
3 Commits
77a02a543d
...
2bd7e3b970
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bd7e3b970 | |||
| abcc82437f | |||
| f6674e0382 |
@@ -4,11 +4,8 @@ namespace JSMR.Application.Scanning.Contracts;
|
||||
|
||||
public class DLSiteWork
|
||||
{
|
||||
//public DLSiteWorkType Type { get; set; }
|
||||
//public DLSiteWorkCategory Category { get; set; }
|
||||
public required string ProductName { get; set; }
|
||||
public required string ProductId { get; set; }
|
||||
//public DateOnly? AnnouncedDate { get; set; }
|
||||
public DateOnly? ExpectedDate { get; set; }
|
||||
public DateOnly? SalesDate { get; set; }
|
||||
public int Downloads { get; set; }
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using JSMR.Application.VoiceWorks.Ports;
|
||||
|
||||
namespace JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
|
||||
public sealed class DeleteVoiceWorkFavoriteHandler(IVoiceWorkWriter writer)
|
||||
{
|
||||
public async Task<DeleteVoiceWorkResponse> HandleAsync(DeleteVoiceWorkRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await writer.DeleteAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
|
||||
public sealed record DeleteVoiceWorkRequest(int[] VoiceWorkIds);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
|
||||
public sealed record DeleteVoiceWorkResponse(Dictionary<int, bool> IsSuccess);
|
||||
@@ -1,8 +1,10 @@
|
||||
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||
using JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||
|
||||
namespace JSMR.Application.VoiceWorks.Ports;
|
||||
|
||||
public interface IVoiceWorkWriter
|
||||
{
|
||||
Task<SetVoiceWorkFavoriteResponse> SetFavoriteAsync(SetVoiceWorkFavoriteRequest request, CancellationToken cancellationToken);
|
||||
Task<DeleteVoiceWorkResponse> DeleteAsync(DeleteVoiceWorkRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||
using JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
using JSMR.Application.VoiceWorks.Commands.SetFavorite;
|
||||
using JSMR.Application.VoiceWorks.Ports;
|
||||
using JSMR.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -17,6 +18,32 @@ public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter
|
||||
return new SetVoiceWorkFavoriteResponse(request.VoiceWorkId, request.IsFavorite);
|
||||
}
|
||||
|
||||
public async Task<DeleteVoiceWorkResponse> DeleteAsync(DeleteVoiceWorkRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
Dictionary<int, bool> isSuccess = [];
|
||||
|
||||
VoiceWork[] voiceWorks = [.. dbContext.VoiceWorks.Where(voiceWork => request.VoiceWorkIds.Contains(voiceWork.VoiceWorkId))
|
||||
.Include(x => x.Circle)];
|
||||
|
||||
foreach (VoiceWork voiceWork in voiceWorks)
|
||||
{
|
||||
isSuccess.Add(voiceWork.VoiceWorkId, false);
|
||||
|
||||
if (voiceWork.Circle is null)
|
||||
continue;
|
||||
|
||||
if (voiceWork.IsValid == true && voiceWork.Circle.Spam == false)
|
||||
continue;
|
||||
|
||||
dbContext.Remove(voiceWork);
|
||||
isSuccess[voiceWork.VoiceWorkId] = true;
|
||||
}
|
||||
|
||||
dbContext.SaveChanges();
|
||||
|
||||
return new DeleteVoiceWorkResponse(isSuccess);
|
||||
}
|
||||
|
||||
private async Task<VoiceWork> GetVoiceWorkAsync(int voiceWorkId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await dbContext.VoiceWorks.FirstOrDefaultAsync(voiceWork => voiceWork.VoiceWorkId == voiceWorkId, cancellationToken)
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace JSMR.Infrastructure.Scanning;
|
||||
|
||||
public class ReleasedWorksProvider(IDLSiteClient dlsiteClient) : IReleasedWorksProvider
|
||||
{
|
||||
private const int MaxPeriodDays = 60;
|
||||
|
||||
public async Task<ReleasedWorksCollection> GetReleasedWorksAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken)
|
||||
{
|
||||
DateOnly[] salesDates =
|
||||
@@ -20,20 +22,72 @@ public class ReleasedWorksProvider(IDLSiteClient dlsiteClient) : IReleasedWorksP
|
||||
if (salesDates.Length == 0)
|
||||
return [];
|
||||
|
||||
HashSet<string> productIds = [.. scanResult.Works.Select(x => x.ProductId)];
|
||||
|
||||
DateOnly minDate = salesDates.Min();
|
||||
DateOnly maxDate = salesDates.Max();
|
||||
|
||||
DateOnly requestDate = minDate.AddDays(-1);
|
||||
DateOnly requestEndDate = maxDate.AddDays(1);
|
||||
ReleasedWorksCollection collection = [];
|
||||
|
||||
int period = (requestEndDate.DayNumber - requestDate.DayNumber) + 1;
|
||||
DateOnly chunkStart = minDate;
|
||||
|
||||
ReleasedWorksRequest releasedWorksRequest = new(
|
||||
while (chunkStart <= maxDate)
|
||||
{
|
||||
int endDayNumber = Math.Min(chunkStart.DayNumber + MaxPeriodDays - 1, maxDate.DayNumber);
|
||||
DateOnly chunkEnd = DateOnly.FromDayNumber(endDayNumber);
|
||||
|
||||
int period = chunkEnd.DayNumber - chunkStart.DayNumber + 1;
|
||||
|
||||
ReleasedWorksRequest request = new(
|
||||
Locale: Locale.English,
|
||||
Date: requestEndDate,
|
||||
Period: period
|
||||
);
|
||||
Date: chunkEnd,
|
||||
Period: period);
|
||||
|
||||
return await dlsiteClient.GetReleasedWorksAsync(releasedWorksRequest, cancellationToken);
|
||||
ReleasedWorksCollection chunk = await dlsiteClient.GetReleasedWorksAsync(request, cancellationToken);
|
||||
|
||||
foreach (string productId in chunk.Keys)
|
||||
{
|
||||
if (productIds.Contains(productId) == false)
|
||||
continue;
|
||||
|
||||
if (collection.ContainsKey(productId))
|
||||
continue;
|
||||
|
||||
collection.Add(productId, chunk[productId]);
|
||||
}
|
||||
|
||||
chunkStart = chunkEnd.AddDays(1);
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
//public async Task<ReleasedWorksCollection> GetReleasedWorksAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken)
|
||||
//{
|
||||
// DateOnly[] salesDates =
|
||||
// [
|
||||
// .. scanResult.Works
|
||||
// .Where(x => x.SalesDate.HasValue)
|
||||
// .Select(x => x.SalesDate!.Value)
|
||||
// ];
|
||||
|
||||
// if (salesDates.Length == 0)
|
||||
// return [];
|
||||
|
||||
// DateOnly minDate = salesDates.Min();
|
||||
// DateOnly maxDate = salesDates.Max();
|
||||
|
||||
// DateOnly requestDate = minDate.AddDays(-1);
|
||||
// DateOnly requestEndDate = maxDate.AddDays(1);
|
||||
|
||||
// int period = (requestEndDate.DayNumber - requestDate.DayNumber) + 1;
|
||||
|
||||
// ReleasedWorksRequest releasedWorksRequest = new(
|
||||
// Locale: Locale.English,
|
||||
// Date: requestEndDate,
|
||||
// Period: period
|
||||
// );
|
||||
|
||||
// return await dlsiteClient.GetReleasedWorksAsync(releasedWorksRequest, cancellationToken);
|
||||
//}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using JSMR.Application.VoiceWorks.Commands.Delete;
|
||||
using JSMR.Infrastructure.Data;
|
||||
using JSMR.Infrastructure.Data.Repositories.VoiceWorks;
|
||||
using JSMR.Tests.Fixtures;
|
||||
using Shouldly;
|
||||
|
||||
namespace JSMR.Tests.Data.Repositories.VoiceWorks;
|
||||
|
||||
public class Delete_Voice_Work_Tests(MariaDbContainerFixture container) : VoiceWorkRepositoryTests(container)
|
||||
{
|
||||
[Fact]
|
||||
public async Task Try_Delete_Invalid_Voice_Work()
|
||||
{
|
||||
await using AppDbContext dbContext = await GetAppDbContextAsync();
|
||||
VoiceWorkWriter writer = new(dbContext);
|
||||
|
||||
int voiceWorkId = 3;
|
||||
DeleteVoiceWorkRequest request = new([voiceWorkId]);
|
||||
|
||||
DeleteVoiceWorkResponse response = await writer.DeleteAsync(request, TestContext.Current.CancellationToken);
|
||||
response.IsSuccess.Count.ShouldBe(1);
|
||||
response.IsSuccess.ShouldContainKey(voiceWorkId);
|
||||
response.IsSuccess[voiceWorkId].ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Try_Delete_Valid_Voice_Work()
|
||||
{
|
||||
await using AppDbContext dbContext = await GetAppDbContextAsync();
|
||||
VoiceWorkWriter writer = new(dbContext);
|
||||
|
||||
int voiceWorkId = 1;
|
||||
DeleteVoiceWorkRequest request = new([voiceWorkId]);
|
||||
|
||||
DeleteVoiceWorkResponse response = await writer.DeleteAsync(request, TestContext.Current.CancellationToken);
|
||||
response.IsSuccess.Count.ShouldBe(1);
|
||||
response.IsSuccess.ShouldContainKey(voiceWorkId);
|
||||
response.IsSuccess[voiceWorkId].ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,65 @@ public static class VoiceWorkRepositorySeedData
|
||||
);
|
||||
|
||||
context.VoiceWorks.AddRange(
|
||||
new() { VoiceWorkId = 1, CircleId = 1, ProductId = "RJ0000001", ProductName = "Today Sounds", Description = "An average product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 1), Downloads = 500, WishlistCount = 750, StarRating = 35 },
|
||||
new() { VoiceWorkId = 2, CircleId = 2, ProductId = "RJ0000002", ProductName = "Super Comfy ASMR", Description = "An amazing product!", Status = (byte)VoiceWorkStatus.NewRelease, SalesDate = new(2025, 1, 3), Downloads = 5000, WishlistCount = 12000, StarRating = 50, Favorite = true },
|
||||
new() { VoiceWorkId = 3, CircleId = 3, ProductId = "RJ0000003", ProductName = "Low Effort", Description = "A bad product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 2), Downloads = 50, WishlistCount = 100, StarRating = 20 },
|
||||
new() { VoiceWorkId = 4, CircleId = 1, ProductId = "RJ0000004", ProductName = "Tomorrow Sounds", Description = "A average upcoming product.", Status = (byte)VoiceWorkStatus.Upcoming, ExpectedDate = new(2025, 1, 1), WishlistCount = 300 },
|
||||
new() { VoiceWorkId = 5, CircleId = 2, ProductId = "RJ0000005", ProductName = "Super Comfy ASMR+", Description = "All your favorite sounds, plus more!", Status = (byte)VoiceWorkStatus.NewAndUpcoming, ExpectedDate = new(2025, 1, 11), WishlistCount = 10000 }
|
||||
new() { VoiceWorkId = 1, CircleId = 1, ProductId = "RJ0000001", ProductName = "Today Sounds", Description = "An average product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 1), Downloads = 500, WishlistCount = 750, StarRating = 35, IsValid = true },
|
||||
new() { VoiceWorkId = 2, CircleId = 2, ProductId = "RJ0000002", ProductName = "Super Comfy ASMR", Description = "An amazing product!", Status = (byte)VoiceWorkStatus.NewRelease, SalesDate = new(2025, 1, 3), Downloads = 5000, WishlistCount = 12000, StarRating = 50, Favorite = true, IsValid = true },
|
||||
new() { VoiceWorkId = 3, CircleId = 3, ProductId = "RJ0000003", ProductName = "Low Effort", Description = "A bad product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 2), Downloads = 50, WishlistCount = 100, StarRating = 20, IsValid = false },
|
||||
new() { VoiceWorkId = 4, CircleId = 1, ProductId = "RJ0000004", ProductName = "Tomorrow Sounds", Description = "A average upcoming product.", Status = (byte)VoiceWorkStatus.Upcoming, ExpectedDate = new(2025, 1, 1), WishlistCount = 300, IsValid = true },
|
||||
new() { VoiceWorkId = 5, CircleId = 2, ProductId = "RJ0000005", ProductName = "Super Comfy ASMR+", Description = "All your favorite sounds, plus more!", Status = (byte)VoiceWorkStatus.NewAndUpcoming, ExpectedDate = new(2025, 1, 11), WishlistCount = 10000, IsValid = true }
|
||||
);
|
||||
|
||||
context.Creators.AddRange(
|
||||
new() { CreatorId = 1, Name = "Average Creator" },
|
||||
new() { CreatorId = 2, Name = "Good Creator" },
|
||||
new() { CreatorId = 3, Name = "Bad Creator" }
|
||||
);
|
||||
|
||||
context.VoiceWorkCreators.AddRange(
|
||||
new() { VoiceWorkId = 1, CreatorId = 1 },
|
||||
new() { VoiceWorkId = 2, CreatorId = 2 },
|
||||
new() { VoiceWorkId = 3, CreatorId = 3 }
|
||||
);
|
||||
|
||||
context.Tags.AddRange(
|
||||
new() { TagId = 1, Name = "ASMR" },
|
||||
new() { TagId = 2, Name = "Tsundere" },
|
||||
new() { TagId = 3, Name = "Office Lady" }
|
||||
);
|
||||
|
||||
context.VoiceWorkTags.AddRange(
|
||||
new() { VoiceWorkId = 1, TagId = 1 },
|
||||
new() { VoiceWorkId = 2, TagId = 2 },
|
||||
new() { VoiceWorkId = 3, TagId = 3 }
|
||||
);
|
||||
|
||||
context.VoiceWorkSupportedLanguages.AddRange(
|
||||
new() { VoiceWorkSupportedLanguageId = 1, VoiceWorkId = 1, Language = "JPN" },
|
||||
new() { VoiceWorkSupportedLanguageId = 2, VoiceWorkId = 2, Language = "JPN" },
|
||||
new() { VoiceWorkSupportedLanguageId = 3, VoiceWorkId = 3, Language = "JPN" }
|
||||
);
|
||||
|
||||
context.EnglishVoiceWorks.AddRange(
|
||||
new() { EnglishVoiceWorkId = 1, VoiceWorkId = 1, ProductName = "Today Sounds", Description = "An average product." },
|
||||
new() { EnglishVoiceWorkId = 2, VoiceWorkId = 2, ProductName = "Super Comfy ASMR", Description = "An amazing product!" },
|
||||
new() { EnglishVoiceWorkId = 3, VoiceWorkId = 3, ProductName = "Low Effort", Description = "A bad product." },
|
||||
new() { EnglishVoiceWorkId = 4, VoiceWorkId = 4, ProductName = "Tomorrow Sounds", Description = "A average upcoming product." },
|
||||
new() { EnglishVoiceWorkId = 5, VoiceWorkId = 5, ProductName = "Super Comfy ASMR+", Description = "All your favorite sounds, plus more!" }
|
||||
);
|
||||
|
||||
context.VoiceWorkLocalizations.AddRange(
|
||||
new() { VoiceWorkLocalizationId = 1, VoiceWorkId = 1, Language = "JPN", ProductName = "Today Sounds", Description = "An average product." },
|
||||
new() { VoiceWorkLocalizationId = 2, VoiceWorkId = 2, Language = "JPN", ProductName = "Super Comfy ASMR", Description = "An amazing product!" },
|
||||
new() { VoiceWorkLocalizationId = 3, VoiceWorkId = 3, Language = "JPN", ProductName = "Low Effort", Description = "A bad product." },
|
||||
new() { VoiceWorkLocalizationId = 4, VoiceWorkId = 4, Language = "JPN", ProductName = "Tomorrow Sounds", Description = "A average upcoming product." },
|
||||
new() { VoiceWorkLocalizationId = 5, VoiceWorkId = 5, Language = "JPN", ProductName = "Super Comfy ASMR+", Description = "All your favorite sounds, plus more!" }
|
||||
);
|
||||
|
||||
context.VoiceWorkSearches.AddRange(
|
||||
new() { VoiceWorkId = 1, SearchText = "Search 1" },
|
||||
new() { VoiceWorkId = 2, SearchText = "Search 2" },
|
||||
new() { VoiceWorkId = 3, SearchText = "Search 3" },
|
||||
new() { VoiceWorkId = 4, SearchText = "Search 4" },
|
||||
new() { VoiceWorkId = 5, SearchText = "Search 5" }
|
||||
);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
288
JSMR.Tests/Unit/ReleasedWorksProviderTests.cs
Normal file
288
JSMR.Tests/Unit/ReleasedWorksProviderTests.cs
Normal file
@@ -0,0 +1,288 @@
|
||||
using JSMR.Application.Enums;
|
||||
using JSMR.Application.Integrations.DLSite.Models.ReleasedWorks;
|
||||
using JSMR.Application.Integrations.DLSite.Ports;
|
||||
using JSMR.Application.Scanning.Contracts;
|
||||
using JSMR.Infrastructure.Scanning;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace JSMR.Tests.Unit;
|
||||
|
||||
public class ReleasedWorksProviderTests
|
||||
{
|
||||
private readonly IDLSiteClient _dlsiteClient = Substitute.For<IDLSiteClient>();
|
||||
|
||||
[Fact]
|
||||
public async Task GetReleasedWorksAsync_WhenNoSalesDates_ReturnsEmptyAndDoesNotCallClient()
|
||||
{
|
||||
VoiceWorkScanResult scanResult = new(
|
||||
Works:
|
||||
[
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
SalesDate = null,
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
}
|
||||
],
|
||||
EndOfResults: false
|
||||
);
|
||||
|
||||
ReleasedWorksProvider provider = new(_dlsiteClient);
|
||||
|
||||
ReleasedWorksCollection result = await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
|
||||
await _dlsiteClient.DidNotReceiveWithAnyArgs().GetReleasedWorksAsync(default!, TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetReleasedWorksAsync_WhenRangeIsUnder60Days_CallsClientOnce()
|
||||
{
|
||||
VoiceWorkScanResult scanResult = new(
|
||||
Works:
|
||||
[
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
SalesDate = new DateOnly(2024, 1, 10),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
},
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ002",
|
||||
SalesDate = new DateOnly(2024, 1, 20),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
}
|
||||
],
|
||||
EndOfResults: false
|
||||
);
|
||||
|
||||
ReleasedWorksCollection apiResult = new()
|
||||
{
|
||||
["RJ001"] = new ReleasedWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
Title = "English title 1",
|
||||
Description = "Description",
|
||||
MaskedTitle = "English title 1",
|
||||
MaskedDescription = "Description",
|
||||
},
|
||||
["RJ002"] = new ReleasedWork
|
||||
{
|
||||
ProductId = "RJ002",
|
||||
Title = "English title 2",
|
||||
Description = "Description",
|
||||
MaskedTitle = "English title 2",
|
||||
MaskedDescription = "Description",
|
||||
}
|
||||
};
|
||||
|
||||
_dlsiteClient
|
||||
.GetReleasedWorksAsync(Arg.Any<ReleasedWorksRequest>(), Arg.Any<CancellationToken>())
|
||||
.Returns(apiResult);
|
||||
|
||||
ReleasedWorksProvider provider = new(_dlsiteClient);
|
||||
|
||||
ReleasedWorksCollection result =
|
||||
await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken);
|
||||
|
||||
result.Keys.ShouldBe(["RJ001", "RJ002"], ignoreOrder: true);
|
||||
|
||||
await _dlsiteClient.Received(1).GetReleasedWorksAsync(
|
||||
Arg.Is<ReleasedWorksRequest>(x =>
|
||||
x.Locale == Locale.English &&
|
||||
x.Date == new DateOnly(2024, 1, 20) &&
|
||||
x.Period == 11),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetReleasedWorksAsync_WhenRangeExceeds60Days_SplitsIntoMultipleRequests()
|
||||
{
|
||||
VoiceWorkScanResult scanResult = new(
|
||||
Works:
|
||||
[
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
SalesDate = new DateOnly(2024, 1, 1),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
},
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ002",
|
||||
SalesDate = new DateOnly(2024, 3, 5),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
}
|
||||
],
|
||||
EndOfResults: false
|
||||
);
|
||||
|
||||
_dlsiteClient
|
||||
.GetReleasedWorksAsync(Arg.Any<ReleasedWorksRequest>(), Arg.Any<CancellationToken>())
|
||||
.Returns([]);
|
||||
|
||||
ReleasedWorksProvider provider = new(_dlsiteClient);
|
||||
|
||||
await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken);
|
||||
|
||||
await _dlsiteClient.Received(1).GetReleasedWorksAsync(
|
||||
Arg.Is<ReleasedWorksRequest>(x =>
|
||||
x.Date == new DateOnly(2024, 2, 29) &&
|
||||
x.Period == 60),
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
await _dlsiteClient.Received(1).GetReleasedWorksAsync(
|
||||
Arg.Is<ReleasedWorksRequest>(x =>
|
||||
x.Date == new DateOnly(2024, 3, 5) &&
|
||||
x.Period == 5),
|
||||
Arg.Any<CancellationToken>());
|
||||
|
||||
await _dlsiteClient.Received(2).GetReleasedWorksAsync(
|
||||
Arg.Any<ReleasedWorksRequest>(),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetReleasedWorksAsync_FiltersOutProductsNotInScanResult()
|
||||
{
|
||||
VoiceWorkScanResult scanResult = new(
|
||||
Works:
|
||||
[
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
SalesDate = new DateOnly(2024, 1, 10),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
}
|
||||
],
|
||||
EndOfResults: false
|
||||
);
|
||||
|
||||
ReleasedWorksCollection apiResult = new()
|
||||
{
|
||||
["RJ001"] = new ReleasedWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
Title = "Keep me",
|
||||
Description = "Description",
|
||||
MaskedTitle = "Keep me",
|
||||
MaskedDescription = "Description",
|
||||
},
|
||||
["RJ999"] = new ReleasedWork
|
||||
{
|
||||
ProductId = "RJ999",
|
||||
Title = "Ignore me",
|
||||
Description = "Description",
|
||||
MaskedTitle = "Ignore me",
|
||||
MaskedDescription = "Description",
|
||||
},
|
||||
};
|
||||
|
||||
_dlsiteClient
|
||||
.GetReleasedWorksAsync(Arg.Any<ReleasedWorksRequest>(), Arg.Any<CancellationToken>())
|
||||
.Returns(apiResult);
|
||||
|
||||
ReleasedWorksProvider provider = new(_dlsiteClient);
|
||||
|
||||
ReleasedWorksCollection result =
|
||||
await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken);
|
||||
|
||||
result.Keys.ShouldBe(["RJ001"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetReleasedWorksAsync_WhenSameProductReturnedTwice_KeepsFirstResult()
|
||||
{
|
||||
VoiceWorkScanResult scanResult = new(
|
||||
Works:
|
||||
[
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
SalesDate = new DateOnly(2024, 1, 1),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
},
|
||||
new DLSiteWork
|
||||
{
|
||||
ProductId = "RJ002",
|
||||
SalesDate = new DateOnly(2024, 3, 5),
|
||||
ProductName = "",
|
||||
MakerId = "",
|
||||
Maker = "",
|
||||
ImageUrl = "",
|
||||
SmallImageUrl = ""
|
||||
}
|
||||
],
|
||||
EndOfResults: false
|
||||
);
|
||||
|
||||
_dlsiteClient
|
||||
.GetReleasedWorksAsync(
|
||||
Arg.Is<ReleasedWorksRequest>(x => x.Period == 60),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(new ReleasedWorksCollection
|
||||
{
|
||||
["RJ001"] = new ReleasedWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
Title = "First",
|
||||
Description = "Description",
|
||||
MaskedTitle = "First",
|
||||
MaskedDescription = "Description",
|
||||
}
|
||||
});
|
||||
|
||||
_dlsiteClient
|
||||
.GetReleasedWorksAsync(
|
||||
Arg.Is<ReleasedWorksRequest>(x => x.Period == 5),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(new ReleasedWorksCollection
|
||||
{
|
||||
["RJ001"] = new ReleasedWork
|
||||
{
|
||||
ProductId = "RJ001",
|
||||
Title = "Second",
|
||||
Description = "Description",
|
||||
MaskedTitle = "Second",
|
||||
MaskedDescription = "Description",
|
||||
}
|
||||
});
|
||||
|
||||
ReleasedWorksProvider provider = new(_dlsiteClient);
|
||||
|
||||
ReleasedWorksCollection result = await provider.GetReleasedWorksAsync(scanResult, TestContext.Current.CancellationToken);
|
||||
|
||||
result["RJ001"].Title.ShouldBe("First");
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
@page "/login"
|
||||
@layout LoginLayout
|
||||
|
||||
@using AntDesign
|
||||
@using JSMR.UI.Blazor.Services
|
||||
@using MudBlazor.Charts
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
|
||||
@inject SessionState Session
|
||||
@inject NavigationManager Nav
|
||||
|
||||
<h3>Login</h3>
|
||||
@* <h3>Login</h3> *@
|
||||
|
||||
<PageTitle>Sign In - JSMR</PageTitle>
|
||||
|
||||
@if (Session.IsAuthenticated)
|
||||
{
|
||||
@@ -15,27 +20,82 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="max-width: 360px;">
|
||||
<BitCard>
|
||||
<BitStack>
|
||||
<BitTextField Label="Username" @bind-Value="username"></BitTextField>
|
||||
<BitTextField Label="Password" @bind-Value="password" Type="BitInputType.Password"></BitTextField>
|
||||
<BitButton OnClick="LoginAsync" IsEnabled="@(!busy)">Login</BitButton>
|
||||
<div class="login-container">
|
||||
<AntDesign.Card Title=@("Sign In") Class="ant-blurred-card">
|
||||
<Body>
|
||||
<AntDesign.Form Model="@loginModel" Layout="FormLayout.Vertical">
|
||||
<AntDesign.FormItem Label="Username">
|
||||
<AntDesign.Input @bind-Value="context.Username"></AntDesign.Input>
|
||||
</AntDesign.FormItem>
|
||||
<AntDesign.FormItem Label="Password">
|
||||
<AntDesign.InputPassword @bind-Value="context.Password"></AntDesign.InputPassword>
|
||||
</AntDesign.FormItem>
|
||||
<AntDesign.Button Class="login-button" OnClick="Login2Async" Disabled="@(busy)" Type="AntDesign.ButtonType.Primary">Sign In</AntDesign.Button>
|
||||
</AntDesign.Form>
|
||||
</Body>
|
||||
</AntDesign.Card>
|
||||
@if (!string.IsNullOrWhiteSpace(error))
|
||||
{
|
||||
<p style="color: crimson; margin-top: 8px;">@error</p>
|
||||
<Alert Type="AlertType.Error" Message="@error" ShowIcon="false" />
|
||||
}
|
||||
</BitStack>
|
||||
</BitCard>
|
||||
</div>
|
||||
}
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-image: url(https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*ETkNSJ-oUGwAAAAAQ_AAAAgAegCCAQ/original);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.ant-card-head-title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ant-form-item-required {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: .25rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
max-width: 450px;
|
||||
padding: 1rem;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-blurred-card {
|
||||
background-color: color-mix(in srgb, #141414 70%, transparent);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private string username = "";
|
||||
private string password = "";
|
||||
private bool busy;
|
||||
private string? error;
|
||||
|
||||
private LoginModel loginModel = new();
|
||||
|
||||
public class LoginModel
|
||||
{
|
||||
[Required]
|
||||
public string? Username { get; set; }
|
||||
|
||||
[Required]
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
|
||||
private async Task LoginAsync()
|
||||
{
|
||||
busy = true;
|
||||
@@ -61,6 +121,43 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Login2Async()
|
||||
{
|
||||
busy = true;
|
||||
error = null;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(loginModel.Username))
|
||||
{
|
||||
error = "Username is required.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(loginModel.Password))
|
||||
{
|
||||
error = "Password is required.";
|
||||
return;
|
||||
}
|
||||
|
||||
var ok = await Session.LoginAsync(loginModel.Username, loginModel.Password);
|
||||
if (!ok)
|
||||
{
|
||||
error = "Invalid username or password.";
|
||||
return;
|
||||
}
|
||||
|
||||
Nav.NavigateTo("/");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Logout()
|
||||
{
|
||||
busy = true;
|
||||
|
||||
Reference in New Issue
Block a user