From aab7bee6943056812b6d6f1df96bc82adb2ace99 Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Sat, 14 Mar 2026 21:46:53 -0400 Subject: [PATCH] Added Chobit integration. Updated tests. --- .../Chobit/Models/ChobitResult.cs | 7 +- .../Chobit/Models/ChobitResultCollection.cs | 3 + .../Integrations/Chobit/Models/ChobitWork.cs | 29 +++- .../Chobit/Models/ChobitWorkResult.cs | 3 - .../Integrations/Ports/IChobitClient.cs | 3 +- .../Scanning/Contracts/VoiceWorkIngest.cs | 9 +- .../Scanning/ScanVoiceWorksHandler.cs | 6 +- ...frastructureServiceCollectionExtensions.cs | 49 +++++- JSMR.Infrastructure/Http/ApiClient.cs | 148 ++++++++++------- .../Ingestion/VoiceWorkUpdater.cs | 2 +- .../Integrations/Chobit/ChobitClient.cs | 14 +- .../Integrations/DLSite/DLSiteClient.cs | 14 +- JSMR.Tests/Http/FakeHttpMessageHandler.cs | 9 + JSMR.Tests/Ingestion/IngestTestFactory.cs | 2 +- .../Japanese/Basic_Insert_And_Update_Tests.cs | 154 ++++++++++++++++++ ...Attempted_Insert_With_Spam_Circle_Tests.cs | 2 +- ...d_Update_With_Decreased_Downloads_Tests.cs | 2 +- ...d_Update_With_Sales_Date_Reversal_Tests.cs | 2 +- ..._New_Release_And_Scan_Again_Later_Tests.cs | 2 +- ...elease_With_New_Tags_And_Creators_Tests.cs | 2 +- ...New_Upcoming_And_Scan_Again_Later_Tests.cs | 2 +- ...ert_New_Upcoming_Release_Same_Day_Tests.cs | 2 +- ...g_With_Existing_Tags_And_Creators_Tests.cs | 2 +- ...te_Upcoming_With_No_Expected_Date_Tests.cs | 2 +- .../Integrations/Chobit/ChobitClientTests.cs | 98 +++++++++++ ...ple-Chobit-Result-Collection-No-Data.jsonp | 1 + .../Sample-Chobit-Result-Collection.jsonp | 1 + .../Chobit/Sample-Chobit-Result-No-Data.jsonp | 1 + .../Chobit/Sample-Chobit-Result.jsonp | 1 + .../Integrations/DLSite/DLSiteClientTests.cs | 37 +++-- JSMR.Tests/JSMR.Tests.csproj | 4 + JSMR.Tests/Scanning/VoiceWorkScannerTests.cs | 16 +- 32 files changed, 521 insertions(+), 108 deletions(-) create mode 100644 JSMR.Application/Integrations/Chobit/Models/ChobitResultCollection.cs delete mode 100644 JSMR.Application/Integrations/Chobit/Models/ChobitWorkResult.cs create mode 100644 JSMR.Tests/Http/FakeHttpMessageHandler.cs create mode 100644 JSMR.Tests/Ingestion/Japanese/Basic_Insert_And_Update_Tests.cs create mode 100644 JSMR.Tests/Integrations/Chobit/ChobitClientTests.cs create mode 100644 JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result-Collection-No-Data.jsonp create mode 100644 JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result-Collection.jsonp create mode 100644 JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result-No-Data.jsonp create mode 100644 JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result.jsonp diff --git a/JSMR.Application/Integrations/Chobit/Models/ChobitResult.cs b/JSMR.Application/Integrations/Chobit/Models/ChobitResult.cs index c4839f3..dde4a89 100644 --- a/JSMR.Application/Integrations/Chobit/Models/ChobitResult.cs +++ b/JSMR.Application/Integrations/Chobit/Models/ChobitResult.cs @@ -1,7 +1,12 @@ -namespace JSMR.Application.Integrations.Chobit.Models; +using System.Text.Json.Serialization; + +namespace JSMR.Application.Integrations.Chobit.Models; public class ChobitResult { + [JsonPropertyName("count")] public int Count { get; set; } + + [JsonPropertyName("works")] public ChobitWork[] Works { get; set; } = []; } \ No newline at end of file diff --git a/JSMR.Application/Integrations/Chobit/Models/ChobitResultCollection.cs b/JSMR.Application/Integrations/Chobit/Models/ChobitResultCollection.cs new file mode 100644 index 0000000..56ea8b7 --- /dev/null +++ b/JSMR.Application/Integrations/Chobit/Models/ChobitResultCollection.cs @@ -0,0 +1,3 @@ +namespace JSMR.Application.Integrations.Chobit.Models; + +public class ChobitResultCollection : Dictionary { } \ No newline at end of file diff --git a/JSMR.Application/Integrations/Chobit/Models/ChobitWork.cs b/JSMR.Application/Integrations/Chobit/Models/ChobitWork.cs index 2fe31c6..12295de 100644 --- a/JSMR.Application/Integrations/Chobit/Models/ChobitWork.cs +++ b/JSMR.Application/Integrations/Chobit/Models/ChobitWork.cs @@ -1,16 +1,39 @@ -namespace JSMR.Application.Integrations.Chobit.Models; +using System.Text.Json.Serialization; + +namespace JSMR.Application.Integrations.Chobit.Models; public class ChobitWork { + [JsonPropertyName("work_id")] public string? WorkId { get; set; } + + [JsonPropertyName("dlsite_work_id")] public string? DLSiteWorkId { get; set; } + + [JsonPropertyName("work_name")] public string? WorkName { get; set; } + + [JsonPropertyName("work_name_kana")] public string? WorkNameKana { get; set; } - public string? URL { get; set; } - public string? EmbedURL { get; set; } + + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("embed_url")] + public string? EmbedUrl { get; set; } + + [JsonPropertyName("thumb")] public string? Thumb { get; set; } + + [JsonPropertyName("mini_thumb")] public string? MiniThumb { get; set; } + + [JsonPropertyName("file_type")] public string? FileType { get; set; } + + [JsonPropertyName("embed_width")] public decimal EmbedWidth { get; set; } + + [JsonPropertyName("embed_height")] public decimal EmbedHeight { get; set; } } \ No newline at end of file diff --git a/JSMR.Application/Integrations/Chobit/Models/ChobitWorkResult.cs b/JSMR.Application/Integrations/Chobit/Models/ChobitWorkResult.cs deleted file mode 100644 index 8cafea8..0000000 --- a/JSMR.Application/Integrations/Chobit/Models/ChobitWorkResult.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace JSMR.Application.Integrations.Chobit.Models; - -public class ChobitWorkResult : Dictionary { } \ No newline at end of file diff --git a/JSMR.Application/Integrations/Ports/IChobitClient.cs b/JSMR.Application/Integrations/Ports/IChobitClient.cs index 07fbaef..a1978c1 100644 --- a/JSMR.Application/Integrations/Ports/IChobitClient.cs +++ b/JSMR.Application/Integrations/Ports/IChobitClient.cs @@ -4,5 +4,6 @@ namespace JSMR.Application.Integrations.Ports; public interface IChobitClient { - Task GetSampleInfoAsync(string[] productIds, CancellationToken cancellationToken = default); + Task GetSampleInfoAsync(string productId, CancellationToken cancellationToken = default); + Task GetSampleInfoAsync(string[] productIds, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/JSMR.Application/Scanning/Contracts/VoiceWorkIngest.cs b/JSMR.Application/Scanning/Contracts/VoiceWorkIngest.cs index a8691f4..d7ae30c 100644 --- a/JSMR.Application/Scanning/Contracts/VoiceWorkIngest.cs +++ b/JSMR.Application/Scanning/Contracts/VoiceWorkIngest.cs @@ -1,4 +1,5 @@ -using JSMR.Application.Integrations.DLSite.Models; +using JSMR.Application.Integrations.Chobit.Models; +using JSMR.Application.Integrations.DLSite.Models; using JSMR.Domain.Enums; using JSMR.Domain.ValueObjects; @@ -16,7 +17,7 @@ public sealed record VoiceWorkIngest public int WishlistCount { get; init; } public int Downloads { get; init; } public bool HasTrial { get; init; } - public bool HasDLPlay { get; init; } + public bool HasChobit { get; init; } public byte? StarRating { get; init; } public int? Votes { get; init; } public AgeRating AgeRating { get; init; } @@ -29,7 +30,7 @@ public sealed record VoiceWorkIngest public VoiceWorkSeries? Series { get; init; } public VoiceWorkTranslation? Translation { get; init; } - public static VoiceWorkIngest From(DLSiteWork work, VoiceWorkDetails? details) + public static VoiceWorkIngest From(DLSiteWork work, VoiceWorkDetails? details, ChobitResult? chobit) { return new VoiceWorkIngest() { @@ -43,7 +44,7 @@ public sealed record VoiceWorkIngest WishlistCount = details?.WishlistCount ?? 0, Downloads = Math.Max(work.Downloads, details?.DownloadCount ?? 0), HasTrial = work.HasTrial || (details?.HasTrial ?? false), - HasDLPlay = details?.HasDLPlay ?? false, + HasChobit = chobit?.Count > 0, StarRating = work.StarRating, Votes = work.Votes, AgeRating = details?.AgeRating ?? work.AgeRating, diff --git a/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs b/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs index 44a80a0..87b3fe5 100644 --- a/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs +++ b/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs @@ -1,4 +1,5 @@ using JSMR.Application.Common.Caching; +using JSMR.Application.Integrations.Chobit.Models; using JSMR.Application.Integrations.DLSite.Models; using JSMR.Application.Integrations.Ports; using JSMR.Application.Scanning.Contracts; @@ -10,6 +11,7 @@ public sealed class ScanVoiceWorksHandler( IVoiceWorkScannerRepository scannerRepository, IVoiceWorkUpdaterRepository updaterRepository, IDLSiteClient dlsiteClient, + IChobitClient chobitClient, ISpamCircleCache spamCircleCache, IVoiceWorkSearchUpdater searchUpdater) { @@ -44,11 +46,13 @@ public sealed class ScanVoiceWorksHandler( string[] productIds = [.. scanResult.Works.Where(x => !string.IsNullOrWhiteSpace(x.ProductId)).Select(x => x.ProductId!)]; VoiceWorkDetailCollection voiceWorkDetails = await dlsiteClient.GetVoiceWorkDetailsAsync(productIds, cancellationToken); + ChobitResultCollection chobitResults = await chobitClient.GetSampleInfoAsync(productIds, cancellationToken); VoiceWorkIngest[] ingests = [.. scanResult.Works.Select(work => { voiceWorkDetails.TryGetValue(work.ProductId!, out VoiceWorkDetails? value); - return VoiceWorkIngest.From(work, value); + chobitResults.TryGetValue(work.ProductId, out ChobitResult? chobit); + return VoiceWorkIngest.From(work, value, chobit); })]; VoiceWorkUpsertResult[] upsertResults = await updater.UpsertAsync(ingests, cancellationToken); diff --git a/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs b/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs index 27ee427..db7f406 100644 --- a/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs +++ b/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs @@ -24,6 +24,7 @@ using JSMR.Infrastructure.Data.Repositories.Users; using JSMR.Infrastructure.Data.Repositories.VoiceWorks; using JSMR.Infrastructure.Http; using JSMR.Infrastructure.Ingestion; +using JSMR.Infrastructure.Integrations.Chobit; using JSMR.Infrastructure.Integrations.DLSite; using JSMR.Infrastructure.Scanning; using Microsoft.Extensions.DependencyInjection; @@ -76,6 +77,9 @@ public static class InfrastructureServiceCollectionExtensions services.AddSingleton(); services.AddHttpServices(); + services.AddNewHttpServices(); + + services.AddScoped(); return services; } @@ -113,7 +117,50 @@ public static class InfrastructureServiceCollectionExtensions // Register DLSiteClient as a normal scoped service services.AddScoped(); - services.AddScoped(); + return services; + } + + private static IServiceCollection AddNewHttpServices(this IServiceCollection services) + { + services.AddHttpClient((sp, http) => + { + http.BaseAddress = new Uri("https://www.dlsite.com/"); + http.DefaultRequestHeaders.UserAgent.ParseAdd("JSMR/1.0"); + http.Timeout = TimeSpan.FromSeconds(15); + }) + .AddResilienceHandler("dlsite", builder => + { + builder.AddRetry(new HttpRetryStrategyOptions + { + MaxRetryAttempts = 3, + UseJitter = true, + Delay = TimeSpan.FromMilliseconds(200), + BackoffType = DelayBackoffType.Exponential, + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(r => (int)r.StatusCode >= 500 || (int)r.StatusCode == 429) + }); + }); + + services.AddHttpClient((sp, http) => + { + http.BaseAddress = new Uri("https://chobit.cc/"); + http.DefaultRequestHeaders.UserAgent.ParseAdd("JSMR/1.0"); + http.Timeout = TimeSpan.FromSeconds(15); + }) + .AddResilienceHandler("chobit", builder => + { + builder.AddRetry(new HttpRetryStrategyOptions + { + MaxRetryAttempts = 3, + UseJitter = true, + Delay = TimeSpan.FromMilliseconds(200), + BackoffType = DelayBackoffType.Exponential, + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(r => (int)r.StatusCode >= 500 || (int)r.StatusCode == 429) + }); + }); return services; } diff --git a/JSMR.Infrastructure/Http/ApiClient.cs b/JSMR.Infrastructure/Http/ApiClient.cs index 2d8c43b..a6516e3 100644 --- a/JSMR.Infrastructure/Http/ApiClient.cs +++ b/JSMR.Infrastructure/Http/ApiClient.cs @@ -1,91 +1,121 @@ using Microsoft.Extensions.Logging; -using System.IO; using System.Net.Http.Headers; using System.Text; using System.Text.Json; namespace JSMR.Infrastructure.Http; -public abstract class ApiClient(IHttpService http, ILogger logger, JsonSerializerOptions? json = null) +public abstract class ApiClient(HttpClient http, ILogger logger, JsonSerializerOptions? json = null) { - protected async Task GetJsonAsync(string url, CancellationToken cancellationToken = default) + protected async Task GetJsonAsync( + string url, + Action? configureHeaders = null, + CancellationToken cancellationToken = default) { - HttpStringResponse response = await http.GetAsync(url, cancellationToken); + using HttpRequestMessage request = new(HttpMethod.Get, url); + configureHeaders?.Invoke(request.Headers); - if (response.Content is null) - throw new Exception("No content to deserialize"); + LogRequest(request); - return JsonSerializer.Deserialize(response.Content, json) - ?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(T).Name} from {url}."); + using HttpResponseMessage response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + await EnsureSuccess(response).ConfigureAwait(false); + + Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + + return await JsonSerializer.DeserializeAsync(stream, json, cancellationToken).ConfigureAwait(false) + ?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}."); } - //protected async Task GetJsonAsync( - // string url, - // Action? configureHeaders = null, - // CancellationToken ct = default - // ) - //{ - // using var req = new HttpRequestMessage(HttpMethod.Get, url); - // configureHeaders?.Invoke(req.Headers); + protected async Task GetJsonpAsync( + string url, + Action? configureHeaders = null, + CancellationToken cancellationToken = default) + { + using HttpRequestMessage request = new(HttpMethod.Get, url); + configureHeaders?.Invoke(request.Headers); - // LogRequest(req); + LogRequest(request); - // using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false); - // await EnsureSuccess(res).ConfigureAwait(false); + using HttpResponseMessage response = await http.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); - // var stream = await res.Content.ReadAsStreamAsync(ct).ConfigureAwait(false); + await EnsureSuccess(response).ConfigureAwait(false); - // var model = await JsonSerializer.DeserializeAsync(stream, json, ct).ConfigureAwait(false) - // ?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(T).Name} from {url}."); + string body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - // return model; - //} + string jsonBody = ExtractJsonFromJsonp(body); - //protected async Task PostJsonAsync( - // string url, - // TRequest payload, - // Action? configureHeaders = null, - // CancellationToken ct = default) - //{ - // var content = new StringContent(JsonSerializer.Serialize(payload, json), Encoding.UTF8, "application/json"); - // using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = content }; - // configureHeaders?.Invoke(req.Headers); + return JsonSerializer.Deserialize(jsonBody, json) + ?? throw new InvalidOperationException($"Failed to deserialize JSONP payload to {typeof(TResponse).Name} from {url}."); + } - // LogRequest(req); + private static string ExtractJsonFromJsonp(string body) + { + if (string.IsNullOrWhiteSpace(body)) + throw new InvalidOperationException("Response body was empty."); - // using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false); - // await EnsureSuccess(res).ConfigureAwait(false); + body = body.Trim(); - // var stream = await res.Content.ReadAsStreamAsync(ct).ConfigureAwait(false); + int firstParen = body.IndexOf('('); + int lastParen = body.LastIndexOf(')'); - // var model = await JsonSerializer.DeserializeAsync(stream, json, ct).ConfigureAwait(false) - // ?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}."); + if (firstParen < 0 || lastParen <= firstParen) + throw new InvalidOperationException("Response was not valid JSONP."); - // return model; - //} + return body[(firstParen + 1)..lastParen].Trim(); + } - //protected virtual void LogRequest(HttpRequestMessage req) - // => logger.LogDebug("HTTP {Method} {Uri}", req.Method, req.RequestUri); + protected async Task PostJsonAsync( + string url, + TRequest payload, + Action? configureHeaders = null, + CancellationToken cancellationToken = default) + { + StringContent content = new(JsonSerializer.Serialize(payload, json), Encoding.UTF8, "application/json"); - //protected virtual void LogFailure(HttpResponseMessage res, string body) - // => logger.LogWarning("HTTP {Status} for {Uri}. Body: {Body}", (int)res.StatusCode, res.RequestMessage?.RequestUri, Truncate(body, 500)); + using HttpRequestMessage request = new(HttpMethod.Post, url) + { + Content = content + }; - //protected static string Truncate(string s, int max) => s.Length <= max ? s : s[..max] + "…"; + configureHeaders?.Invoke(request.Headers); - //protected async Task EnsureSuccess(HttpResponseMessage res) - //{ - // if (res.IsSuccessStatusCode) return; + LogRequest(request); - // string body; - // try { body = await res.Content.ReadAsStringAsync().ConfigureAwait(false); } - // catch { body = ""; } + using HttpResponseMessage response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + await EnsureSuccess(response).ConfigureAwait(false); - // LogFailure(res, body); + Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - // Throw a richer exception(you can customize per API) - // throw new HttpRequestException( - // $"Request to {res.RequestMessage?.RequestUri} failed: {(int)res.StatusCode} {res.ReasonPhrase}. Body: {Truncate(body, 1000)}", - // null, - // res.StatusCode); - //} + return await JsonSerializer.DeserializeAsync(stream, json, cancellationToken).ConfigureAwait(false) + ?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}."); + } + + protected virtual void LogRequest(HttpRequestMessage request) + => logger.LogDebug("HTTP {Method} {Uri}", request.Method, request.RequestUri); + + protected virtual void LogFailure(HttpResponseMessage response, string body) + => logger.LogWarning("HTTP {Status} for {Uri}. Body: {Body}", (int)response.StatusCode, response.RequestMessage?.RequestUri, Truncate(body, 500)); + + protected static string Truncate(string s, int max) => s.Length <= max ? s : s[..max] + "…"; + + protected async Task EnsureSuccess(HttpResponseMessage res) + { + if (res.IsSuccessStatusCode) + return; + + string body; + try { body = await res.Content.ReadAsStringAsync().ConfigureAwait(false); } + catch { body = ""; } + + LogFailure(res, body); + + //Throw a richer exception(you can customize per API) + throw new HttpRequestException( + $"Request to {res.RequestMessage?.RequestUri} failed: {(int)res.StatusCode} {res.ReasonPhrase}. Body: {Truncate(body, 1000)}", + null, + res.StatusCode); + } } \ No newline at end of file diff --git a/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs b/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs index ccc92ce..dfcd9bf 100644 --- a/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs +++ b/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs @@ -206,7 +206,7 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider voiceWork.Downloads = ingest.Downloads; voiceWork.WishlistCount = ingest.WishlistCount; voiceWork.HasTrial = ingest.HasTrial; - voiceWork.HasChobit = ingest.HasDLPlay; + voiceWork.HasChobit = ingest.HasChobit; voiceWork.StarRating = ingest.StarRating; voiceWork.Votes = ingest.Votes; voiceWork.OriginalProductId = ingest.Translation?.OriginalProductId; diff --git a/JSMR.Infrastructure/Integrations/Chobit/ChobitClient.cs b/JSMR.Infrastructure/Integrations/Chobit/ChobitClient.cs index 37edbe8..ec47962 100644 --- a/JSMR.Infrastructure/Integrations/Chobit/ChobitClient.cs +++ b/JSMR.Infrastructure/Integrations/Chobit/ChobitClient.cs @@ -5,11 +5,17 @@ using Microsoft.Extensions.Logging; namespace JSMR.Infrastructure.Integrations.Chobit; -public class ChobitClient(IHttpService http, ILogger logger) : ApiClient(http, logger), IChobitClient +public class ChobitClient(HttpClient http, ILogger logger) : ApiClient(http, logger), IChobitClient { - public Task GetSampleInfoAsync(string[] productIds, CancellationToken cancellationToken = default) + public Task GetSampleInfoAsync(string productId, CancellationToken cancellationToken = default) { - var url = $"api/v2/dlsite/embed?workno_list=${string.Join(",", productIds)}"; - return GetJsonAsync(url, cancellationToken); + var url = $"api/v1/dlsite/embed?workno={productId}"; + return GetJsonpAsync(url, cancellationToken: cancellationToken); + } + + public Task GetSampleInfoAsync(string[] productIds, CancellationToken cancellationToken = default) + { + var url = $"api/v2/dlsite/embed?workno_list={string.Join(",", productIds)}"; + return GetJsonpAsync(url, cancellationToken: cancellationToken); } } \ No newline at end of file diff --git a/JSMR.Infrastructure/Integrations/DLSite/DLSiteClient.cs b/JSMR.Infrastructure/Integrations/DLSite/DLSiteClient.cs index fb81d46..83bd7a1 100644 --- a/JSMR.Infrastructure/Integrations/DLSite/DLSiteClient.cs +++ b/JSMR.Infrastructure/Integrations/DLSite/DLSiteClient.cs @@ -7,21 +7,19 @@ using Microsoft.Extensions.Logging; namespace JSMR.Infrastructure.Integrations.DLSite; -public class DLSiteClient(IHttpService http, ILogger logger) : ApiClient(http, logger), IDLSiteClient +public class DLSiteClient(HttpClient http, ILogger logger) : ApiClient(http, logger), IDLSiteClient { public async Task GetVoiceWorkDetailsAsync(string[] productIds, CancellationToken cancellationToken = default) { - if (productIds.Length == 0) - return []; - - string productIdCollection = string.Join(",", productIds.Where(x => !string.IsNullOrWhiteSpace(x))); - - if (string.IsNullOrWhiteSpace(productIdCollection)) + string[] validProductIds = [.. productIds.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()]; + + if (validProductIds.Length == 0) return []; + string productIdCollection = string.Join(",", validProductIds); string url = $"maniax/product/info/ajax?product_id={productIdCollection}&cdn_cache_min=1"; - var productInfoCollection = await GetJsonAsync(url, cancellationToken); + ProductInfoCollection productInfoCollection = await GetJsonAsync(url, cancellationToken: cancellationToken); return DLSiteToDomainMapper.Map(productInfoCollection); } diff --git a/JSMR.Tests/Http/FakeHttpMessageHandler.cs b/JSMR.Tests/Http/FakeHttpMessageHandler.cs new file mode 100644 index 0000000..d5563c1 --- /dev/null +++ b/JSMR.Tests/Http/FakeHttpMessageHandler.cs @@ -0,0 +1,9 @@ +namespace JSMR.Tests.Http; + +internal sealed class FakeHttpMessageHandler(Func handler) : HttpMessageHandler +{ + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(handler(request)); + } +} \ No newline at end of file diff --git a/JSMR.Tests/Ingestion/IngestTestFactory.cs b/JSMR.Tests/Ingestion/IngestTestFactory.cs index b7c36e7..405777d 100644 --- a/JSMR.Tests/Ingestion/IngestTestFactory.cs +++ b/JSMR.Tests/Ingestion/IngestTestFactory.cs @@ -21,7 +21,7 @@ internal class IngestTestFactory WishlistCount = 100, Downloads = 0, HasTrial = false, - HasDLPlay = false, + HasChobit = false, AgeRating = AgeRating.AllAges, HasImage = false, SupportedLanguages = [SupportedLanguage.Japanese], diff --git a/JSMR.Tests/Ingestion/Japanese/Basic_Insert_And_Update_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Basic_Insert_And_Update_Tests.cs new file mode 100644 index 0000000..60a1c1d --- /dev/null +++ b/JSMR.Tests/Ingestion/Japanese/Basic_Insert_And_Update_Tests.cs @@ -0,0 +1,154 @@ +using JSMR.Application.Scanning.Contracts; +using JSMR.Domain.Entities; +using JSMR.Domain.Enums; +using JSMR.Domain.ValueObjects; +using JSMR.Infrastructure.Data; +using JSMR.Tests.Fixtures; +using Microsoft.EntityFrameworkCore; +using Shouldly; + +namespace JSMR.Tests.Ingestion.Japanese; + +public class Basic_Insert_And_Update_Tests(MariaDbContainerFixture fixture) : JapaneseIngestionTestsBase(fixture) +{ + [Fact] + public async Task Basic_Insert_And_Update_Test() + { + await using AppDbContext dbContext = await GetAppDbContextAsync(); + + VoiceWorkIngest ingest = new() + { + MakerId = "RG1", + MakerName = "My Maker", + ProductId = "RJ1", + Title = "My Product", + Description = "My description", + Tags = ["Tag 1", "Tag 2"], + Creators = ["Creator 1"], + WishlistCount = 100, + Downloads = 0, + HasTrial = true, + HasChobit = false, + AgeRating = AgeRating.AllAges, + HasImage = false, + SupportedLanguages = [SupportedLanguage.Japanese], + SalesDate = null, + ExpectedDate = new DateOnly(2025, 2, 1) + }; + + DateTime dateTime = TokyoLocalToUtc(2025, 01, 15, 00, 00, 00); + await UpsertAsync(dbContext, dateTime, [ingest]); + + Circle? circle = await dbContext.Circles.FirstOrDefaultAsync(v => v.MakerId == ingest.MakerId, TestContext.Current.CancellationToken); + circle.ShouldNotBeNull(); + circle.Name.ShouldBe(ingest.MakerName); + + VoiceWork? voiceWork = await dbContext.VoiceWorks.FirstOrDefaultAsync(v => v.ProductId == ingest.ProductId, TestContext.Current.CancellationToken); + voiceWork.ShouldNotBeNull(); + voiceWork.ProductName.ShouldBe(ingest.Title); + voiceWork.Description.ShouldBe(ingest.Description); + voiceWork.WishlistCount.ShouldBe(ingest.WishlistCount); + voiceWork.Downloads.ShouldBe(ingest.Downloads); + voiceWork.HasTrial.ShouldBe(ingest.HasTrial); + voiceWork.HasChobit.ShouldBe(ingest.HasChobit); + voiceWork.Rating.ShouldBe((int)ingest.AgeRating); + voiceWork.HasImage.ShouldBe(ingest.HasImage); + voiceWork.SubtitleLanguage.ShouldBe((byte)Language.Japanese); + voiceWork.SalesDate.ShouldBeNull(); + voiceWork.ExpectedDate.ShouldBe(ingest.ExpectedDate!.Value.ToDateTime(TimeOnly.MinValue)); + + foreach (string tagName in ingest.Tags) + { + Tag? tag = await dbContext.Tags.FirstOrDefaultAsync(t => t.Name == tagName, TestContext.Current.CancellationToken); + tag.ShouldNotBeNull(); + + VoiceWorkTag? voiceWorkTag = await dbContext.VoiceWorkTags.FirstOrDefaultAsync(vwt => + vwt.VoiceWorkId == voiceWork.VoiceWorkId && vwt.TagId == tag.TagId, TestContext.Current.CancellationToken); + + voiceWorkTag.ShouldNotBeNull(); + } + + foreach (string creatorName in ingest.Creators) + { + Creator? creator = await dbContext.Creators.FirstOrDefaultAsync(c => c.Name == creatorName, TestContext.Current.CancellationToken); + creator.ShouldNotBeNull(); + + VoiceWorkCreator? voiceWorkCreator = await dbContext.VoiceWorkCreators.FirstOrDefaultAsync(vwc => + vwc.VoiceWorkId == voiceWork.VoiceWorkId && vwc.CreatorId == creator.CreatorId, TestContext.Current.CancellationToken); + + voiceWorkCreator.ShouldNotBeNull(); + } + + foreach (SupportedLanguage supportedLanguage in ingest.SupportedLanguages) + { + VoiceWorkSupportedLanguage? voiceWorkSupportedLanauge = await dbContext.VoiceWorkSupportedLanguages.FirstOrDefaultAsync(x => + x.VoiceWorkId == voiceWork.VoiceWorkId && x.Language == supportedLanguage.Code, TestContext.Current.CancellationToken); + + voiceWorkSupportedLanauge.ShouldNotBeNull(); + } + + VoiceWorkIngest updatedIngest = ingest with + { + MakerName = "My Maker (Updated)", + Tags = ["Tag 1", "Not Tag 2"], + Creators = ["Not Creator 1"], + Downloads = 50, + HasChobit = true, + SupportedLanguages = [SupportedLanguage.Japanese, SupportedLanguage.English], + HasImage = true, + SalesDate = new DateOnly(2025, 2, 5), + ExpectedDate = null + }; + + DateTime updateDateTime = TokyoLocalToUtc(2025, 02, 03, 00, 00, 00); + await UpsertAsync(dbContext, dateTime, [updatedIngest]); + + circle = await dbContext.Circles.FirstOrDefaultAsync(v => v.MakerId == updatedIngest.MakerId, TestContext.Current.CancellationToken); + circle.ShouldNotBeNull(); + circle.Name.ShouldBe(updatedIngest.MakerName); + + voiceWork = await dbContext.VoiceWorks.FirstOrDefaultAsync(v => v.ProductId == updatedIngest.ProductId, TestContext.Current.CancellationToken); + voiceWork.ShouldNotBeNull(); + voiceWork.ProductName.ShouldBe(updatedIngest.Title); + voiceWork.Description.ShouldBe(updatedIngest.Description); + voiceWork.WishlistCount.ShouldBe(updatedIngest.WishlistCount); + voiceWork.Downloads.ShouldBe(updatedIngest.Downloads); + voiceWork.HasTrial.ShouldBe(updatedIngest.HasTrial); + voiceWork.HasChobit.ShouldBe(updatedIngest.HasChobit); + voiceWork.Rating.ShouldBe((int)updatedIngest.AgeRating); + voiceWork.HasImage.ShouldBe(updatedIngest.HasImage); + voiceWork.SubtitleLanguage.ShouldBe((byte)Language.English); + voiceWork.SalesDate.ShouldBe(updatedIngest.SalesDate!.Value.ToDateTime(TimeOnly.MinValue)); + voiceWork.ExpectedDate.ShouldBeNull(); + + foreach (string tagName in updatedIngest.Tags.Union(ingest.Tags)) + { + Tag? tag = await dbContext.Tags.FirstOrDefaultAsync(t => t.Name == tagName, TestContext.Current.CancellationToken); + tag.ShouldNotBeNull(); + + VoiceWorkTag? voiceWorkTag = await dbContext.VoiceWorkTags.FirstOrDefaultAsync(vwt => + vwt.VoiceWorkId == voiceWork.VoiceWorkId && vwt.TagId == tag.TagId, TestContext.Current.CancellationToken); + + voiceWorkTag.ShouldNotBeNull(); + } + + foreach (string creatorName in updatedIngest.Creators.Union(ingest.Creators)) + { + Creator? creator = await dbContext.Creators.FirstOrDefaultAsync(c => c.Name == creatorName, TestContext.Current.CancellationToken); + creator.ShouldNotBeNull(); + + VoiceWorkCreator? voiceWorkCreator = await dbContext.VoiceWorkCreators.FirstOrDefaultAsync(vwc => + vwc.VoiceWorkId == voiceWork.VoiceWorkId && vwc.CreatorId == creator.CreatorId, TestContext.Current.CancellationToken); + + voiceWorkCreator.ShouldNotBeNull(); + } + + foreach (SupportedLanguage supportedLanguage in updatedIngest.SupportedLanguages.Union(ingest.SupportedLanguages)) + { + VoiceWorkSupportedLanguage? voiceWorkSupportedLanauge = await dbContext.VoiceWorkSupportedLanguages.FirstOrDefaultAsync(x => + x.VoiceWorkId == voiceWork.VoiceWorkId && x.Language == supportedLanguage.Code, TestContext.Current.CancellationToken); + + voiceWorkSupportedLanauge.ShouldNotBeNull(); + } + } +} \ No newline at end of file diff --git a/JSMR.Tests/Ingestion/Japanese/Fail_Attempted_Insert_With_Spam_Circle_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Fail_Attempted_Insert_With_Spam_Circle_Tests.cs index 4fc6609..c177171 100644 --- a/JSMR.Tests/Ingestion/Japanese/Fail_Attempted_Insert_With_Spam_Circle_Tests.cs +++ b/JSMR.Tests/Ingestion/Japanese/Fail_Attempted_Insert_With_Spam_Circle_Tests.cs @@ -29,7 +29,7 @@ public class Fail_Attempted_Insert_With_Spam_Circle_Tests(MariaDbContainerFixtur WishlistCount = 100, Downloads = 0, HasTrial = false, - HasDLPlay = false, + HasChobit = false, AgeRating = AgeRating.R18, HasImage = false, SupportedLanguages = [SupportedLanguage.Japanese], diff --git a/JSMR.Tests/Ingestion/Japanese/Fail_Attempted_Update_With_Decreased_Downloads_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Fail_Attempted_Update_With_Decreased_Downloads_Tests.cs index 66178f4..75a167a 100644 --- a/JSMR.Tests/Ingestion/Japanese/Fail_Attempted_Update_With_Decreased_Downloads_Tests.cs +++ b/JSMR.Tests/Ingestion/Japanese/Fail_Attempted_Update_With_Decreased_Downloads_Tests.cs @@ -29,7 +29,7 @@ public class Fail_Attempted_Update_With_Decreased_Downloads_Tests(MariaDbContain WishlistCount = 50, Downloads = 10, HasTrial = false, - HasDLPlay = false, + HasChobit = false, StarRating = null, Votes = null, AgeRating = AgeRating.AllAges, diff --git a/JSMR.Tests/Ingestion/Japanese/Fail_Attempted_Update_With_Sales_Date_Reversal_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Fail_Attempted_Update_With_Sales_Date_Reversal_Tests.cs index b713412..1fcdd07 100644 --- a/JSMR.Tests/Ingestion/Japanese/Fail_Attempted_Update_With_Sales_Date_Reversal_Tests.cs +++ b/JSMR.Tests/Ingestion/Japanese/Fail_Attempted_Update_With_Sales_Date_Reversal_Tests.cs @@ -29,7 +29,7 @@ public class Fail_Attempted_Update_With_Sales_Date_Reversal_Tests(MariaDbContain WishlistCount = 50, Downloads = 10, HasTrial = false, - HasDLPlay = false, + HasChobit = false, StarRating = null, Votes = null, AgeRating = AgeRating.AllAges, diff --git a/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_And_Scan_Again_Later_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_And_Scan_Again_Later_Tests.cs index cc1d690..d7703f7 100644 --- a/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_And_Scan_Again_Later_Tests.cs +++ b/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_And_Scan_Again_Later_Tests.cs @@ -28,7 +28,7 @@ public class Insert_New_Release_And_Scan_Again_Later_Tests(MariaDbContainerFixtu WishlistCount = 50, Downloads = 10, HasTrial = false, - HasDLPlay = false, + HasChobit = false, StarRating = null, Votes = null, AgeRating = AgeRating.AllAges, diff --git a/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_With_New_Tags_And_Creators_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_With_New_Tags_And_Creators_Tests.cs index 8514870..23301a3 100644 --- a/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_With_New_Tags_And_Creators_Tests.cs +++ b/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_With_New_Tags_And_Creators_Tests.cs @@ -29,7 +29,7 @@ public class Insert_New_Release_With_New_Tags_And_Creators_Tests(MariaDbContaine WishlistCount = 50, Downloads = 10, HasTrial = false, - HasDLPlay = false, + HasChobit = false, StarRating = null, Votes = null, AgeRating = AgeRating.AllAges, diff --git a/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_And_Scan_Again_Later_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_And_Scan_Again_Later_Tests.cs index 80553e2..a079267 100644 --- a/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_And_Scan_Again_Later_Tests.cs +++ b/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_And_Scan_Again_Later_Tests.cs @@ -28,7 +28,7 @@ public class Insert_New_Upcoming_And_Scan_Again_Later_Tests(MariaDbContainerFixt WishlistCount = 100, Downloads = 0, HasTrial = false, - HasDLPlay = false, + HasChobit = false, AgeRating = AgeRating.AllAges, HasImage = false, SupportedLanguages = [SupportedLanguage.Japanese], diff --git a/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_Release_Same_Day_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_Release_Same_Day_Tests.cs index e86929d..29c76ad 100644 --- a/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_Release_Same_Day_Tests.cs +++ b/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_Release_Same_Day_Tests.cs @@ -28,7 +28,7 @@ public class Insert_New_Upcoming_Release_Same_Day_Tests(MariaDbContainerFixture WishlistCount = 100, Downloads = 0, HasTrial = false, - HasDLPlay = false, + HasChobit = false, AgeRating = AgeRating.AllAges, HasImage = false, SupportedLanguages = [SupportedLanguage.Japanese], diff --git a/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_With_Existing_Tags_And_Creators_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_With_Existing_Tags_And_Creators_Tests.cs index 50cef0b..85c4c23 100644 --- a/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_With_Existing_Tags_And_Creators_Tests.cs +++ b/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_With_Existing_Tags_And_Creators_Tests.cs @@ -29,7 +29,7 @@ public class Insert_New_Upcoming_With_Existing_Tags_And_Creators_Tests(MariaDbCo WishlistCount = 250, Downloads = 0, HasTrial = false, - HasDLPlay = false, + HasChobit = false, StarRating = null, Votes = null, AgeRating = AgeRating.R15, diff --git a/JSMR.Tests/Ingestion/Japanese/Update_Upcoming_With_No_Expected_Date_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Update_Upcoming_With_No_Expected_Date_Tests.cs index db2b45a..2bdadb4 100644 --- a/JSMR.Tests/Ingestion/Japanese/Update_Upcoming_With_No_Expected_Date_Tests.cs +++ b/JSMR.Tests/Ingestion/Japanese/Update_Upcoming_With_No_Expected_Date_Tests.cs @@ -26,7 +26,7 @@ public class Update_Upcoming_With_No_Expected_Date_Tests(MariaDbContainerFixture WishlistCount = 250, Downloads = 0, HasTrial = false, - HasDLPlay = false, + HasChobit = false, StarRating = null, Votes = null, AgeRating = AgeRating.R15, diff --git a/JSMR.Tests/Integrations/Chobit/ChobitClientTests.cs b/JSMR.Tests/Integrations/Chobit/ChobitClientTests.cs new file mode 100644 index 0000000..c681edf --- /dev/null +++ b/JSMR.Tests/Integrations/Chobit/ChobitClientTests.cs @@ -0,0 +1,98 @@ +using JSMR.Application.Integrations.Chobit.Models; +using JSMR.Infrastructure.Integrations.Chobit; +using JSMR.Tests.Http; +using JSMR.Tests.Utilities; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Shouldly; +using System.Net; +using System.Text; + +namespace JSMR.Tests.Integrations.Chobit; + +public class ChobitClientTests +{ + private static async Task ReadJsonResourceAsync(string resourceName) + { + return await ResourceHelper.ReadAsync($"JSMR.Tests.Integrations.Chobit.{resourceName}"); + } + + private static async Task GetChobitClientThatReturns(string resourceName) + { + string content = await ReadJsonResourceAsync(resourceName); + + FakeHttpMessageHandler handler = new(request => + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(content, Encoding.UTF8, "application/json") + }; + }); + + HttpClient httpClient = new(handler) + { + BaseAddress = new Uri("https://fake-chobit.cc/") + }; + + var logger = Substitute.For>(); + var client = new ChobitClient(httpClient, logger); + + return client; + } + + [Fact] + public async Task Deserialize_Chobit_Sample_Info() + { + var client = await GetChobitClientThatReturns("Sample-Chobit-Result.jsonp"); + var result = await client.GetSampleInfoAsync("RJ01430276", CancellationToken.None); + + result.Count.ShouldBe(1); + result.Works.Length.ShouldBe(1); + + ChobitWork work = result.Works[0]; + work.WorkId.ShouldBe("70nh8"); + work.DLSiteWorkId.ShouldBe("RJ01430276"); + work.WorkName.ShouldBe("【ブルーアーカイブ】シグレ(温泉)ASMR~溶けていく温度を交わして~"); + } + + [Fact] + public async Task Deserialize_Chobit_Sample_Info_Collection() + { + var client = await GetChobitClientThatReturns("Sample-Chobit-Result-Collection.jsonp"); + var result = await client.GetSampleInfoAsync(["RJ01430276"], CancellationToken.None); + + result.Count.ShouldBe(1); + result.ShouldContainKey("RJ01430276"); + + result["RJ01430276"].Count.ShouldBe(1); + result["RJ01430276"].Works.Length.ShouldBe(1); + + ChobitWork work = result["RJ01430276"].Works[0]; + work.WorkId.ShouldBe("70nh8"); + work.DLSiteWorkId.ShouldBe("RJ01430276"); + work.WorkName.ShouldBe("【ブルーアーカイブ】シグレ(温泉)ASMR~溶けていく温度を交わして~"); + } + + [Fact] + public async Task Deserialize_Chobit_Sample_Info_No_Data() + { + var client = await GetChobitClientThatReturns("Sample-Chobit-Result-No-Data.jsonp"); + var result = await client.GetSampleInfoAsync("RJ01585659", CancellationToken.None); + + result.Count.ShouldBe(0); + result.Works.Length.ShouldBe(0); + } + + [Fact] + public async Task Deserialize_Chobit_Sample_Info_Collection_No_Data() + { + var client = await GetChobitClientThatReturns("Sample-Chobit-Result-Collection-No-Data.jsonp"); + var result = await client.GetSampleInfoAsync(["RJ01585659"], CancellationToken.None); + + result.Count.ShouldBe(1); + result.ShouldContainKey("RJ01585659"); + + result["RJ01585659"].Count.ShouldBe(0); + result["RJ01585659"].Works.Length.ShouldBe(0); + } +} \ No newline at end of file diff --git a/JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result-Collection-No-Data.jsonp b/JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result-Collection-No-Data.jsonp new file mode 100644 index 0000000..e86119a --- /dev/null +++ b/JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result-Collection-No-Data.jsonp @@ -0,0 +1 @@ +response({"RJ01585659":{"count":0,"works":[]}}) \ No newline at end of file diff --git a/JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result-Collection.jsonp b/JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result-Collection.jsonp new file mode 100644 index 0000000..54ef371 --- /dev/null +++ b/JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result-Collection.jsonp @@ -0,0 +1 @@ +response({"RJ01430276":{"count":1,"works":[{"work_id":"70nh8","dlsite_work_id":"RJ01430276","work_name":"\u3010\u30d6\u30eb\u30fc\u30a2\u30fc\u30ab\u30a4\u30d6\u3011\u30b7\u30b0\u30ec(\u6e29\u6cc9)ASMR\uff5e\u6eb6\u3051\u3066\u3044\u304f\u6e29\u5ea6\u3092\u4ea4\u308f\u3057\u3066\uff5e","work_name_kana":"\u30d6\u30eb\u30fc\u30a2\u30fc\u30ab\u30a4\u30d6\u30b7\u30b0\u30ec\u30aa\u30f3\u30bb\u30f3\u30a8\u30fc\u30a8\u30b9\u30a8\u30e0\u30a2\u30fc\u30eb\u30c8\u30b1\u30c6\u30a4\u30af\u30aa\u30f3\u30c9\u30f2\u30ab\u30ef\u30b7\u30c6","url":"https:\/\/chobit.cc\/70nh8\/vgj6safb","embed_url":"https:\/\/chobit.cc\/embed\/70nh8\/vgj6safb?dlsite=1","thumb":"https:\/\/media.dlsite.com\/chobit\/contents\/2507\/f1kkssulligowgkkcc0k80soo\/f1kkssulligowgkkcc0k80soo_cover.jpg?w=560\u0026h=420","mini_thumb":"https:\/\/media.dlsite.com\/chobit\/contents\/2507\/f1kkssulligowgkkcc0k80soo\/f1kkssulligowgkkcc0k80soo_cover.jpg?w=100\u0026h=100","file_type":"audio","embed_width":740,"embed_height":215}]}}) \ No newline at end of file diff --git a/JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result-No-Data.jsonp b/JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result-No-Data.jsonp new file mode 100644 index 0000000..7d37fd9 --- /dev/null +++ b/JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result-No-Data.jsonp @@ -0,0 +1 @@ +response({"count":0,"works":[]}) \ No newline at end of file diff --git a/JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result.jsonp b/JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result.jsonp new file mode 100644 index 0000000..aef7a79 --- /dev/null +++ b/JSMR.Tests/Integrations/Chobit/Sample-Chobit-Result.jsonp @@ -0,0 +1 @@ +response({"count":1,"works":[{"work_id":"70nh8","dlsite_work_id":"RJ01430276","work_name":"\u3010\u30d6\u30eb\u30fc\u30a2\u30fc\u30ab\u30a4\u30d6\u3011\u30b7\u30b0\u30ec(\u6e29\u6cc9)ASMR\uff5e\u6eb6\u3051\u3066\u3044\u304f\u6e29\u5ea6\u3092\u4ea4\u308f\u3057\u3066\uff5e","work_name_kana":"\u30d6\u30eb\u30fc\u30a2\u30fc\u30ab\u30a4\u30d6\u30b7\u30b0\u30ec\u30aa\u30f3\u30bb\u30f3\u30a8\u30fc\u30a8\u30b9\u30a8\u30e0\u30a2\u30fc\u30eb\u30c8\u30b1\u30c6\u30a4\u30af\u30aa\u30f3\u30c9\u30f2\u30ab\u30ef\u30b7\u30c6","url":"https:\/\/chobit.cc\/70nh8\/vgj6safb","embed_url":"https:\/\/chobit.cc\/embed\/70nh8\/vgj6safb?dlsite=1","thumb":"https:\/\/media.dlsite.com\/chobit\/contents\/2507\/f1kkssulligowgkkcc0k80soo\/f1kkssulligowgkkcc0k80soo_cover.jpg?w=560\u0026h=420","mini_thumb":"https:\/\/media.dlsite.com\/chobit\/contents\/2507\/f1kkssulligowgkkcc0k80soo\/f1kkssulligowgkkcc0k80soo_cover.jpg?w=100\u0026h=100","file_type":"audio","embed_width":740,"embed_height":215}]}) \ No newline at end of file diff --git a/JSMR.Tests/Integrations/DLSite/DLSiteClientTests.cs b/JSMR.Tests/Integrations/DLSite/DLSiteClientTests.cs index ea36f7c..ba19f6e 100644 --- a/JSMR.Tests/Integrations/DLSite/DLSiteClientTests.cs +++ b/JSMR.Tests/Integrations/DLSite/DLSiteClientTests.cs @@ -1,15 +1,16 @@ using JSMR.Application.Integrations.DLSite.Models; using JSMR.Domain.Enums; using JSMR.Domain.ValueObjects; -using JSMR.Infrastructure.Http; using JSMR.Infrastructure.Integrations.DLSite; using JSMR.Infrastructure.Integrations.DLSite.Mapping; using JSMR.Infrastructure.Integrations.DLSite.Models; -using JSMR.Tests.Extensions; +using JSMR.Tests.Http; using JSMR.Tests.Utilities; using Microsoft.Extensions.Logging; using NSubstitute; using Shouldly; +using System.Net; +using System.Text; namespace JSMR.Tests.Integrations.DLSite; @@ -20,17 +21,33 @@ public class DLSiteClientTests return await ResourceHelper.ReadAsync($"JSMR.Tests.Integrations.DLSite.{resourceName}"); } + private static async Task GetDLSiteClientThatReturns(string resourceName) + { + string content = await ReadJsonResourceAsync(resourceName); + + FakeHttpMessageHandler handler = new(request => + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(content, Encoding.UTF8, "application/json") + }; + }); + + HttpClient httpClient = new(handler) + { + BaseAddress = new Uri("https://www.fake-dlsite.com/") + }; + + var logger = Substitute.For>(); + var client = new DLSiteClient(httpClient, logger); + + return client; + } + [Fact] public async Task Deserialize_Product_Info_Collection() { - string productInfoJson = await ReadJsonResourceAsync("Product-Info.json"); - - IHttpService httpService = Substitute.For(); - httpService.ReturnsContent(productInfoJson); - - var logger = Substitute.For>(); - var client = new DLSiteClient(httpService, logger); - + var client = await GetDLSiteClientThatReturns("Product-Info.json"); var result = await client.GetVoiceWorkDetailsAsync(["RJ01230163", "RJ01536422"], CancellationToken.None); result.Count.ShouldBe(2); diff --git a/JSMR.Tests/JSMR.Tests.csproj b/JSMR.Tests/JSMR.Tests.csproj index ec66cbf..e2da759 100644 --- a/JSMR.Tests/JSMR.Tests.csproj +++ b/JSMR.Tests/JSMR.Tests.csproj @@ -13,6 +13,10 @@ + + + + diff --git a/JSMR.Tests/Scanning/VoiceWorkScannerTests.cs b/JSMR.Tests/Scanning/VoiceWorkScannerTests.cs index bc847a2..0bf3c7e 100644 --- a/JSMR.Tests/Scanning/VoiceWorkScannerTests.cs +++ b/JSMR.Tests/Scanning/VoiceWorkScannerTests.cs @@ -1,4 +1,5 @@ -using JSMR.Application.Integrations.DLSite.Models; +using JSMR.Application.Integrations.Chobit.Models; +using JSMR.Application.Integrations.DLSite.Models; using JSMR.Application.Integrations.Ports; using JSMR.Application.Scanning.Contracts; using JSMR.Application.Scanning.Ports; @@ -141,10 +142,21 @@ public class VoiceWorkScannerTests } }; + ChobitResultCollection chobitResultCollection = new() + { + { + "RJ1", + new ChobitResult() + { + Count = 0 + } + } + }; + dlsiteClient.GetVoiceWorkDetailsAsync(Arg.Any(), CancellationToken.None) .Returns(Task.FromResult(detailCollection)); - VoiceWorkIngest ingest = VoiceWorkIngest.From(scannedWorks[0], detailCollection["RJ1"]); + VoiceWorkIngest ingest = VoiceWorkIngest.From(scannedWorks[0], detailCollection["RJ1"], chobitResultCollection["RJ1"]); // TODO: Test other fields ingest.Title.ShouldBe("Product Title");