Added Chobit integration. Updated tests.
All checks were successful
ci / build-test (push) Successful in 2m27s
ci / publish-image (push) Has been skipped

This commit is contained in:
2026-03-14 21:46:53 -04:00
parent ee809e374f
commit aab7bee694
32 changed files with 521 additions and 108 deletions

View File

@@ -0,0 +1,9 @@
namespace JSMR.Tests.Http;
internal sealed class FakeHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler) : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(handler(request));
}
}

View File

@@ -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],

View File

@@ -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();
}
}
}

View File

@@ -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],

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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],

View File

@@ -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],

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<string> ReadJsonResourceAsync(string resourceName)
{
return await ResourceHelper.ReadAsync($"JSMR.Tests.Integrations.Chobit.{resourceName}");
}
private static async Task<ChobitClient> 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<ILogger<ChobitClient>>();
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);
}
}

View File

@@ -0,0 +1 @@
response({"RJ01585659":{"count":0,"works":[]}})

View File

@@ -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}]}})

View File

@@ -0,0 +1 @@
response({"count":0,"works":[]})

View File

@@ -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}]})

View File

@@ -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<DLSiteClient> 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<ILogger<DLSiteClient>>();
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<IHttpService>();
httpService.ReturnsContent(productInfoJson);
var logger = Substitute.For<ILogger<DLSiteClient>>();
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);

View File

@@ -13,6 +13,10 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result-Collection-No-Data.jsonp" />
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result-No-Data.jsonp" />
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result-Collection.jsonp" />
<EmbeddedResource Include="Integrations\Chobit\Sample-Chobit-Result.jsonp" />
<EmbeddedResource Include="Integrations\DLSite\Product-Info.json" />
<EmbeddedResource Include="Scanning\English-Page-Updated.html" />
<EmbeddedResource Include="Scanning\Japanese-Page-Updated.html" />

View File

@@ -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<string[]>(), 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");