diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 2a50610cc..2968fc10e 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -1,97 +1,27 @@ - - + - Debug - AnyCPU - {855D6B1D-847B-42DA-BE6A-23683EA89511} - Library - Properties - Discord.Tests - Discord.Net.Tests - v4.6.1 - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest - + Exe + netcoreapp1.1 + $(PackageTargetFallback);portable-net45+win8+wp8+wpa81 - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll - True - - - - - - - - - - - - - - - - - + + + + PreserveNewest + - + + + - - {c6a50d24-cbd3-4e76-852c-4dca60bbd608} - Discord.Net.Net45 - + + + + + + - - - - - False - - - False - - - False - - - False - - - - - - - - \ No newline at end of file + diff --git a/test/Discord.Net.Tests/Net/CacheInfo.cs b/test/Discord.Net.Tests/Net/CacheInfo.cs new file mode 100644 index 000000000..ed2820b8e --- /dev/null +++ b/test/Discord.Net.Tests/Net/CacheInfo.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.Net +{ + internal class CacheInfo + { + [JsonProperty("guild_id")] + public ulong? GuildId { get; set; } + [JsonProperty("version")] + public uint Version { get; set; } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Net/CachedRestClient.cs b/test/Discord.Net.Tests/Net/CachedRestClient.cs new file mode 100644 index 000000000..324510688 --- /dev/null +++ b/test/Discord.Net.Tests/Net/CachedRestClient.cs @@ -0,0 +1,120 @@ +using Akavache; +using Akavache.Sqlite3; +using Discord.Net.Rest; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Splat; +using System.Reactive.Concurrency; + +namespace Discord.Net +{ + internal class CachedRestClient : IRestClient + { + private readonly Dictionary _headers; + private IBlobCache _blobCache; + private string _baseUrl; + private CancellationTokenSource _cancelTokenSource; + private CancellationToken _cancelToken, _parentToken; + private bool _isDisposed; + + public CacheInfo Info { get; private set; } + + public CachedRestClient() + { + _headers = new Dictionary(); + + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationToken.None; + _parentToken = CancellationToken.None; + + Locator.CurrentMutable.Register(() => Scheduler.Default, typeof(IScheduler), "Taskpool"); + Locator.CurrentMutable.Register(() => new FilesystemProvider(), typeof(IFilesystemProvider), null); + Locator.CurrentMutable.Register(() => new HttpMixin(), typeof(IAkavacheHttpMixin), null); + //new Akavache.Sqlite3.Registrations().Register(Locator.CurrentMutable); + _blobCache = new SQLitePersistentBlobCache("cache.db"); + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + _blobCache.Dispose(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + public void SetUrl(string url) + { + _baseUrl = url; + } + public void SetHeader(string key, string value) + { + _headers[key] = value; + } + public void SetCancelToken(CancellationToken cancelToken) + { + _parentToken = cancelToken; + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + } + + public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly) + { + if (method != "GET") + throw new InvalidOperationException("This RestClient only supports GET requests."); + + string uri = Path.Combine(_baseUrl, endpoint); + var bytes = await _blobCache.DownloadUrl(uri, _headers); + return new RestResponse(HttpStatusCode.OK, _headers, new MemoryStream(bytes)); + } + public Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly) + { + throw new InvalidOperationException("This RestClient does not support payloads."); + } + public Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly) + { + throw new InvalidOperationException("This RestClient does not support multipart requests."); + } + + public async Task ClearAsync() + { + await _blobCache.InvalidateAll(); + } + + public async Task LoadInfoAsync(ulong guildId) + { + if (Info != null) + return; + + bool needsReset = false; + try + { + Info = await _blobCache.GetObject("info"); + if (Info.GuildId != guildId) + needsReset = true; + } + catch (KeyNotFoundException) + { + needsReset = true; + } + if (needsReset) + { + Info = new CacheInfo() { GuildId = guildId, Version = 0 }; + await SaveInfoAsync().ConfigureAwait(false); + } + } + public async Task SaveInfoAsync() + { + await ClearAsync().ConfigureAwait(false); //Version changed, invalidate cache + await _blobCache.InsertObject("info", Info); + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Net/FilesystemProvider.cs b/test/Discord.Net.Tests/Net/FilesystemProvider.cs new file mode 100644 index 000000000..efa56ff66 --- /dev/null +++ b/test/Discord.Net.Tests/Net/FilesystemProvider.cs @@ -0,0 +1,124 @@ +using Akavache; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reactive; +using System.Reactive.Concurrency; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Reflection; + +namespace Discord +{ + public class FilesystemProvider : IFilesystemProvider + { + public IObservable OpenFileForReadAsync(string path, IScheduler scheduler) + { + return SafeOpenFileAsync(path, FileMode.Open, FileAccess.Read, FileShare.Read, scheduler); + } + + public IObservable OpenFileForWriteAsync(string path, IScheduler scheduler) + { + return SafeOpenFileAsync(path, FileMode.Create, FileAccess.Write, FileShare.None, scheduler); + } + + public IObservable CreateRecursive(string path) + { + CreateRecursive(new DirectoryInfo(path)); + return Observable.Return(Unit.Default); + } + + public IObservable Delete(string path) + { + return Observable.Start(() => File.Delete(path), Scheduler.Default); + } + + public string GetDefaultRoamingCacheDirectory() + { + throw new NotSupportedException(); + } + + public string GetDefaultSecretCacheDirectory() + { + throw new NotSupportedException(); + } + + public string GetDefaultLocalMachineCacheDirectory() + { + throw new NotSupportedException(); + } + + protected static string GetAssemblyDirectoryName() + { + var assemblyDirectoryName = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + Debug.Assert(assemblyDirectoryName != null, "The directory name of the assembly location is null"); + return assemblyDirectoryName; + } + + private static IObservable SafeOpenFileAsync(string path, FileMode mode, FileAccess access, FileShare share, IScheduler scheduler = null) + { + scheduler = scheduler ?? Scheduler.Default; + var ret = new AsyncSubject(); + + Observable.Start(() => + { + try + { + var createModes = new[] + { + FileMode.Create, + FileMode.CreateNew, + FileMode.OpenOrCreate, + }; + + + // NB: We do this (even though it's incorrect!) because + // throwing lots of 1st chance exceptions makes debugging + // obnoxious, as well as a bug in VS where it detects + // exceptions caught by Observable.Start as Unhandled. + if (!createModes.Contains(mode) && !File.Exists(path)) + { + ret.OnError(new FileNotFoundException()); + return; + } + + Observable.Start(() => new FileStream(path, mode, access, share, 4096, false), scheduler).Cast().Subscribe(ret); + } + catch (Exception ex) + { + ret.OnError(ex); + } + }, scheduler); + + return ret; + } + private static void CreateRecursive(DirectoryInfo info) + { + SplitFullPath(info).Aggregate((parent, dir) => + { + var path = Path.Combine(parent, dir); + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + return path; + }); + } + + private static IEnumerable SplitFullPath(DirectoryInfo info) + { + var root = Path.GetPathRoot(info.FullName); + var components = new List(); + for (var path = info.FullName; path != root && path != null; path = Path.GetDirectoryName(path)) + { + var filename = Path.GetFileName(path); + if (String.IsNullOrEmpty(filename)) + continue; + components.Add(filename); + } + components.Add(root); + components.Reverse(); + return components; + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Net/HttpMixin.cs b/test/Discord.Net.Tests/Net/HttpMixin.cs new file mode 100644 index 000000000..0c7bf2c3d --- /dev/null +++ b/test/Discord.Net.Tests/Net/HttpMixin.cs @@ -0,0 +1,139 @@ +using Akavache; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Text; +using System.Reactive; +using System.Reactive.Threading.Tasks; + +namespace Discord.Net +{ + public class HttpMixin : IAkavacheHttpMixin + { + /// + /// Download data from an HTTP URL and insert the result into the + /// cache. If the data is already in the cache, this returns + /// a cached value. The URL itself is used as the key. + /// + /// The URL to download. + /// An optional Dictionary containing the HTTP + /// request headers. + /// Force a web request to always be issued, skipping the cache. + /// An optional expiration date. + /// The data downloaded from the URL. + public IObservable DownloadUrl(IBlobCache This, string url, IDictionary headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) + { + return This.DownloadUrl(url, url, headers, fetchAlways, absoluteExpiration); + } + + /// + /// Download data from an HTTP URL and insert the result into the + /// cache. If the data is already in the cache, this returns + /// a cached value. An explicit key is provided rather than the URL itself. + /// + /// The key to store with. + /// The URL to download. + /// An optional Dictionary containing the HTTP + /// request headers. + /// Force a web request to always be issued, skipping the cache. + /// An optional expiration date. + /// The data downloaded from the URL. + public IObservable DownloadUrl(IBlobCache This, string key, string url, IDictionary headers = null, bool fetchAlways = false, DateTimeOffset? absoluteExpiration = null) + { + var doFetch = MakeWebRequest(new Uri(url), headers).SelectMany(x => ProcessWebResponse(x, url, absoluteExpiration)); + var fetchAndCache = doFetch.SelectMany(x => This.Insert(key, x, absoluteExpiration).Select(_ => x)); + + var ret = default(IObservable); + if (!fetchAlways) + { + ret = This.Get(key).Catch(fetchAndCache); + } + else + { + ret = fetchAndCache; + } + + var conn = ret.PublishLast(); + conn.Connect(); + return conn; + } + + IObservable ProcessWebResponse(WebResponse wr, string url, DateTimeOffset? absoluteExpiration) + { + var hwr = (HttpWebResponse)wr; + Debug.Assert(hwr != null, "The Web Response is somehow null but shouldn't be."); + if ((int)hwr.StatusCode >= 400) + { + return Observable.Throw(new WebException(hwr.StatusDescription)); + } + + var ms = new MemoryStream(); + using (var responseStream = hwr.GetResponseStream()) + { + Debug.Assert(responseStream != null, "The response stream is somehow null"); + responseStream.CopyTo(ms); + } + + var ret = ms.ToArray(); + return Observable.Return(ret); + } + + static IObservable MakeWebRequest( + Uri uri, + IDictionary headers = null, + string content = null, + int retries = 3, + TimeSpan? timeout = null) + { + IObservable request; + + request = Observable.Defer(() => + { + var hwr = CreateWebRequest(uri, headers); + + if (content == null) + return Observable.FromAsyncPattern(hwr.BeginGetResponse, hwr.EndGetResponse)(); + + var buf = Encoding.UTF8.GetBytes(content); + + // NB: You'd think that BeginGetResponse would never block, + // seeing as how it's asynchronous. You'd be wrong :-/ + var ret = new AsyncSubject(); + Observable.Start(() => + { + Observable.FromAsyncPattern(hwr.BeginGetRequestStream, hwr.EndGetRequestStream)() + .SelectMany(x => WriteAsyncRx(x, buf, 0, buf.Length)) + .SelectMany(_ => Observable.FromAsyncPattern(hwr.BeginGetResponse, hwr.EndGetResponse)()) + .Multicast(ret).Connect(); + }, BlobCache.TaskpoolScheduler); + + return ret; + }); + + return request.Timeout(timeout ?? TimeSpan.FromSeconds(15), BlobCache.TaskpoolScheduler).Retry(retries); + } + + private static WebRequest CreateWebRequest(Uri uri, IDictionary headers) + { + var hwr = WebRequest.Create(uri); + if (headers != null) + { + foreach (var x in headers) + { + hwr.Headers[x.Key] = x.Value; + } + } + return hwr; + } + + private static IObservable WriteAsyncRx(Stream stream, byte[] data, int start, int length) + { + return stream.WriteAsync(data, start, length).ToObservable(); + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Properties/AssemblyInfo.cs b/test/Discord.Net.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 5b1c7b125..000000000 --- a/test/Discord.Net.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Discord.Net.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Discord.Net.Tests")] -[assembly: AssemblyCopyright("Copyright © 2015")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("855d6b1d-847b-42da-be6a-23683ea89511")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/Discord.Net.Tests/TestConfig.cs b/test/Discord.Net.Tests/TestConfig.cs new file mode 100644 index 000000000..21c2dcb8d --- /dev/null +++ b/test/Discord.Net.Tests/TestConfig.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System.IO; + +namespace Discord +{ + internal class TestConfig + { + [JsonProperty("token")] + public string Token { get; private set; } + [JsonProperty("guild_id")] + public ulong GuildId { get; private set; } + + public static TestConfig LoadFile(string path) + { + using (var stream = new FileStream(path, FileMode.Open)) + using (var reader = new StreamReader(stream)) + using (var jsonReader = new JsonTextReader(reader)) + return new JsonSerializer().Deserialize(jsonReader); + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Tests.Channels.cs b/test/Discord.Net.Tests/Tests.Channels.cs new file mode 100644 index 000000000..1e707b329 --- /dev/null +++ b/test/Discord.Net.Tests/Tests.Channels.cs @@ -0,0 +1,142 @@ +using Discord.Rest; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Discord +{ + public partial class Tests + { + internal static async Task Migration_CreateTextChannels(DiscordRestClient client, RestGuild guild) + { + var text1 = await guild.GetDefaultChannelAsync(); + var text2 = await guild.CreateTextChannelAsync("text2"); + var text3 = await guild.CreateTextChannelAsync("text3"); + var text4 = await guild.CreateTextChannelAsync("text4"); + var text5 = await guild.CreateTextChannelAsync("text5"); + + //Modify #general + await text1.ModifyAsync(x => + { + x.Name = "text1"; + x.Position = 1; + x.Topic = "Topic1"; + }); + + await text2.ModifyAsync(x => + { + x.Position = 2; + }); + await text3.ModifyAsync(x => + { + x.Topic = "Topic2"; + }); + await text4.ModifyAsync(x => + { + x.Position = 3; + x.Topic = "Topic2"; + }); + await text5.ModifyAsync(x => + { + }); + + CheckTextChannels(guild, text1, text2, text3, text4, text5); + } + [Fact] + public async Task TestTextChannels() + { + CheckTextChannels(_guild, (await _guild.GetTextChannelsAsync()).ToArray()); + } + private static void CheckTextChannels(RestGuild guild, params RestTextChannel[] textChannels) + { + Assert.Equal(textChannels.Length, 5); + Assert.All(textChannels, x => + { + Assert.NotNull(x); + Assert.NotEqual(x.Id, 0UL); + Assert.True(x.Position >= 0); + }); + + var text1 = textChannels.Where(x => x.Name == "text1").FirstOrDefault(); + var text2 = textChannels.Where(x => x.Name == "text2").FirstOrDefault(); + var text3 = textChannels.Where(x => x.Name == "text3").FirstOrDefault(); + var text4 = textChannels.Where(x => x.Name == "text4").FirstOrDefault(); + var text5 = textChannels.Where(x => x.Name == "text5").FirstOrDefault(); + + Assert.NotNull(text1); + Assert.True(text1.Id == guild.DefaultChannelId); + Assert.Equal(text1.Position, 1); + Assert.Equal(text1.Topic, "Topic1"); + + Assert.NotNull(text2); + Assert.Equal(text2.Position, 2); + Assert.Null(text2.Topic); + + Assert.NotNull(text3); + Assert.Equal(text3.Topic, "Topic2"); + + Assert.NotNull(text4); + Assert.Equal(text4.Position, 3); + Assert.Equal(text4.Topic, "Topic2"); + + Assert.NotNull(text5); + Assert.Null(text5.Topic); + } + + internal static async Task Migration_CreateVoiceChannels(DiscordRestClient client, RestGuild guild) + { + var voice1 = await guild.CreateVoiceChannelAsync("voice1"); + var voice2 = await guild.CreateVoiceChannelAsync("voice2"); + var voice3 = await guild.CreateVoiceChannelAsync("voice3"); + + await voice1.ModifyAsync(x => + { + x.Bitrate = 96000; + x.Position = 1; + x.UserLimit = 8; + }); + await voice2.ModifyAsync(x => + { + x.Bitrate = 64000; + x.Position = 1; + x.UserLimit = null; + }); + await voice3.ModifyAsync(x => + { + x.Bitrate = 8000; + x.Position = 1; + x.UserLimit = 16; + }); + + CheckVoiceChannels(guild, voice1, voice2, voice3); + } + [Fact] + public async Task TestVoiceChannels() + { + CheckVoiceChannels(_guild, (await _guild.GetVoiceChannelsAsync()).ToArray()); + } + private static void CheckVoiceChannels(RestGuild guild, params RestVoiceChannel[] voiceChannels) + { + Assert.Equal(voiceChannels.Length, 3); + + var voice1 = voiceChannels.Where(x => x.Name == "voice1").FirstOrDefault(); + var voice2 = voiceChannels.Where(x => x.Name == "voice2").FirstOrDefault(); + var voice3 = voiceChannels.Where(x => x.Name == "voice3").FirstOrDefault(); + + Assert.NotNull(voice1); + Assert.Equal(voice1.Bitrate, 96000); + Assert.Equal(voice1.Position, 1); + Assert.Equal(voice1.UserLimit, 8); + + Assert.NotNull(voice2); + Assert.Equal(voice2.Bitrate, 64000); + Assert.Equal(voice2.Position, 1); + Assert.Equal(voice2.UserLimit, null); + + Assert.NotNull(voice3); + Assert.Equal(voice3.Bitrate, 8000); + Assert.Equal(voice3.Position, 1); + Assert.Equal(voice3.UserLimit, 16); + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Tests.Migrations.cs b/test/Discord.Net.Tests/Tests.Migrations.cs new file mode 100644 index 000000000..e786329cd --- /dev/null +++ b/test/Discord.Net.Tests/Tests.Migrations.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading.Tasks; +using Discord.Rest; + +namespace Discord +{ + public partial class TestsFixture + { + public const uint MigrationCount = 3; + + public async Task MigrateAsync() + { + DiscordRestClient client = null; + RestGuild guild = null; + + await _cache.LoadInfoAsync(_config.GuildId).ConfigureAwait(false); + while (_cache.Info.Version != MigrationCount) + { + if (client == null) + { + client = new DiscordRestClient(); + await client.LoginAsync(TokenType.Bot, _config.Token, false).ConfigureAwait(false); + guild = await client.GetGuildAsync(_config.GuildId); + } + + uint nextVer = _cache.Info.Version + 1; + try + { + await DoMigrateAsync(client, guild, nextVer).ConfigureAwait(false); + _cache.Info.Version = nextVer; + await _cache.SaveInfoAsync().ConfigureAwait(false); + } + catch + { + await _cache.ClearAsync().ConfigureAwait(false); + throw; + } + } + } + + private static Task DoMigrateAsync(DiscordRestClient client, RestGuild guild, uint toVersion) + { + switch (toVersion) + { + case 1: return Migration_WipeGuild(client, guild); + case 2: return Tests.Migration_CreateTextChannels(client, guild); + case 3: return Tests.Migration_CreateVoiceChannels(client, guild); + default: throw new InvalidOperationException("Unknown migration: " + toVersion); + } + } + + private static async Task Migration_WipeGuild(DiscordRestClient client, RestGuild guild) + { + var textChannels = await guild.GetTextChannelsAsync(); + var voiceChannels = await guild.GetVoiceChannelsAsync(); + var roles = guild.Roles; + + foreach (var channel in textChannels) + { + if (channel.Id != guild.DefaultChannelId) + await channel.DeleteAsync(); + } + foreach (var channel in voiceChannels) + await channel.DeleteAsync(); + foreach (var role in roles) + { + if (role.Id != guild.EveryoneRole.Id) + await role.DeleteAsync(); + } + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/Tests.cs b/test/Discord.Net.Tests/Tests.cs index d8d09cd3d..df156d254 100644 --- a/test/Discord.Net.Tests/Tests.cs +++ b/test/Discord.Net.Tests/Tests.cs @@ -1,494 +1,53 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using Discord.Net; +using Discord.Rest; +using Xunit; -namespace Discord.Tests +namespace Discord { - //TODO: Tests are massively incomplete and out of date, needing a full rewrite - - [TestClass] - public class Tests + public partial class TestsFixture : IDisposable { - private const int EventTimeout = 10000; //Max time in milliseconds to wait for an event response from our test actions - - private static DiscordSocketClient _hostBot, _targetBot, _observerBot; - private static Guild _testGuild; - private static TextChannel _testGuildChannel; - private static Random _random; - private static PublicInvite _testGuildInvite; - - private static TestContext _context; - - private static string _hostToken; - private static string _observerToken; - private static string _targetToken; - - private static string GetRandomText() - { - lock (_random) - return $"test_{_random.Next()}"; - } - - #region Initialization - - [ClassInitialize] - public static void Initialize(TestContext testContext) - { - _context = testContext; - - _hostToken = Environment.GetEnvironmentVariable("discord-unit-host_token"); - _observerToken = Environment.GetEnvironmentVariable("discord-unit-observer_token"); - _targetToken = Environment.GetEnvironmentVariable("discord-unit-target_token"); - } - - [TestMethod] - [Priority(1)] - public async Task TestInitialize() - { - _context.WriteLine("Initializing."); - - _random = new Random(); - - _hostBot = new DiscordSocketClient(_hostToken); - _targetBot = new DiscordSocketClient(_targetToken); - _observerBot = new DiscordSocketClient(_observerToken); - - await _hostBot.Login(); - - await Task.Delay(3000); - - //Cleanup existing Guilds - (await _hostBot.GetGuilds()).Select(x => x.Owner.Id == _hostBot.CurrentUser.Id ? x.Delete() : x.Leave()); - - //Create new Guild and invite the other bots to it - - _testGuild = await _hostBot.CreateGuild("Discord.Net Testing", _hostBot.GetOptimalVoiceRegion()); - - await Task.Delay(1000); - - PublicInvite invite = await _testGuild.CreateInvite(60, 3, false, false); - _testGuildInvite = invite; - - _context.WriteLine($"Host: {_hostBot.CurrentUser.Username} in {(await _hostBot.GetGuilds()).Count()}"); - } - - [TestMethod] - [Priority(2)] - public async Task TestTokenLogin_Ready() - { - AssertEvent( - "READY never received", - async () => await _observerBot.Login(), - x => _observerBot.Connected += x, - x => _observerBot.Connected -= x, - null, - true); - (await _observerBot.GetGuilds()).Select(x => x.Owner.Id == _observerBot.CurrentUser.Id ? x.Delete() : x.Leave()); - await _observerBot.RestClient.Send(new API.Rest.AcceptInviteRequest(_testGuildInvite.Code)); - } - - [TestMethod] - [Priority(2)] - public async Task TestReady() - { - AssertEvent( - "READY never received", - async () => await _targetBot.Login(), - x => _targetBot.Connected += x, - x => _targetBot.Connected -= x, - null, - true); - - (await _targetBot.GetGuilds()).Select(x => x.Owner.Id == _targetBot.CurrentUser.Id ? x.Delete() : x.Leave()); - _testGuildChannel = _testGuild.DefaultChannel; - } - - #endregion + private readonly TestConfig _config; + private readonly CachedRestClient _cache; + internal readonly DiscordRestClient _client; + internal readonly RestGuild _guild; - // Guilds - - #region Guild Tests - - [TestMethod] - [Priority(3)] - public void TestJoinedGuild() + public TestsFixture() { - AssertEvent( - "Never Got JoinedGuild", - async () => await _targetBot.RestClient.Send(new API.Rest.AcceptInviteRequest(_testGuildInvite.Code)), - x => _targetBot.JoinedGuild += x, - x => _targetBot.JoinedGuild -= x); - } - - #endregion - - #region Channel Tests + _cache = new CachedRestClient(); - //Channels - [TestMethod] - public void TestCreateTextChannel() - { - GuildChannel channel = null; - string name = GetRandomText(); - AssertEvent( - "ChannelCreated event never received", - async () => channel = await _testGuild.CreateTextChannel(name), - x => _targetBot.ChannelCreated += x, - x => _targetBot.ChannelCreated -= x, - (s, e) => e.Channel.Id == channel.Id); - - AssertEvent( - "ChannelDestroyed event never received", - async () => await channel.Delete(), - x => _targetBot.ChannelDestroyed += x, - x => _targetBot.ChannelDestroyed -= x, - (s, e) => e.Channel.Id == channel.Id); - } - [TestMethod] - public void TestCreateVoiceChannel() - { - GuildChannel channel = null; - string name = GetRandomText(); - AssertEvent( - "ChannelCreated event never received", - async () => channel = await _testGuild.CreateVoiceChannel(name), - x => _targetBot.ChannelCreated += x, - x => _targetBot.ChannelCreated -= x, - (s, e) => e.Channel.Id == channel.Id); - - AssertEvent( - "ChannelDestroyed event never received", - async () => await channel.Delete(), - x => _targetBot.ChannelDestroyed += x, - x => _targetBot.ChannelDestroyed -= x, - (s, e) => e.Channel.Id == channel.Id); - } - - [TestMethod] - [ExpectedException(typeof(Net.HttpException))] - public async Task TestCreateChannel_NoName() - { - await _testGuild.CreateTextChannel($""); - } - [TestMethod] - public async Task Test_CreateGetChannel() - { - var name = GetRandomText(); - var channel = await _testGuild.CreateTextChannel(name); - var get_channel = _testGuild.GetChannel(channel.Id); - Assert.AreEqual(channel.Id, get_channel.Id, "ID of Channel and GetChannel were not equal."); - } - [TestMethod] - public void TestSendTyping() - { - var channel = _testGuildChannel; - AssertEvent( - "UserUpdated event never fired.", - async () => await channel.TriggerTyping(), - x => _targetBot.UserIsTyping += x, - x => _targetBot.UserIsTyping -= x); - } - [TestMethod] - public void TestEditChannel() - { - var channel = _testGuildChannel; - AssertEvent( - "ChannelUpdated Never Received", - async () => await channel.Modify(x => - { - x.Name = GetRandomText(); - x.Topic = $"topic - {GetRandomText()}"; - x.Position = 26; - }), - x => _targetBot.ChannelUpdated += x, - x => _targetBot.ChannelUpdated -= x); - } - [TestMethod] - public void TestChannelMention() - { - var channel = _testGuildChannel; - Assert.AreEqual($"<#{channel.Id}>", channel.Mention, "Generated channel mention was not the expected channel mention."); - } - [TestMethod] - public void TestChannelUserCount() - { - Assert.AreEqual(3, _testGuildChannel.Users.Count(), "Read an incorrect number of users in a channel"); - } - - #endregion - - #region Message Tests - - //Messages - [TestMethod] - public async Task TestMessageEvents() - { - string name = GetRandomText(); - var channel = await _testGuild.CreateTextChannel(name); - _context.WriteLine($"Channel Name: {channel.Name} / {channel.Guild.Name}"); - string text = GetRandomText(); - Message message = null; - AssertEvent( - "MessageCreated event never received", - async () => message = await channel.SendMessage(text), - x => _targetBot.MessageReceived += x, - x => _targetBot.MessageReceived -= x, - (s, e) => e.Message.Text == text); - - AssertEvent( - "MessageUpdated event never received", - async () => await message.Modify(x => - { - x.Content = text + " updated"; - }), - x => _targetBot.MessageUpdated += x, - x => _targetBot.MessageUpdated -= x, - (s, e) => e.Before.Text == text && e.After.Text == text + " updated"); - - AssertEvent( - "MessageDeleted event never received", - async () => await message.Delete(), - x => _targetBot.MessageDeleted += x, - x => _targetBot.MessageDeleted -= x, - (s, e) => e.Message.Id == message.Id); - } - [TestMethod] - public async Task TestDownloadMessages() - { - string name = GetRandomText(); - var channel = await _testGuild.CreateTextChannel(name); - for (var i = 0; i < 10; i++) await channel.SendMessage(GetRandomText()); - while (channel.Discord.MessageQueue.Count > 0) await Task.Delay(100); - var messages = await channel.GetMessages(10); - Assert.AreEqual(10, messages.Count(), "Expected 10 messages in downloaded array, did not see 10."); - } - [TestMethod] - public async Task TestSendTTSMessage() - { - var channel = await _testGuild.CreateTextChannel(GetRandomText()); - AssertEvent( - "MessageCreated event never fired", - async () => await channel.SendMessage(GetRandomText(), true), - x => _targetBot.MessageReceived += x, - x => _targetBot.MessageReceived -= x, - (s, e) => e.Message.IsTTS); - } - - #endregion - - #region User Tests - - [TestMethod] - public async Task TestUserMentions() - { - var user = (await _targetBot.GetGuild(_testGuild.Id)).CurrentUser; - Assert.AreEqual($"<@{user.Id}>", user.Mention); - } - [TestMethod] - public void TestUserEdit() - { - var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); - AssertEvent( - "UserUpdated never fired", - async () => await user.Modify(x => - { - x.Deaf = true; - x.Mute = true; - }), - x => _targetBot.UserUpdated += x, - x => _targetBot.UserUpdated -= x); - } - [TestMethod] - public void TestEditSelf() - { - throw new NotImplementedException(); - /*var name = RandomText - AssertEvent( - "UserUpdated never fired", - async () => await _targetBot.CurrentUser.Modify(TargetPassword, name), - x => _obGuildBot.UserUpdated += x, - x => _obGuildBot.UserUpdated -= x, - (s, e) => e.After.Username == name);*/ - } - [TestMethod] - public void TestSetStatus() - { - AssertEvent( - "UserUpdated never fired", - async () => await SetStatus(_targetBot, UserStatus.Idle), - x => _observerBot.UserUpdated += x, - x => _observerBot.UserUpdated -= x, - (s, e) => e.After.Status == UserStatus.Idle); - } - private Task SetStatus(DiscordClient _client, UserStatus status) - { - throw new NotImplementedException(); - /*_client.SetStatus(status); - await Task.Delay(50);*/ - } - [TestMethod] - public void TestSetGame() - { - AssertEvent( - "UserUpdated never fired", - async () => await SetGame(_targetBot, "test game"), - x => _observerBot.UserUpdated += x, - x => _observerBot.UserUpdated -= x, - (s, e) => _targetBot.CurrentUser.CurrentGame == "test game"); - - } - private Task SetGame(DiscordClient _client, string game) - { - throw new NotImplementedException(); - //_client.SetGame(game); - //await Task.Delay(5); - } - - #endregion - - #region Permission Tests - - // Permissions - [TestMethod] - public async Task Test_AddGet_PermissionsRule() - { - var channel = await _testGuild.CreateTextChannel(GetRandomText()); - var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); - var perms = new OverwritePermissions(sendMessages: PermValue.Deny); - await channel.UpdatePermissionOverwrite(user, perms); - var resultPerms = channel.GetPermissionOverwrite(user); - Assert.IsNotNull(resultPerms, "Perms retrieved from Guild were null."); - } - [TestMethod] - public async Task Test_AddRemove_PermissionsRule() - { - var channel = await _testGuild.CreateTextChannel(GetRandomText()); - var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); - var perms = new OverwritePermissions(sendMessages: PermValue.Deny); - await channel.UpdatePermissionOverwrite(user, perms); - await channel.RemovePermissionOverwrite(user); - await Task.Delay(200); - Assert.AreEqual(PermValue.Inherit, channel.GetPermissionOverwrite(user)?.SendMessages); - } - [TestMethod] - public async Task Test_Permissions_Event() - { - var channel = await _testGuild.CreateTextChannel(GetRandomText()); - var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); - var perms = new OverwritePermissions(sendMessages: PermValue.Deny); - AssertEvent - ("ChannelUpdatedEvent never fired.", - async () => await channel.UpdatePermissionOverwrite(user, perms), - x => _targetBot.ChannelUpdated += x, - x => _targetBot.ChannelUpdated -= x, - (s, e) => e.Channel == channel && (e.After as GuildChannel).PermissionOverwrites.Count() != (e.Before as GuildChannel).PermissionOverwrites.Count()); - } - [TestMethod] - [ExpectedException(typeof(Net.HttpException))] - public async Task Test_Affect_Permissions_Invalid_Channel() - { - var channel = await _testGuild.CreateTextChannel(GetRandomText()); - var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); - var perms = new OverwritePermissions(sendMessages: PermValue.Deny); - await channel.Delete(); - await channel.UpdatePermissionOverwrite(user, perms); - } - - #endregion - - - [ClassCleanup] - public static async Task Cleanup() - { - WaitMany( - (await _hostBot.GetGuilds()).Select(x => x.Owner.Id == _hostBot.CurrentUser.Id ? x.Delete() : x.Leave()), - (await _targetBot.GetGuilds()).Select(x => x.Owner.Id == _targetBot.CurrentUser.Id ? x.Delete() : x.Leave()), - (await _observerBot.GetGuilds()).Select(x => x.Owner.Id == _observerBot.CurrentUser.Id ? x.Delete() : x.Leave())); - - WaitAll( - _hostBot.Disconnect(), - _targetBot.Disconnect(), - _observerBot.Disconnect()); - } - - #region Helpers - - // Task Helpers - - private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) - { - AssertEvent(msg, action, addEvent, removeEvent, test, true); - } - private static void AssertNoEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) - { - AssertEvent(msg, action, addEvent, removeEvent, test, false); - } - private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test, bool assertTrue) - { - ManualResetEventSlim trigger = new ManualResetEventSlim(false); - bool result = false; - - EventHandler handler = (s, e) => + _config = TestConfig.LoadFile("./config.json"); + var config = new DiscordRestConfig { - if (test != null) + RestClientProvider = url => { - result |= test(s, e); - trigger.Set(); + _cache.SetUrl(url); + return _cache; } - else - result = true; }; + _client = new DiscordRestClient(config); + _client.LoginAsync(TokenType.Bot, _config.Token).Wait(); - addEvent(handler); - var task = action(); - trigger.Wait(EventTimeout); - task.Wait(); - removeEvent(handler); - - Assert.AreEqual(assertTrue, result, msg); + MigrateAsync().Wait(); + _guild = _client.GetGuildAsync(_config.GuildId).Result; } - private static void AssertEvent(string msg, Func action, Action addEvent, Action removeEvent, Func test, bool assertTrue) + public void Dispose() { - ManualResetEventSlim trigger = new ManualResetEventSlim(false); - bool result = false; - - EventHandler handler = (s, e) => - { - if (test != null) - { - result |= test(s); - trigger.Set(); - } - else - result = true; - }; - - addEvent(handler); - var task = action(); - trigger.Wait(EventTimeout); - task.Wait(); - removeEvent(handler); - - Assert.AreEqual(assertTrue, result, msg); + _client.Dispose(); + _cache.Dispose(); } + } - private static void WaitAll(params Task[] tasks) - { - Task.WaitAll(tasks); - } - private static void WaitAll(IEnumerable tasks) - { - Task.WaitAll(tasks.ToArray()); - } - private static void WaitMany(params IEnumerable[] tasks) + public partial class Tests : IClassFixture + { + private DiscordRestClient _client; + private RestGuild _guild; + + public Tests(TestsFixture fixture) { - Task.WaitAll(tasks.Where(x => x != null).SelectMany(x => x).ToArray()); + _client = fixture._client; + _guild = fixture._guild; } - - #endregion } -} +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/config.json.example b/test/Discord.Net.Tests/config.json.example deleted file mode 100644 index 638d65b4d..000000000 --- a/test/Discord.Net.Tests/config.json.example +++ /dev/null @@ -1,14 +0,0 @@ -{ - "user1": { - "email": "user1@example.com", - "password": "password123" - }, - "user2": { - "email": "user2@example.com", - "password": "password456" - }, - "user3": { - "email": "user3@example.com", - "password": "password789" - } -} \ No newline at end of file diff --git a/test/Discord.Net.Tests/packages.config b/test/Discord.Net.Tests/packages.config deleted file mode 100644 index 2abc396bb..000000000 --- a/test/Discord.Net.Tests/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file