Basic automated tests through the CLI

This commit is contained in:
Tyrrrz 2021-07-19 20:09:35 +03:00
parent daa8c0a735
commit 85d53d0e94
11 changed files with 445 additions and 2 deletions

View file

@ -20,6 +20,14 @@ jobs:
with:
dotnet-version: 5.0.x
- name: Build & test
run: dotnet test --configuration Release --logger GitHubActions
- name: Upload coverage
uses: codecov/codecov-action@v1.0.5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Build & publish (CLI)
run: dotnet publish DiscordChatExporter.Cli/ -o DiscordChatExporter.Cli/bin/Publish/ --configuration Release
@ -36,4 +44,4 @@ jobs:
uses: actions/upload-artifact@v1
with:
name: DiscordChatExporter
path: DiscordChatExporter.Gui/bin/Publish/
path: DiscordChatExporter.Gui/bin/Publish/

5
.gitignore vendored
View file

@ -19,4 +19,7 @@ bld/
[Oo]bj/
# Coverage
*.opencover.xml
*.opencover.xml
# Secrets
*.secret

View file

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>opencover</CoverletOutputFormat>
</PropertyGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="*.secret" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="0.16.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
<PackageReference Include="JsonExtensions" Version="1.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="coverlet.msbuild" Version="3.0.3" PrivateAssets="all" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DiscordChatExporter.Cli\DiscordChatExporter.Cli.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,25 @@
using System;
using System.IO;
namespace DiscordChatExporter.Cli.Tests.Fixtures
{
public class TempOutputFixture : IDisposable
{
public string DirPath => Path.Combine(
Path.GetDirectoryName(typeof(TempOutputFixture).Assembly.Location) ?? Directory.GetCurrentDirectory(),
"Temp"
);
public TempOutputFixture() => Directory.CreateDirectory(DirPath);
public string GetTempFilePath(string fileName) => Path.Combine(DirPath, fileName);
public string GetTempFilePath() => GetTempFilePath(Guid.NewGuid().ToString());
public void Dispose()
{
if (Directory.Exists(DirPath))
Directory.Delete(DirPath, true);
}
}
}

View file

@ -0,0 +1,39 @@
using System;
using System.IO;
namespace DiscordChatExporter.Cli.Tests.Infra
{
public static class Secrets
{
private static readonly Lazy<string> DiscordTokenLazy = new(() =>
{
var fromEnvironment = Environment.GetEnvironmentVariable("DISCORD_TOKEN");
if (!string.IsNullOrWhiteSpace(fromEnvironment))
return fromEnvironment;
var secretFilePath = Path.Combine(
Path.GetDirectoryName(typeof(Secrets).Assembly.Location) ?? Directory.GetCurrentDirectory(),
"DiscordToken.secret"
);
if (File.Exists(secretFilePath))
return File.ReadAllText(secretFilePath);
throw new InvalidOperationException("Discord token not provided for tests.");
});
private static readonly Lazy<bool> IsDiscordTokenBotLazy = new(() =>
{
// Default to true
var fromEnvironment = Environment.GetEnvironmentVariable("DISCORD_TOKEN_BOT");
if (string.IsNullOrWhiteSpace(fromEnvironment))
return true;
return string.Equals(fromEnvironment, "true", StringComparison.OrdinalIgnoreCase);
});
public static string DiscordToken => DiscordTokenLazy.Value;
public static bool IsDiscordTokenBot => IsDiscordTokenBotLazy.Value;
}
}

View file

@ -0,0 +1,304 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AngleSharp.Dom;
using CliFx.Infrastructure;
using DiscordChatExporter.Cli.Commands;
using DiscordChatExporter.Cli.Tests.Fixtures;
using DiscordChatExporter.Cli.Tests.Infra;
using DiscordChatExporter.Cli.Tests.TestData;
using DiscordChatExporter.Cli.Tests.Utils;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Exporting;
using FluentAssertions;
using JsonExtensions;
using Xunit;
using Xunit.Abstractions;
namespace DiscordChatExporter.Cli.Tests
{
public class MentionSpecs : IClassFixture<TempOutputFixture>
{
private readonly ITestOutputHelper _testOutput;
private readonly TempOutputFixture _tempOutput;
public MentionSpecs(ITestOutputHelper testOutput, TempOutputFixture tempOutput)
{
_testOutput = testOutput;
_tempOutput = tempOutput;
}
[Fact]
public async Task User_mention_is_rendered_correctly_in_JSON()
{
// Arrange
var outputFilePath = Path.ChangeExtension(_tempOutput.GetTempFilePath(), "json");
// Act
await new ExportChannelsCommand
{
TokenValue = Secrets.DiscordToken,
IsBotToken = Secrets.IsDiscordTokenBot,
ChannelIds = new[] {Snowflake.Parse(ChannelIds.MentionTestCases)},
ExportFormat = ExportFormat.Json,
OutputPath = outputFilePath
}.ExecuteAsync(new FakeConsole());
var jsonData = await File.ReadAllTextAsync(outputFilePath);
_testOutput.WriteLine(jsonData);
var json = Json.Parse(jsonData);
var messageJson = json
.GetProperty("messages")
.EnumerateArray()
.Single(j => string.Equals(
j.GetProperty("id").GetString(),
"866458840245076028",
StringComparison.OrdinalIgnoreCase
));
var content = messageJson
.GetProperty("content")
.GetString();
var mentionedUserIds = messageJson
.GetProperty("mentions")
.EnumerateArray()
.Select(j => j.GetProperty("id").GetString())
.ToArray();
// Assert
content.Should().Be("User mention: @Tyrrrz");
mentionedUserIds.Should().Contain("128178626683338752");
}
[Fact]
public async Task User_mention_is_rendered_correctly_in_HTML()
{
// Arrange
var outputFilePath = Path.ChangeExtension(_tempOutput.GetTempFilePath(), "html");
// Act
await new ExportChannelsCommand
{
TokenValue = Secrets.DiscordToken,
IsBotToken = Secrets.IsDiscordTokenBot,
ChannelIds = new[] {Snowflake.Parse(ChannelIds.MentionTestCases)},
ExportFormat = ExportFormat.HtmlDark,
OutputPath = outputFilePath
}.ExecuteAsync(new FakeConsole());
var htmlData = await File.ReadAllTextAsync(outputFilePath);
_testOutput.WriteLine(htmlData);
var html = Html.Parse(htmlData);
var messageHtml = html.GetElementById("message-866458840245076028");
// Assert
messageHtml.Should().NotBeNull();
messageHtml?.Text().Trim().Should().Be("User mention: @Tyrrrz");
messageHtml?.InnerHtml.Should().Contain("Tyrrrz#5447");
}
[Fact]
public async Task Text_channel_mention_is_rendered_correctly_in_JSON()
{
// Arrange
var outputFilePath = Path.ChangeExtension(_tempOutput.GetTempFilePath(), "json");
// Act
await new ExportChannelsCommand
{
TokenValue = Secrets.DiscordToken,
IsBotToken = Secrets.IsDiscordTokenBot,
ChannelIds = new[] {Snowflake.Parse(ChannelIds.MentionTestCases)},
ExportFormat = ExportFormat.Json,
OutputPath = outputFilePath
}.ExecuteAsync(new FakeConsole());
var jsonData = await File.ReadAllTextAsync(outputFilePath);
_testOutput.WriteLine(jsonData);
var json = Json.Parse(jsonData);
var messageJson = json
.GetProperty("messages")
.EnumerateArray()
.Single(j => string.Equals(
j.GetProperty("id").GetString(),
"866459040480624680",
StringComparison.OrdinalIgnoreCase
));
var content = messageJson
.GetProperty("content")
.GetString();
// Assert
content.Should().Be("Text channel mention: #mention-tests");
}
[Fact]
public async Task Text_channel_mention_is_rendered_correctly_in_HTML()
{
// Arrange
var outputFilePath = Path.ChangeExtension(_tempOutput.GetTempFilePath(), "html");
// Act
await new ExportChannelsCommand
{
TokenValue = Secrets.DiscordToken,
IsBotToken = Secrets.IsDiscordTokenBot,
ChannelIds = new[] {Snowflake.Parse(ChannelIds.MentionTestCases)},
ExportFormat = ExportFormat.HtmlDark,
OutputPath = outputFilePath
}.ExecuteAsync(new FakeConsole());
var htmlData = await File.ReadAllTextAsync(outputFilePath);
_testOutput.WriteLine(htmlData);
var html = Html.Parse(htmlData);
var messageHtml = html.GetElementById("message-866459040480624680");
// Assert
messageHtml.Should().NotBeNull();
messageHtml?.Text().Trim().Should().Be("Text channel mention: #mention-tests");
}
[Fact]
public async Task Voice_channel_mention_is_rendered_correctly_in_JSON()
{
// Arrange
var outputFilePath = Path.ChangeExtension(_tempOutput.GetTempFilePath(), "json");
// Act
await new ExportChannelsCommand
{
TokenValue = Secrets.DiscordToken,
IsBotToken = Secrets.IsDiscordTokenBot,
ChannelIds = new[] {Snowflake.Parse(ChannelIds.MentionTestCases)},
ExportFormat = ExportFormat.Json,
OutputPath = outputFilePath
}.ExecuteAsync(new FakeConsole());
var jsonData = await File.ReadAllTextAsync(outputFilePath);
_testOutput.WriteLine(jsonData);
var json = Json.Parse(jsonData);
var messageJson = json
.GetProperty("messages")
.EnumerateArray()
.Single(j => string.Equals(
j.GetProperty("id").GetString(),
"866459175462633503",
StringComparison.OrdinalIgnoreCase
));
var content = messageJson
.GetProperty("content")
.GetString();
// Assert
content.Should().Be("Voice channel mention: #chaos-vc [voice]");
}
[Fact]
public async Task Voice_channel_mention_is_rendered_correctly_in_HTML()
{
// Arrange
var outputFilePath = Path.ChangeExtension(_tempOutput.GetTempFilePath(), "html");
// Act
await new ExportChannelsCommand
{
TokenValue = Secrets.DiscordToken,
IsBotToken = Secrets.IsDiscordTokenBot,
ChannelIds = new[] {Snowflake.Parse(ChannelIds.MentionTestCases)},
ExportFormat = ExportFormat.HtmlDark,
OutputPath = outputFilePath
}.ExecuteAsync(new FakeConsole());
var htmlData = await File.ReadAllTextAsync(outputFilePath);
_testOutput.WriteLine(htmlData);
var html = Html.Parse(htmlData);
var messageHtml = html.GetElementById("message-866459175462633503");
// Assert
messageHtml.Should().NotBeNull();
messageHtml?.Text().Trim().Should().Be("Voice channel mention: 🔊chaos-vc");
}
[Fact]
public async Task Role_mention_is_rendered_correctly_in_JSON()
{
// Arrange
var outputFilePath = Path.ChangeExtension(_tempOutput.GetTempFilePath(), "json");
// Act
await new ExportChannelsCommand
{
TokenValue = Secrets.DiscordToken,
IsBotToken = Secrets.IsDiscordTokenBot,
ChannelIds = new[] {Snowflake.Parse(ChannelIds.MentionTestCases)},
ExportFormat = ExportFormat.Json,
OutputPath = outputFilePath
}.ExecuteAsync(new FakeConsole());
var jsonData = await File.ReadAllTextAsync(outputFilePath);
_testOutput.WriteLine(jsonData);
var json = Json.Parse(jsonData);
var messageJson = json
.GetProperty("messages")
.EnumerateArray()
.Single(j => string.Equals(
j.GetProperty("id").GetString(),
"866459254693429258",
StringComparison.OrdinalIgnoreCase
));
var content = messageJson
.GetProperty("content")
.GetString();
// Assert
content.Should().Be("Role mention: @Role 1");
}
[Fact]
public async Task Role_mention_is_rendered_correctly_in_HTML()
{
// Arrange
var outputFilePath = Path.ChangeExtension(_tempOutput.GetTempFilePath(), "html");
// Act
await new ExportChannelsCommand
{
TokenValue = Secrets.DiscordToken,
IsBotToken = Secrets.IsDiscordTokenBot,
ChannelIds = new[] {Snowflake.Parse(ChannelIds.MentionTestCases)},
ExportFormat = ExportFormat.HtmlDark,
OutputPath = outputFilePath
}.ExecuteAsync(new FakeConsole());
var htmlData = await File.ReadAllTextAsync(outputFilePath);
_testOutput.WriteLine(htmlData);
var html = Html.Parse(htmlData);
var messageHtml = html.GetElementById("message-866459254693429258");
// Assert
messageHtml.Should().NotBeNull();
messageHtml?.Text().Trim().Should().Be("Role mention: @Role 1");
}
}
}

View file

@ -0,0 +1,9 @@
namespace DiscordChatExporter.Cli.Tests.TestData
{
public static class ChannelIds
{
public static string MentionTestCases { get; } = "866458801389174794";
public static string ReplyTestCases { get; } = "866459871934677052";
}
}

View file

@ -0,0 +1,12 @@
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
namespace DiscordChatExporter.Cli.Tests.Utils
{
internal static class Html
{
private static readonly IHtmlParser Parser = new HtmlParser();
public static IHtmlDocument Parse(string source) => Parser.ParseDocument(source);
}
}

View file

@ -0,0 +1,5 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"methodDisplayOptions": "all",
"methodDisplay": "method"
}

View file

@ -18,6 +18,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordChatExporter.Cli", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordChatExporter.Core", "DiscordChatExporter.Core\DiscordChatExporter.Core.csproj", "{E19980B9-2B84-4257-A517-540FF1E3FCDD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordChatExporter.Cli.Tests", "DiscordChatExporter.Cli.Tests\DiscordChatExporter.Cli.Tests.csproj", "{C5064B8B-692E-4515-BA55-A9BE392EE540}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -36,6 +38,10 @@ Global
{E19980B9-2B84-4257-A517-540FF1E3FCDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E19980B9-2B84-4257-A517-540FF1E3FCDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E19980B9-2B84-4257-A517-540FF1E3FCDD}.Release|Any CPU.Build.0 = Release|Any CPU
{C5064B8B-692E-4515-BA55-A9BE392EE540}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5064B8B-692E-4515-BA55-A9BE392EE540}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5064B8B-692E-4515-BA55-A9BE392EE540}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5064B8B-692E-4515-BA55-A9BE392EE540}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -1,6 +1,7 @@
# DiscordChatExporter
[![Build](https://github.com/Tyrrrz/DiscordChatExporter/workflows/CI/badge.svg?branch=master)](https://github.com/Tyrrrz/DiscordChatExporter/actions)
[![Coverage](https://codecov.io/gh/Tyrrrz/DiscordChatExporter/branch/master/graph/badge.svg)](https://codecov.io/gh/Tyrrrz/DiscordChatExporter)
[![Release](https://img.shields.io/github/release/Tyrrrz/DiscordChatExporter.svg)](https://github.com/Tyrrrz/DiscordChatExporter/releases)
[![Downloads](https://img.shields.io/github/downloads/Tyrrrz/DiscordChatExporter/total.svg)](https://github.com/Tyrrrz/DiscordChatExporter/releases)
[![Donate](https://img.shields.io/badge/donate-$$$-purple.svg)](https://tyrrrz.me/donate)