diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3729ff0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index dff3a43..f798420 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ -# Directories -.vs/ -.usr/ +# Ignore user-specific settings, except GitHub workflow files +.* +!.github + +# Project directories should be ignored bin/ obj/ Properties/ -# File extensions -*.json \ No newline at end of file +# Some file extensions should be ignored +*.json +*.db +*.bat diff --git a/Code2Gether-Discord-Bot.ConsoleDiagnostics/Code2Gether-Discord-Bot.ConsoleDiagnostics.csproj b/Code2Gether-Discord-Bot.ConsoleDiagnostics/Code2Gether-Discord-Bot.ConsoleDiagnostics.csproj new file mode 100644 index 0000000..2b85e37 --- /dev/null +++ b/Code2Gether-Discord-Bot.ConsoleDiagnostics/Code2Gether-Discord-Bot.ConsoleDiagnostics.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + Code2Gether_Discord_Bot.ConsoleDiagnostics + + + + + + + diff --git a/Code2Gether-Discord-Bot.ConsoleDiagnostics/Program.cs b/Code2Gether-Discord-Bot.ConsoleDiagnostics/Program.cs new file mode 100644 index 0000000..3932a7a --- /dev/null +++ b/Code2Gether-Discord-Bot.ConsoleDiagnostics/Program.cs @@ -0,0 +1,56 @@ +using Code2Gether_Discord_Bot.Library.Models; +using Code2Gether_Discord_Bot.Library.Models.Repositories; +using System.Linq; +using System.Threading.Tasks; + +namespace Code2Gether_Discord_Bot.ConsoleDiagnostics +{ + class Program + { + static async Task Main(string[] args) + { + var connectionString = "https://localhost:5001/"; + + var memberDal = new MemberDAL(connectionString); + var projectDal = new ProjectDAL(connectionString); + + // Create a new member. + var member = new Member + { + SnowflakeId = 12345 + }; + + await memberDal.CreateAsync(member); + + var memberRetrieved = (await memberDal.ReadAllAsync()).FirstOrDefault(); + + // Create a new project with the member as an author. + var project = new Project + { + Name = "MyProject", + Author = memberRetrieved, + }; + + project.Members.Add(memberRetrieved); + + await projectDal.CreateAsync(project); + + // Create another new member. + var member2 = new Member + { + SnowflakeId = 23456 + }; + + await memberDal.CreateAsync(member2); + + var member2Retrieved = await memberDal.ReadFromSnowflakeAsync(23456); + + // Add new member to project, and update. + var project2 = await projectDal.ReadAsync("MyProject"); + + project2.Members.Add(member2Retrieved); + + await projectDal.UpdateAsync(project2); + } + } +} diff --git a/Code2Gether-Discord-Bot.Library/BusinessLogic/CreateProjectLogic.cs b/Code2Gether-Discord-Bot.Library/BusinessLogic/CreateProjectLogic.cs index 45f72a3..b69e0d8 100644 --- a/Code2Gether-Discord-Bot.Library/BusinessLogic/CreateProjectLogic.cs +++ b/Code2Gether-Discord-Bot.Library/BusinessLogic/CreateProjectLogic.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using Code2Gether_Discord_Bot.Library.Models; -using Code2Gether_Discord_Bot.Library.Models.Repositories.ProjectRepository; using Code2Gether_Discord_Bot.Library.Static; using Discord; using Discord.Commands; @@ -19,39 +18,48 @@ public CreateProjectLogic(ILogger logger, ICommandContext context, IProjectManag _arguments = arguments; } - public override Task ExecuteAsync() + public override async Task ExecuteAsync() { - CreateInactiveProject(out string title, out string description); + var embedContent = await CreateInactiveProjectAsync(); var embed = new EmbedBuilder() .WithColor(Color.Purple) - .WithTitle($"Create Project: {title}") - .WithDescription(description) + .WithTitle($"Create Project: {embedContent.Title}") + .WithDescription(embedContent.Description) .WithAuthor(_context.User) .Build(); - return Task.FromResult(embed); + return embed; } - private void CreateInactiveProject(out string title, out string description) + private async Task CreateInactiveProjectAsync() { - var projectName = _arguments - .Trim() - .Split(' ')[0]; + var embedContent = new EmbedContent(); - if (_projectManager.DoesProjectExist(projectName)) + var projectName = ParseCommandArguments.ParseBy(' ', _arguments)[0]; + + // Check if a project exists before creating one (unique project names) + if (await _projectManager.DoesProjectExistAsync(projectName)) { - title = "Failed"; - description = $"Could not create new inactive project, **{projectName}** already exists!"; + embedContent.Title = "Failed"; + embedContent.Description = $"Could not create new inactive project, **{projectName}** already exists!"; } + + // If no project exists by that name else { - Project newProject = _projectManager.CreateProject(projectName, _context.User); - title = "Success"; - description = $"Successfully created inactive project **{newProject.Name}**!" - + Environment.NewLine - + Environment.NewLine - + $"{newProject}"; + var user = new Member(_context.User); + + // Create a new project + Project newProject = await _projectManager.CreateProjectAsync(projectName, user); + + embedContent.Title = "Success"; + embedContent.Description = $"Successfully created inactive project **{newProject.Name}**!" + + Environment.NewLine + + Environment.NewLine + + newProject; } + + return embedContent; } } } \ No newline at end of file diff --git a/Code2Gether-Discord-Bot.Library/BusinessLogic/IBusinessLogic.cs b/Code2Gether-Discord-Bot.Library/BusinessLogic/IBusinessLogic.cs index 46041f2..490afce 100644 --- a/Code2Gether-Discord-Bot.Library/BusinessLogic/IBusinessLogic.cs +++ b/Code2Gether-Discord-Bot.Library/BusinessLogic/IBusinessLogic.cs @@ -1,5 +1,5 @@ -using System.Threading.Tasks; -using Discord; +using Discord; +using System.Threading.Tasks; namespace Code2Gether_Discord_Bot.Library.BusinessLogic { diff --git a/Code2Gether-Discord-Bot.Library/BusinessLogic/JoinProjectLogic.cs b/Code2Gether-Discord-Bot.Library/BusinessLogic/JoinProjectLogic.cs index edc2733..f9328b7 100644 --- a/Code2Gether-Discord-Bot.Library/BusinessLogic/JoinProjectLogic.cs +++ b/Code2Gether-Discord-Bot.Library/BusinessLogic/JoinProjectLogic.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using Code2Gether_Discord_Bot.Library.Models; +using Code2Gether_Discord_Bot.Library.Static; using Discord; using Discord.Commands; @@ -18,78 +19,120 @@ public JoinProjectLogic(ILogger logger, ICommandContext context, IProjectManager _arguments = arguments; } - public override Task ExecuteAsync() + public override async Task ExecuteAsync() { - JoinProject(out string title, out string description); + var embedContent = await JoinProjectAsync(); var embed = new EmbedBuilder() .WithColor(Color.Purple) - .WithTitle($"Join Project: {title}") - .WithDescription(description) + .WithTitle($"Join Project: {embedContent.Title}") + .WithDescription(embedContent.Description) .WithAuthor(_context.User) .Build(); - return Task.FromResult(embed); + + return embed; } - private void JoinProject(out string title, out string description) + private async Task JoinProjectAsync() { - var projectName = _arguments - .Trim() - .Split(' ')[0]; + var embedContent = new EmbedContent(); + + var projectName = ParseCommandArguments.ParseBy(' ', _arguments)[0]; - _ = _projectManager.DoesProjectExist(projectName, out Project tProject); + var user = new Member(_context.User); + + // Attempt to join the project + var result = await _projectManager.JoinProjectAsync(projectName, user); - if (_projectManager.JoinProject(projectName, _context.User, out Project project)) + // Get the updated project object + var project = await _projectManager.GetProjectAsync(projectName); + + // If joining was successful + if (result) { - title = "Success"; - description = $"{_context.User} has successfully joined project **{projectName}**!" - + Environment.NewLine - + Environment.NewLine - + $"{project}"; + embedContent.Title = "Success"; + embedContent.Description = $"{_context.User} has successfully joined project **{projectName}**!" + + Environment.NewLine + + Environment.NewLine + + project; - if (!tProject.IsActive && project.IsActive) // If project has become active from new user + // If project has become active from new user + if (project.IsActive) TransitionToActiveProject(project); } else { - title = "Failed"; - description = $"{_context.User} failed to join project **{projectName}**!" - + Environment.NewLine - + Environment.NewLine - + $"{project}"; + embedContent.Title = "Failed"; + embedContent.Description = $"{_context.User} failed to join project **{projectName}**!" + + Environment.NewLine + + Environment.NewLine + + project; } + + return embedContent; } private async void TransitionToActiveProject(Project project) { + // Find a category in the guild called "PROJECTS" ulong? projCategoryId = _context.Guild .GetCategoriesAsync().Result .FirstOrDefault(c => c.Name .Contains("PROJECTS"))?.Id; - var channel = await _context.Guild.CreateTextChannelAsync(project.Name, p => + // Create new text channel under that category + bool channelAlreadyCreated = false; + var channels = await _context.Guild.GetChannelsAsync(); + ITextChannel channel = null; + if (channels.Count(c => c.Name.Contains(project.Name)) == 0) + { + channel = await _context.Guild.CreateTextChannelAsync(project.Name, p => + { + if (projCategoryId != null) + p.CategoryId = projCategoryId; + }); + } + else { - if (projCategoryId != null) - p.CategoryId = projCategoryId; - }); + channelAlreadyCreated = true; + } - var role = await _context.Guild - .CreateRoleAsync($"project-{project.Name}", GuildPermissions.None, null, false, true); + // Create new role + var roleName = $"project-{project.Name}"; + var roles = _context.Guild.Roles; + IRole role; + if (roles.Count(r => r.Name.Contains(roleName)) == 0) + { + role = await _context.Guild + .CreateRoleAsync(roleName, GuildPermissions.None, null, false, true); + } + else + { + role = _context.Guild.Roles.FirstOrDefault(r => r.Name.Contains(roleName)); + } - foreach (var member in project.ProjectMembers) + // Give every project member the role + foreach (var member in project.Members) { + // todo: populate DiscordUserId based on the snowflake ID here. + // Causes a null refernece exception if it doesn't. + await _context.Guild - .GetUserAsync(member.Id).Result + .GetUserAsync(member.SnowflakeId).Result .AddRoleAsync(role); } - await channel.SendMessageAsync(embed: new EmbedBuilder() - .WithColor(Color.Purple) - .WithTitle("New Active Project") - .WithDescription($"A new project has gained enough members to become active!" - + Environment.NewLine - + project) - .Build()); + if (!channelAlreadyCreated) + { + // Notify members in new channel + await channel.SendMessageAsync(embed: new EmbedBuilder() + .WithColor(Color.Purple) + .WithTitle("New Active Project") + .WithDescription($"A new project has gained enough members to become active!" + + Environment.NewLine + + project) + .Build()); + } } } } diff --git a/Code2Gether-Discord-Bot.Library/BusinessLogic/ListProjectsLogic.cs b/Code2Gether-Discord-Bot.Library/BusinessLogic/ListProjectsLogic.cs index e1fb608..8913b19 100644 --- a/Code2Gether-Discord-Bot.Library/BusinessLogic/ListProjectsLogic.cs +++ b/Code2Gether-Discord-Bot.Library/BusinessLogic/ListProjectsLogic.cs @@ -1,8 +1,9 @@ using System; +using System.Linq; using System.Text; using System.Threading.Tasks; using Code2Gether_Discord_Bot.Library.Models; -using Code2Gether_Discord_Bot.Library.Models.Repositories.ProjectRepository; +using Code2Gether_Discord_Bot.Library.Models.Repositories; using Discord; using Discord.Commands; @@ -17,38 +18,52 @@ public ListProjectsLogic(ILogger logger, ICommandContext context, IProjectReposi _projectRepository = projectRepository; } - public override Task ExecuteAsync() + public override async Task ExecuteAsync() { - ListProjects(out string title, out string description); + var embedContent = await ListProjectsAsync(); var embed = new EmbedBuilder() .WithColor(Color.Purple) - .WithTitle(title) - .WithDescription(description) + .WithTitle(embedContent.Title) + .WithDescription(embedContent.Description) .WithAuthor(_context.User) .Build(); - return Task.FromResult(embed); + + return embed; } - private void ListProjects(out string title, out string description) + private async Task ListProjectsAsync() { + var embedContent = new EmbedContent(); + var sb = new StringBuilder(); - var projects = _projectRepository.ReadAll(); + + // Read all projects + var projects = await _projectRepository.ReadAllAsync(); foreach (var project in projects) { - sb.Append(project.Value + + // Get Discord User details for project's author + var authorUser = await _context.Guild.GetUserAsync(project.Author.SnowflakeId); + + sb.Append($"Project name: {project.Name}; Author: {authorUser.Username}#{authorUser.Discriminator}" + Environment.NewLine + "Current Members: "); - foreach (var member in project.Value.ProjectMembers) + + foreach (var member in project.Members) { - sb.Append($"{member}; "); + // Get the Discord user for each project's member + var user = await _context.Guild.GetUserAsync(member.SnowflakeId); + sb.Append($"{user}; "); } + sb.Append(Environment.NewLine); } - title = $"List Projects ({projects.Values.Count})"; - description = sb.ToString(); + embedContent.Title = $"List Projects ({projects.Count()})"; // "List Projects (0)" + embedContent.Description = sb.ToString(); // "some-project ; Created by: SomeUser#1234 \r\n Current Members: SomeUser#1234 \r\n " + return embedContent; } } } \ No newline at end of file diff --git a/Code2Gether-Discord-Bot.Library/BusinessLogic/MakeChannelLogic.cs b/Code2Gether-Discord-Bot.Library/BusinessLogic/MakeChannelLogic.cs index 6288911..959ec44 100644 --- a/Code2Gether-Discord-Bot.Library/BusinessLogic/MakeChannelLogic.cs +++ b/Code2Gether-Discord-Bot.Library/BusinessLogic/MakeChannelLogic.cs @@ -17,25 +17,31 @@ public MakeChannelLogic(ILogger logger, ICommandContext context, string newChann _newChannelName = newChannelName; } - public override Task ExecuteAsync() + public override async Task ExecuteAsync() { - if (!MakeNewTextChannel(_newChannelName, out IChannel newChannelObj)) - throw new Exception($"Failed to create channel: {_newChannelName}"); + var newChannel = await _context.Guild.CreateTextChannelAsync(_newChannelName); + + var title = string.Empty; + var description = string.Empty; + + if (newChannel is not null) + { + title = $"Make Channel: {newChannel.Name}"; + description = $"Successfully made new channel: <#{newChannel.Id}>"; + } + else + { + title = $"Make Channel: {newChannel.Name}"; + description = $"Failed to make new channel: {newChannel.Name}"; + } var embed = new EmbedBuilder() .WithColor(Color.Purple) - .WithTitle($"Made Channel: {newChannelObj.Name}") - .WithDescription($"Successfully made new channel: <#{newChannelObj.Id}>") + .WithTitle(title) + .WithDescription(description) .WithAuthor(_context.Message.Author) .Build(); - return Task.FromResult(embed); - - } - - private bool MakeNewTextChannel(string newChannel, out IChannel newChannelObj) - { - newChannelObj = _context.Guild.CreateTextChannelAsync(newChannel).Result; - return newChannel != null; + return embed; } } } diff --git a/Code2Gether-Discord-Bot.Library/Code2Gether-Discord-Bot.Library.csproj b/Code2Gether-Discord-Bot.Library/Code2Gether-Discord-Bot.Library.csproj index a855825..94ad8e3 100644 --- a/Code2Gether-Discord-Bot.Library/Code2Gether-Discord-Bot.Library.csproj +++ b/Code2Gether-Discord-Bot.Library/Code2Gether-Discord-Bot.Library.csproj @@ -7,8 +7,14 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + diff --git a/Code2Gether-Discord-Bot.Library/Models/EmbedContent.cs b/Code2Gether-Discord-Bot.Library/Models/EmbedContent.cs new file mode 100644 index 0000000..e2eeefb --- /dev/null +++ b/Code2Gether-Discord-Bot.Library/Models/EmbedContent.cs @@ -0,0 +1,8 @@ +namespace Code2Gether_Discord_Bot.Library.Models +{ + public class EmbedContent + { + public string Title { get; set; } + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/Code2Gether-Discord-Bot.Library/Models/IDataModel.cs b/Code2Gether-Discord-Bot.Library/Models/IDataModel.cs new file mode 100644 index 0000000..671ca1e --- /dev/null +++ b/Code2Gether-Discord-Bot.Library/Models/IDataModel.cs @@ -0,0 +1,7 @@ +namespace Code2Gether_Discord_Bot.Library.Models +{ + public interface IDataModel + { + public int ID { get; set; } + } +} diff --git a/Code2Gether-Discord-Bot.Library/Models/IProjectManager.cs b/Code2Gether-Discord-Bot.Library/Models/IProjectManager.cs index 054f221..8a71a04 100644 --- a/Code2Gether-Discord-Bot.Library/Models/IProjectManager.cs +++ b/Code2Gether-Discord-Bot.Library/Models/IProjectManager.cs @@ -1,15 +1,13 @@ using System; -using System.Collections.Generic; -using System.Text; -using Discord; +using System.Threading.Tasks; namespace Code2Gether_Discord_Bot.Library.Models { public interface IProjectManager { - bool DoesProjectExist(string projectName); - bool DoesProjectExist(string projectName, out Project project); - Project CreateProject(string projectName, IUser author); - bool JoinProject(string projectName, IUser user, out Project project); + Task DoesProjectExistAsync(string projectName); + Task GetProjectAsync(string projectName); + Task CreateProjectAsync(string projectName, Member author); + Task JoinProjectAsync(string projectName, Member member); } } diff --git a/Code2Gether-Discord-Bot.Library/Models/Member.cs b/Code2Gether-Discord-Bot.Library/Models/Member.cs new file mode 100644 index 0000000..5ffd11b --- /dev/null +++ b/Code2Gether-Discord-Bot.Library/Models/Member.cs @@ -0,0 +1,39 @@ +using Discord; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Code2Gether_Discord_Bot.Library.Models +{ + [Table("MEMBERS")] + public class Member : IDataModel + { + #region Properties + [Column("MEMBER_ID")] + [Key] + public virtual int ID { get; set; } + [Column("MEMBER_SNOWFLAKE_ID")] + public virtual ulong SnowflakeId { get; set; } + [NotMapped] + public virtual List Projects { get; set; } = new List(); + [NotMapped] + [JsonIgnore] + public IUser DiscordUserInfo { get; set; } + #endregion + + #region Constructor + public Member() { } + + public Member(IUser user) : this() + { + DiscordUserInfo = user; + SnowflakeId = DiscordUserInfo.Id; + } + #endregion + + #region Methods + public override string ToString() => $"{SnowflakeId}"; + #endregion + } +} diff --git a/Code2Gether-Discord-Bot.Library/Models/Project.cs b/Code2Gether-Discord-Bot.Library/Models/Project.cs index 0697c32..d362fb1 100644 --- a/Code2Gether-Discord-Bot.Library/Models/Project.cs +++ b/Code2Gether-Discord-Bot.Library/Models/Project.cs @@ -1,32 +1,76 @@ -using Discord; -using SQLite; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; namespace Code2Gether_Discord_Bot.Library.Models { - public class Project + [Table("PROJECTS")] + public class Project : IDataModel { - [PrimaryKey, AutoIncrement] - public int ID { get; } - public string Name { get; } - public IUser Author { get; } - public List ProjectMembers { get; set; } = new List(); - public bool IsActive + #region Fields + private Member author; + #endregion + + #region Properies + [Column("PROJECT_ID")] + [Key] + public virtual int ID { get; set; } + [Column("PROJECT_NAME")] + [Required] + public virtual string Name { get; set; } + [JsonIgnore] + [Required] + [Column("AUTHOR_ID")] + public int AuthorId { get; set; } + [NotMapped] + public virtual Member Author { - get + get => author; + set { - return ProjectMembers.Count > 2; + author = value; + AuthorId = author?.ID ?? 0; } } + [NotMapped] + public virtual List Members { get; set; } = new List(); + [NotMapped] + public bool IsActive => Members.Count() >= 2; + #endregion + + #region Constructors + public Project() { } - public Project(int id, string name, IUser author) + public Project(string name, Member author) : this() { - ID = id; Name = name; Author = author; } + #endregion + + #region Methods + + public override string ToString() + { + var nl = Environment.NewLine; + + var sb = new StringBuilder(); - public override string ToString() => - $"Project Name: {Name}; Is Active: {IsActive}; Created by: {Author}; Members: {ProjectMembers.Count}"; + sb.Append($"Project Name: {Name}{nl}"); + sb.Append($"Author: {Author}{nl}"); + + sb.Append($"Project Members:{nl}"); + foreach (var member in Members) + { + sb.Append($"{member}; "); + } + + return sb.ToString(); + } + #endregion } } diff --git a/Code2Gether-Discord-Bot.Library/Models/ProjectManager.cs b/Code2Gether-Discord-Bot.Library/Models/ProjectManager.cs index 1551711..4bf47ea 100644 --- a/Code2Gether-Discord-Bot.Library/Models/ProjectManager.cs +++ b/Code2Gether-Discord-Bot.Library/Models/ProjectManager.cs @@ -1,63 +1,131 @@ using System; using System.Linq; -using Code2Gether_Discord_Bot.Library.Models.Repositories.ProjectRepository; -using Discord; +using System.Threading.Tasks; +using Code2Gether_Discord_Bot.Library.Models.Repositories; namespace Code2Gether_Discord_Bot.Library.Models { public class ProjectManager : IProjectManager { + private IMemberRepository _memberRepository; private IProjectRepository _projectRepository; - public ProjectManager(IProjectRepository projectRepository) + public ProjectManager(IMemberRepository memberRepository, IProjectRepository projectRepository) { + _memberRepository = memberRepository; _projectRepository = projectRepository; } - public bool DoesProjectExist(string projectName) => - _projectRepository.Read(projectName) != null; + /// + /// Checks if a project exists by a given . + /// + /// Project name to check for + /// true if the project exists. false if the project does not exist. + public async Task DoesProjectExistAsync(string projectName) + { + return await _projectRepository.ReadAsync(projectName) is not null; + } - public bool DoesProjectExist(string projectName, out Project project) + /// + /// Get a project by a given + /// + /// Project name to search for + /// a project if it is found or otherwise returns null + public Task GetProjectAsync(string projectName) { - project = _projectRepository.Read(projectName); - return project != null; + return _projectRepository.ReadAsync(projectName); } - public Project CreateProject(string projectName, IUser author) + /// + /// Creates a new project with a given name and an given author. + /// The author is automatically added to the project. + /// + /// Name for new project + /// Member that is requesting project be made + /// A new project instance + /// or throws an exception if project was not created + /// or author failed to join the project. + public async Task CreateProjectAsync(string projectName, Member author) { - var newId = GetNextId(); - var newProject = new Project(newId, projectName, author); - if (_projectRepository.Create(newProject)) + var retrievedAuthor = await _memberRepository.ReadFromSnowflakeAsync(author.SnowflakeId); + + // If author doesn't exist + if (retrievedAuthor == null) { - JoinProject(projectName, author, out newProject); - return newProject; + // Create author member + if (await _memberRepository.CreateAsync(author)) + author = await _memberRepository.ReadFromSnowflakeAsync(author.SnowflakeId); // Update author + else + throw new Exception($"Failed to create new member: {author}!"); + } + else // Author exists + { + // Update local object for author + author = retrievedAuthor; } - throw new Exception($"Failed to create new project: {newProject}!"); - } - public bool JoinProject(string projectName, IUser user, out Project project) - { - project = _projectRepository.Read(projectName); + var newProject = new Project(projectName, author); - if (project == null) return false; // Project must exist - if (project.ProjectMembers.Contains(user)) return false; // User may not already be in project + if (!await _projectRepository.CreateAsync(newProject)) + throw new Exception($"Failed to create new project: {projectName}!"); + // Retrieve project to add member to. + newProject = await _projectRepository.ReadAsync(newProject.Name); + await _projectRepository.AddMemberAsync(newProject, author); - project.ProjectMembers.Add(user); + // Retrieve project with added member. + newProject = await _projectRepository.ReadAsync(newProject.Name); + + return newProject; - return _projectRepository.Update(project); } - private int GetNextId() + /// + /// Attempt to join a project by a given name with a given member. + /// + /// Project name to join + /// Member to join a project + /// true if update was successful and the new member is apart of the project + /// or false if the user is already in the project. + public async Task JoinProjectAsync(string projectName, Member member) { - int i = 0; + var retrievedMember = await _memberRepository.ReadFromSnowflakeAsync(member.SnowflakeId); + + // If member isn't in db + if (retrievedMember == null) + { + // Create member + if (!await _memberRepository.CreateAsync(member)) + throw new Exception($"Failed to add member: {member}"); - try + member = await _memberRepository.ReadFromSnowflakeAsync(member.SnowflakeId); + } + else { - i = _projectRepository.ReadAll().Keys.Max() + 1; + member = retrievedMember; } - catch (InvalidOperationException) { } // No projects available yet - return i; + // Get project matching projectName + var project = await _projectRepository.ReadAsync(projectName); + + // If the given member by SnowflakeId does not exist in the project as a member + // Add the member to the project. + if (project != null && !project.Members.Any(m => m.SnowflakeId == member.SnowflakeId)) + { + if (!await _projectRepository.AddMemberAsync(project, member)) + throw new Exception($"Failed to add member: {member}"); + } + else + { + return false; // Else they are already in the project or project doesn't exist + } + + // Get the updated project with new member. + project = await _projectRepository.ReadAsync(projectName); + + // Check if project join is successful. + return project.Members + .Select(x => x.SnowflakeId) + .Contains(member.SnowflakeId); } } } diff --git a/Code2Gether-Discord-Bot.Library/Models/ProjectMember.cs b/Code2Gether-Discord-Bot.Library/Models/ProjectMember.cs new file mode 100644 index 0000000..967480e --- /dev/null +++ b/Code2Gether-Discord-Bot.Library/Models/ProjectMember.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace Code2Gether_Discord_Bot.Library.Models +{ + [Table("PROJECT_MEMBER")] + public class ProjectMember + { + #region Fields + private Project project; + private Member member; + #endregion + + #region Properties + [JsonIgnore] + [Column("PROJECT_ID")] + public int ProjectID { get; set; } + [JsonIgnore] + [Column("MEMBER_ID")] + public int MemberID { get; set; } + #endregion + } +} diff --git a/Code2Gether-Discord-Bot.Library/Models/Repositories/IDataRepository.cs b/Code2Gether-Discord-Bot.Library/Models/Repositories/IDataRepository.cs new file mode 100644 index 0000000..63c575c --- /dev/null +++ b/Code2Gether-Discord-Bot.Library/Models/Repositories/IDataRepository.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Code2Gether_Discord_Bot.Library.Models.Repositories +{ + public interface IDataRepository + { + Task CreateAsync(TModel newModel); + Task ReadAsync(int id); + Task> ReadAllAsync(); + Task UpdateAsync(TModel existingModel); + Task DeleteAsync(int id); + } +} \ No newline at end of file diff --git a/Code2Gether-Discord-Bot.Library/Models/Repositories/IMemberRepository.cs b/Code2Gether-Discord-Bot.Library/Models/Repositories/IMemberRepository.cs new file mode 100644 index 0000000..198b5cf --- /dev/null +++ b/Code2Gether-Discord-Bot.Library/Models/Repositories/IMemberRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Code2Gether_Discord_Bot.Library.Models.Repositories +{ + public interface IMemberRepository : IDataRepository + { + public Task ReadFromSnowflakeAsync(ulong snowflakeId); + } +} diff --git a/Code2Gether-Discord-Bot.Library/Models/Repositories/IProjectRepository.cs b/Code2Gether-Discord-Bot.Library/Models/Repositories/IProjectRepository.cs new file mode 100644 index 0000000..b3abd24 --- /dev/null +++ b/Code2Gether-Discord-Bot.Library/Models/Repositories/IProjectRepository.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace Code2Gether_Discord_Bot.Library.Models.Repositories +{ + public interface IProjectRepository : IDataRepository + { + Task ReadAsync(string projectName); + Task AddMemberAsync(Project project, Member member); + Task RemoveMemberAsync(Project project, Member member); + } +} diff --git a/Code2Gether-Discord-Bot.Library/Models/Repositories/MemberDAL.cs b/Code2Gether-Discord-Bot.Library/Models/Repositories/MemberDAL.cs new file mode 100644 index 0000000..5f02b7e --- /dev/null +++ b/Code2Gether-Discord-Bot.Library/Models/Repositories/MemberDAL.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Newtonsoft.Json; +using RestSharp; + +namespace Code2Gether_Discord_Bot.Library.Models.Repositories +{ + public class MemberDAL : WebApiDALBase, IMemberRepository + { + protected override string _tableRoute => "Members"; + + public MemberDAL(string connectionString) : base(connectionString) { } + + protected override string SerializeModel(Member memberToSerialize) + { + var newMember = new Member { SnowflakeId = memberToSerialize.SnowflakeId }; + + var json = JsonConvert.SerializeObject(newMember); + + return json; + } + + /// + /// Finds a member with the snowflake ID. + /// + /// Snowflake ID of member to find. + /// Member with snowflake ID. Null if no record found. + public async Task ReadFromSnowflakeAsync(ulong snowflakeId) + { + var request = new RestRequest($"{_tableRoute}/snowflakeID={snowflakeId}"); + + var result = await GetClient().ExecuteGetAsync(request); + + return result.IsSuccessful ? result.Data : null; + } + } +} diff --git a/Code2Gether-Discord-Bot.Library/Models/Repositories/ProjectDAL.cs b/Code2Gether-Discord-Bot.Library/Models/Repositories/ProjectDAL.cs new file mode 100644 index 0000000..a0c2eef --- /dev/null +++ b/Code2Gether-Discord-Bot.Library/Models/Repositories/ProjectDAL.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; +using RestSharp; + +namespace Code2Gether_Discord_Bot.Library.Models.Repositories +{ + public class ProjectDAL : WebApiDALBase, IProjectRepository + { + protected override string _tableRoute => "Projects"; + + public ProjectDAL(string connectionString) : base(connectionString) { } + + /// + /// Retrieves project based on project name. + /// + /// Name of project to retrieve. + /// Data for project to retrieve. Null if not found. + public async Task ReadAsync(string projectName) + { + var request = new RestRequest($"{_tableRoute}/projectName={projectName}"); + + var result = await GetClient().ExecuteGetAsync(request); + + return result.IsSuccessful ? result.Data : null; + } + + /// + /// Adds a member to a project. + /// + /// Project of member to add. + /// Member to add to project. + /// True if add is successful. + public async Task AddMemberAsync(Project project, Member member) + { + var request = new RestRequest($"{_tableRoute}/projectId={project.ID};memberId={member.ID}"); + + var result = await GetClient().ExecutePostAsync(request); + + return result.IsSuccessful; + } + + /// + /// Adds a member to a project. + /// + /// Project of member to delete. + /// Member to delete from project. + /// True if delete is successful. + public async Task RemoveMemberAsync(Project project, Member member) + { + var request = new RestRequest($"{_tableRoute}/projectId={project.ID};memberId={member.ID}", Method.DELETE); + + var result = await GetClient().ExecuteAsync(request); + + return result.IsSuccessful; + } + } +} diff --git a/Code2Gether-Discord-Bot.Library/Models/Repositories/ProjectRepository/IProjectRepository.cs b/Code2Gether-Discord-Bot.Library/Models/Repositories/ProjectRepository/IProjectRepository.cs deleted file mode 100644 index 58975a9..0000000 --- a/Code2Gether-Discord-Bot.Library/Models/Repositories/ProjectRepository/IProjectRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace Code2Gether_Discord_Bot.Library.Models.Repositories.ProjectRepository -{ - public interface IProjectRepository - { - bool Create(Project newProject); - Project Read(int id); - Project Read(string projectName); - IDictionary ReadAll(); - bool Update(Project newProject); - bool Delete(int id); - } -} \ No newline at end of file diff --git a/Code2Gether-Discord-Bot.Library/Models/Repositories/ProjectRepository/ProjectDAL.cs b/Code2Gether-Discord-Bot.Library/Models/Repositories/ProjectRepository/ProjectDAL.cs deleted file mode 100644 index 715542c..0000000 --- a/Code2Gether-Discord-Bot.Library/Models/Repositories/ProjectRepository/ProjectDAL.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Code2Gether_Discord_Bot.Library.Models.Repositories.ProjectRepository -{ - /// - /// TODO: replace with actual database access implementation - /// - public class ProjectDAL : IProjectRepository - { - private ConcurrentDictionary _projects = new ConcurrentDictionary(); - - public bool Create(Project newProject) - { - return _projects.TryAdd(newProject.ID, newProject); - } - - public Project Read(int id) - { - if (_projects.TryGetValue(id, out Project project)) - return project; - throw new Exception($"Failed to read project with ID {id}"); - } - - public Project Read(string projectName) - { - return ReadAll().Values.FirstOrDefault(p => p.Name == projectName); - } - - public IDictionary ReadAll() - { - return _projects; - } - - public bool Update(Project newProject) - { - return _projects.TryUpdate(newProject.ID, newProject, Read(newProject.ID)); - } - - public bool Delete(int id) - { - return _projects.TryRemove(id, out _); - } - } -} diff --git a/Code2Gether-Discord-Bot.Library/Models/Repositories/WebApiDALBase.cs b/Code2Gether-Discord-Bot.Library/Models/Repositories/WebApiDALBase.cs new file mode 100644 index 0000000..db9d443 --- /dev/null +++ b/Code2Gether-Discord-Bot.Library/Models/Repositories/WebApiDALBase.cs @@ -0,0 +1,123 @@ +using RestSharp; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Code2Gether_Discord_Bot.Library.Models.Repositories +{ + public abstract class WebApiDALBase : IDataRepository + where TDataModel : class, IDataModel + { + protected string _connectionString; + protected abstract string _tableRoute { get; } + + protected WebApiDALBase(string connectionString) + { + _connectionString = connectionString; + } + + /// + /// Creates a new record of the input model. + /// + /// Model to add to DB. + /// True if successful. + public async Task CreateAsync(TDataModel newModel) + { + var client = GetClient(); + + var request = new RestRequest(_tableRoute); + + var jsonBody = SerializeModel(newModel); + + request.AddJsonBody(jsonBody, "application/json"); + + var result = await client.ExecutePostAsync(request); + + return result.IsSuccessful; + } + + protected virtual string SerializeModel(TDataModel modelToSerialize) + { + var settings = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; + return JsonConvert.SerializeObject(modelToSerialize, settings); + } + + /// + /// Get all records in the DB. + /// + /// All records in the DB. + public async Task> ReadAllAsync() + { + var client = GetClient(); + + var request = new RestRequest(_tableRoute); + + var result = await client.ExecuteGetAsync>(request); + + return result.IsSuccessful ? result.Data : null; + } + + /// + /// Gets the record for the input primary key. + /// + /// Primary key of record to retrieve. + /// Record data if retrieved, null if not. + public async Task ReadAsync(int id) + { + var client = GetClient(); + + var request = new RestRequest($"{_tableRoute}/{id}"); + + var result = await client.ExecuteGetAsync(request); + + return result.IsSuccessful ? result.Data : null; + } + + /// + /// Deletes the record with the input primary key. + /// + /// Primary key of record to delete. + /// True if delete is successful. + public async Task DeleteAsync(int id) + { + var client = GetClient(); + + var request = new RestRequest($"{_tableRoute}/{id}", Method.DELETE); + + var result = await client.ExecuteAsync(request); + + return result.IsSuccessful; + } + + /// + /// Updates the selected record. + /// + /// Record to update. + /// False if the record doesn't exist, or if the update failed. + public async Task UpdateAsync(TDataModel modelToReplace) + { + var modelToUpdate = await ReadAsync(modelToReplace.ID); + + if (modelToUpdate == null) + return false; + + var client = GetClient(); + + var request = new RestRequest($"{_tableRoute}/{modelToReplace.ID}", Method.PUT); + + var jsonBody = SerializeModel(modelToReplace); // Had this set to the old model. Whoops! + + request.AddJsonBody(jsonBody); + + var result = await client.ExecuteAsync(request); + + return result.IsSuccessful; + } + + protected RestClient GetClient() + { + // Put any authentication protocols here? + return new RestClient(_connectionString); + } + } +} diff --git a/Code2Gether-Discord-Bot.Library/Static/ParseCommandArguments.cs b/Code2Gether-Discord-Bot.Library/Static/ParseCommandArguments.cs new file mode 100644 index 0000000..27f178f --- /dev/null +++ b/Code2Gether-Discord-Bot.Library/Static/ParseCommandArguments.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Code2Gether_Discord_Bot.Library.Static +{ + public static class ParseCommandArguments + { + public static string[] ParseBy (char deliminator, string arguments) => + arguments.Trim().Split(deliminator); + } +} diff --git a/Code2Gether-Discord-Bot.Library/Static/RepositoryFactory.cs b/Code2Gether-Discord-Bot.Library/Static/RepositoryFactory.cs deleted file mode 100644 index 9169d4a..0000000 --- a/Code2Gether-Discord-Bot.Library/Static/RepositoryFactory.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Reflection.Metadata.Ecma335; -using Code2Gether_Discord_Bot.Library.Models.Repositories.ProjectRepository; - -namespace Code2Gether_Discord_Bot.Library.Static -{ - public static class RepositoryFactory - { - // TODO: replace with actual database, and remove this - private static IProjectRepository instance = GetProjectRepository(); - - public static IProjectRepository GetProjectRepository() => - instance != null ? instance : new ProjectDAL(); - - //public static IProjectRepository GetProjectRepository() => - // new ProjectDAL(); - } -} diff --git a/Code2Gether-Discord-Bot.Tests/Code2Gether-Discord-Bot.Tests.csproj b/Code2Gether-Discord-Bot.Tests/Code2Gether-Discord-Bot.Tests.csproj index 363520a..ae875ea 100644 --- a/Code2Gether-Discord-Bot.Tests/Code2Gether-Discord-Bot.Tests.csproj +++ b/Code2Gether-Discord-Bot.Tests/Code2Gether-Discord-Bot.Tests.csproj @@ -9,13 +9,12 @@ - - + + - diff --git a/Code2Gether-Discord-Bot.Tests/CreateProjectTests.cs b/Code2Gether-Discord-Bot.Tests/CreateProjectTests.cs index 2ab48fb..a214f95 100644 --- a/Code2Gether-Discord-Bot.Tests/CreateProjectTests.cs +++ b/Code2Gether-Discord-Bot.Tests/CreateProjectTests.cs @@ -1,13 +1,15 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Threading.Tasks; using Code2Gether_Discord_Bot.Library.BusinessLogic; using Code2Gether_Discord_Bot.Library.Models; -using Code2Gether_Discord_Bot.Library.Models.Repositories.ProjectRepository; -using Code2Gether_Discord_Bot.Static; +using Code2Gether_Discord_Bot.Library.Models.Repositories; using Code2Gether_Discord_Bot.Tests.Fakes; -using Discord; +using Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord; +using Code2Gether_Discord_Bot.Tests.Fakes.FakeRepositories; using NUnit.Framework; namespace Code2Gether_Discord_Bot.Tests @@ -15,130 +17,89 @@ namespace Code2Gether_Discord_Bot.Tests internal class CreateProjectTests { private IBusinessLogic _logic; - private IProjectRepository _repo; - private IUser _user; + private IMemberRepository _memberRepository; + private IProjectRepository _projectRepository; [SetUp] public void Setup() { - _user = new FakeUser() + var fakeUser = new FakeDiscordUser() { Username = "UnitTest", DiscriminatorValue = 1234, Id = 123456789123456789 }; - _repo = new FakeProjectRepository() - { - Projects = new Dictionary() - }; - } - - [Test] - public void InstantiationTest() => - Assert.IsTrue(_logic != null); - - /// - /// Executing should add a new one from a previously empty project list. - /// - [Test] - public async Task AddFromEmptyExecutionTest() - { - #region Arrange + var user = new Member(fakeUser); var client = new FakeDiscordClient() { FakeApplication = new FakeApplication() { - Owner = _user + Owner = fakeUser } }; - var message = new FakeUserMessage() + var guild = new FakeGuild() { - Author = _user + }; - _logic = new CreateProjectLogic( - UtilityFactory.GetLogger(GetType()), - new FakeCommandContext() - { - Channel = new FakeMessageChannel(), - Client = client, - Guild = new FakeGuild(), - Message = message, - User = _user - }, - new ProjectManager(_repo), - "UnitTestProject" - ); + var messageChannel = new FakeMessageChannel() + { - #endregion + }; - #region Act + var message = new FakeUserMessage() + { + Author = fakeUser + }; - await _logic.ExecuteAsync(); + _memberRepository = new FakeMemberRepository(); + _projectRepository = new FakeProjectRepository(); - #endregion + _logic = new CreateProjectLogic(new Logger(GetType()), new FakeCommandContext() + { + Channel = messageChannel, + Client = client, + Guild = guild, + Message = message, + User = fakeUser + }, new ProjectManager(_memberRepository, _projectRepository), "unittest"); + } - #region Assert + [Test] + public void InstantiationTest() => + Assert.IsNotNull(_logic); - Assert.IsTrue(_repo.ReadAll().Count == 1); + /// + /// Passes: If when creating a new project, project repo contains an additional project + /// Fails: If when creating a new project, project repo does not change + /// + [Test] + public async Task SingleExecutionTest() + { + var initialProjects = await _projectRepository.ReadAllAsync(); + await _logic.ExecuteAsync(); + var finalProjects = await _projectRepository.ReadAllAsync(); - #endregion + Assert.AreEqual(initialProjects.Count() + 1, finalProjects.Count()); } /// - /// Since a project already exists in the repository. Executing again should add a new one. + /// Passes: If when creating a duplicate project, total projects do not change during second execution + /// Fails: If when creating a duplicate project, an two additional projects now exist in project repo /// [Test] - public async Task AddAnotherProjectExecutionTest() + public async Task DoubleExecutionTest() { - #region Arrange - - var client = new FakeDiscordClient() - { - FakeApplication = new FakeApplication() - { - Owner = _user - } - }; - - var message = new FakeUserMessage() - { - Author = _user - }; - - var projectManager = new ProjectManager(_repo); - projectManager.CreateProject("UnitTestProject1", _user); - - _logic = new CreateProjectLogic( - UtilityFactory.GetLogger(GetType()), - new FakeCommandContext() - { - Channel = new FakeMessageChannel(), - Client = client, - Guild = new FakeGuild(), - Message = message, - User = _user - }, - projectManager, - "UnitTestProject2" - ); - - #endregion - - #region Act - + await _logic.ExecuteAsync(); + var intermediaryProjects = await _projectRepository.ReadAllAsync(); await _logic.ExecuteAsync(); - #endregion - - #region Assert - - Assert.IsTrue(_repo.ReadAll().Count == 2); + var finalProjects = await _projectRepository.ReadAllAsync(); - #endregion + Assert.AreEqual(intermediaryProjects.Count(), finalProjects.Count()); } } } diff --git a/Code2Gether-Discord-Bot.Tests/ExcuseGeneratorTest.cs b/Code2Gether-Discord-Bot.Tests/ExcuseGeneratorTest.cs index e09ef91..8c80ba8 100644 --- a/Code2Gether-Discord-Bot.Tests/ExcuseGeneratorTest.cs +++ b/Code2Gether-Discord-Bot.Tests/ExcuseGeneratorTest.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; using Code2Gether_Discord_Bot.Library.BusinessLogic; using Code2Gether_Discord_Bot.Library.Models; -using Code2Gether_Discord_Bot.Static; using Code2Gether_Discord_Bot.Tests.Fakes; +using Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord; using NUnit.Framework; namespace Code2Gether_Discord_Bot.Tests @@ -14,7 +14,7 @@ internal class ExcuseGeneratorTest [SetUp] public void Setup() { - var user = new FakeUser() + var user = new FakeDiscordUser() { Username = "UnitTest", DiscriminatorValue = 1234, @@ -44,7 +44,7 @@ public void Setup() Author = user }; - _logic = BusinessLogicFactory.ExcuseGeneratorLogic(GetType(), new FakeCommandContext() + _logic = new ExcuseGeneratorLogic(new Logger(GetType()), new FakeCommandContext() { Client = client, Guild = guild, @@ -57,7 +57,7 @@ public void Setup() [Test] public void InstantiationTest() => - Assert.IsTrue(_logic != null); + Assert.IsNotNull(_logic); [Test] public async Task EmbedAuthorHasValueTest() diff --git a/Code2Gether-Discord-Bot.Tests/Fakes/FakeApplication.cs b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeApplication.cs similarity index 100% rename from Code2Gether-Discord-Bot.Tests/Fakes/FakeApplication.cs rename to Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeApplication.cs diff --git a/Code2Gether-Discord-Bot.Tests/Fakes/FakeCommandContext.cs b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeCommandContext.cs similarity index 100% rename from Code2Gether-Discord-Bot.Tests/Fakes/FakeCommandContext.cs rename to Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeCommandContext.cs diff --git a/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscordClient.cs b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeDiscordClient.cs similarity index 97% rename from Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscordClient.cs rename to Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeDiscordClient.cs index 85ee32b..94423d7 100644 --- a/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscordClient.cs +++ b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeDiscordClient.cs @@ -1,11 +1,10 @@ -using Discord; -using System; +using System; using System.Collections.Generic; using System.IO; -using System.Text; using System.Threading.Tasks; +using Discord; -namespace Code2Gether_Discord_Bot.Tests.Fakes +namespace Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord { internal class FakeDiscordClient : IDiscordClient { diff --git a/Code2Gether-Discord-Bot.Tests/Fakes/FakeUser.cs b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeDiscordUser.cs similarity index 86% rename from Code2Gether-Discord-Bot.Tests/Fakes/FakeUser.cs rename to Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeDiscordUser.cs index 4e6b885..a5f6980 100644 --- a/Code2Gether-Discord-Bot.Tests/Fakes/FakeUser.cs +++ b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeDiscordUser.cs @@ -1,13 +1,11 @@ -using Discord; -using System; -using System.Collections.Generic; +using System; using System.Collections.Immutable; -using System.Text; using System.Threading.Tasks; +using Discord; -namespace Code2Gether_Discord_Bot.Tests.Fakes +namespace Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord { - internal class FakeUser : IUser + internal class FakeDiscordUser : IUser { public string AvatarId { get; set; } diff --git a/Code2Gether-Discord-Bot.Tests/Fakes/FakeGuild.cs b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeGuild.cs similarity index 96% rename from Code2Gether-Discord-Bot.Tests/Fakes/FakeGuild.cs rename to Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeGuild.cs index 0714df7..13f8a76 100644 --- a/Code2Gether-Discord-Bot.Tests/Fakes/FakeGuild.cs +++ b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeGuild.cs @@ -1,13 +1,11 @@ -using Discord; -using Discord.Audio; -using System; +using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Globalization; -using System.Text; using System.Threading.Tasks; +using Discord; +using Discord.Audio; -namespace Code2Gether_Discord_Bot.Tests.Fakes +namespace Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord { internal class FakeGuild : IGuild { @@ -129,11 +127,6 @@ public Task CreateRoleAsync(string name, GuildPermissions? permissions = public Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null) { throw new NotImplementedException(); - //var rng = new Random(DateTime.Now.Millisecond); - //return Task.Run(() => - //{ - // _guildChannels.TryAdd((ulong) rng.Next(0, int.MaxValue)); - //}); } public Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null) @@ -257,7 +250,8 @@ public Task> GetTextChannelsAsync(CacheMode mo public Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) { - throw new NotImplementedException(); + IGuildUser user = new FakeGuildUser(id); + return Task.FromResult(user); } public Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) diff --git a/Code2Gether-Discord-Bot.Tests/Fakes/FakeGuildChannel.cs b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeGuildChannel.cs similarity index 97% rename from Code2Gether-Discord-Bot.Tests/Fakes/FakeGuildChannel.cs rename to Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeGuildChannel.cs index cac0a52..642c0cf 100644 --- a/Code2Gether-Discord-Bot.Tests/Fakes/FakeGuildChannel.cs +++ b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeGuildChannel.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using Discord; -namespace Code2Gether_Discord_Bot.Tests.Fakes +namespace Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord { internal class FakeGuildChannel : IGuildChannel { diff --git a/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeGuildUser.cs b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeGuildUser.cs new file mode 100644 index 0000000..c824de0 --- /dev/null +++ b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeGuildUser.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord; + +namespace Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord +{ + public class FakeGuildUser : IGuildUser + { + public FakeGuildUser(ulong id) + { + Id = id; + } + + public ulong Id { get; } + public DateTimeOffset CreatedAt { get; } + public string Mention { get; } + public IActivity Activity { get; } + public UserStatus Status { get; } + public IImmutableSet ActiveClients { get; } + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + { + throw new NotImplementedException(); + } + + public string GetDefaultAvatarUrl() + { + throw new NotImplementedException(); + } + + public Task GetOrCreateDMChannelAsync(RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public string AvatarId { get; } + public string Discriminator { get; } + public ushort DiscriminatorValue { get; } + public bool IsBot { get; } + public bool IsWebhook { get; } + public string Username { get; } + public bool IsDeafened { get; } + public bool IsMuted { get; } + public bool IsSelfDeafened { get; } + public bool IsSelfMuted { get; } + public bool IsSuppressed { get; } + public IVoiceChannel VoiceChannel { get; } + public string VoiceSessionId { get; } + public bool IsStreaming { get; } + public ChannelPermissions GetPermissions(IGuildChannel channel) + { + throw new NotImplementedException(); + } + + public Task KickAsync(string reason = null, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task ModifyAsync(Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task AddRoleAsync(IRole role, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemoveRoleAsync(IRole role, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) + { + throw new NotImplementedException(); + } + + public DateTimeOffset? JoinedAt { get; } + public string Nickname { get; } + public GuildPermissions GuildPermissions { get; } + public IGuild Guild { get; } + public ulong GuildId { get; } + public DateTimeOffset? PremiumSince { get; } + public IReadOnlyCollection RoleIds { get; } + } +} diff --git a/Code2Gether-Discord-Bot.Tests/Fakes/FakeMessageChannel.cs b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeMessageChannel.cs similarity index 97% rename from Code2Gether-Discord-Bot.Tests/Fakes/FakeMessageChannel.cs rename to Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeMessageChannel.cs index 7fdda2e..259df7f 100644 --- a/Code2Gether-Discord-Bot.Tests/Fakes/FakeMessageChannel.cs +++ b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeMessageChannel.cs @@ -1,11 +1,10 @@ -using Discord; -using System; +using System; using System.Collections.Generic; using System.IO; -using System.Text; using System.Threading.Tasks; +using Discord; -namespace Code2Gether_Discord_Bot.Tests.Fakes +namespace Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord { internal class FakeMessageChannel : IMessageChannel { diff --git a/Code2Gether-Discord-Bot.Tests/Fakes/FakeUserMessage.cs b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeUserMessage.cs similarity index 97% rename from Code2Gether-Discord-Bot.Tests/Fakes/FakeUserMessage.cs rename to Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeUserMessage.cs index 0bae743..eadda89 100644 --- a/Code2Gether-Discord-Bot.Tests/Fakes/FakeUserMessage.cs +++ b/Code2Gether-Discord-Bot.Tests/Fakes/FakeDiscord/FakeUserMessage.cs @@ -1,10 +1,9 @@ -using Discord; -using System; +using System; using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; +using Discord; -namespace Code2Gether_Discord_Bot.Tests.Fakes +namespace Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord { internal class FakeUserMessage : IUserMessage { diff --git a/Code2Gether-Discord-Bot.Tests/Fakes/FakeProjectRepository.cs b/Code2Gether-Discord-Bot.Tests/Fakes/FakeProjectRepository.cs deleted file mode 100644 index f254691..0000000 --- a/Code2Gether-Discord-Bot.Tests/Fakes/FakeProjectRepository.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Code2Gether_Discord_Bot.Library.Models; -using Code2Gether_Discord_Bot.Library.Models.Repositories.ProjectRepository; - -namespace Code2Gether_Discord_Bot.Tests.Fakes -{ - internal class FakeProjectRepository : IProjectRepository - { - public Dictionary Projects = new Dictionary(); - - public bool Create(Project newProject) - { - return Projects.TryAdd(newProject.ID, newProject); - } - - public Project Read(int id) - { - if (Projects.TryGetValue(id, out Project project)) - return project; - throw new Exception($"Failed to read project with ID {id}"); - } - - public Project Read(string projectName) - { - return ReadAll().Values.FirstOrDefault(p => p.Name == projectName); - } - - public IDictionary ReadAll() - { - return Projects; - } - - public bool Update(Project newProject) - { - Delete(newProject.ID); - return Create(newProject); - } - - public bool Delete(int id) - { - return Projects.Remove(id, out _); - } - } -} diff --git a/Code2Gether-Discord-Bot.Tests/Fakes/FakeRepositories/FakeMemberRepository.cs b/Code2Gether-Discord-Bot.Tests/Fakes/FakeRepositories/FakeMemberRepository.cs new file mode 100644 index 0000000..afc332a --- /dev/null +++ b/Code2Gether-Discord-Bot.Tests/Fakes/FakeRepositories/FakeMemberRepository.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Code2Gether_Discord_Bot.Library.Models; +using Code2Gether_Discord_Bot.Library.Models.Repositories; + +namespace Code2Gether_Discord_Bot.Tests.Fakes.FakeRepositories +{ + internal class FakeMemberRepository : IMemberRepository + { + public IDictionary Members = new ConcurrentDictionary(); + + public Task CreateAsync(Member newMember) + { + return Task.FromResult(Members.TryAdd(newMember.ID, newMember)); + } + + public Task ReadAsync(int id) + { + if (Members.TryGetValue(id, out Member member)) + return Task.FromResult(member); + + throw new Exception($"Failed to member project with ID {id}"); + } + + public Task ReadFromSnowflakeAsync(ulong memberSnowflakeId) + { + return Task.FromResult(Members.Values.FirstOrDefault(p => p.SnowflakeId == memberSnowflakeId)); + } + + public Task> ReadAllAsync() + { + return Task.FromResult(Members.Select(x => x.Value)); + } + + public async Task UpdateAsync(Member existingMember) + { + if (await DeleteAsync(existingMember.ID)) + return Members.TryAdd(existingMember.ID, existingMember); + return false; + } + + public Task DeleteAsync(int id) + { + return Task.FromResult(Members.Remove(id, out _)); + } + } +} diff --git a/Code2Gether-Discord-Bot.Tests/Fakes/FakeRepositories/FakeProjectRepository.cs b/Code2Gether-Discord-Bot.Tests/Fakes/FakeRepositories/FakeProjectRepository.cs new file mode 100644 index 0000000..44f9189 --- /dev/null +++ b/Code2Gether-Discord-Bot.Tests/Fakes/FakeRepositories/FakeProjectRepository.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Code2Gether_Discord_Bot.Library.Models; +using Code2Gether_Discord_Bot.Library.Models.Repositories; + +namespace Code2Gether_Discord_Bot.Tests.Fakes.FakeRepositories +{ + public class FakeProjectRepository : IProjectRepository + { + public IDictionary Projects = new ConcurrentDictionary(); + + public Task CreateAsync(Project newProject) + { + return Task.FromResult(Projects.TryAdd(newProject.ID, newProject)); + } + + public Task ReadAsync(int id) + { + if (Projects.TryGetValue(id, out Project project)) + return Task.FromResult(project); + + throw new Exception($"Failed to read project with ID {id}"); + } + + public Task ReadAsync(string projectName) + { + return Task.FromResult(Projects + .Values + .FirstOrDefault(p => p.Name == projectName)); + } + + public Task> ReadAllAsync() + { + return Task.FromResult(Projects.Values.AsEnumerable()); + } + + public async Task UpdateAsync(Project existingProject) + { + if (await DeleteAsync(existingProject.ID)) + return Projects.TryAdd(existingProject.ID, existingProject); + return false; + } + + public Task DeleteAsync(int id) + { + return Task.FromResult(Projects.Remove(id, out _)); + } + + public Task AddMemberAsync(Project project, Member member) + { + if (!project.Members.Any(m => m.SnowflakeId == member.SnowflakeId)) + project.Members.Add(member); + + return Task.FromResult(project.Members.Any(m => m.SnowflakeId == member.SnowflakeId)); + } + + public Task RemoveMemberAsync(Project project, Member member) + { + throw new NotImplementedException(); + } + } +} diff --git a/Code2Gether-Discord-Bot.Tests/InfoLogicTests.cs b/Code2Gether-Discord-Bot.Tests/InfoLogicTests.cs index ae152c0..d242653 100644 --- a/Code2Gether-Discord-Bot.Tests/InfoLogicTests.cs +++ b/Code2Gether-Discord-Bot.Tests/InfoLogicTests.cs @@ -1,7 +1,7 @@ using Code2Gether_Discord_Bot.Library.BusinessLogic; using Code2Gether_Discord_Bot.Library.Models; -using Code2Gether_Discord_Bot.Static; using Code2Gether_Discord_Bot.Tests.Fakes; +using Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord; using NUnit.Framework; namespace Code2Gether_Discord_Bot.Tests @@ -13,7 +13,7 @@ internal class InfoLogicTests [SetUp] public void Setup() { - var user = new FakeUser() + var user = new FakeDiscordUser() { Username = "UnitTest", DiscriminatorValue = 1234, @@ -43,7 +43,7 @@ public void Setup() Author = user }; - _logic = BusinessLogicFactory.GetInfoLogic(GetType(), new FakeCommandContext() + _logic = new InfoLogic(new Logger(GetType()), new FakeCommandContext() { Client = client, Guild = guild, @@ -55,6 +55,6 @@ public void Setup() [Test] public void InstantiationTest() => - Assert.IsTrue(_logic != null); + Assert.IsNotNull(_logic); } } diff --git a/Code2Gether-Discord-Bot.Tests/JoinProjectTests.cs b/Code2Gether-Discord-Bot.Tests/JoinProjectTests.cs index 6371b95..bd4ad06 100644 --- a/Code2Gether-Discord-Bot.Tests/JoinProjectTests.cs +++ b/Code2Gether-Discord-Bot.Tests/JoinProjectTests.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Threading.Tasks; using Code2Gether_Discord_Bot.Library.BusinessLogic; using Code2Gether_Discord_Bot.Library.Models; -using Code2Gether_Discord_Bot.Library.Models.Repositories.ProjectRepository; -using Code2Gether_Discord_Bot.Static; +using Code2Gether_Discord_Bot.Library.Models.Repositories; using Code2Gether_Discord_Bot.Tests.Fakes; +using Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord; +using Code2Gether_Discord_Bot.Tests.Fakes.FakeRepositories; using NUnit.Framework; namespace Code2Gether_Discord_Bot.Tests @@ -14,23 +16,26 @@ namespace Code2Gether_Discord_Bot.Tests internal class JoinProjectTests { private IBusinessLogic _logic; - private IProjectRepository _repo; + private IMemberRepository _memberRepository; + private IProjectRepository _projectRepository; [SetUp] public void Setup() { - var user = new FakeUser() + var fakeuser = new FakeDiscordUser() { Username = "UnitTest", DiscriminatorValue = 1234, Id = 123456789123456789 }; + var user = new Member(fakeuser); + var client = new FakeDiscordClient() { FakeApplication = new FakeApplication() { - Owner = user + Owner = fakeuser } }; @@ -46,37 +51,57 @@ public void Setup() var message = new FakeUserMessage() { - Author = user + Author = fakeuser }; - _repo = new FakeProjectRepository() + _memberRepository = new FakeMemberRepository(); + + _projectRepository = new FakeProjectRepository() { Projects = new Dictionary() { - {0, new Project(0, "UnitTestProject", user)}, + {0, new Project("UnitTestProject", user)}, } }; - _logic = new JoinProjectLogic(UtilityFactory.GetLogger(GetType()), new FakeCommandContext() + _logic = new JoinProjectLogic(new Logger(GetType()), new FakeCommandContext() { Channel = messageChannel, Client = client, Guild = guild, Message = message, - User = user - }, new ProjectManager(_repo), "UnitTestProject"); + User = fakeuser + }, new ProjectManager(_memberRepository, _projectRepository), "UnitTestProject"); } [Test] public void InstantiationTest() => - Assert.IsTrue(_logic != null); + Assert.IsNotNull(_logic); + + /// + /// Passes: If joining an existing project results in 1 member + /// Fails: If joining an existing project results in 0 members + /// + [Test] + public async Task SingleExecutionTest() + { + await _logic.ExecuteAsync(); + var project = await _projectRepository.ReadAsync(0); + Assert.AreEqual(1, project.Members.Count); + } + /// + /// Passes: If joining an existing project twice results in only 1 member + /// Fails: If joining an existing project twice results 2 duplicate members + /// [Test] - public async Task ExecutionTest() + public async Task DoubleExecutionTest() { await _logic.ExecuteAsync(); + await _logic.ExecuteAsync(); + var project = await _projectRepository.ReadAsync(0); - Assert.IsTrue(_repo.Read(0).ProjectMembers.Count > 0); + Assert.AreEqual(1, project.Members.Count); } } } diff --git a/Code2Gether-Discord-Bot.Tests/ListProjectsTests.cs b/Code2Gether-Discord-Bot.Tests/ListProjectsTests.cs index ff5fcfc..b648a95 100644 --- a/Code2Gether-Discord-Bot.Tests/ListProjectsTests.cs +++ b/Code2Gether-Discord-Bot.Tests/ListProjectsTests.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; -using System.Text; +using System.Linq; using System.Threading.Tasks; using Code2Gether_Discord_Bot.Library.BusinessLogic; using Code2Gether_Discord_Bot.Library.Models; -using Code2Gether_Discord_Bot.Library.Models.Repositories.ProjectRepository; -using Code2Gether_Discord_Bot.Static; +using Code2Gether_Discord_Bot.Library.Models.Repositories; using Code2Gether_Discord_Bot.Tests.Fakes; +using Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord; +using Code2Gether_Discord_Bot.Tests.Fakes.FakeRepositories; using NUnit.Framework; namespace Code2Gether_Discord_Bot.Tests @@ -19,18 +20,20 @@ internal class ListProjectsTests [SetUp] public void Setup() { - var user = new FakeUser() + var fakeUser = new FakeDiscordUser() { Username = "UnitTest", DiscriminatorValue = 1234, Id = 123456789123456789 }; + var user = new Member(fakeUser); + var client = new FakeDiscordClient() { FakeApplication = new FakeApplication() { - Owner = user + Owner = fakeUser } }; @@ -46,43 +49,57 @@ public void Setup() var message = new FakeUserMessage() { - Author = user + Author = fakeUser }; _repo = new FakeProjectRepository() { Projects = new Dictionary() { - {0, new Project(0, "unittest", user)}, + {0, new Project("unittest", user)}, } }; - _logic = new ListProjectsLogic(UtilityFactory.GetLogger(GetType()), new FakeCommandContext() + _logic = new ListProjectsLogic(new Logger(GetType()), new FakeCommandContext() { Channel = messageChannel, Client = client, Guild = guild, Message = message, - User = user + User = fakeUser }, _repo); } [Test] public void InstantiationTest() => - Assert.IsTrue(_logic != null); + Assert.IsNotNull(_logic); [Test] - public async Task ExecutionTest() + public async Task EmbedContainsEveryProjectNameExecutionTest() { - _ = await _logic.ExecuteAsync(); - Assert.IsTrue(_repo.ReadAll().Count > 0); + var embed = await _logic.ExecuteAsync(); + var projects = await _repo.ReadAllAsync(); + var projectNames = projects.Select(p => p.Name); + + bool hasEveryProjectName = true; + foreach (var projectName in projectNames) + { + if (!embed.Description.Contains(projectName)) + { + hasEveryProjectName = false; + } + } + + Assert.IsTrue(hasEveryProjectName); } [Test] - public async Task EmbedExecutionTest() + public async Task EmbedContainsTotalProjectCountExecutionTest() { var embed = await _logic.ExecuteAsync(); - Assert.IsTrue(embed.Description.Contains(_repo.Read(0).ToString())); + var projects = await _repo.ReadAllAsync(); + + Assert.IsTrue(embed.Title.Contains(projects.Count().ToString())); } } } diff --git a/Code2Gether-Discord-Bot.Tests/MakeChannelTests.cs b/Code2Gether-Discord-Bot.Tests/MakeChannelTests.cs index 1f73ea8..4d526e5 100644 --- a/Code2Gether-Discord-Bot.Tests/MakeChannelTests.cs +++ b/Code2Gether-Discord-Bot.Tests/MakeChannelTests.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Code2Gether_Discord_Bot.Library.BusinessLogic; +using Code2Gether_Discord_Bot.Library.BusinessLogic; using Code2Gether_Discord_Bot.Library.Models; -using Code2Gether_Discord_Bot.Static; using Code2Gether_Discord_Bot.Tests.Fakes; +using Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord; using NUnit.Framework; namespace Code2Gether_Discord_Bot.Tests @@ -16,7 +13,7 @@ public class MakeChannelTests [SetUp] public void Setup() { - var user = new FakeUser() + var user = new FakeDiscordUser() { Username = "UnitTest", DiscriminatorValue = 1234, @@ -47,7 +44,7 @@ public void Setup() Content = "debug!makechannel make-me" }; - _logic = BusinessLogicFactory.GetMakeChannelLogic(GetType(), new FakeCommandContext() + _logic = new MakeChannelLogic( new Logger(GetType()), new FakeCommandContext() { Client = client, Guild = guild, @@ -59,6 +56,6 @@ public void Setup() [Test] public void InstantiationTest() => - Assert.IsTrue(_logic != null); + Assert.IsNotNull(_logic); } } diff --git a/Code2Gether-Discord-Bot.Tests/PingLogicTests.cs b/Code2Gether-Discord-Bot.Tests/PingLogicTests.cs index 6438bbc..bba72ce 100644 --- a/Code2Gether-Discord-Bot.Tests/PingLogicTests.cs +++ b/Code2Gether-Discord-Bot.Tests/PingLogicTests.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; using Code2Gether_Discord_Bot.Library.BusinessLogic; using Code2Gether_Discord_Bot.Library.Models; -using Code2Gether_Discord_Bot.Static; using Code2Gether_Discord_Bot.Tests.Fakes; +using Code2Gether_Discord_Bot.Tests.Fakes.FakeDiscord; using NUnit.Framework; namespace Code2Gether_Discord_Bot.Tests @@ -14,7 +14,7 @@ public class PingLogicTests [SetUp] public void Setup() { - var user = new FakeUser() + var user = new FakeDiscordUser() { Username = "UnitTest", DiscriminatorValue = 1234, @@ -44,7 +44,7 @@ public void Setup() Author = user }; - _logic = BusinessLogicFactory.GetPingLogic(GetType(), new FakeCommandContext() + _logic = new PingLogic(new Logger(GetType()), new FakeCommandContext() { Client = client, Guild = guild, @@ -56,7 +56,7 @@ public void Setup() [Test] public void InstantiationTest() => - Assert.IsTrue(_logic != null); + Assert.IsNotNull(_logic); [Test] public async Task EmbedAuthorHasValueTest() diff --git a/Code2Gether-Discord-Bot.WebApi/Code2Gether-Discord-Bot.WebApi-Database-ERD.vsdx b/Code2Gether-Discord-Bot.WebApi/Code2Gether-Discord-Bot.WebApi-Database-ERD.vsdx new file mode 100644 index 0000000..f5c442f Binary files /dev/null and b/Code2Gether-Discord-Bot.WebApi/Code2Gether-Discord-Bot.WebApi-Database-ERD.vsdx differ diff --git a/Code2Gether-Discord-Bot.WebApi/Code2Gether-Discord-Bot.WebApi.csproj b/Code2Gether-Discord-Bot.WebApi/Code2Gether-Discord-Bot.WebApi.csproj new file mode 100644 index 0000000..3d6fec8 --- /dev/null +++ b/Code2Gether-Discord-Bot.WebApi/Code2Gether-Discord-Bot.WebApi.csproj @@ -0,0 +1,40 @@ + + + + net5.0 + Code2Gether_Discord_Bot.WebApi + Linux + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/Code2Gether-Discord-Bot.WebApi/Code2Gether-Discord-Bot.WebApi.csproj.user b/Code2Gether-Discord-Bot.WebApi/Code2Gether-Discord-Bot.WebApi.csproj.user new file mode 100644 index 0000000..97bd5f3 --- /dev/null +++ b/Code2Gether-Discord-Bot.WebApi/Code2Gether-Discord-Bot.WebApi.csproj.user @@ -0,0 +1,18 @@ + + + + Code2Gether-Discord-Bot.WebApi + ApiControllerWithContextScaffolder + root/Common/Api + 600 + True + False + True + + Code2Gether_Discord_Bot.WebApi.DbContexts.DiscordBotDbContext + False + + + ProjectDebugger + + \ No newline at end of file diff --git a/Code2Gether-Discord-Bot.WebApi/Controllers/MembersController.cs b/Code2Gether-Discord-Bot.WebApi/Controllers/MembersController.cs new file mode 100644 index 0000000..623e421 --- /dev/null +++ b/Code2Gether-Discord-Bot.WebApi/Controllers/MembersController.cs @@ -0,0 +1,167 @@ +using Code2Gether_Discord_Bot.Library.Models; +using Code2Gether_Discord_Bot.WebApi.DbContexts; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Code2Gether_Discord_Bot.WebApi.Controllers +{ + /// + /// A Web API controller that manages the users in the Code2Gether Discord Bot's Project Database. + /// + [ApiController] + [Route("[controller]")] + public class MembersController : Controller + { + #region Fields + private readonly DiscordBotDbContext _dbContext; + #endregion + + #region Constructor + public MembersController(DiscordBotDbContext dbContext) + { + _dbContext = dbContext; + } + #endregion + + #region REST API Methods + /// + /// Create a new member. + /// + /// Member to add to database. + /// Action result containing details of added member. + [HttpPost(Name = "PostMember")] + public async Task> AddMemberAsync(Member memberToAdd) + { + if (memberToAdd == null) + return BadRequest("User is null."); + + // Ensures we don't replace an existing user. + memberToAdd.ID = 0; + + await _dbContext.Members.AddAsync(memberToAdd); + await _dbContext.SaveChangesAsync(); + + var result = CreatedAtAction(actionName: "GetMember", + routeValues: new { ID = memberToAdd.ID }, + value: memberToAdd); + + return result; + } + #endregion + + /// + /// Updates the user with the input ID. + /// + /// ID of the member to update. + /// Member info to replace the current member. + /// No content. + [HttpPut("{ID}", Name = "PutMember")] + public async Task> UpdateMemberAsync(int ID, Member memberToUpdate) + { + var memberToRemove = await _dbContext.Members.FindAsync(ID); + + if (memberToRemove == null) + return NotFound("Unable to find user"); + + memberToUpdate.ID = ID; + + _dbContext.Members.Remove(memberToUpdate); + await _dbContext.SaveChangesAsync(); + + await _dbContext.Members.AddAsync(memberToUpdate); + await _dbContext.SaveChangesAsync(); + + return NoContent(); + } + + /// + /// Get all members in the database. + /// + /// All members in the database. + [HttpGet(Name = "GetAllMembers")] + public async Task>> GetAllMembersAsync() + { + var members = await _dbContext.Members.ToArrayAsync(); + + foreach (var member in members) + await JoinProjectsAsync(member); + + return members; + } + + /// + /// Gets a single member based on the input ID. + /// + /// ID of the member to retrieve. + /// The data for the retrieved member. + [HttpGet("{ID}", Name = "GetMember")] + public async Task> GetMemberAsync(int ID) + { + var memberToReturn = await _dbContext.Members.FindAsync(ID); + + if (memberToReturn == null) + return NotFound("Could not find user."); + + await JoinProjectsAsync(memberToReturn); + + return memberToReturn; + } + + /// + /// Gets a single member based on the member's snowflake ID. + /// + /// Snowflake ID of the member to retrieve. + /// The data for the retrieved member. + [HttpGet("snowflakeID={snowflakeID}", Name = "GetMemberSnowflake")] + public async Task> GetMemberAsync(ulong snowflakeID) + { + var memberToReturn = await _dbContext.Members + .FirstOrDefaultAsync(x => x.SnowflakeId == snowflakeID); + + if (memberToReturn == null) + return NotFound($"Could not find member with snowflake ID {snowflakeID}"); + + await JoinProjectsAsync(memberToReturn); + + return memberToReturn; + } + + /// + /// Deletes the member with the input ID. + /// + /// The ID of the member to delete. + /// No content. + [HttpDelete("{ID}", Name = "DeleteUser")] + public async Task DeleteMemberASync(int ID) + { + var memberToDelete = await _dbContext.Members.FindAsync(ID); + + if (memberToDelete == null) + return NotFound("Unable to find user."); + + _dbContext.Members.Remove(memberToDelete); + + await _dbContext.SaveChangesAsync(); + + return NoContent(); + } + + private async Task JoinProjectsAsync(Member member) + { + var projectIDs = await _dbContext.ProjectMembers + .AsAsyncEnumerable() + .Where(x => x.MemberID == member.ID) + .Select(x => x.ProjectID) + .ToListAsync(); + + var projects = await _dbContext.Projects + .AsAsyncEnumerable() + .Where(x => projectIDs.Contains(x.ID)) + .ToListAsync(); + + member.Projects = projects; + } + } +} diff --git a/Code2Gether-Discord-Bot.WebApi/Controllers/ProjectsController.cs b/Code2Gether-Discord-Bot.WebApi/Controllers/ProjectsController.cs new file mode 100644 index 0000000..27b3b79 --- /dev/null +++ b/Code2Gether-Discord-Bot.WebApi/Controllers/ProjectsController.cs @@ -0,0 +1,259 @@ +using System; +using Code2Gether_Discord_Bot.Library.Models; +using Code2Gether_Discord_Bot.WebApi.DbContexts; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Code2Gether_Discord_Bot.WebApi.Controllers +{ + /// + /// A Web API controller that manages the projects in the Code2Gether Discord Bot's Project Database. + /// + [ApiController] + [Route("[controller]")] + public class ProjectsController : Controller + { + #region Fields + private readonly DiscordBotDbContext _dbContext; + #endregion + + #region Constructor + public ProjectsController(DiscordBotDbContext dbContext) + { + _dbContext = dbContext; + } + #endregion + + #region REST API Methods + /// + /// Creates a new project. + /// + /// Project to add to database. + /// Action result containing the details of the added project. + [HttpPost(Name = "PostProject")] + public async Task AddProjectAsync(Project projectToAdd) + { + if (projectToAdd == null) + return BadRequest("Project is null."); + + // Ensures we don't replace an existing project + projectToAdd.ID = 0; + await ProcessAuthorAsync(projectToAdd); + + var query = await _dbContext.Projects.AddAsync(projectToAdd); + await _dbContext.SaveChangesAsync(); + + return NoContent(); + } + + /// + /// Updates the project with the input ID. + /// + /// ID of the project to update. + /// Project info to replace the current project. + /// No content. + [HttpPut("{ID}", Name = "PutProject")] + public async Task> UpdateProjectAsync(int ID, Project projectToUpdate) + { + var projectToRemove = await _dbContext.Projects.FindAsync(ID); + + if (projectToRemove == null) + return NotFound("Unable to find project."); + + // Migrate the author and the members to the new project. + var authorId = projectToRemove.AuthorId; + + var members = await _dbContext.ProjectMembers + .AsAsyncEnumerable() + .Where(x => x.ProjectID == projectToUpdate.ID) + .Select(x => x.MemberID) + .ToListAsync(); + + projectToUpdate.ID = ID; + await ProcessAuthorAsync(projectToUpdate); + + // Delete the old project. + _dbContext.Projects.Remove(projectToRemove); + await _dbContext.SaveChangesAsync(); + + projectToUpdate.AuthorId = authorId; + await _dbContext.Projects.AddAsync(projectToUpdate); + + foreach(var member in members) + { + await _dbContext.ProjectMembers.AddAsync(new ProjectMember + { + ProjectID = projectToUpdate.ID, + MemberID = member, + }); + } + + await _dbContext.SaveChangesAsync(); + + return NoContent(); + } + + /// + /// Gets all projects in the database. + /// + /// All projects in the database. + [HttpGet(Name = "GetAllProjects")] + public async Task>> GetAllProjectsAsync() + { + var projectsToReturn = await _dbContext.Projects.ToArrayAsync(); + + foreach (var project in projectsToReturn) + await JoinMembersAsync(project); + + return projectsToReturn; + } + + /// + /// Gets a single project based on the input ID. + /// + /// ID of the project to retrieve. + /// The data for the retrieved project. + [HttpGet("{ID}", Name = "GetProject")] + public async Task> GetProjectAsync(int ID) + { + var projectToReturn = await _dbContext.Projects.FindAsync(ID); + + if (projectToReturn == null) + return NotFound($"Could not find project with ID {ID}"); + + await JoinMembersAsync(projectToReturn); + + return projectToReturn; + } + + /// + /// Gets a single project based on project name. + /// + /// Bame of the project to retrieve. + /// The data for the retrieved project. + [HttpGet("projectName={projectName}", Name = "GetProjectName")] + public async Task> GetProjectAsync(string projectName) + { + var projectToReturn = await _dbContext.Projects + .FirstOrDefaultAsync(x => x.Name == projectName); + + if (projectToReturn == null) + return NotFound($"Unable to find project with name {projectName}"); + + await JoinMembersAsync(projectToReturn); + + return projectToReturn; + } + + /// + /// Add a member to a project. + /// + /// ID of project to add a member to. + /// ID of member to add to the project. + /// + [HttpPost("projectId={projectId};memberId={memberId}", Name = "AddMemberToProject")] + public async Task AddMemberAsync(int projectId, int memberId) + { + var project = _dbContext.ProjectMembers + .AsAsyncEnumerable() + .Where(x => x.ProjectID == projectId); + + if (project == null) + return NotFound($"Could not find project with ID {projectId}"); + + var member = await _dbContext.ProjectMembers + .AsAsyncEnumerable() + .FirstOrDefaultAsync(x => x.MemberID == memberId); + + if (member != null) + return BadRequest($"Member {memberId} is already in project {projectId}"); + + var hasMember = await _dbContext.Members + .AsAsyncEnumerable() + .AnyAsync(x => x.ID == memberId); + + if (!hasMember) + return BadRequest($"Could not find member with ID {memberId}"); + + var projectMember = new ProjectMember + { + ProjectID = projectId, + MemberID = memberId, + }; + + await _dbContext.ProjectMembers.AddAsync(projectMember); + await _dbContext.SaveChangesAsync(); + + return NoContent(); + } + + [HttpDelete("projectId={projectId};memberId={memberId}", Name = "DeleteMemberFromProject")] + public async Task RemoveMemberAsync(int projectId, int memberId) + { + var projectMemberToDelete = await _dbContext.ProjectMembers + .AsAsyncEnumerable() + .Where(x => x.ProjectID == projectId) + .FirstOrDefaultAsync(x => x.MemberID == memberId); + + if (projectMemberToDelete == null) + return NotFound($"Could not find project {projectId} with member {memberId}"); + + _dbContext.ProjectMembers.Remove(projectMemberToDelete); + await _dbContext.SaveChangesAsync(); + + return NoContent(); + } + + /// + /// Deletes the project with the input ID. + /// + /// THe ID of the project to delte. + /// No content. + [HttpDelete("{ID}", Name = "DeleteProject")] + public async Task DeleteProjectAsync(int ID) + { + var projectToDelete = await _dbContext.Projects.FindAsync(ID); + + if (projectToDelete == null) + return NotFound("Unable to find project."); + + _dbContext.Projects.Remove(projectToDelete); + await _dbContext.SaveChangesAsync(); + + return NoContent(); + } + #endregion + + #region Methods + private async Task ProcessAuthorAsync(Project projectToAdd) + { + var author = await _dbContext.Members.FindAsync(projectToAdd.Author.ID); + + projectToAdd.Author = author; + } + + private async Task JoinMembersAsync(Project project) + { + var author = await _dbContext.Members + .FindAsync(project.AuthorId); + + project.Author = author; + + var memberIDs = await _dbContext.ProjectMembers + .AsAsyncEnumerable() + .Where(x => x.ProjectID == project.ID) + .Select(x => x.MemberID) + .ToListAsync(); + + var members = await _dbContext.Members + .AsAsyncEnumerable() + .Where(x => memberIDs.Contains(x.ID)) + .ToListAsync(); + + project.Members = members; + } + #endregion + } +} diff --git a/Code2Gether-Discord-Bot.WebApi/DbContexts/DiscordBotDbContext.cs b/Code2Gether-Discord-Bot.WebApi/DbContexts/DiscordBotDbContext.cs new file mode 100644 index 0000000..b522b05 --- /dev/null +++ b/Code2Gether-Discord-Bot.WebApi/DbContexts/DiscordBotDbContext.cs @@ -0,0 +1,56 @@ +using System; +using System.Reflection; +using Code2Gether_Discord_Bot.Library.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Code2Gether_Discord_Bot.WebApi.DbContexts +{ + public class DiscordBotDbContext : DbContext + { + #region Fields + public static readonly ILoggerFactory factory = LoggerFactory.Create(x => x.AddConsole()); + #endregion + + #region DbSets + public virtual DbSet Projects { get; set; } + public virtual DbSet Members { get; set; } + public virtual DbSet ProjectMembers { get; set; } + #endregion + + #region Methods + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + + if (!optionsBuilder.IsConfigured) + { + optionsBuilder + .UseSqlite(@"DataSource=Code2GetherDiscordBot.db", o => + { + o.MigrationsAssembly(Assembly.GetExecutingAssembly().FullName); + }) + .UseLoggerFactory(factory); + } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasKey(x => new { x.MemberID, x.ProjectID }); + + modelBuilder.Entity() + .HasOne() + .WithMany() + .HasForeignKey(x => x.ProjectID); + + modelBuilder.Entity() + .HasOne() + .WithMany() + .HasForeignKey(x => x.MemberID); + } + #endregion + } +} diff --git a/Code2Gether-Discord-Bot.WebApi/Dockerfile b/Code2Gether-Discord-Bot.WebApi/Dockerfile new file mode 100644 index 0000000..0bd526d --- /dev/null +++ b/Code2Gether-Discord-Bot.WebApi/Dockerfile @@ -0,0 +1,21 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build +WORKDIR /src +COPY ["Code2Gether-Discord-Bot.WebApi/Code2Gether-Discord-Bot.WebApi.csproj", "Code2Gether-Discord-Bot.WebApi/"] +RUN dotnet restore "Code2Gether-Discord-Bot.WebApi/Code2Gether-Discord-Bot.WebApi.csproj" +COPY . . +WORKDIR "/src/Code2Gether-Discord-Bot.WebApi" +RUN dotnet build "Code2Gether-Discord-Bot.WebApi.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Code2Gether-Discord-Bot.WebApi.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Code2Gether-Discord-Bot.WebApi.dll"] \ No newline at end of file diff --git a/Code2Gether-Discord-Bot.WebApi/Migrations/20201220163406_InitialCreate.Designer.cs b/Code2Gether-Discord-Bot.WebApi/Migrations/20201220163406_InitialCreate.Designer.cs new file mode 100644 index 0000000..8c7d4cc --- /dev/null +++ b/Code2Gether-Discord-Bot.WebApi/Migrations/20201220163406_InitialCreate.Designer.cs @@ -0,0 +1,91 @@ +// +using Code2Gether_Discord_Bot.WebApi.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Code2Gether_Discord_Bot.WebApi.Migrations +{ + [DbContext(typeof(DiscordBotDbContext))] + [Migration("20201220163406_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("Code2Gether_Discord_Bot.Library.Models.Member", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("MEMBER_ID"); + + b.Property("SnowflakeId") + .HasColumnType("INTEGER") + .HasColumnName("MEMBER_SNOWFLAKE_ID"); + + b.HasKey("ID"); + + b.ToTable("MEMBERS"); + }); + + modelBuilder.Entity("Code2Gether_Discord_Bot.Library.Models.Project", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("PROJECT_ID"); + + b.Property("AuthorId") + .HasColumnType("INTEGER") + .HasColumnName("AUTHOR_ID"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("PROJECT_NAME"); + + b.HasKey("ID"); + + b.ToTable("PROJECTS"); + }); + + modelBuilder.Entity("Code2Gether_Discord_Bot.Library.Models.ProjectMember", b => + { + b.Property("MemberID") + .HasColumnType("INTEGER") + .HasColumnName("MEMBER_ID"); + + b.Property("ProjectID") + .HasColumnType("INTEGER") + .HasColumnName("PROJECT_ID"); + + b.HasKey("MemberID", "ProjectID"); + + b.HasIndex("ProjectID"); + + b.ToTable("PROJECT_MEMBER"); + }); + + modelBuilder.Entity("Code2Gether_Discord_Bot.Library.Models.ProjectMember", b => + { + b.HasOne("Code2Gether_Discord_Bot.Library.Models.Member", null) + .WithMany() + .HasForeignKey("MemberID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Code2Gether_Discord_Bot.Library.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Code2Gether-Discord-Bot.WebApi/Migrations/20201220163406_InitialCreate.cs b/Code2Gether-Discord-Bot.WebApi/Migrations/20201220163406_InitialCreate.cs new file mode 100644 index 0000000..d95ea6f --- /dev/null +++ b/Code2Gether-Discord-Bot.WebApi/Migrations/20201220163406_InitialCreate.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Code2Gether_Discord_Bot.WebApi.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "MEMBERS", + columns: table => new + { + MEMBER_ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + MEMBER_SNOWFLAKE_ID = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MEMBERS", x => x.MEMBER_ID); + }); + + migrationBuilder.CreateTable( + name: "PROJECTS", + columns: table => new + { + PROJECT_ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + PROJECT_NAME = table.Column(type: "TEXT", nullable: false), + AUTHOR_ID = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PROJECTS", x => x.PROJECT_ID); + }); + + migrationBuilder.CreateTable( + name: "PROJECT_MEMBER", + columns: table => new + { + PROJECT_ID = table.Column(type: "INTEGER", nullable: false), + MEMBER_ID = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PROJECT_MEMBER", x => new { x.MEMBER_ID, x.PROJECT_ID }); + table.ForeignKey( + name: "FK_PROJECT_MEMBER_MEMBERS_MEMBER_ID", + column: x => x.MEMBER_ID, + principalTable: "MEMBERS", + principalColumn: "MEMBER_ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PROJECT_MEMBER_PROJECTS_PROJECT_ID", + column: x => x.PROJECT_ID, + principalTable: "PROJECTS", + principalColumn: "PROJECT_ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PROJECT_MEMBER_PROJECT_ID", + table: "PROJECT_MEMBER", + column: "PROJECT_ID"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PROJECT_MEMBER"); + + migrationBuilder.DropTable( + name: "MEMBERS"); + + migrationBuilder.DropTable( + name: "PROJECTS"); + } + } +} diff --git a/Code2Gether-Discord-Bot.WebApi/Migrations/DiscordBotDbContextModelSnapshot.cs b/Code2Gether-Discord-Bot.WebApi/Migrations/DiscordBotDbContextModelSnapshot.cs new file mode 100644 index 0000000..c00458b --- /dev/null +++ b/Code2Gether-Discord-Bot.WebApi/Migrations/DiscordBotDbContextModelSnapshot.cs @@ -0,0 +1,89 @@ +// +using Code2Gether_Discord_Bot.WebApi.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Code2Gether_Discord_Bot.WebApi.Migrations +{ + [DbContext(typeof(DiscordBotDbContext))] + partial class DiscordBotDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("Code2Gether_Discord_Bot.Library.Models.Member", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("MEMBER_ID"); + + b.Property("SnowflakeId") + .HasColumnType("INTEGER") + .HasColumnName("MEMBER_SNOWFLAKE_ID"); + + b.HasKey("ID"); + + b.ToTable("MEMBERS"); + }); + + modelBuilder.Entity("Code2Gether_Discord_Bot.Library.Models.Project", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("PROJECT_ID"); + + b.Property("AuthorId") + .HasColumnType("INTEGER") + .HasColumnName("AUTHOR_ID"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("PROJECT_NAME"); + + b.HasKey("ID"); + + b.ToTable("PROJECTS"); + }); + + modelBuilder.Entity("Code2Gether_Discord_Bot.Library.Models.ProjectMember", b => + { + b.Property("MemberID") + .HasColumnType("INTEGER") + .HasColumnName("MEMBER_ID"); + + b.Property("ProjectID") + .HasColumnType("INTEGER") + .HasColumnName("PROJECT_ID"); + + b.HasKey("MemberID", "ProjectID"); + + b.HasIndex("ProjectID"); + + b.ToTable("PROJECT_MEMBER"); + }); + + modelBuilder.Entity("Code2Gether_Discord_Bot.Library.Models.ProjectMember", b => + { + b.HasOne("Code2Gether_Discord_Bot.Library.Models.Member", null) + .WithMany() + .HasForeignKey("MemberID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Code2Gether_Discord_Bot.Library.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Code2Gether-Discord-Bot.WebApi/Program.cs b/Code2Gether-Discord-Bot.WebApi/Program.cs new file mode 100644 index 0000000..0f2acad --- /dev/null +++ b/Code2Gether-Discord-Bot.WebApi/Program.cs @@ -0,0 +1,42 @@ +using Code2Gether_Discord_Bot.WebApi.DbContexts; +using Code2Gether_Discord_Bot.WebApi.Utilities; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Threading.Tasks; + +namespace Code2Gether_Discord_Bot.WebApi +{ + public class Program + { + public async static Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + using var scope = host.Services.CreateScope(); + var services = scope.ServiceProvider; + + try + { + var dbContext = services.GetRequiredService(); + await dbContext.Database.MigrateAsync(); + // await dbContext.TestAsync(); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + await host.RunAsync(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/Code2Gether-Discord-Bot.WebApi/Startup.cs b/Code2Gether-Discord-Bot.WebApi/Startup.cs new file mode 100644 index 0000000..4d6f13b --- /dev/null +++ b/Code2Gether-Discord-Bot.WebApi/Startup.cs @@ -0,0 +1,60 @@ +using System; +using Code2Gether_Discord_Bot.WebApi.DbContexts; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Newtonsoft.Json; + +namespace Code2Gether_Discord_Bot.WebApi +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers() + .AddNewtonsoftJson(x => x.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore); + + services.AddHealthChecks(); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Code2Gether_Discord_Bot.WebApi", Version = "v1" }); + }); + + // Entity Framework for SQLite. + services.AddDbContext(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Code2Gether_Discord_Bot.WebApi v1")); + } + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapHealthChecks("/health"); + }); + } + } +} diff --git a/Code2Gether-Discord-Bot.WebApi/Utilities/TestDbContext.cs b/Code2Gether-Discord-Bot.WebApi/Utilities/TestDbContext.cs new file mode 100644 index 0000000..edb5966 --- /dev/null +++ b/Code2Gether-Discord-Bot.WebApi/Utilities/TestDbContext.cs @@ -0,0 +1,36 @@ +using Code2Gether_Discord_Bot.Library.Models; +using Code2Gether_Discord_Bot.WebApi.DbContexts; +using System.Linq; +using System.Threading.Tasks; + +namespace Code2Gether_Discord_Bot.WebApi.Utilities +{ + public static class TestDbContext + { + public static async Task TestAsync(this DiscordBotDbContext dbContext) + { + /* + var member = new Member + { + SnowflakeId = 12345 + }; + + await dbContext.Members.AddAsync(member); + await dbContext.SaveChangesAsync(); + */ + + var retrievedMember = await dbContext.Members.FirstOrDefaultAsync(); + + var project = new Project + { + Name = "MyProject", + Author = retrievedMember, + }; + + project.Members.Add(retrievedMember); + + await dbContext.Projects.AddAsync(project); + await dbContext.SaveChangesAsync(); + } + } +} diff --git a/Code2Gether-Discord-Bot.sln b/Code2Gether-Discord-Bot.sln index 0cb2ba0..4aea5ce 100644 --- a/Code2Gether-Discord-Bot.sln +++ b/Code2Gether-Discord-Bot.sln @@ -3,11 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30517.126 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Code2Gether-Discord-Bot", "Code2Gether-Discord-Bot\Code2Gether-Discord-Bot.csproj", "{D9824F19-2899-46C7-A72D-B2A1CEFE59A9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Code2Gether-Discord-Bot", "Code2Gether-Discord-Bot\Code2Gether-Discord-Bot.csproj", "{D9824F19-2899-46C7-A72D-B2A1CEFE59A9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Code2Gether-Discord-Bot.Library", "Code2Gether-Discord-Bot.Library\Code2Gether-Discord-Bot.Library.csproj", "{A9DFC356-9751-45FD-9625-5A738FB3971B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Code2Gether-Discord-Bot.Library", "Code2Gether-Discord-Bot.Library\Code2Gether-Discord-Bot.Library.csproj", "{A9DFC356-9751-45FD-9625-5A738FB3971B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Code2Gether-Discord-Bot.Tests", "Code2Gether-Discord-Bot.Tests\Code2Gether-Discord-Bot.Tests.csproj", "{473A61A7-CBF3-40F8-A183-06C0C466523A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Code2Gether-Discord-Bot.Tests", "Code2Gether-Discord-Bot.Tests\Code2Gether-Discord-Bot.Tests.csproj", "{473A61A7-CBF3-40F8-A183-06C0C466523A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Code2Gether-Discord-Bot.WebApi", "Code2Gether-Discord-Bot.WebApi\Code2Gether-Discord-Bot.WebApi.csproj", "{08CDB4A5-BD9F-466F-98F6-2DFEF259FCCF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Code2Gether-Discord-Bot.ConsoleDiagnostics", "Code2Gether-Discord-Bot.ConsoleDiagnostics\Code2Gether-Discord-Bot.ConsoleDiagnostics.csproj", "{47604C05-CF23-46C1-8C5C-38EA4E5E9C22}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,6 +31,14 @@ Global {473A61A7-CBF3-40F8-A183-06C0C466523A}.Debug|Any CPU.Build.0 = Debug|Any CPU {473A61A7-CBF3-40F8-A183-06C0C466523A}.Release|Any CPU.ActiveCfg = Release|Any CPU {473A61A7-CBF3-40F8-A183-06C0C466523A}.Release|Any CPU.Build.0 = Release|Any CPU + {08CDB4A5-BD9F-466F-98F6-2DFEF259FCCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08CDB4A5-BD9F-466F-98F6-2DFEF259FCCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08CDB4A5-BD9F-466F-98F6-2DFEF259FCCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08CDB4A5-BD9F-466F-98F6-2DFEF259FCCF}.Release|Any CPU.Build.0 = Release|Any CPU + {47604C05-CF23-46C1-8C5C-38EA4E5E9C22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47604C05-CF23-46C1-8C5C-38EA4E5E9C22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47604C05-CF23-46C1-8C5C-38EA4E5E9C22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47604C05-CF23-46C1-8C5C-38EA4E5E9C22}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Code2Gether-Discord-Bot/Code2Gether-Discord-Bot.csproj b/Code2Gether-Discord-Bot/Code2Gether-Discord-Bot.csproj index c5658db..b501cba 100644 --- a/Code2Gether-Discord-Bot/Code2Gether-Discord-Bot.csproj +++ b/Code2Gether-Discord-Bot/Code2Gether-Discord-Bot.csproj @@ -8,7 +8,7 @@ - + diff --git a/Code2Gether-Discord-Bot/Models/Config.cs b/Code2Gether-Discord-Bot/Models/Config.cs index 64790a6..7875a9f 100644 --- a/Code2Gether-Discord-Bot/Models/Config.cs +++ b/Code2Gether-Discord-Bot/Models/Config.cs @@ -7,6 +7,7 @@ public interface IConfig string DiscordToken { get; set; } string Prefix { get; set; } bool Debug { get; set; } + string ConnectionString { get; set; } } public class Config : IConfig @@ -20,13 +21,17 @@ public class Config : IConfig [JsonProperty("debug")] public bool Debug { get; set; } + [JsonProperty("connectionString")] + public string ConnectionString { get; set; } + public Config() { } - public Config(string prefix, string token, bool debug) + public Config(string prefix, string token, bool debug, string connectionString) { Prefix = prefix; DiscordToken = token; Debug = debug; + ConnectionString = connectionString; } } } diff --git a/Code2Gether-Discord-Bot/Static/BusinessLogicFactory.cs b/Code2Gether-Discord-Bot/Static/BusinessLogicFactory.cs index 73f3638..a550373 100644 --- a/Code2Gether-Discord-Bot/Static/BusinessLogicFactory.cs +++ b/Code2Gether-Discord-Bot/Static/BusinessLogicFactory.cs @@ -1,7 +1,7 @@ -using Code2Gether_Discord_Bot.Library.BusinessLogic; -using Code2Gether_Discord_Bot.Library.Static; -using Discord.Commands; +using Discord.Commands; using System; +using Code2Gether_Discord_Bot.Library.BusinessLogic; +using Code2Gether_Discord_Bot.Library.Static; namespace Code2Gether_Discord_Bot.Static { diff --git a/Code2Gether-Discord-Bot/Static/ConfigProvider.cs b/Code2Gether-Discord-Bot/Static/ConfigProvider.cs index e33d278..fe5ffb4 100644 --- a/Code2Gether-Discord-Bot/Static/ConfigProvider.cs +++ b/Code2Gether-Discord-Bot/Static/ConfigProvider.cs @@ -11,7 +11,7 @@ public static class ConfigProvider public static IConfig GetConfig() { // Default value - IConfig config = new Config("c!", "PLACEHOLDER", true); + IConfig config = new Config("c!", "PLACEHOLDER", true, "PLACEHOLDER"); try { config = JsonConvert.DeserializeObject(File.ReadAllText(configFile.FullName)); diff --git a/Code2Gether-Discord-Bot/Static/ManagerFactory.cs b/Code2Gether-Discord-Bot/Static/ManagerFactory.cs index e07db51..e4b2517 100644 --- a/Code2Gether-Discord-Bot/Static/ManagerFactory.cs +++ b/Code2Gether-Discord-Bot/Static/ManagerFactory.cs @@ -9,6 +9,6 @@ namespace Code2Gether_Discord_Bot.Static public class ManagerFactory { public static IProjectManager GetProjectManager() => - new ProjectManager(RepositoryFactory.GetProjectRepository()); + new ProjectManager(RepositoryFactory.GetMemberRepository(), RepositoryFactory.GetProjectRepository()); } } diff --git a/Code2Gether-Discord-Bot/Static/RepositoryFactory.cs b/Code2Gether-Discord-Bot/Static/RepositoryFactory.cs new file mode 100644 index 0000000..e8c6b85 --- /dev/null +++ b/Code2Gether-Discord-Bot/Static/RepositoryFactory.cs @@ -0,0 +1,13 @@ +using Code2Gether_Discord_Bot.Library.Models.Repositories; + +namespace Code2Gether_Discord_Bot.Static +{ + public static class RepositoryFactory + { + public static IProjectRepository GetProjectRepository() => + new ProjectDAL(UtilityFactory.GetConfig().ConnectionString); + + public static IMemberRepository GetMemberRepository() => + new MemberDAL(UtilityFactory.GetConfig().ConnectionString); + } +} diff --git a/Code2Gether-Discord-Bot/Static/UtilityFactory.cs b/Code2Gether-Discord-Bot/Static/UtilityFactory.cs index 9970056..edc5098 100644 --- a/Code2Gether-Discord-Bot/Static/UtilityFactory.cs +++ b/Code2Gether-Discord-Bot/Static/UtilityFactory.cs @@ -1,9 +1,6 @@ using Code2Gether_Discord_Bot.Library.Models; using Code2Gether_Discord_Bot.Models; using System; -using System.Runtime.CompilerServices; -using Code2Gether_Discord_Bot.Library.Models.Repositories.ProjectRepository; -using Code2Gether_Discord_Bot.Library.Static; namespace Code2Gether_Discord_Bot.Static {