diff --git a/DiscordBot/DiscordBot.csproj b/DiscordBot/DiscordBot.csproj index c87f489..f80cc67 100644 --- a/DiscordBot/DiscordBot.csproj +++ b/DiscordBot/DiscordBot.csproj @@ -15,6 +15,7 @@ full + AnyCPU full diff --git a/DiscordBotCore/API/Endpoints/ApiEndpointBase.cs b/DiscordBotCore/API/Endpoints/ApiEndpointBase.cs new file mode 100644 index 0000000..3a372cc --- /dev/null +++ b/DiscordBotCore/API/Endpoints/ApiEndpointBase.cs @@ -0,0 +1,12 @@ +using DiscordBotCore.Online; + +namespace DiscordBotCore.API.Endpoints; + +public class ApiEndpointBase +{ + internal IPluginManager PluginManager { get; } + public ApiEndpointBase(IPluginManager pluginManager) + { + PluginManager = pluginManager; + } +} \ No newline at end of file diff --git a/DiscordBotCore/API/Endpoints/ApiManager.cs b/DiscordBotCore/API/Endpoints/ApiManager.cs index 1d36165..b86d66b 100644 --- a/DiscordBotCore/API/Endpoints/ApiManager.cs +++ b/DiscordBotCore/API/Endpoints/ApiManager.cs @@ -24,7 +24,7 @@ public class ApiManager AddEndpoint(new HomeEndpoint()); AddEndpoint(new PluginListEndpoint()); AddEndpoint(new PluginListInstalledEndpoint()); - AddEndpoint(new PluginInstallEndpoint()); + AddEndpoint(new PluginInstallEndpoint(Application.CurrentApplication.PluginManager)); AddEndpoint(new SettingsChangeEndpoint()); AddEndpoint(new SettingsGetEndpoint()); diff --git a/DiscordBotCore/API/Endpoints/PluginManagement/PluginInstallEndpoint.cs b/DiscordBotCore/API/Endpoints/PluginManagement/PluginInstallEndpoint.cs index 9444f66..f5a42b0 100644 --- a/DiscordBotCore/API/Endpoints/PluginManagement/PluginInstallEndpoint.cs +++ b/DiscordBotCore/API/Endpoints/PluginManagement/PluginInstallEndpoint.cs @@ -2,28 +2,34 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using DiscordBotCore.Interfaces.API; +using DiscordBotCore.Online; using DiscordBotCore.Others; using DiscordBotCore.Plugin; namespace DiscordBotCore.API.Endpoints.PluginManagement; -public class PluginInstallEndpoint : IEndpoint +public class PluginInstallEndpoint : ApiEndpointBase, IEndpoint { + public PluginInstallEndpoint(IPluginManager pluginManager) : base(pluginManager) + { + } + public string Path => "/api/plugin/install"; public EndpointType HttpMethod => EndpointType.Post; + public async Task HandleRequest(string? jsonRequest) { Dictionary jsonDict = await JsonManager.ConvertFromJson>(jsonRequest); string pluginName = jsonDict["pluginName"]; - OnlinePlugin? pluginInfo = await Application.CurrentApplication.PluginManager.GetPluginDataByName(pluginName); + OnlinePlugin? pluginInfo = await PluginManager.GetPluginDataByName(pluginName); if (pluginInfo == null) { return ApiResponse.Fail("Plugin not found."); } - Application.CurrentApplication.PluginManager.InstallPluginNoProgress(pluginInfo); + PluginManager.InstallPluginNoProgress(pluginInfo); return ApiResponse.Ok(); } } diff --git a/DiscordBotCore/Database/SqlDatabase.cs b/DiscordBotCore/Database/SqlDatabase.cs index 84226ce..740635d 100644 --- a/DiscordBotCore/Database/SqlDatabase.cs +++ b/DiscordBotCore/Database/SqlDatabase.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Data; -using System.Data.SQLite; +using Microsoft.Data.Sqlite; using System.IO; using System.Threading.Tasks; @@ -9,7 +9,7 @@ namespace DiscordBotCore.Database; public class SqlDatabase { - private readonly SQLiteConnection _Connection; + private readonly SqliteConnection _Connection; /// /// Initialize a SQL connection by specifying its private path @@ -17,10 +17,9 @@ public class SqlDatabase /// The path to the database (it is starting from ./Data/Resources/) public SqlDatabase(string fileName) { - if (!File.Exists(fileName)) - SQLiteConnection.CreateFile(fileName); - var connectionString = $"URI=file:{fileName}"; - _Connection = new SQLiteConnection(connectionString); + var connectionString = $"Data Source={fileName}"; + _Connection = new SqliteConnection(connectionString); + } @@ -53,7 +52,7 @@ public class SqlDatabase query += ")"; - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); await command.ExecuteNonQueryAsync(); } @@ -77,7 +76,7 @@ public class SqlDatabase query += ")"; - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); command.ExecuteNonQuery(); } @@ -92,7 +91,7 @@ public class SqlDatabase { var query = $"DELETE FROM {tableName} WHERE {KeyName} = '{KeyValue}'"; - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); await command.ExecuteNonQueryAsync(); } @@ -107,7 +106,7 @@ public class SqlDatabase { var query = $"DELETE FROM {tableName} WHERE {KeyName} = '{KeyValue}'"; - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); command.ExecuteNonQuery(); } @@ -341,7 +340,7 @@ public class SqlDatabase { if (!_Connection.State.HasFlag(ConnectionState.Open)) await _Connection.OpenAsync(); - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); var answer = await command.ExecuteNonQueryAsync(); return answer; } @@ -355,7 +354,7 @@ public class SqlDatabase { if (!_Connection.State.HasFlag(ConnectionState.Open)) _Connection.Open(); - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); var r = command.ExecuteNonQuery(); return r; @@ -370,7 +369,7 @@ public class SqlDatabase { if (!_Connection.State.HasFlag(ConnectionState.Open)) await _Connection.OpenAsync(); - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); var reader = await command.ExecuteReaderAsync(); var values = new object[reader.FieldCount]; @@ -394,7 +393,7 @@ public class SqlDatabase if (!_Connection.State.HasFlag(ConnectionState.Open)) await _Connection.OpenAsync(); - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); foreach (var parameter in parameters) { var p = CreateParameter(parameter); @@ -423,7 +422,7 @@ public class SqlDatabase { if (!_Connection.State.HasFlag(ConnectionState.Open)) _Connection.Open(); - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); var reader = command.ExecuteReader(); var values = new object[reader.FieldCount]; @@ -445,7 +444,7 @@ public class SqlDatabase { if (!_Connection.State.HasFlag(ConnectionState.Open)) await _Connection.OpenAsync(); - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); var reader = await command.ExecuteReaderAsync(); var values = new object[reader.FieldCount]; @@ -463,7 +462,7 @@ public class SqlDatabase if (!_Connection.State.HasFlag(ConnectionState.Open)) await _Connection.OpenAsync(); - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); foreach (var parameter in parameters) { var p = CreateParameter(parameter); @@ -493,7 +492,7 @@ public class SqlDatabase { if (!_Connection.State.HasFlag(ConnectionState.Open)) _Connection.Open(); - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); var reader = command.ExecuteReader(); var values = new object[reader.FieldCount]; @@ -516,7 +515,7 @@ public class SqlDatabase { if (!_Connection.State.HasFlag(ConnectionState.Open)) await _Connection.OpenAsync(); - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); var reader = await command.ExecuteReaderAsync(); if (!reader.HasRows) @@ -541,9 +540,10 @@ public class SqlDatabase /// The name of the parameter /// The value of the parameter /// The SQLiteParameter that has the name, value and DBType set according to your inputs - private static SQLiteParameter? CreateParameter(string name, object value) + private static SqliteParameter? CreateParameter(string name, object value) { - var parameter = new SQLiteParameter(name); + var parameter = new SqliteParameter(); + parameter.ParameterName = name; parameter.Value = value; if (value is string) @@ -598,7 +598,7 @@ public class SqlDatabase /// /// The parameter raw inputs. The Key is name and the Value is the value of the parameter /// The SQLiteParameter that has the name, value and DBType set according to your inputs - private static SQLiteParameter? CreateParameter(KeyValuePair parameterValues) + private static SqliteParameter? CreateParameter(KeyValuePair parameterValues) { return CreateParameter(parameterValues.Key, parameterValues.Value); } @@ -614,7 +614,7 @@ public class SqlDatabase if (!_Connection.State.HasFlag(ConnectionState.Open)) await _Connection.OpenAsync(); - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); foreach (var parameter in parameters) { var p = CreateParameter(parameter); @@ -638,7 +638,7 @@ public class SqlDatabase if (!_Connection.State.HasFlag(ConnectionState.Open)) await _Connection.OpenAsync(); - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); foreach (var parameter in parameters) { var p = CreateParameter(parameter); @@ -672,7 +672,7 @@ public class SqlDatabase if (!_Connection.State.HasFlag(ConnectionState.Open)) await _Connection.OpenAsync(); - var command = new SQLiteCommand(query, _Connection); + var command = new SqliteCommand(query, _Connection); foreach (var parameter in parameters) { var p = CreateParameter(parameter); diff --git a/DiscordBotCore/DiscordBotCore.csproj b/DiscordBotCore/DiscordBotCore.csproj index 67ee68c..c9555dd 100644 --- a/DiscordBotCore/DiscordBotCore.csproj +++ b/DiscordBotCore/DiscordBotCore.csproj @@ -4,9 +4,12 @@ enable Library + + AnyCPU + - + diff --git a/DiscordBotCore/Online/Helpers/PluginRepository.cs b/DiscordBotCore/Online/Helpers/PluginRepository.cs index 6b2770c..4101b2c 100644 --- a/DiscordBotCore/Online/Helpers/PluginRepository.cs +++ b/DiscordBotCore/Online/Helpers/PluginRepository.cs @@ -7,11 +7,12 @@ using DiscordBotCore.Plugin; namespace DiscordBotCore.Online.Helpers; -internal class PluginRepository : IPluginRepository +public class PluginRepository : IPluginRepository { private readonly IPluginRepositoryConfiguration _pluginRepositoryConfiguration; - private readonly HttpClient _httpClient; - internal PluginRepository(IPluginRepositoryConfiguration pluginRepositoryConfiguration) + public HttpClient _httpClient; + + public PluginRepository(IPluginRepositoryConfiguration pluginRepositoryConfiguration) { _pluginRepositoryConfiguration = pluginRepositoryConfiguration; _httpClient = new HttpClient(); diff --git a/DiscordBotCore/Online/Helpers/PluginRepositoryConfiguration.cs b/DiscordBotCore/Online/Helpers/PluginRepositoryConfiguration.cs index 4b309dd..3f73858 100644 --- a/DiscordBotCore/Online/Helpers/PluginRepositoryConfiguration.cs +++ b/DiscordBotCore/Online/Helpers/PluginRepositoryConfiguration.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using DiscordBotCore.Interfaces.PluginManagement; namespace DiscordBotCore.Online.Helpers; @@ -10,6 +11,7 @@ public class PluginRepositoryConfiguration : IPluginRepositoryConfiguration public string PluginRepositoryLocation { get; } public string DependenciesRepositoryLocation { get; } + [JsonConstructor] public PluginRepositoryConfiguration(string baseUrl, string pluginRepositoryLocation, string dependenciesRepositoryLocation) { BaseUrl = baseUrl; diff --git a/DiscordBotCore/Online/PluginManager.cs b/DiscordBotCore/Online/PluginManager.cs index 312cf59..ce14969 100644 --- a/DiscordBotCore/Online/PluginManager.cs +++ b/DiscordBotCore/Online/PluginManager.cs @@ -9,7 +9,24 @@ using DiscordBotCore.Plugin; namespace DiscordBotCore.Online; -public sealed class PluginManager +public interface IPluginManager +{ + Task> GetPluginsList(); + Task GetPluginDataByName(string pluginName); + Task AppendPluginToDatabase(PluginInfo pluginData); + Task> GetInstalledPlugins(); + Task IsPluginInstalled(string pluginName); + Task MarkPluginToUninstall(string pluginName); + Task UninstallMarkedPlugins(); + Task GetDependencyLocation(string dependencyName); + Task GetDependencyLocation(string dependencyName, string pluginName); + string GenerateDependencyRelativePath(string pluginName, string dependencyPath); + Task InstallPluginNoProgress(OnlinePlugin plugin); + Task, List>> GatherInstallDataForPlugin(OnlinePlugin plugin); + Task SetEnabledStatus(string pluginName, bool status); +} + +public sealed class PluginManager : IPluginManager { private static readonly string _LibrariesBaseFolder = "Libraries"; private readonly IPluginRepository _PluginRepository; diff --git a/SethCoreTests/ApiTests.cs b/SethCoreTests/ApiTests.cs new file mode 100644 index 0000000..b467d10 --- /dev/null +++ b/SethCoreTests/ApiTests.cs @@ -0,0 +1,45 @@ +using DiscordBotCore; +using DiscordBotCore.API.Endpoints.PluginManagement; +using DiscordBotCore.Online; +using DiscordBotCore.Plugin; +using Moq; + +namespace SethCoreTests; + +public class PluginInstallEndpointTests +{ + private readonly Mock _mockPluginManager; + private readonly PluginInstallEndpoint _endpoint; + + public PluginInstallEndpointTests() + { + _mockPluginManager = new Mock(); + _endpoint = new PluginInstallEndpoint(_mockPluginManager.Object); + } + + [Fact] + public async Task HandleRequest_SuccessfulPluginInstallation_ReturnsOk() + { + var pluginName = "TestPlugin"; + var pluginInfo = new OnlinePlugin(1, pluginName, "Description", "1.0", "Author", "http://link", 1); + _mockPluginManager.Setup(pm => pm.GetPluginDataByName(pluginName)).ReturnsAsync(pluginInfo); + _mockPluginManager.Setup(pm => pm.InstallPluginNoProgress(pluginInfo)).Returns(Task.CompletedTask); + + var jsonRequest = $"{{\"pluginName\":\"{pluginName}\"}}"; + var response = await _endpoint.HandleRequest(jsonRequest); + + Assert.True(response.Success); + } + + [Fact] + public async Task HandleRequest_PluginNotFound_ReturnsFail() + { + var pluginName = "NonExistentPlugin"; + _mockPluginManager.Setup(pm => pm.GetPluginDataByName(pluginName)).ReturnsAsync((OnlinePlugin?)null); + + var jsonRequest = $"{{\"pluginName\":\"{pluginName}\"}}"; + var response = await _endpoint.HandleRequest(jsonRequest); + + Assert.False(response.Success); + } +} \ No newline at end of file diff --git a/SethCoreTests/PluginRepositoryTests.cs b/SethCoreTests/PluginRepositoryTests.cs new file mode 100644 index 0000000..f08417b --- /dev/null +++ b/SethCoreTests/PluginRepositoryTests.cs @@ -0,0 +1,69 @@ +using System.Net; +using DiscordBotCore.Interfaces.PluginManagement; +using DiscordBotCore.Online.Helpers; +using Moq; +using Moq.Protected; + +namespace SethCoreTests; + +public class PluginRepositoryTests +{ + private readonly Mock _mockConfig; + private readonly Mock _mockHttpMessageHandler; + private readonly PluginRepository _pluginRepository; + + public PluginRepositoryTests() + { + _mockConfig = new Mock(); + _mockConfig.SetupGet(c => c.BaseUrl).Returns("http://localhost/"); + _mockConfig.SetupGet(c => c.PluginRepositoryLocation).Returns("api/plugins/"); + _mockConfig.SetupGet(c => c.DependenciesRepositoryLocation).Returns("api/dependencies/"); + + _mockHttpMessageHandler = new Mock(); + var httpClient = new HttpClient(_mockHttpMessageHandler.Object) + { + BaseAddress = new System.Uri(_mockConfig.Object.BaseUrl) + }; + + _pluginRepository = new PluginRepository(_mockConfig.Object) + { + _httpClient = httpClient + }; + } + + [Fact] + public async Task GetAllPlugins_ReturnsListOfPlugins() + { + var pluginsJson = "[{\"PluginId\":1,\"PluginName\":\"TestPlugin\",\"PluginDescription\":\"Description\",\"LatestVersion\":\"1.0\",\"PluginAuthor\":\"Author\",\"PluginLink\":\"http://link\",\"OperatingSystem\":1}]"; + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(pluginsJson) + }); + + var result = await _pluginRepository.GetAllPlugins(); + + Assert.Single(result); + Assert.Equal("TestPlugin", result[0].PluginName); + } + + [Fact] + public async Task GetPluginById_ReturnsPlugin() + { + var pluginJson = "{\"PluginId\":1,\"PluginName\":\"TestPlugin\",\"PluginDescription\":\"Description\",\"LatestVersion\":\"1.0\",\"PluginAuthor\":\"Author\",\"PluginLink\":\"http://link\",\"OperatingSystem\":1}"; + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(pluginJson) + }); + + var result = await _pluginRepository.GetPluginById(1); + + Assert.NotNull(result); + Assert.Equal("TestPlugin", result.PluginName); + } +} \ No newline at end of file diff --git a/SethCoreTests/SethCoreTests.csproj b/SethCoreTests/SethCoreTests.csproj new file mode 100644 index 0000000..a915d1b --- /dev/null +++ b/SethCoreTests/SethCoreTests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/SethDiscordBot.sln b/SethDiscordBot.sln index 0b60efd..0aabc83 100644 --- a/SethDiscordBot.sln +++ b/SethDiscordBot.sln @@ -25,6 +25,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CppCompatibilityModule", "M EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordBotWebUI", "DiscordBotWebUI\DiscordBotWebUI.csproj", "{8683B195-B729-48BB-805A-D44CA98A0BF6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SethTests", "SethTests", "{9D2F471B-89EE-4F17-B1EA-869069A9A3B8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SethCoreTests", "SethCoreTests\SethCoreTests.csproj", "{AB4BD8D1-7384-4669-9D75-3BBECFA0A96E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +67,10 @@ Global {8683B195-B729-48BB-805A-D44CA98A0BF6}.Debug|Any CPU.Build.0 = Debug|Any CPU {8683B195-B729-48BB-805A-D44CA98A0BF6}.Release|Any CPU.ActiveCfg = Release|Any CPU {8683B195-B729-48BB-805A-D44CA98A0BF6}.Release|Any CPU.Build.0 = Release|Any CPU + {AB4BD8D1-7384-4669-9D75-3BBECFA0A96E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB4BD8D1-7384-4669-9D75-3BBECFA0A96E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB4BD8D1-7384-4669-9D75-3BBECFA0A96E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB4BD8D1-7384-4669-9D75-3BBECFA0A96E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -73,6 +81,7 @@ Global {FCE9743F-7EB4-4639-A080-FCDDFCC7D689} = {5CF9AD7B-6BF0-4035-835F-722F989C01E1} {F3C61A47-F758-4145-B496-E3ECCF979D89} = {5CF9AD7B-6BF0-4035-835F-722F989C01E1} {C67908F9-4A55-4DD8-B993-C26C648226F1} = {EA4FA308-7B2C-458E-8485-8747D745DD59} + {AB4BD8D1-7384-4669-9D75-3BBECFA0A96E} = {9D2F471B-89EE-4F17-B1EA-869069A9A3B8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3FB3C5DE-ED21-4D2E-ABDD-3A00EE4A2FFF}