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 class DLSiteWork
|
||||||
{
|
{
|
||||||
//public DLSiteWorkType Type { get; set; }
|
|
||||||
//public DLSiteWorkCategory Category { get; set; }
|
|
||||||
public required string ProductName { get; set; }
|
public required string ProductName { get; set; }
|
||||||
public required string ProductId { get; set; }
|
public required string ProductId { get; set; }
|
||||||
//public DateOnly? AnnouncedDate { get; set; }
|
|
||||||
public DateOnly? ExpectedDate { get; set; }
|
public DateOnly? ExpectedDate { get; set; }
|
||||||
public DateOnly? SalesDate { get; set; }
|
public DateOnly? SalesDate { get; set; }
|
||||||
public int Downloads { 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;
|
namespace JSMR.Application.VoiceWorks.Ports;
|
||||||
|
|
||||||
public interface IVoiceWorkWriter
|
public interface IVoiceWorkWriter
|
||||||
{
|
{
|
||||||
Task<SetVoiceWorkFavoriteResponse> SetFavoriteAsync(SetVoiceWorkFavoriteRequest request, CancellationToken cancellationToken);
|
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.Application.VoiceWorks.Ports;
|
||||||
using JSMR.Domain.Entities;
|
using JSMR.Domain.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -17,6 +18,32 @@ public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter
|
|||||||
return new SetVoiceWorkFavoriteResponse(request.VoiceWorkId, request.IsFavorite);
|
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)
|
private async Task<VoiceWork> GetVoiceWorkAsync(int voiceWorkId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await dbContext.VoiceWorks.FirstOrDefaultAsync(voiceWork => voiceWork.VoiceWorkId == voiceWorkId, 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
|
public class ReleasedWorksProvider(IDLSiteClient dlsiteClient) : IReleasedWorksProvider
|
||||||
{
|
{
|
||||||
|
private const int MaxPeriodDays = 60;
|
||||||
|
|
||||||
public async Task<ReleasedWorksCollection> GetReleasedWorksAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken)
|
public async Task<ReleasedWorksCollection> GetReleasedWorksAsync(VoiceWorkScanResult scanResult, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
DateOnly[] salesDates =
|
DateOnly[] salesDates =
|
||||||
@@ -20,20 +22,72 @@ public class ReleasedWorksProvider(IDLSiteClient dlsiteClient) : IReleasedWorksP
|
|||||||
if (salesDates.Length == 0)
|
if (salesDates.Length == 0)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
|
HashSet<string> productIds = [.. scanResult.Works.Select(x => x.ProductId)];
|
||||||
|
|
||||||
DateOnly minDate = salesDates.Min();
|
DateOnly minDate = salesDates.Min();
|
||||||
DateOnly maxDate = salesDates.Max();
|
DateOnly maxDate = salesDates.Max();
|
||||||
|
|
||||||
DateOnly requestDate = minDate.AddDays(-1);
|
ReleasedWorksCollection collection = [];
|
||||||
DateOnly requestEndDate = maxDate.AddDays(1);
|
|
||||||
|
|
||||||
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,
|
Locale: Locale.English,
|
||||||
Date: requestEndDate,
|
Date: chunkEnd,
|
||||||
Period: period
|
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(
|
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 = 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 },
|
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 },
|
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 },
|
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 }
|
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();
|
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"
|
@page "/login"
|
||||||
@layout LoginLayout
|
@layout LoginLayout
|
||||||
|
|
||||||
|
@using AntDesign
|
||||||
@using JSMR.UI.Blazor.Services
|
@using JSMR.UI.Blazor.Services
|
||||||
|
@using MudBlazor.Charts
|
||||||
|
@using System.ComponentModel.DataAnnotations
|
||||||
|
|
||||||
@inject SessionState Session
|
@inject SessionState Session
|
||||||
@inject NavigationManager Nav
|
@inject NavigationManager Nav
|
||||||
|
|
||||||
<h3>Login</h3>
|
@* <h3>Login</h3> *@
|
||||||
|
|
||||||
|
<PageTitle>Sign In - JSMR</PageTitle>
|
||||||
|
|
||||||
@if (Session.IsAuthenticated)
|
@if (Session.IsAuthenticated)
|
||||||
{
|
{
|
||||||
@@ -15,27 +20,82 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div style="max-width: 360px;">
|
<div class="login-container">
|
||||||
<BitCard>
|
<AntDesign.Card Title=@("Sign In") Class="ant-blurred-card">
|
||||||
<BitStack>
|
<Body>
|
||||||
<BitTextField Label="Username" @bind-Value="username"></BitTextField>
|
<AntDesign.Form Model="@loginModel" Layout="FormLayout.Vertical">
|
||||||
<BitTextField Label="Password" @bind-Value="password" Type="BitInputType.Password"></BitTextField>
|
<AntDesign.FormItem Label="Username">
|
||||||
<BitButton OnClick="LoginAsync" IsEnabled="@(!busy)">Login</BitButton>
|
<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))
|
@if (!string.IsNullOrWhiteSpace(error))
|
||||||
{
|
{
|
||||||
<p style="color: crimson; margin-top: 8px;">@error</p>
|
<Alert Type="AlertType.Error" Message="@error" ShowIcon="false" />
|
||||||
}
|
}
|
||||||
</BitStack>
|
|
||||||
</BitCard>
|
|
||||||
</div>
|
</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 {
|
@code {
|
||||||
private string username = "";
|
private string username = "";
|
||||||
private string password = "";
|
private string password = "";
|
||||||
private bool busy;
|
private bool busy;
|
||||||
private string? error;
|
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()
|
private async Task LoginAsync()
|
||||||
{
|
{
|
||||||
busy = true;
|
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()
|
private async Task Logout()
|
||||||
{
|
{
|
||||||
busy = true;
|
busy = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user