Added abstraction layeer IHtmlLoader. Finished reorganizing test project folder structure.

This commit is contained in:
2025-06-09 00:09:59 -04:00
parent b5d22c3c7e
commit c26ed11bfc
30 changed files with 1966 additions and 132 deletions

View File

@@ -4,11 +4,10 @@ public class Manga
{
public int MangaId { get; set; }
public required string Slug { get; set; }
public required string Title { get; set; }
public string? Description { get; set; }
public virtual ICollection<MangaCover> Covers { get; set; } = [];
public virtual ICollection<MangaTitle> Titles { get; set; } = [];
public virtual ICollection<MangaDescription> Descriptions { get; set; } = [];
public virtual ICollection<MangaSource> Sources { get; set; } = [];
public virtual ICollection<MangaContributor> Contributors { get; set; } = [];
public virtual ICollection<MangaGenre> Genres { get; set; } = [];

View File

@@ -7,6 +7,7 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
public DbSet<Manga> Mangas { get; set; }
public DbSet<MangaCover> MangaCovers { get; set; }
public DbSet<MangaTitle> MangaTitles { get; set; }
public DbSet<MangaDescription> MangaDescriptions { get; set; }
public DbSet<Source> Sources { get; set; }
public DbSet<MangaSource> MangaSources { get; set; }
public DbSet<Contributor> Contributors { get; set; }
@@ -24,6 +25,7 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
ConfigureManga(modelBuilder);
ConfigureMangaCover(modelBuilder);
ConfigureMangaTitle(modelBuilder);
ConfigureMangaDescription(modelBuilder);
ConfigureSource(modelBuilder);
ConfigureMangaSource(modelBuilder);
ConfigureContributor(modelBuilder);
@@ -41,9 +43,9 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
.Entity<Manga>()
.HasKey(x => x.MangaId);
modelBuilder.Entity<Manga>()
.HasIndex(x => x.Title)
.IsUnique();
//modelBuilder.Entity<Manga>()
// .HasIndex(x => x.Title)
// .IsUnique();
modelBuilder.Entity<Manga>()
.HasIndex(x => x.Slug)
@@ -81,7 +83,15 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
.HasKey(mangaTitle => mangaTitle.MangaTitleId);
modelBuilder.Entity<MangaTitle>()
.HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.Name })
.Property(mt => mt.Name)
.IsRequired();
modelBuilder.Entity<MangaTitle>()
.Property(mt => mt.Language)
.IsRequired();
modelBuilder.Entity<MangaTitle>()
.HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.Name, mangaTitle.Language })
.IsUnique();
modelBuilder
@@ -96,6 +106,36 @@ public class MangaContext(DbContextOptions options) : DbContext(options)
.OnDelete(DeleteBehavior.Cascade);
}
private static void ConfigureMangaDescription(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<MangaDescription>()
.HasKey(mangaTitle => mangaTitle.MangaTitleId);
modelBuilder.Entity<MangaDescription>()
.Property(mt => mt.Name)
.IsRequired();
modelBuilder.Entity<MangaDescription>()
.Property(mt => mt.Language)
.IsRequired();
modelBuilder.Entity<MangaDescription>()
.HasIndex(mangaTitle => new { mangaTitle.MangaId, mangaTitle.Name, mangaTitle.Language })
.IsUnique();
modelBuilder
.Entity<MangaDescription>()
.HasIndex(mangaTitle => mangaTitle.Name);
modelBuilder
.Entity<MangaDescription>()
.HasOne(x => x.Manga)
.WithMany(x => x.Descriptions)
.HasForeignKey(x => x.MangaId)
.OnDelete(DeleteBehavior.Cascade);
}
private static void ConfigureSource(ModelBuilder modelBuilder)
{
modelBuilder

View File

@@ -1,4 +1,6 @@
namespace MangaReader.Core.Data;
using MangaReader.Core.Common;
namespace MangaReader.Core.Data;
public class MangaTitle
{
@@ -8,5 +10,6 @@ public class MangaTitle
public required Manga Manga { get; set; }
public required string Name { get; set; }
public TitleType TitleType { get; set; }
public required Language Language { get; set; }
public bool IsPrimary { get; set; }
}

View File

@@ -1,12 +1,12 @@
namespace MangaReader.Core.Data;
public enum TitleType
{
Primary,
OfficialTranslation,
FanTranslation,
Synonym,
Abbreviation,
Romaji,
Japanese
}
//public enum TitleType
//{
// Primary,
// OfficialTranslation,
// FanTranslation,
// Synonym,
// Abbreviation,
// Romaji,
// Japanese
//}

View File

@@ -1,4 +1,4 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Http;
using MangaReader.Core.Metadata;
using MangaReader.Core.Search;
using MangaReader.Core.Sources.MangaDex.Api;

View File

@@ -0,0 +1,16 @@
using HtmlAgilityPack;
namespace MangaReader.Core.Http;
public class HtmlLoader(IHttpService httpService) : IHtmlLoader
{
public async Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken)
{
string html = await httpService.GetStringAsync(url, cancellationToken);
HtmlDocument doc = new();
doc.LoadHtml(html);
return doc;
}
}

View File

@@ -1,4 +1,4 @@
namespace MangaReader.Core.HttpService;
namespace MangaReader.Core.Http;
public class HttpService(HttpClient httpClient) : IHttpService
{

View File

@@ -0,0 +1,8 @@
using HtmlAgilityPack;
namespace MangaReader.Core.Http;
public interface IHtmlLoader
{
Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken);
}

View File

@@ -1,4 +1,4 @@
namespace MangaReader.Core.HttpService;
namespace MangaReader.Core.Http;
public interface IHttpService
{

View File

@@ -1,21 +1,17 @@
using HtmlAgilityPack;
namespace MangaReader.Core.Metadata;
namespace MangaReader.Core.Metadata;
public abstract class MangaWebCrawler : IMangaMetadataProvider
{
public abstract string SourceId { get; }
public abstract Task<SourceManga?> GetMangaAsync(string url, CancellationToken cancellationToken);
protected virtual async Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken)
{
HtmlWeb web = new()
{
UsingCacheIfExists = false
};
//protected virtual async Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken)
//{
// HtmlWeb web = new()
// {
// UsingCacheIfExists = false
// };
//return web.Load(url);
return await web.LoadFromWebAsync(url, cancellationToken);
}
// return await web.LoadFromWebAsync(url, cancellationToken);
//}
}

View File

@@ -2,8 +2,8 @@
public class SourceManga
{
public required string Title { get; set; }
public string? Description { get; set; }
public required SourceMangaTitle Title { get; set; }
public SourceMangaDescription? Description { get; set; }
public List<SourceMangaTitle> AlternateTitles { get; set; } = [];
public SourceMangaContributor[] Contributors { get; set; } = [];
public MangaStatus Status { get; set; } = MangaStatus.Unknown;

View File

@@ -0,0 +1,9 @@
using MangaReader.Core.Common;
namespace MangaReader.Core.Metadata;
public class SourceMangaDescription
{
public required string Name { get; set; }
public Language Language { get; set; }
}

View File

@@ -4,6 +4,6 @@ namespace MangaReader.Core.Metadata;
public class SourceMangaTitle
{
public required string Title { get; set; }
public required string Name { get; set; }
public Language Language { get; set; }
}

View File

@@ -7,6 +7,12 @@ namespace MangaReader.Core.Pipeline;
public partial class MangaPipeline(MangaContext context) : IMangaPipeline
{
enum TitleType
{
Primary,
Secondary
}
public async Task RunAsync(MangaPipelineRequest request)
{
string sourceName = request.SourceName;
@@ -17,10 +23,12 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
Manga manga = await GetOrAddMangaAsync(sourceManga);
await AddMangaSourceAsync(sourceUrl, manga, source);
await AddTitleAsync(manga, sourceManga.Title, TitleType.Primary);
await AddDescriptionAsync(manga, sourceManga.Description);
foreach (SourceMangaTitle alternateTitle in sourceManga.AlternateTitles)
{
await AddTitleAsync(manga, alternateTitle);
await AddTitleAsync(manga, alternateTitle, TitleType.Secondary);
}
foreach (string genre in sourceManga.Genres)
@@ -55,15 +63,15 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
private async Task<Manga> GetOrAddMangaAsync(SourceManga sourceManga)
{
Manga? manga = await context.Mangas.FirstOrDefaultAsync(manga => manga.Title == sourceManga.Title);
Manga? manga = await context.Mangas.FirstOrDefaultAsync(manga =>
manga.Titles.Any(mangaTitle => mangaTitle.Name == sourceManga.Title.Name));
if (manga != null)
return manga;
manga = new()
{
Title = sourceManga.Title,
Slug = GenerateSlug(sourceManga.Title),
Slug = GenerateSlug(sourceManga.Title.Name),
};
context.Add(manga);
@@ -104,10 +112,10 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
context.MangaSources.Add(mangaSource);
}
private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle)
private async Task AddTitleAsync(Manga manga, SourceMangaTitle sourceMangaTitle, TitleType titleType)
{
MangaTitle? mangaTitle = await context.MangaTitles.FirstOrDefaultAsync(mt =>
mt.Manga == manga && mt.Name == sourceMangaTitle.Title);
mt.Manga == manga && mt.Name == sourceMangaTitle.Name);
if (mangaTitle != null)
return;
@@ -115,12 +123,35 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
mangaTitle = new()
{
Manga = manga,
Name = sourceMangaTitle.Title,
Name = sourceMangaTitle.Name,
Language = sourceMangaTitle.Language,
IsPrimary = titleType == TitleType.Primary
};
context.MangaTitles.Add(mangaTitle);
}
private async Task AddDescriptionAsync(Manga manga, SourceMangaDescription? sourceMangaDescription)
{
if (sourceMangaDescription == null)
return;
MangaDescription? mangaDescription = await context.MangaDescriptions.FirstOrDefaultAsync(md =>
md.Manga == manga && md.Name == sourceMangaDescription.Name);
if (mangaDescription != null)
return;
mangaDescription = new()
{
Manga = manga,
Name = sourceMangaDescription.Name,
Language = sourceMangaDescription.Language
};
context.MangaDescriptions.Add(mangaDescription);
}
private async Task LinkGenreAsync(Manga manga, string genreName)
{
Genre genre = await GetOrAddGenreAsync(genreName);
@@ -156,28 +187,28 @@ public partial class MangaPipeline(MangaContext context) : IMangaPipeline
return genre;
}
private async Task AddChapterAsync(Manga manga, SourceMangaChapter sourceeMangaChapter)
private async Task AddChapterAsync(Manga manga, SourceMangaChapter sourceMangaChapter)
{
MangaChapter mangaChapter = await context.MangaChapters.FirstOrDefaultAsync(x => x.ChapterNumber == sourceeMangaChapter.Number)
?? AddMangaChapter(manga, sourceeMangaChapter);
MangaChapter mangaChapter = await context.MangaChapters.FirstOrDefaultAsync(x => x.ChapterNumber == sourceMangaChapter.Number)
?? AddMangaChapter(manga, sourceMangaChapter);
if (mangaChapter.VolumeNumber is null && sourceeMangaChapter.Volume is not null)
if (mangaChapter.VolumeNumber is null && sourceMangaChapter.Volume is not null)
{
mangaChapter.VolumeNumber = sourceeMangaChapter.Volume;
mangaChapter.VolumeNumber = sourceMangaChapter.Volume;
}
if (mangaChapter.Title is null && sourceeMangaChapter.Title is not null)
if (mangaChapter.Title is null && sourceMangaChapter.Title is not null)
{
mangaChapter.Title = sourceeMangaChapter.Title;
mangaChapter.Title = sourceMangaChapter.Title;
}
}
private MangaChapter AddMangaChapter(Manga manga, SourceMangaChapter sourceeMangaChapter)
private MangaChapter AddMangaChapter(Manga manga, SourceMangaChapter sourceMangaChapter)
{
MangaChapter mangaChapter = new()
{
Manga = manga,
ChapterNumber = sourceeMangaChapter.Number
ChapterNumber = sourceMangaChapter.Number
};
context.MangaChapters.Add(mangaChapter);

View File

@@ -1,4 +1,4 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Http;
using System.Text.Json;
namespace MangaReader.Core.Search;

View File

@@ -1,4 +1,4 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Http;
using System.Text;
using System.Text.Json;

View File

@@ -50,7 +50,16 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
return mangaGuid;
}
private static string GetTitle(MangaAttributes attributes)
private static SourceMangaTitle GetTitle(MangaAttributes attributes)
{
return new()
{
Name = GetTileName(attributes),
Language = Language.English
};
}
private static string GetTileName(MangaAttributes attributes)
{
if (attributes.Title.TryGetValue("en", out string? title))
return title;
@@ -81,7 +90,7 @@ public class MangaDexMetadataProvider(IMangaDexClient mangaDexClient) : IMangaMe
SourceMangaTitle sourceMangaTitle = new()
{
Title = alternateTitle[alternateTitleKey],
Name = alternateTitle[alternateTitleKey],
Language = language
};

View File

@@ -1,23 +1,28 @@
using HtmlAgilityPack;
using MangaReader.Core.Common;
using MangaReader.Core.Http;
using MangaReader.Core.Metadata;
using System.Text;
using System.Web;
namespace MangaReader.Core.Sources.MangaNato.Metadata;
public class MangaNatoWebCrawler : MangaWebCrawler
public class MangaNatoWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler
{
public override string SourceId => "MangaNato";
public override async Task<SourceManga?> GetMangaAsync(string url, CancellationToken cancellationToken)
{
HtmlDocument document = await GetHtmlDocumentAsync(url, cancellationToken);
HtmlDocument document = await htmlLoader.GetHtmlDocumentAsync(url, cancellationToken);
MangaNatoMangaDocument node = new(document);
SourceManga manga = new()
{
Title = node.TitleNode?.InnerText ?? string.Empty,
Title = new()
{
Name = node.TitleNode?.InnerText ?? string.Empty,
Language = Language.Unknown
},
AlternateTitles = GetAlternateTitles(node.AlternateTitlesNode),
Contributors = GetContributors(node.AuthorsNode),
Status = GetStatus(node.StatusNode),
@@ -26,7 +31,11 @@ public class MangaNatoWebCrawler : MangaWebCrawler
RatingPercent = GetRatingPercent(node.AverageRatingNode, node.BestRatingNode),
Votes = node.VotesNode != null ? int.Parse(node.VotesNode.InnerText) : 0,
Views = GetViews(node.ViewsNode),
Description = GetTextFromNodes(node.StoryDescriptionTextNodes),
Description = new()
{
Name = GetTextFromNodes(node.StoryDescriptionTextNodes),
Language = Language.Unknown
},
Chapters = GetChapters(node.ChapterNodes)
};
@@ -46,7 +55,7 @@ public class MangaNatoWebCrawler : MangaWebCrawler
{
SourceMangaTitle sourceMangaTitle = new()
{
Title = title,
Name = title,
Language = Language.Unknown
};

View File

@@ -1,4 +1,4 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Http;
using System.Globalization;
using System.Text;
using System.Text.Json;

View File

@@ -1,20 +1,25 @@
using HtmlAgilityPack;
using MangaReader.Core.Http;
using MangaReader.Core.Metadata;
namespace MangaReader.Core.Sources.NatoManga.Metadata;
public class NatoMangaWebCrawler : MangaWebCrawler
public class NatoMangaWebCrawler(IHtmlLoader htmlLoader) : MangaWebCrawler
{
public override string SourceId => "NatoManga";
public override async Task<SourceManga?> GetMangaAsync(string url, CancellationToken cancellationToken)
{
HtmlDocument document = await GetHtmlDocumentAsync(url, cancellationToken);
HtmlDocument document = await htmlLoader.GetHtmlDocumentAsync(url, cancellationToken);
NatoMangaHtmlDocument node = new(document);
SourceManga manga = new()
{
Title = node.TitleNode?.InnerText ?? string.Empty,
Title = new()
{
Name = node.TitleNode?.InnerText ?? string.Empty,
Language = Common.Language.Unknown
},
Genres = GetGenres(node.GenresNode),
Chapters = GetChapters(node.ChapterNodes)
};

View File

@@ -13,16 +13,17 @@
<None Remove="Sources\MangaDex\Api\Manga-Chapter-Response.json" />
<None Remove="Sources\MangaDex\Api\Manga-Cover-Art-Response.json" />
<None Remove="Sources\MangaDex\Api\Manga-Search-Response-2.json" />
<None Remove="Sources\NatoManga\Api\Manga-Chapter-Response.html" />
<None Remove="WebCrawlers\Samples\MangaNato - Please Go Home, Akutsu-San!.htm" />
</ItemGroup>
<ItemGroup>
<Content Include="WebCrawlers\NatoManga\SampleMangaPage.html">
<EmbeddedResource Include="Sources\NatoManga\Metadata\Manga-Response.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="WebCrawlers\Samples\MangaNato - Please Go Home, Akutsu-San!.htm">
</EmbeddedResource>
<EmbeddedResource Include="Sources\MangaNato\Metadata\Manga-Response.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
@@ -30,6 +31,7 @@
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Cover-Art-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Search-Response-2.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Search-Response.json" />
<EmbeddedResource Include="Sources\NatoManga\Metadata\Manga-Chapter-Response.html" />
<EmbeddedResource Include="Sources\NatoManga\Api\Manga-Search-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Feed-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Response.json" />
@@ -59,4 +61,9 @@
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<Folder Include="WebCrawlers\NatoManga\" />
<Folder Include="WebCrawlers\Samples\" />
</ItemGroup>
</Project>

View File

@@ -17,12 +17,16 @@ public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture<Te
var sourceManga = new SourceManga
{
Title = "Fullmetal Alchemist",
Title = new()
{
Name = "Fullmetal Alchemist",
Language = Language.English
},
AlternateTitles =
[
new()
{
Title = "Hagane no Renkinjutsushi",
Name = "Hagane no Renkinjutsushi",
Language = Language.Romaji
}
],
@@ -49,7 +53,10 @@ public class MangaPipelineTests(TestDbContextFactory factory) : IClassFixture<Te
await pipeline.RunAsync(request);
context.Mangas.ShouldHaveSingleItem();
context.MangaTitles.ShouldHaveSingleItem();
context.MangaTitles.Count().ShouldBe(2);
context.MangaTitles.Where(mt => mt.IsPrimary).ShouldHaveSingleItem();
context.MangaTitles.Where(mt => mt.IsPrimary).First().Name.ShouldBe("Fullmetal Alchemist");
context.MangaTitles.Where(mt => mt.IsPrimary).First().Language.ShouldBe(Language.English);
context.Genres.Count().ShouldBe(2);
context.MangaChapters.ShouldHaveSingleItem();
}

View File

@@ -1,4 +1,4 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Http;
using MangaReader.Core.Sources.MangaDex.Api;
using MangaReader.Tests.Utilities;
using NSubstitute;

View File

@@ -229,23 +229,23 @@ public class MangaDexMetadataTests
SourceManga? sourceManga = await metadataProvider.GetMangaAsync("https://mangadex.org/title/ee96e2b7-9af2-4864-9656-649f4d3b6fec/gals-can-t-be-kind-to-otaku", CancellationToken.None);
sourceManga.ShouldNotBeNull();
sourceManga.Title.ShouldBe("Gals Cant Be Kind to Otaku!?");
sourceManga.Title.Name.ShouldBe("Gals Cant Be Kind to Otaku!?");
sourceManga.AlternateTitles.Count.ShouldBe(5);
sourceManga.AlternateTitles[0].Title.ShouldBe("オタクに優しいギャルはいない!?");
sourceManga.AlternateTitles[0].Name.ShouldBe("オタクに優しいギャルはいない!?");
sourceManga.AlternateTitles[0].Language.ShouldBe(Language.Japanese);
sourceManga.AlternateTitles[1].Title.ShouldBe("Otaku ni Yasashii Gal wa Inai!?");
sourceManga.AlternateTitles[1].Name.ShouldBe("Otaku ni Yasashii Gal wa Inai!?");
sourceManga.AlternateTitles[1].Language.ShouldBe(Language.Romaji);
sourceManga.AlternateTitles[2].Title.ShouldBe("Otaku ni Yasashii Gyaru ha Inai!?");
sourceManga.AlternateTitles[2].Name.ShouldBe("Otaku ni Yasashii Gyaru ha Inai!?");
sourceManga.AlternateTitles[2].Language.ShouldBe(Language.Romaji);
sourceManga.AlternateTitles[3].Title.ShouldBe("Gal Can't Be Kind to Otaku!?");
sourceManga.AlternateTitles[3].Name.ShouldBe("Gal Can't Be Kind to Otaku!?");
sourceManga.AlternateTitles[3].Language.ShouldBe(Language.English);
sourceManga.AlternateTitles[4].Title.ShouldBe("Gals Can't Be Kind To A Geek!?");
sourceManga.AlternateTitles[4].Name.ShouldBe("Gals Can't Be Kind To A Geek!?");
sourceManga.AlternateTitles[4].Language.ShouldBe(Language.English);
sourceManga.Genres.Count.ShouldBe(5);

View File

@@ -1,47 +1,35 @@
using HtmlAgilityPack;
using MangaReader.Core.Http;
using MangaReader.Core.Metadata;
using MangaReader.Core.Sources.MangaNato.Metadata;
using MangaReader.Tests.Utilities;
using NSubstitute;
using Shouldly;
using System.Data;
using System.Xml.Linq;
namespace MangaReader.Tests.WebCrawlers;
namespace MangaReader.Tests.Sources.MangaNato.Metadata;
public class UnitTest1
public class MangaNatoMetadataTests
{
class TestMangaNatoWebCrawler : MangaNatoWebCrawler
{
protected override Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken)
{
HtmlWeb web = new()
{
UsingCacheIfExists = false
};
return Task.FromResult(web.Load(url));
}
}
private readonly string samplesPath;
private readonly string mangaNatoSampleFilePath;
public UnitTest1()
{
samplesPath = Path.Combine(AppContext.BaseDirectory, "WebCrawlers", "Samples");
mangaNatoSampleFilePath = Path.Combine(samplesPath, "MangaNato - Please Go Home, Akutsu-San!.htm");
}
[Fact]
public async Task Get_Manga()
{
var webCrawler = new TestMangaNatoWebCrawler();
var manga = await webCrawler.GetMangaAsync(mangaNatoSampleFilePath, CancellationToken.None);
string mangaHtml = await ReadJsonResourceAsync("Manga-Response.html");
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(mangaHtml));
HtmlLoader htmlLoader = new(httpService);
MangaNatoWebCrawler webCrawler = new(htmlLoader);
SourceManga? manga = await webCrawler.GetMangaAsync("/test-url", CancellationToken.None);
manga.ShouldNotBeNull();
manga.Title.ShouldBe("Please Go Home, Akutsu-San!");
manga.Title.Name.ShouldBe("Please Go Home, Akutsu-San!");
manga.AlternateTitles.Select(x => x.Title).ShouldBe([
manga.AlternateTitles.Select(x => x.Name).ShouldBe([
"Kaette kudasai! Akutsu-san",
"Yankee Musume",
"ヤンキー娘",
@@ -62,8 +50,8 @@ public class UnitTest1
manga.Votes.ShouldBe(15979);
//manga.Description.ShouldStartWith("Ooyama-kun normally doesnt get involved with Akutsu-san, a delinquent girl in his class");
manga.Description.ShouldStartWith("Ooyama-kun normally doesnt get involved with Akutsu-san, a delinquent girl in his class");
manga.Description.ShouldEndWith("Artist's Pixiv: https://www.pixiv.net/member.php?id=133935");
manga.Description?.Name.ShouldStartWith("Ooyama-kun normally doesnt get involved with Akutsu-san, a delinquent girl in his class");
manga.Description?.Name.ShouldEndWith("Artist's Pixiv: https://www.pixiv.net/member.php?id=133935");
manga.Chapters.Count.ShouldBe(236);
@@ -79,4 +67,9 @@ public class UnitTest1
manga.Chapters[235].Views.ShouldBe(232_200);
manga.Chapters[235].UploadDate.ShouldBe(new DateTime(2021, 8, 24, 1, 8, 0));
}
private static async Task<string> ReadJsonResourceAsync(string resourceName)
{
return await ResourceHelper.ReadJsonResourceAsync($"MangaReader.Tests.Sources.MangaNato.Metadata.{resourceName}");
}
}

View File

@@ -1,4 +1,4 @@
using MangaReader.Core.HttpService;
using MangaReader.Core.Http;
using MangaReader.Core.Sources.NatoManga.Api;
using MangaReader.Tests.Utilities;
using NSubstitute;
@@ -34,6 +34,9 @@ public class NatoMangaClientTests
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
httpService.GetStringAsync(Arg.Any<string>(), Arg.Any<IDictionary<string,string>>(), CancellationToken.None)
.Returns(Task.FromResult(searchResultJson));
NatoMangaClient natoMangaClient = new(httpService);
NatoMangaSearchResult[] searchResults = await natoMangaClient.SearchAsync("Gal Can't Be Kind", CancellationToken.None);

File diff suppressed because one or more lines are too long

View File

@@ -1,35 +1,32 @@
using HtmlAgilityPack;
using MangaReader.Core.Http;
using MangaReader.Core.Metadata;
using MangaReader.Core.Sources.NatoManga.Metadata;
using MangaReader.Tests.Utilities;
using NSubstitute;
using Shouldly;
namespace MangaReader.Tests.WebCrawlers.NatoManga;
namespace MangaReader.Tests.Sources.NatoManga.Metadata;
public class NatoMangaWebCrawlerTests
{
class TestNatoMangaWebCrawler : NatoMangaWebCrawler
{
protected override Task<HtmlDocument> GetHtmlDocumentAsync(string url, CancellationToken cancellationToken)
{
HtmlWeb web = new()
{
UsingCacheIfExists = false
};
return Task.FromResult(web.Load(url));
}
}
[Fact]
public async Task Get_Manga()
{
string sampleFilePath = Path.Combine(AppContext.BaseDirectory, "WebCrawlers", "NatoManga", "SampleMangaPage.html");
string mangaHtml = await ReadJsonResourceAsync("Manga-Response.html");
var webCrawler = new TestNatoMangaWebCrawler();
var manga = await webCrawler.GetMangaAsync(sampleFilePath, CancellationToken.None);
IHttpService httpService = Substitute.For<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), CancellationToken.None)
.Returns(Task.FromResult(mangaHtml));
HtmlLoader htmlLoader = new(httpService);
NatoMangaWebCrawler webCrawler = new(htmlLoader);
SourceManga? manga = await webCrawler.GetMangaAsync("/test-url", CancellationToken.None);
manga.ShouldNotBeNull();
manga.Title.ShouldBe("Gal Cant Be Kind to Otaku!?");
manga.Title.Name.ShouldBe("Gal Cant Be Kind to Otaku!?");
//manga.AlternateTitles.ShouldBe([
// "Kaette kudasai! Akutsu-san",
@@ -63,4 +60,9 @@ public class NatoMangaWebCrawlerTests
//manga.Chapters[235].Views.ShouldBe(232_200);
//manga.Chapters[235].UploadDate.ShouldBe(new DateTime(2021, 8, 24, 1, 8, 0));
}
private static async Task<string> ReadJsonResourceAsync(string resourceName)
{
return await ResourceHelper.ReadJsonResourceAsync($"MangaReader.Tests.Sources.NatoManga.Metadata.{resourceName}");
}
}