Project restructuring.

This commit is contained in:
2025-05-26 22:03:08 -04:00
parent ea8b4a36ff
commit 6accb373cd
26 changed files with 421 additions and 156 deletions

View File

@@ -11,8 +11,7 @@
<ItemGroup>
<None Remove="Search\MangaDex\SampleSearchResult.json" />
<None Remove="WebCrawlers\MangaDex\MetadataSample-Feed.json" />
<None Remove="WebCrawlers\MangaDex\MetadataSample.json" />
<None Remove="Sources\MangaDex\Api\Manga-Chapter-Response.json" />
<None Remove="WebCrawlers\Samples\MangaNato - Please Go Home, Akutsu-San!.htm" />
<None Remove="Search\NatoManga\SampleSearchResult.json" />
</ItemGroup>
@@ -27,10 +26,11 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Search\MangaDex\SampleSearchResult.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Chapter-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Search-Response.json" />
<EmbeddedResource Include="Search\NatoManga\SampleSearchResult.json" />
<EmbeddedResource Include="WebCrawlers\MangaDex\MetadataSample-Feed.json" />
<EmbeddedResource Include="WebCrawlers\MangaDex\MetadataSample.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Feed-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Response.json" />
</ItemGroup>
<ItemGroup>
@@ -56,4 +56,8 @@
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<Folder Include="Search\MangaDex\" />
</ItemGroup>
</Project>

View File

@@ -1,50 +0,0 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Search;
using MangaReader.Core.Search.MangaDex;
using MangaReader.Tests.Utilities;
using NSubstitute;
using Shouldly;
namespace MangaReader.Tests.Search.MangaDex;
public class MangaDexSearchTests
{
class MangaDexSearchProviderTestWrapper(IHttpService httpService) : MangaDexSearchProvider(httpService)
{
internal string Test_GetSearchUrl(string keyword) => GetSearchUrl(keyword);
}
[Fact]
public void Get_Search_Url()
{
// Arrange
IHttpService httpService = Substitute.For<IHttpService>();
MangaDexSearchProviderTestWrapper searchProvider = new(httpService);
// Act
string url = searchProvider.Test_GetSearchUrl("Gal can't be");
// Assert
url.ShouldBe("https://api.mangadex.org/manga?title=gal can't be&limit=5");
}
[Fact]
public async Task Get_Search_Result()
{
string resourceName = "MangaReader.Tests.Search.MangaDex.SampleSearchResult.json";
string searchResultJson = await ResourceHelper.ReadJsonResourceAsync(resourceName);
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
MangaDexSearchProvider searchProvider = new(httpService);
MangaSearchResult[] searchResult = await searchProvider.SearchAsync("Gal Can't Be Kind", CancellationToken.None);
searchResult.Length.ShouldBe(3);
searchResult[0].Title.ShouldBe("Gals Cant Be Kind to Otaku!?");
searchResult[0].Url.ShouldBe("https://mangadex.org/title/ee96e2b7-9af2-4864-9656-649f4d3b6fec/gals-can-t-be-kind-to-otaku");
searchResult[0].Thumbnail.ShouldBe("https://mangadex.org/covers/ee96e2b7-9af2-4864-9656-649f4d3b6fec/6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg");
}
}

View File

@@ -0,0 +1,9 @@
{
"result": "ok",
"baseUrl": "https:\/\/cmdxd98sb0x3yprd.mangadex.network",
"chapter": {
"hash": "f867bd09bc8b19a37cf5486134acdda1",
"data": [ "1-5ae1738e10f5440a74f11832cf6203be5bd938f72d5a80d42cd149ee21287901.png", "2-e65c0795407b284664a83fba4c80ed5efceece5e74c4b0d5e0539890aa0d6dcc.png", "3-7506a36de3ec1f02bf5f85350a4b1676da89d095cfb98e88cd9a5840c9b72f3f.png", "4-3fb99ff194103ab51ece97f4a02a130742c7fa9413926ac1437992fc881d5d26.png", "5-21111c18ae1c04294a8ded2b0ef5395ae14a3d8d9e49b32a8187f71d12e5c4e4.png", "6-1168409fbb61f5c58746b324d6e9fe6225437f86b29da25e5c9bc9c6921f5afd.png", "7-1cf3170f68e0e1e8baf187635c721783c809ed2af2842ae194ad88b5ffcd8863.png", "8-b36237f62aadb2d6e9cb21ed65723d0e020e7509143c9186049e79591b2cc7b0.png", "9-d438f6d05eccfdf827b825e78432dcff0632df6193ce10288ce049774327b9ad.png", "10-ad8e53b6d8276bee6a05fefb41eb84ddb19a88ae3d7481f9ded7eaf43907cb7a.png", "11-219faea5a79409d92a6c02a97a5ef84e34c046ba3e5e079c87d1ecb6fc9b90a4.png", "12-dd32f11cdc417859593250c0315da7cad42e458f44ee17753794deca9b0c4d6f.png", "13-b6b8e15b5abd7e53af32cbfa1ac6dc1cb48fb314f2b5bb7412c9e393cf78224e.png" ],
"dataSaver": [ "1-7b0a40f35edd75f14d0aa9c0369f8bb05e41687165d97368b572f3c3c5b3db31.jpg", "2-9034c103958951d81c7ef3aedd43ad7ce58be33b85d6bfa72fd5e9cd3b2645b0.jpg", "3-62889381cc52713e234d2423d5da8a8da35d69f2b0a5a2671770922a91a86bdd.jpg", "4-864816f3e007c7a0cf7bd8f8fb4ee4bda5f3df04b32806737e323ab6a29f1c75.jpg", "5-b4beb6f2d40c13c96c9520de77ae00b24cea5357bf6e5d1eb1b04cc4e28f6abe.jpg", "6-ea57e27e2802b112a600a450f78e397f54377ffa8065c5eee9cb155e630df162.jpg", "7-ff7f9b9018b804c4edadaa32993585f1dab48919a6a51ccd7244020458f6bc4e.jpg", "8-100943bb7543c35265d8b05ba5a14aab8e2c92897e6fedd6ae5252d555a6c5a7.jpg", "9-2e618c48e4f98a8641b9b480e9da5ebb41dac59420500b7375454dfe643fabec.jpg", "10-327e4bb775e9f0c0964d5f7a35ed5c1f44feb09d5adf9eaad835b1e313422707.jpg", "11-6657ffa04caba29942063836f75f69979c4a7415243a8340bedaa3fdf07fc72b.jpg", "12-550cc7aac42468711ad9233392cf437576f6bfc54f067eb7493578813ceb6609.jpg", "13-b886b4ed986a473478e3db7bb18fe2faea567a1ad5e520408967410dcf8838d1.jpg" ]
}
}

View File

@@ -0,0 +1,161 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Sources.MangaDex.Api;
using MangaReader.Tests.Utilities;
using NSubstitute;
using Shouldly;
namespace MangaReader.Tests.Sources.MangaDex.Api;
public class MangaDexClientTests
{
[Fact]
public async Task Search_Manga()
{
string searchResultJson = await ReadJsonResourceAsync("Manga-Search-Response.json");
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
MangaDexClient mangaDexClient = new(httpService);
MangaDexResponse? mangaDexResponse = await mangaDexClient.SearchMangaByTitleAsync("Some random text", CancellationToken.None);
// Testing here
}
[Fact]
public async Task Get_Manga_Metadata()
{
string searchResultJson = await ReadJsonResourceAsync("Manga-Response.json");
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
MangaDexClient mangaDexClient = new(httpService);
MangaDexResponse? mangaDexResponse = await mangaDexClient.GetMangaAsync(Guid.NewGuid(), CancellationToken.None);
mangaDexResponse.ShouldNotBeNull();
mangaDexResponse.Response.ShouldBe("entity");
mangaDexResponse.ShouldBeOfType<MangaDexEntityResponse>();
MangaDexEntityResponse? mangaDexEntityResponse = mangaDexResponse as MangaDexEntityResponse;
mangaDexEntityResponse.ShouldNotBeNull();
mangaDexEntityResponse.Data.ShouldNotBeNull();
mangaDexEntityResponse.Data.ShouldBeOfType<MangaEntity>();
MangaEntity? mangaEntity = mangaDexEntityResponse.Data as MangaEntity;
mangaEntity.ShouldNotBeNull();
mangaEntity.Attributes.ShouldNotBeNull();
mangaEntity.Attributes.Title.ShouldContainKey("en");
mangaEntity.Attributes.Title["en"].ShouldBe("Gals Cant Be Kind to Otaku!?");
mangaEntity.Attributes.Description.ShouldContainKey("en");
mangaEntity.Attributes.Description["en"].ShouldBe("Takuya Seo is an otaku who likes \"anime for girls\" and can't say he likes it out loud. One day, he hooks up with two gals from his class, Amane and Ijichi, but it seems that Amane is also an otaku...");
mangaEntity.Attributes.Tags.Count.ShouldBe(5);
mangaEntity.Attributes.Tags[0].Attributes.ShouldNotBeNull();
mangaEntity.Attributes.Tags[0].Attributes!.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[0].Attributes!.Name["en"].ShouldBe("Romance");
mangaEntity.Attributes.Tags[1].Attributes.ShouldNotBeNull();
mangaEntity.Attributes.Tags[1].Attributes!.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[1].Attributes!.Name["en"].ShouldBe("Comedy");
mangaEntity.Attributes.Tags[2].Attributes.ShouldNotBeNull();
mangaEntity.Attributes.Tags[2].Attributes!.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[2].Attributes!.Name["en"].ShouldBe("School Life");
mangaEntity.Attributes.Tags[3].Attributes.ShouldNotBeNull();
mangaEntity.Attributes.Tags[3].Attributes!.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[3].Attributes!.Name["en"].ShouldBe("Slice of Life");
mangaEntity.Attributes.Tags[4].Attributes.ShouldNotBeNull();
mangaEntity.Attributes.Tags[4].Attributes!.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[4].Attributes!.Name["en"].ShouldBe("Gyaru");
mangaEntity.Relationships.Count.ShouldBe(4);
mangaEntity.Relationships[0].ShouldBeOfType<AuthorEntity>();
mangaEntity.Relationships[1].ShouldBeOfType<ArtistEntity>();
mangaEntity.Relationships[2].ShouldBeOfType<CoverArtEntity>();
mangaEntity.Relationships[3].ShouldBeOfType<CreatorEntity>();
AuthorEntity authorEntity = (mangaEntity.Relationships[0] as AuthorEntity)!;
authorEntity.Attributes.ShouldNotBeNull();
authorEntity.Attributes.Name.ShouldBe("Norishiro-chan");
ArtistEntity artistEntity = (mangaEntity.Relationships[1] as ArtistEntity)!;
artistEntity.Attributes.ShouldNotBeNull();
artistEntity.Attributes.Name.ShouldBe("Sakana Uozimi");
CoverArtEntity coverArtEntity = (mangaEntity.Relationships[2] as CoverArtEntity)!;
coverArtEntity.Attributes.ShouldNotBeNull();
coverArtEntity.Attributes.FileName.ShouldBe("6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg");
}
[Fact]
public async Task Get_Manga_Feed()
{
string searchResultJson = await ReadJsonResourceAsync("Manga-Feed-Response.json");
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
MangaDexClient mangaDexClient = new(httpService);
MangaDexResponse? mangaDexResponse = await mangaDexClient.GetFeedAsync(Guid.NewGuid(), CancellationToken.None);
mangaDexResponse.ShouldNotBeNull();
mangaDexResponse.Response.ShouldBe("collection");
mangaDexResponse.ShouldBeOfType<MangaDexCollectionResponse>();
MangaDexCollectionResponse mangaDexEntityResponse = (mangaDexResponse as MangaDexCollectionResponse)!;
List<ChapterEntity> chapterEntities = [.. mangaDexEntityResponse.Data.FindAll(entity => entity is ChapterEntity).Cast<ChapterEntity>()];
chapterEntities.Count.ShouldBe(82);
chapterEntities[0].Attributes.ShouldNotBeNull();
chapterEntities[0].Attributes!.Volume.ShouldBeNull();
chapterEntities[0].Attributes!.Chapter.ShouldBe("69");
chapterEntities[0].Attributes!.Title.ShouldBe("Otaku & Gyaru & Playing Couple");
chapterEntities[1].Attributes.ShouldNotBeNull();
chapterEntities[1].Attributes!.Volume.ShouldBe("9");
chapterEntities[1].Attributes!.Chapter.ShouldBe("68");
chapterEntities[1].Attributes!.Title.ShouldBe("Otaku & Gyaru & A Couple Date");
}
[Fact]
public async Task Get_Chapters()
{
string searchResultJson = await ReadJsonResourceAsync("Manga-Chapter-Response.json");
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
MangaDexClient mangaDexClient = new(httpService);
MangaDexChapterResponse? mangaDexChapterResponse = await mangaDexClient.GetChapterAsync(Guid.NewGuid(), CancellationToken.None);
mangaDexChapterResponse.ShouldNotBeNull();
mangaDexChapterResponse.Chapter.ShouldNotBeNull();
mangaDexChapterResponse.Chapter.Hash.ShouldBe("f867bd09bc8b19a37cf5486134acdda1");
mangaDexChapterResponse.Chapter.Data.Count.ShouldBe(13);
mangaDexChapterResponse.Chapter.Data[0].ShouldBe("1-5ae1738e10f5440a74f11832cf6203be5bd938f72d5a80d42cd149ee21287901.png");
mangaDexChapterResponse.Chapter.Data[12].ShouldBe("13-b6b8e15b5abd7e53af32cbfa1ac6dc1cb48fb314f2b5bb7412c9e393cf78224e.png");
mangaDexChapterResponse.Chapter.DataSaver.Count.ShouldBe(13);
mangaDexChapterResponse.Chapter.DataSaver[0].ShouldBe("1-7b0a40f35edd75f14d0aa9c0369f8bb05e41687165d97368b572f3c3c5b3db31.jpg");
mangaDexChapterResponse.Chapter.DataSaver[12].ShouldBe("13-b886b4ed986a473478e3db7bb18fe2faea567a1ad5e520408967410dcf8838d1.jpg");
}
private static async Task<string> ReadJsonResourceAsync(string resourceName)
{
return await ResourceHelper.ReadJsonResourceAsync($"MangaReader.Tests.Sources.MangaDex.Api.{resourceName}");
}
}

View File

@@ -0,0 +1,108 @@
using MangaReader.Core.Search;
using MangaReader.Core.Sources.MangaDex.Api;
using MangaReader.Core.Sources.MangaDex.Search;
using NSubstitute;
using Shouldly;
namespace MangaReader.Tests.Sources.MangaDex.Search;
public class MangaDexSearchTests
{
[Fact]
public async Task Get_Search_Result()
{
MangaDexCollectionResponse collectionResponse = new()
{
Result = "ok",
Response = "collection",
Data =
[
new MangaEntity()
{
Id = new Guid("ee96e2b7-9af2-4864-9656-649f4d3b6fec"),
Type = "manga",
Attributes = new()
{
Title = new()
{
{ "en", "Gals Cant Be Kind to Otaku!?" }
}
},
Relationships =
[
new CoverArtEntity()
{
Id = new Guid("a06943fd-6309-49a8-a66a-8df0f6dc41eb"),
Type = "cover_art",
Attributes = new()
{
FileName = "6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg"
}
}
]
},
new MangaEntity()
{
Id = new Guid("16c34950-954c-4f0d-808e-d8278a546339"),
Type = "manga",
Attributes = new()
{
Title = new()
{
{ "en", "Suufungo no Mirai ga Wakaru You ni Natta Kedo, Onnagokoro wa Wakaranai." }
}
},
Relationships =
[
new CoverArtEntity()
{
Id = new Guid("ee8588b5-145f-4eee-981a-eb604856fbd2"),
Type = "cover_art",
Attributes = new()
{
FileName = "7d301e1e-642b-4b7d-b65b-9777b36e80bf.jpg"
}
}
]
},
new MangaEntity()
{
Id = new Guid("f395bfc6-e52f-4f64-9cfb-87037215d214"),
Type = "manga",
Attributes = new()
{
Title = new()
{
{ "en", "Ienai Himitsu No Aishikata" }
}
},
Relationships =
[
new CoverArtEntity()
{
Id = new Guid("40df2d2e-b786-4aa9-9218-e3ed168cd96e"),
Type = "cover_art",
Attributes = new()
{
FileName = "c00a33cd-b26b-4554-a9f0-d6885c81eb36.jpg"
}
}
]
}
]
};
IMangaDexClient mangaDexClient = Substitute.For<IMangaDexClient>();
mangaDexClient.SearchMangaByTitleAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(collectionResponse);
MangaDexSearchProvider searchProvider = new(mangaDexClient);
MangaSearchResult[] searchResult = await searchProvider.SearchAsync("Gal Can't Be Kind", CancellationToken.None);
searchResult.Length.ShouldBe(3);
searchResult[0].Title.ShouldBe("Gals Cant Be Kind to Otaku!?");
searchResult[0].Url.ShouldBe("https://mangadex.org/title/ee96e2b7-9af2-4864-9656-649f4d3b6fec/gals-can-t-be-kind-to-otaku");
searchResult[0].Thumbnail.ShouldBe("https://mangadex.org/covers/ee96e2b7-9af2-4864-9656-649f4d3b6fec/6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg");
}
}

View File

@@ -1,51 +0,0 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Sources.MangaDex.Api;
using MangaReader.Tests.Utilities;
using NSubstitute;
using Shouldly;
namespace MangaReader.Tests.WebCrawlers.MangaDex;
public class MangaDexMetadataTests
{
[Fact]
public async Task Get_Manga_Metadata()
{
string resourceName = "MangaReader.Tests.WebCrawlers.MangaDex.MetadataSample.json";
string searchResultJson = await ResourceHelper.ReadJsonResourceAsync(resourceName);
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
MangaDexClient mangaDexClient = new(httpService);
MangaDexResponse mangaDexResponse = await mangaDexClient.GetMangaAsync(Guid.NewGuid(), CancellationToken.None);
mangaDexResponse.Response.ShouldBe("entity");
mangaDexResponse.ShouldBeOfType<MangaDexEntityResponse>();
MangaDexEntityResponse? mangaDexEntityResponse = mangaDexResponse as MangaDexEntityResponse;
mangaDexEntityResponse.ShouldNotBeNull();
mangaDexEntityResponse.Data.ShouldNotBeNull();
mangaDexEntityResponse.Data.ShouldBeOfType<MangaEntity>();
MangaEntity? mangaEntity = mangaDexEntityResponse.Data as MangaEntity;
mangaEntity.ShouldNotBeNull();
mangaEntity.Attributes.Title.ShouldContainKey("en");
mangaEntity.Attributes.Title["en"].ShouldBe("Gals Cant Be Kind to Otaku!?");
mangaEntity.Attributes.Description.ShouldContainKey("en");
mangaEntity.Attributes.Description["en"].ShouldBe("Takuya Seo is an otaku who likes \"anime for girls\" and can't say he likes it out loud. One day, he hooks up with two gals from his class, Amane and Ijichi, but it seems that Amane is also an otaku...");
mangaEntity.Attributes.Tags.Count.ShouldBe(5);
mangaEntity.Attributes.Tags[0].Attributes.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[0].Attributes.Name["en"].ShouldBe("Romance");
mangaEntity.Attributes.Tags[1].Attributes.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[1].Attributes.Name["en"].ShouldBe("Comedy");
mangaEntity.Attributes.Tags[2].Attributes.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[2].Attributes.Name["en"].ShouldBe("School Life");
mangaEntity.Attributes.Tags[3].Attributes.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[3].Attributes.Name["en"].ShouldBe("Slice of Life");
mangaEntity.Attributes.Tags[4].Attributes.Name.ShouldContainKey("en");
mangaEntity.Attributes.Tags[4].Attributes.Name["en"].ShouldBe("Gyaru");
}
}