Move source code into "Source" folder

This commit is contained in:
Robert McRackan 2022-05-09 10:31:45 -04:00
parent 1ee73fa1a7
commit 389fbb2371
287 changed files with 26 additions and 8 deletions

View file

@ -0,0 +1,593 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi.Authorization;
using AudibleUtilities;
using Dinah.Core;
using FluentAssertions;
using FluentAssertions.Common;
using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Moq.Protected;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace AccountsTests
{
public class AccountsTestBase
{
protected const string EMPTY_FILE = "{\r\n \"Accounts\": []\r\n}";
protected string TestFile;
protected Locale usLocale => Localization.Get("us");
protected Locale ukLocale => Localization.Get("uk");
protected void WriteToTestFile(string contents)
=> File.WriteAllText(TestFile, contents);
[TestInitialize]
public void TestInit()
=> TestFile = Guid.NewGuid() + ".txt";
[TestCleanup]
public void TestCleanup()
{
if (File.Exists(TestFile))
File.Delete(TestFile);
}
}
[TestClass]
public class FromJson : AccountsTestBase
{
[TestMethod]
public void _0_accounts()
{
var accountsSettings = AccountsSettings.FromJson(EMPTY_FILE);
accountsSettings.Accounts.Count.Should().Be(0);
}
[TestMethod]
public void _1_account_new()
{
var json = @"
{
""Accounts"": [
{
""AccountId"": ""cng"",
""AccountName"": ""my main login"",
""DecryptKey"": ""asdfasdf"",
""IdentityTokens"": null
}
]
}
".Trim();
var accountsSettings = AccountsSettings.FromJson(json);
accountsSettings.Accounts.Count.Should().Be(1);
accountsSettings.Accounts[0].AccountId.Should().Be("cng");
accountsSettings.Accounts[0].IdentityTokens.Should().BeNull();
}
}
[TestClass]
public class ctor : AccountsTestBase
{
[TestMethod]
public void create_file()
{
File.Exists(TestFile).Should().BeFalse();
var accountsSettings = new AccountsSettings();
_ = new AccountsSettingsPersister(accountsSettings, TestFile);
File.Exists(TestFile).Should().BeTrue();
File.ReadAllText(TestFile).Should().Be(EMPTY_FILE);
}
[TestMethod]
public void overwrite_existing_file()
{
File.Exists(TestFile).Should().BeFalse();
WriteToTestFile("foo");
File.Exists(TestFile).Should().BeTrue();
var accountsSettings = new AccountsSettings();
_ = new AccountsSettingsPersister(accountsSettings, TestFile);
File.Exists(TestFile).Should().BeTrue();
File.ReadAllText(TestFile).Should().Be(EMPTY_FILE);
}
[TestMethod]
public void save_multiple_children()
{
var accountsSettings = new AccountsSettings();
accountsSettings.Add(new Account("a0") { AccountName = "n0" });
accountsSettings.Add(new Account("a1") { AccountName = "n1" });
// dispose to cease auto-updates
using (var p = new AccountsSettingsPersister(accountsSettings, TestFile)) { }
var persister = new AccountsSettingsPersister(TestFile);
persister.AccountsSettings.Accounts.Count.Should().Be(2);
persister.AccountsSettings.Accounts[1].AccountName.Should().Be("n1");
}
[TestMethod]
public void save_with_identity()
{
var id = new Identity(usLocale);
var idJson = JsonConvert.SerializeObject(id, Identity.GetJsonSerializerSettings());
var accountsSettings = new AccountsSettings();
accountsSettings.Add(new Account("a0") { AccountName = "n0", IdentityTokens = id });
// dispose to cease auto-updates
using (var p = new AccountsSettingsPersister(accountsSettings, TestFile)) { }
var persister = new AccountsSettingsPersister(TestFile);
var acct = persister.AccountsSettings.Accounts[0];
acct.AccountName.Should().Be("n0");
acct.Locale.CountryCode.Should().Be("us");
}
}
[TestClass]
public class save : AccountsTestBase
{
// add/save account after file creation
[TestMethod]
public void save_1_account()
{
// create initial file
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { }
// load file. create account
using (var p = new AccountsSettingsPersister(TestFile))
{
var idIn = new Identity(usLocale);
var acctIn = new Account("a0") { AccountName = "n0", IdentityTokens = idIn };
p.AccountsSettings.Add(acctIn);
}
// re-load file. ensure account still exists
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts.Count.Should().Be(1);
var acct0 = p.AccountsSettings.Accounts[0];
acct0.AccountName.Should().Be("n0");
acct0.Locale.CountryCode.Should().Be("us");
}
}
// add/save mult accounts after file creation
// separately create 2 accounts. ensure both still exist in the end
[TestMethod]
public void save_2_accounts()
{
// create initial file
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { }
// load file. create account 0
using (var p = new AccountsSettingsPersister(TestFile))
{
var idIn = new Identity(usLocale);
var acctIn = new Account("a0") { AccountName = "n0", IdentityTokens = idIn };
p.AccountsSettings.Add(acctIn);
}
// re-load file. ensure account still exists
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts.Count.Should().Be(1);
var acct0 = p.AccountsSettings.Accounts[0];
acct0.AccountName.Should().Be("n0");
acct0.Locale.CountryCode.Should().Be("us");
}
// load file. create account 1
using (var p = new AccountsSettingsPersister(TestFile))
{
var idIn = new Identity(ukLocale);
var acctIn = new Account("a1") { AccountName = "n1", IdentityTokens = idIn };
p.AccountsSettings.Add(acctIn);
}
// re-load file. ensure both accounts still exist
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts.Count.Should().Be(2);
var acct0 = p.AccountsSettings.Accounts[0];
acct0.AccountName.Should().Be("n0");
acct0.Locale.CountryCode.Should().Be("us");
var acct1 = p.AccountsSettings.Accounts[1];
acct1.AccountName.Should().Be("n1");
acct1.Locale.CountryCode.Should().Be("uk");
}
}
[TestMethod]
public void update_Account_field_just_added()
{
// create initial file
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { }
// load file. create 2 accounts
using (var p = new AccountsSettingsPersister(TestFile))
{
var id1 = new Identity(usLocale);
var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 };
p.AccountsSettings.Add(acct1);
// update just-added item. note: this is different than the subscription which happens on initial collection load. ensure this works also
acct1.AccountName = "new";
}
// verify save property
using (var p = new AccountsSettingsPersister(TestFile))
{
var acct0 = p.AccountsSettings.Accounts[0];
acct0.AccountName.Should().Be("new");
}
}
// update Account property. must be non-destructive to all other data
[TestMethod]
public void update_Account_field()
{
// create initial file
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { }
// load file. create 2 accounts
using (var p = new AccountsSettingsPersister(TestFile))
{
var id1 = new Identity(usLocale);
var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 };
p.AccountsSettings.Add(acct1);
var id2 = new Identity(ukLocale);
var acct2 = new Account("a1") { AccountName = "n1", IdentityTokens = id2 };
p.AccountsSettings.Add(acct2);
}
// update AccountName on existing file
using (var p = new AccountsSettingsPersister(TestFile))
{
var acct0 = p.AccountsSettings.Accounts[0];
acct0.AccountName = "new";
}
// re-load file. ensure both accounts still exist
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts.Count.Should().Be(2);
var acct0 = p.AccountsSettings.Accounts[0];
// new
acct0.AccountName.Should().Be("new");
// still here
acct0.Locale.CountryCode.Should().Be("us");
var acct1 = p.AccountsSettings.Accounts[1];
acct1.AccountName.Should().Be("n1");
acct1.Locale.CountryCode.Should().Be("uk");
}
}
// update identity. must be non-destructive to all other data
[TestMethod]
public void replace_identity()
{
// create initial file
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { }
// load file. create 2 accounts
using (var p = new AccountsSettingsPersister(TestFile))
{
var id1 = new Identity(usLocale);
var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 };
p.AccountsSettings.Add(acct1);
var id2 = new Identity(ukLocale);
var acct2 = new Account("a1") { AccountName = "n1", IdentityTokens = id2 };
p.AccountsSettings.Add(acct2);
}
// update identity on existing file
using (var p = new AccountsSettingsPersister(TestFile))
{
var id = new Identity(ukLocale);
var acct0 = p.AccountsSettings.Accounts[0];
acct0.IdentityTokens = id;
}
// re-load file. ensure both accounts still exist
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts.Count.Should().Be(2);
var acct0 = p.AccountsSettings.Accounts[0];
// new
acct0.Locale.CountryCode.Should().Be("uk");
// still here
acct0.AccountName.Should().Be("n0");
var acct1 = p.AccountsSettings.Accounts[1];
acct1.AccountName.Should().Be("n1");
acct1.Locale.CountryCode.Should().Be("uk");
}
}
// multi-level subscribe => update
// edit field of existing identity. must be non-destructive to all other data
[TestMethod]
public void update_identity_field()
{
// create initial file
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { }
// load file. create 2 accounts
using (var p = new AccountsSettingsPersister(TestFile))
{
var id1 = new Identity(usLocale);
var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 };
p.AccountsSettings.Add(acct1);
var id2 = new Identity(ukLocale);
var acct2 = new Account("a1") { AccountName = "n1", IdentityTokens = id2 };
p.AccountsSettings.Add(acct2);
}
// update identity on existing file
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts[0]
.IdentityTokens
.Update(new AccessToken("Atna|_NEW_", DateTime.Now.AddDays(1)));
}
// re-load file. ensure both accounts still exist
using (var p = new AccountsSettingsPersister(TestFile))
{
p.AccountsSettings.Accounts.Count.Should().Be(2);
var acct0 = p.AccountsSettings.Accounts[0];
// new
acct0.IdentityTokens.ExistingAccessToken.TokenValue.Should().Be("Atna|_NEW_");
// still here
acct0.AccountName.Should().Be("n0");
acct0.Locale.CountryCode.Should().Be("us");
var acct1 = p.AccountsSettings.Accounts[1];
acct1.AccountName.Should().Be("n1");
acct1.Locale.CountryCode.Should().Be("uk");
}
}
}
[TestClass]
public class retrieve : AccountsTestBase
{
[TestMethod]
public void get_where()
{
var idUs = new Identity(usLocale);
var acct1 = new Account("cng") { IdentityTokens = idUs, AccountName = "foo" };
var idUk = new Identity(ukLocale);
var acct2 = new Account("cng") { IdentityTokens = idUk, AccountName = "bar" };
var accountsSettings = new AccountsSettings();
accountsSettings.Add(acct1);
accountsSettings.Add(acct2);
accountsSettings.GetAccount("cng", "uk").AccountName.Should().Be("bar");
}
}
[TestClass]
public class upsert : AccountsTestBase
{
[TestMethod]
public void upsert_new()
{
var accountsSettings = new AccountsSettings();
accountsSettings.Accounts.Count.Should().Be(0);
accountsSettings.Upsert("cng", "us");
accountsSettings.Accounts.Count.Should().Be(1);
accountsSettings.GetAccount("cng", "us").AccountId.Should().Be("cng");
}
[TestMethod]
public void upsert_exists()
{
var accountsSettings = new AccountsSettings();
var orig = accountsSettings.Upsert("cng", "us");
orig.AccountName = "foo";
var exists = accountsSettings.Upsert("cng", "us");
exists.AccountName.Should().Be("foo");
orig.Should().BeSameAs(exists);
}
}
[TestClass]
public class delete : AccountsTestBase
{
[TestMethod]
public void delete_account()
{
var accountsSettings = new AccountsSettings();
var acct = accountsSettings.Upsert("cng", "us");
accountsSettings.Accounts.Count.Should().Be(1);
var removed = accountsSettings.Delete(acct);
removed.Should().BeTrue();
accountsSettings.Accounts.Count.Should().Be(0);
}
[TestMethod]
public void delete_where()
{
var accountsSettings = new AccountsSettings();
_ = accountsSettings.Upsert("cng", "us");
accountsSettings.Accounts.Count.Should().Be(1);
accountsSettings.Delete("baz", "baz").Should().BeFalse();
accountsSettings.Accounts.Count.Should().Be(1);
accountsSettings.Delete("cng", "us").Should().BeTrue();
accountsSettings.Accounts.Count.Should().Be(0);
}
[TestMethod]
public void delete_updates()
{
var i = 0;
void update(object sender, EventArgs e) => i++;
var accountsSettings = new AccountsSettings();
accountsSettings.Updated += update;
accountsSettings.Accounts.Count.Should().Be(0);
i.Should().Be(0);
_ = accountsSettings.Upsert("cng", "us");
accountsSettings.Accounts.Count.Should().Be(1);
i.Should().Be(1);
accountsSettings.Delete("baz", "baz").Should().BeFalse();
accountsSettings.Accounts.Count.Should().Be(1);
i.Should().Be(1);
accountsSettings.Delete("cng", "us").Should().BeTrue();
accountsSettings.Accounts.Count.Should().Be(0);
i.Should().Be(2); // <== this is the one being tested
}
[TestMethod]
public void deleted_account_should_not_persist_file()
{
Account acct;
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile))
{
acct = p.AccountsSettings.Upsert("foo", "us");
p.AccountsSettings.Accounts.Count.Should().Be(1);
acct.AccountName = "old";
}
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile))
{
p.AccountsSettings.Delete(acct);
p.AccountsSettings.Accounts.Count.Should().Be(0);
}
using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile))
{
File.ReadAllText(TestFile).Should().Be(EMPTY_FILE);
acct.AccountName = "new";
File.ReadAllText(TestFile).Should().Be(EMPTY_FILE);
}
}
}
// account.Id + Locale.Name -- must be unique
[TestClass]
public class validate : AccountsTestBase
{
[TestMethod]
public void violate_validation()
{
var accountsSettings = new AccountsSettings();
var idIn = new Identity(usLocale);
var a1 = new Account("a") { AccountName = "one", IdentityTokens = idIn };
accountsSettings.Add(a1);
var a2 = new Account("a") { AccountName = "two", IdentityTokens = idIn };
// violation: validate()
Assert.ThrowsException<InvalidOperationException>(() => accountsSettings.Add(a2));
}
[TestMethod]
public void identity_violate_validation()
{
var accountsSettings = new AccountsSettings();
var idIn = new Identity(usLocale);
var a1 = new Account("a") { AccountName = "one", IdentityTokens = idIn };
accountsSettings.Add(a1);
var a2 = new Account("a") { AccountName = "two" };
accountsSettings.Add(a2);
// violation: GetAccount.SingleOrDefault
Assert.ThrowsException<InvalidOperationException>(() => a2.IdentityTokens = idIn);
}
}
[TestClass]
public class transactions : AccountsTestBase
{
[TestMethod]
public void atomic_update_at_end()
{
var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile);
p.BeginTransation();
// upserted account will not persist until CommitTransation
var acct = p.AccountsSettings.Upsert("cng", "us");
acct.AccountName = "foo";
File.ReadAllText(TestFile).Should().Be(EMPTY_FILE);
p.IsInTransaction.Should().BeTrue();
p.CommitTransation();
p.IsInTransaction.Should().BeFalse();
}
[TestMethod]
public void abandoned_transaction()
{
var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile);
try
{
p.BeginTransation();
var acct = p.AccountsSettings.Upsert("cng", "us");
acct.AccountName = "foo";
throw new Exception();
}
catch { }
finally
{
File.ReadAllText(TestFile).Should().Be(EMPTY_FILE);
p.IsInTransaction.Should().BeTrue();
}
}
}
}

View file

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\AudibleUtilities\AudibleUtilities.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\FileLiberator\FileLiberator.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\FileManager\FileManager.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core;
using FileManager;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FileNamingTemplateTests
{
[TestClass]
public class GetFilePath
{
[TestMethod]
public void equiv_GetValidFilename()
{
var expected = @"C:\foo\bar\my_ book LONG_1234567890_1234567890_1234567890_123 [ID123456].txt";
var f1 = OLD_GetValidFilename(@"C:\foo\bar", "my: book LONG_1234567890_1234567890_1234567890_12345", "txt", "ID123456");
var f2 = NEW_GetValidFilename_FileNamingTemplate(@"C:\foo\bar", "my: book LONG_1234567890_1234567890_1234567890_12345", "txt", "ID123456");
f1.Should().Be(expected);
f1.Should().Be(f2);
}
private static string OLD_GetValidFilename(string dirFullPath, string filename, string extension, string metadataSuffix)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(dirFullPath, nameof(dirFullPath));
filename ??= "";
// sanitize. omit invalid characters. exception: colon => underscore
filename = filename.Replace(":", "_");
filename = FileUtility.GetSafeFileName(filename);
if (filename.Length > 50)
filename = filename.Substring(0, 50);
if (!string.IsNullOrWhiteSpace(metadataSuffix))
filename += $" [{metadataSuffix}]";
// extension is null when this method is used for directory names
extension = FileUtility.GetStandardizedExtension(extension);
// ensure uniqueness
var fullfilename = Path.Combine(dirFullPath, filename + extension);
var i = 0;
while (File.Exists(fullfilename))
fullfilename = Path.Combine(dirFullPath, filename + $" ({++i})" + extension);
return fullfilename;
}
private static string NEW_GetValidFilename_FileNamingTemplate(string dirFullPath, string filename, string extension, string metadataSuffix)
{
var template = $"<title> [<id>]";
var fullfilename = Path.Combine(dirFullPath, template + FileUtility.GetStandardizedExtension(extension));
var fileNamingTemplate = new FileNamingTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
fileNamingTemplate.AddParameterReplacement("title", filename);
fileNamingTemplate.AddParameterReplacement("id", metadataSuffix);
return fileNamingTemplate.GetFilePath();
}
[TestMethod]
public void equiv_GetMultipartFileName()
{
var expected = @"C:\foo\bar\my file - 002 - title.txt";
var f1 = OLD_GetMultipartFileName(@"C:\foo\bar\my file.txt", 2, 100, "title");
var f2 = NEW_GetMultipartFileName_FileNamingTemplate(@"C:\foo\bar\my file.txt", 2, 100, "title");
f1.Should().Be(expected);
f1.Should().Be(f2);
}
private static string OLD_GetMultipartFileName(string originalPath, int partsPosition, int partsTotal, string suffix)
{
// 1-9 => 1-9
// 10-99 => 01-99
// 100-999 => 001-999
var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0');
string extension = Path.GetExtension(originalPath);
var filenameBase = $"{Path.GetFileNameWithoutExtension(originalPath)} - {chapterCountLeadingZeros}";
if (!string.IsNullOrWhiteSpace(suffix))
filenameBase += $" - {suffix}";
// Replace illegal path characters with spaces
var fileName = FileUtility.GetSafeFileName(filenameBase, " ");
var path = Path.Combine(Path.GetDirectoryName(originalPath), fileName + extension);
return path;
}
private static string NEW_GetMultipartFileName_FileNamingTemplate(string originalPath, int partsPosition, int partsTotal, string suffix)
{
// 1-9 => 1-9
// 10-99 => 01-99
// 100-999 => 001-999
var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0');
var t = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + Path.GetExtension(originalPath);
var fileNamingTemplate = new FileNamingTemplate(t) { IllegalCharacterReplacements = " " };
fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
fileNamingTemplate.AddParameterReplacement("title", suffix);
return fileNamingTemplate.GetFilePath();
}
[TestMethod]
public void remove_slashes()
{
var fileNamingTemplate = new FileNamingTemplate(@"\foo\<title>.txt");
fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s");
fileNamingTemplate.GetFilePath().Should().Be(@"\foo\slashes.txt");
}
}
}

View file

@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core;
using FileManager;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FileUtilityTests
{
[TestClass]
public class GetSafePath
{
[TestMethod]
public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null));
// needs separate method. middle null param not running correctly in TestExplorer when used in DataRow()
[TestMethod]
[DataRow("http://test.com/a/b/c", @"http\test.com\a\b\c")]
public void null_replacement(string inStr, string outStr) => Tests(inStr, null, outStr);
[TestMethod]
// empty replacement
[DataRow("abc*abc.txt", "", "abcabc.txt")]
// non-empty replacement
[DataRow("abc*abc.txt", "ZZZ", "abcZZZabc.txt")]
// standardize slashes
[DataRow(@"a/b\c/d", "Z", @"a\b\c\d")]
// remove illegal chars
[DataRow("a*?:z.txt", "Z", "aZZZz.txt")]
// retain drive letter path colon
[DataRow(@"C:\az.txt", "Z", @"C:\az.txt")]
// replace all other colons
[DataRow(@"a\b:c\d.txt", "ZZZ", @"a\bZZZc\d.txt")]
// remove empty directories
[DataRow(@"C:\a\\\b\c\\\d.txt", "ZZZ", @"C:\a\b\c\d.txt")]
[DataRow(@"C:\""foo\<id>", "ZZZ", @"C:\ZZZfoo\ZZZidZZZ")]
public void Tests(string inStr, string replacement, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, replacement));
}
[TestClass]
public class GetSafeFileName
{
// needs separate method. middle null param not running correctly in TestExplorer when used in DataRow()
[TestMethod]
[DataRow("http://test.com/a/b/c", "httptest.comabc")]
public void url_null_replacement(string inStr, string outStr) => ReplacementTests(inStr, null, outStr);
[TestMethod]
// empty replacement
[DataRow("http://test.com/a/b/c", "", "httptest.comabc")]
// single char replace
[DataRow("http://test.com/a/b/c", "_", "http___test.com_a_b_c")]
// multi char replace
[DataRow("http://test.com/a/b/c", "!!!", "http!!!!!!!!!test.com!!!a!!!b!!!c")]
public void ReplacementTests(string inStr, string replacement, string outStr) => FileUtility.GetSafeFileName(inStr, replacement).Should().Be(outStr);
}
[TestClass]
public class GetSequenceFormatted
{
[TestMethod]
public void negative_partsPosition() => Assert.ThrowsException<ArgumentException>(()
=> FileUtility.GetSequenceFormatted(-1, 2)
);
[TestMethod]
public void zero_partsPosition() => Assert.ThrowsException<ArgumentException>(()
=> FileUtility.GetSequenceFormatted(0, 2)
);
[TestMethod]
public void negative_partsTotal() => Assert.ThrowsException<ArgumentException>(()
=> FileUtility.GetSequenceFormatted(2, -1)
);
[TestMethod]
public void zero_partsTotal() => Assert.ThrowsException<ArgumentException>(()
=> FileUtility.GetSequenceFormatted(2, 0)
);
[TestMethod]
public void partsPosition_greater_than_partsTotal() => Assert.ThrowsException<ArgumentException>(()
=> FileUtility.GetSequenceFormatted(2, 1)
);
[TestMethod]
// only part
[DataRow(1, 1, "1")]
// 2 digits
[DataRow(2, 90, "02")]
// 3 digits
[DataRow(2, 900, "002")]
public void Tests(int partsPosition, int partsTotal, string expected)
=> FileUtility.GetSequenceFormatted(partsPosition, partsTotal).Should().Be(expected);
}
[TestClass]
public class GetStandardizedExtension
{
[TestMethod]
public void is_null() => Tests(null, "");
[TestMethod]
public void is_empty() => Tests("", "");
[TestMethod]
public void is_whitespace() => Tests(" ", "");
[TestMethod]
[DataRow("txt", ".txt")]
[DataRow(".txt", ".txt")]
[DataRow(" .txt ", ".txt")]
public void Tests(string input, string expected)
=> FileUtility.GetStandardizedExtension(input).Should().Be(expected);
}
[TestClass]
public class GetValidFilename
{
[TestMethod]
// dot-files
[DataRow(@"C:\a bc\x y z\.f i l e.txt")]
// dot-folders
[DataRow(@"C:\a bc\.x y z\f i l e.txt")]
public void Valid(string input) => Tests(input, input);
[TestMethod]
// folder spaces
[DataRow(@"C:\ a bc \x y z ", @"C:\a bc\x y z")]
// file spaces
[DataRow(@"C:\a bc\x y z\ f i l e.txt ", @"C:\a bc\x y z\f i l e.txt")]
// eliminate beginning space and end dots and spaces
[DataRow(@"C:\a bc\ . . . x y z . . . \f i l e.txt", @"C:\a bc\. . . x y z\f i l e.txt")]
// file end dots
[DataRow(@"C:\a bc\x y z\f i l e.txt . . .", @"C:\a bc\x y z\f i l e.txt")]
public void Tests(string input, string expected)
=> FileUtility.GetValidFilename(input).Should().Be(expected);
}
[TestClass]
public class RemoveLastCharacter
{
[TestMethod]
public void is_null() => Tests(null, null);
[TestMethod]
public void empty() => Tests("", "");
[TestMethod]
public void single_space() => Tests(" ", "");
[TestMethod]
public void multiple_space() => Tests(" ", " ");
[TestMethod]
[DataRow("1", "")]
[DataRow("1 ", "1")]
[DataRow("12", "1")]
[DataRow("123", "12")]
public void Tests(string input, string expected)
=> FileUtility.RemoveLastCharacter(input).Should().Be(expected);
}
}

View file

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\LibationFileManager\LibationFileManager.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,393 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dinah.Core;
using FluentAssertions;
using LibationFileManager;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static TemplatesTests.Shared;
namespace TemplatesTests
{
public static class Shared
{
public static LibraryBookDto GetLibraryBook(string asin, string seriesName = "Sherlock Holmes")
=> new()
{
Account = "my account",
AudibleProductId = asin,
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
Locale = "us",
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
Narrators = new List<string> { "Stephen Fry" },
SeriesName = seriesName ?? "",
SeriesNumber = "1"
};
}
[TestClass]
public class ContainsChapterOnlyTags
{
[TestMethod]
[DataRow("<ch>", false)]
[DataRow("<ch#>", true)]
[DataRow("<id>", false)]
[DataRow("<id><ch#>", true)]
public void Tests(string template, bool expected) => Templates.ContainsChapterOnlyTags(template).Should().Be(expected);
}
[TestClass]
public class ContainsTag
{
[TestMethod]
[DataRow("<ch#>", "ch#", true)]
[DataRow("<id>", "ch#", false)]
[DataRow("<id><ch#>", "ch#", true)]
public void Tests(string template, string tag, bool expected) => Templates.ContainsTag(template, tag).Should().Be(expected);
}
[TestClass]
public class getFileNamingTemplate
{
[TestMethod]
[DataRow(null, "asin", @"C:\", "ext")]
[ExpectedException(typeof(ArgumentNullException))]
public void arg_null_exception(string template, string asin, string dirFullPath, string extension)
=> Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension);
[TestMethod]
[DataRow("", "asin", @"C:\foo\bar", "ext")]
[DataRow(" ", "asin", @"C:\foo\bar", "ext")]
[ExpectedException(typeof(ArgumentException))]
public void arg_exception(string template, string asin, string dirFullPath, string extension)
=> Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension);
[TestMethod]
public void null_extension() => Tests("f.txt", "asin", @"C:\foo\bar", null, @"C:\foo\bar\f.txt");
[TestMethod]
[DataRow("f.txt", "asin", @"C:\foo\bar", "ext", @"C:\foo\bar\f.txt.ext")]
[DataRow("f", "asin", @"C:\foo\bar", "ext", @"C:\foo\bar\f.ext")]
[DataRow("<id>", "asin", @"C:\foo\bar", "ext", @"C:\foo\bar\asin.ext")]
public void Tests(string template, string asin, string dirFullPath, string extension, string expected)
=> Templates.getFileNamingTemplate(GetLibraryBook(asin), template, dirFullPath, extension)
.GetFilePath()
.Should().Be(expected);
[TestMethod]
public void IfSeries_empty()
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series-><-if series>bar", @"C:\a\b", "ext")
.GetFilePath()
.Should().Be(@"C:\a\b\foobar.ext");
[TestMethod]
public void IfSeries_no_series()
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", ""), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
.GetFilePath()
.Should().Be(@"C:\a\b\foobar.ext");
[TestMethod]
public void IfSeries_with_series()
=> Templates.getFileNamingTemplate(GetLibraryBook("asin", "Sherlock Holmes"), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
.GetFilePath()
.Should().Be(@"C:\a\b\foo-Sherlock Holmes-asin-bar.ext");
}
}
namespace Templates_Folder_Tests
{
[TestClass]
public class GetErrors
{
[TestMethod]
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
[TestMethod]
public void empty_is_valid() => valid_tests("");
[TestMethod]
public void whitespace_is_valid() => valid_tests(" ");
[TestMethod]
[DataRow(@"foo")]
[DataRow(@"\foo")]
[DataRow(@"foo\")]
[DataRow(@"\foo\")]
[DataRow(@"foo\bar")]
[DataRow(@"<id>")]
[DataRow(@"<id>\<title>")]
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
[TestMethod]
[DataRow(@"C:\", Templates.ERROR_FULL_PATH_IS_INVALID)]
public void Tests(string template, params string[] expected)
{
var result = Templates.Folder.GetErrors(template);
result.Count().Should().Be(expected.Length);
result.Should().BeEquivalentTo(expected);
}
}
[TestClass]
public class IsValid
{
[TestMethod]
public void null_is_invalid() => Tests(null, false);
[TestMethod]
public void empty_is_valid() => Tests("", true);
[TestMethod]
public void whitespace_is_valid() => Tests(" ", true);
[TestMethod]
[DataRow(@"C:\", false)]
[DataRow(@"foo", true)]
[DataRow(@"\foo", true)]
[DataRow(@"foo\", true)]
[DataRow(@"\foo\", true)]
[DataRow(@"foo\bar", true)]
[DataRow(@"<id>", true)]
[DataRow(@"<id>\<title>", true)]
public void Tests(string template, bool expected) => Templates.Folder.IsValid(template).Should().Be(expected);
}
[TestClass]
public class GetWarnings
{
[TestMethod]
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
[TestMethod]
public void empty_has_warnings() => Tests("", Templates.WARNING_EMPTY, Templates.WARNING_NO_TAGS);
[TestMethod]
public void whitespace_has_warnings() => Tests(" ", Templates.WARNING_WHITE_SPACE, Templates.WARNING_NO_TAGS);
[TestMethod]
[DataRow(@"<id>\foo\bar")]
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
[TestMethod]
[DataRow(@"no tags", Templates.WARNING_NO_TAGS)]
[DataRow("<ch#> <id>", Templates.WARNING_HAS_CHAPTER_TAGS)]
[DataRow("<ch#> chapter tag", Templates.WARNING_NO_TAGS, Templates.WARNING_HAS_CHAPTER_TAGS)]
public void Tests(string template, params string[] expected)
{
var result = Templates.Folder.GetWarnings(template);
result.Count().Should().Be(expected.Length);
result.Should().BeEquivalentTo(expected);
}
}
[TestClass]
public class HasWarnings
{
[TestMethod]
public void null_has_warnings() => Tests(null, true);
[TestMethod]
public void empty_has_warnings() => Tests("", true);
[TestMethod]
public void whitespace_has_warnings() => Tests(" ", true);
[TestMethod]
[DataRow(@"no tags", true)]
[DataRow(@"<id>\foo\bar", false)]
[DataRow("<ch#> <id>", true)]
[DataRow("<ch#> chapter tag", true)]
public void Tests(string template, bool expected) => Templates.Folder.HasWarnings(template).Should().Be(expected);
}
[TestClass]
public class TagCount
{
[TestMethod]
public void null_throws() => Assert.ThrowsException<NullReferenceException>(() => Templates.Folder.TagCount(null));
[TestMethod]
public void empty() => Tests("", 0);
[TestMethod]
public void whitespace() => Tests(" ", 0);
[TestMethod]
[DataRow("no tags", 0)]
[DataRow(@"<id>\foo\bar", 1)]
[DataRow("<id> <id>", 2)]
[DataRow("<id <id> >", 1)]
[DataRow("<id> <title>", 2)]
[DataRow("id> <title incomplete tags", 0)]
[DataRow("<not a real tag>", 0)]
[DataRow("<ch#> non-folder tag", 0)]
[DataRow("<ID> case specific", 0)]
public void Tests(string template, int expected) => Templates.Folder.TagCount(template).Should().Be(expected);
}
}
namespace Templates_File_Tests
{
[TestClass]
public class GetErrors
{
[TestMethod]
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
[TestMethod]
public void empty_is_valid() => valid_tests("");
[TestMethod]
public void whitespace_is_valid() => valid_tests(" ");
[TestMethod]
[DataRow(@"foo")]
[DataRow(@"<id>")]
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
[TestMethod]
[DataRow(@"C:\", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
[DataRow(@"\foo", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
[DataRow(@"/foo", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
[DataRow(@"C:\", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
public void Tests(string template, params string[] expected)
{
var result = Templates.File.GetErrors(template);
result.Count().Should().Be(expected.Length);
result.Should().BeEquivalentTo(expected);
}
}
[TestClass]
public class IsValid
{
[TestMethod]
public void null_is_invalid() => Tests(null, false);
[TestMethod]
public void empty_is_valid() => Tests("", true);
[TestMethod]
public void whitespace_is_valid() => Tests(" ", true);
[TestMethod]
[DataRow(@"C:\", false)]
[DataRow(@"foo", true)]
[DataRow(@"\foo", false)]
[DataRow(@"/foo", false)]
[DataRow(@"<id>", true)]
public void Tests(string template, bool expected) => Templates.File.IsValid(template).Should().Be(expected);
}
// same as Templates.Folder.GetWarnings
//[TestClass]
//public class GetWarnings { }
// same as Templates.Folder.HasWarnings
//[TestClass]
//public class HasWarnings { }
// same as Templates.Folder.TagCount
//[TestClass]
//public class TagCount { }
}
namespace Templates_ChapterFile_Tests
{
// same as Templates.File.GetErrors
//[TestClass]
//public class GetErrors { }
// same as Templates.File.IsValid
//[TestClass]
//public class IsValid { }
[TestClass]
public class GetWarnings
{
[TestMethod]
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
[TestMethod]
public void empty_has_warnings() => Tests("", Templates.WARNING_EMPTY, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
[TestMethod]
public void whitespace_has_warnings() => Tests(" ", Templates.WARNING_WHITE_SPACE, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
[TestMethod]
[DataRow("<ch#>")]
[DataRow("<ch#> <id>")]
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
[TestMethod]
[DataRow(@"no tags", Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
[DataRow(@"<id>\foo\bar", Templates.ERROR_INVALID_FILE_NAME_CHAR, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
public void Tests(string template, params string[] expected)
{
var result = Templates.ChapterFile.GetWarnings(template);
result.Count().Should().Be(expected.Length);
result.Should().BeEquivalentTo(expected);
}
}
[TestClass]
public class HasWarnings
{
[TestMethod]
public void null_has_warnings() => Tests(null, true);
[TestMethod]
public void empty_has_warnings() => Tests("", true);
[TestMethod]
public void whitespace_has_warnings() => Tests(" ", true);
[TestMethod]
[DataRow(@"no tags", true)]
[DataRow(@"<id>\foo\bar", true)]
[DataRow("<ch#> <id>", false)]
[DataRow("<ch#> -- chapter tag", false)]
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", true)]
public void Tests(string template, bool expected) => Templates.ChapterFile.HasWarnings(template).Should().Be(expected);
}
[TestClass]
public class TagCount
{
[TestMethod]
public void null_is_not_recommended() => Assert.ThrowsException<NullReferenceException>(() => Tests(null, -1));
[TestMethod]
public void empty_is_not_recommended() => Tests("", 0);
[TestMethod]
public void whitespace_is_not_recommended() => Tests(" ", 0);
[TestMethod]
[DataRow("no tags", 0)]
[DataRow(@"<id>\foo\bar", 1)]
[DataRow("<id> <id>", 2)]
[DataRow("<id <id> >", 1)]
[DataRow("<id> <title>", 2)]
[DataRow("id> <title incomplete tags", 0)]
[DataRow("<not a real tag>", 0)]
[DataRow("<ch#> non-folder tag", 1)]
[DataRow("<ID> case specific", 0)]
public void Tests(string template, int expected) => Templates.ChapterFile.TagCount(template).Should().Be(expected);
}
[TestClass]
public class GetPortionFilename
{
[TestMethod]
[DataRow("asin", "[<id>] <ch# 0> of <ch count> - <ch title>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\[asin] 06 of 10 - chap.txt")]
[DataRow("asin", "<ch#>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\6.txt")]
public void Tests(string asin, string template, string dir, string ext, int pos, int total, string chapter, string expected)
=> Templates.ChapterFile.GetPortionFilename(GetLibraryBook(asin), template, new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir)
.Should().Be(expected);
}
}

View file

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\LibationSearchEngine\LibationSearchEngine.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Dinah.Core;
using FluentAssertions;
using FluentAssertions.Common;
using LibationSearchEngine;
using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Moq.Protected;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace SearchEngineTests
{
[TestClass]
public class FormatSearchQuery
{
[TestMethod]
// null, empty, whitespace -- *:*
[DataRow(null, "*:*")]
[DataRow("", "*:*")]
[DataRow(" ", "*:*")]
// tag surrounded by spaces
[DataRow("[foo]", "tags:foo")]
[DataRow(" [foo]", " tags:foo")]
[DataRow("[foo] ", "tags:foo ")]
[DataRow(" [foo] ", " tags:foo ")]
[DataRow("-[foo]", "-tags:foo")]
[DataRow(" -[foo]", " -tags:foo")]
[DataRow("-[foo] ", "-tags:foo ")]
[DataRow(" -[foo] ", " -tags:foo ")]
// tag case irrelevant
[DataRow("[FoO]", "tags:FoO")]
// bool keyword surrounded by spaces
[DataRow("israted", "israted:True")]
[DataRow(" israted", " israted:True")]
[DataRow("israted ", "israted:True ")]
[DataRow(" israted ", " israted:True ")]
[DataRow("-israted", "-israted:True")]
[DataRow(" -israted", " -israted:True")]
[DataRow("-israted ", "-israted:True ")]
[DataRow(" -israted ", " -israted:True ")]
// bool keyword. Append :True
[DataRow("israted", "israted:True")]
// bool keyword with [:bool]. Do not add :True
[DataRow("israted:True", "israted:True")]
[DataRow("isRated:false", "israted:false")]
// tag which happens to be a bool keyword >> parse as tag
[DataRow("[israted]", "tags:israted")]
// numbers with "to". TO all caps, numbers [8.2] format
[DataRow("1 to 10", "00000001.00 TO 00000010.00")]
[DataRow("19990101 to 20001231", "19990101.00 TO 20001231.00")]
// field to lowercase
[DataRow("Author:Doyle", "author:Doyle")]
// bool field to lowercase
[DataRow("IsRated", "israted:True")]
[DataRow("-isRATED", "-israted:True")]
public void FormattingTest(string input, string output)
=> SearchEngine.FormatSearchQuery(input).Should().Be(output);
}
}