Refactoring: Added interfaces, custom exceptions, UserManager unit tests, dependency injection/inversion; Regex match search by date, keywords

This commit is contained in:
Dimitar Byalkov
2023-06-06 17:52:36 +02:00
parent 180b261d37
commit 53c42a35d8
43 changed files with 668 additions and 211 deletions

View File

@@ -1,14 +1,17 @@
using Models;
using Logic;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Logic.Exceptions;
using System.Reflection;
namespace Data
{
public class AnnouncementRepository
public class AnnouncementRepository : IAnnouncementRepository
{
public AnnouncementRepository() { }
public List<Announcement> GetAllAnnouncements()
@@ -58,21 +61,21 @@ namespace Data
return announcement;
}
}
public List<Announcement> GetAnnouncementsByPage(int? p, int? c)
public List<Announcement> GetAnnouncementsByPage(int p, int c)
{
List<Announcement> announcements = new List<Announcement>();
UserRepository userRepository = new UserRepository();
if (c == null || c < 0)
if (c == null)
{
c = 10;
throw new DatabaseOperationException("Get announcements: Invalid item count");
}
if (p == null || p < 0)
if (p == null)
{
p = 0;
throw new DatabaseOperationException("Get announcements: Invalid page number");
}
using (SqlConnection conn = SqlConnectionHelper.CreateConnection())
{
string sql = "SELECT * FROM Announcements ORDER BY ID OFFSET @start ROWS FETCH NEXT @count ROWS ONLY;";
string sql = "SELECT * FROM Announcements ORDER BY ID DESC OFFSET @start ROWS FETCH NEXT @count ROWS ONLY;";
SqlCommand sqlCommand = new SqlCommand(sql, conn);
sqlCommand.Parameters.AddWithValue("@start", p * c);
sqlCommand.Parameters.AddWithValue("@count", c);
@@ -89,7 +92,7 @@ namespace Data
}
return announcements;
}
public bool CreateAnnouncement(string title, string description, User author, DateTime publishDate, bool isImportant, bool isSticky)
public void CreateAnnouncement(string title, string description, User author, DateTime publishDate, bool isImportant, bool isSticky)
{
using (SqlConnection conn = SqlConnectionHelper.CreateConnection())
{
@@ -102,15 +105,13 @@ namespace Data
cmd.Parameters.AddWithValue("@important", isImportant);
cmd.Parameters.AddWithValue("@sticky", isSticky);
int writer = cmd.ExecuteNonQuery();
if (writer == 1)
if (writer != 1)
{
return true;
throw new DatabaseOperationException("Database error: Announcement not created");
}
else return false;
}
}
public bool UpdateAnnouncement(int id, string title, string description, bool isImportant, bool isSticky)
public void UpdateAnnouncement(int id, string title, string description, bool isImportant, bool isSticky)
{
using (SqlConnection conn = SqlConnectionHelper.CreateConnection())
{
@@ -124,15 +125,13 @@ namespace Data
cmd.Parameters.AddWithValue("@sticky", isSticky);
cmd.Parameters.AddWithValue("@id", id);
int writer = cmd.ExecuteNonQuery();
if (writer == 1)
if (writer != 1)
{
return true;
throw new DatabaseOperationException("Database error: Announcement not updated");
}
else return false;
}
}
public bool DeleteAnnouncement(int id)
public void DeleteAnnouncement(int id)
{
using (SqlConnection conn = SqlConnectionHelper.CreateConnection())
{
@@ -140,13 +139,54 @@ namespace Data
SqlCommand cmd = new SqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@id", id);
int writer = cmd.ExecuteNonQuery();
if (writer == 1)
if (writer != 1)
{
return true;
throw new DatabaseOperationException("Database error: Announcement not deleted");
}
else return false;
}
}
public List<Announcement> SearchAnnouncement(string query)
{
if (string.IsNullOrEmpty(query))
{
throw new DatabaseOperationException("Search announements error: Search query is empty");
}
List<Announcement> announcements = new List<Announcement>();
UserRepository userRepository = new UserRepository();
StringBuilder sql = new StringBuilder();
sql.Append("SELECT * FROM Announcements ");
string[] searchStrings = query.Trim().Split(' ');
for (int i = 0; i < searchStrings.Length; i++)
{
if (i == 0)
{
sql.Append($"WHERE Title LIKE @query{i} OR Description LIKE @query{i} ");
}
else
{
sql.Append($"OR Title LIKE @query{i} OR Description LIKE @query{i} ");
}
}
sql.Append(';');
using (SqlConnection conn = SqlConnectionHelper.CreateConnection())
{
SqlCommand sqlCommand = new SqlCommand(sql.ToString(), conn);
for (int i = 0; i < searchStrings.Length; i++)
{
sqlCommand.Parameters.AddWithValue($"@query{i}", $"%{searchStrings[i]}%");
}
var reader = sqlCommand.ExecuteReader();
while (reader.Read())
{
announcements.Add(new Announcement(Convert.ToInt32(reader["ID"]),
userRepository.GetUserById(Convert.ToInt32(reader["Author"])),
reader["Description"].ToString(), reader["Title"].ToString(),
(DateTime)reader["PublishDate"], (bool)reader["IsImportant"],
(bool)reader["IsSticky"]));
}
}
return announcements;
}
}
}

View File

@@ -1,10 +1,12 @@
using System.ComponentModel.Design;
using System.Data.SqlClient;
using Models;
using Logic;
using Logic.Exceptions;
namespace Data;
public class CommentRepository
public class CommentRepository : ICommentRepository
{
public CommentRepository()
{
@@ -100,10 +102,14 @@ public class CommentRepository
cmd.Parameters.AddWithValue("@desc", description);
cmd.Parameters.AddWithValue("@id", id);
int writer = cmd.ExecuteNonQuery();
if (writer != 1)
{
throw new DatabaseOperationException("Database error: Comment not updated");
}
}
}
private Comment CreateComment(User author, string description, string title, DateTime publishDate)
public Comment CreateComment(User author, string description, string title, DateTime publishDate)
{
using (SqlConnection connection = SqlConnectionHelper.CreateConnection())
{
@@ -128,6 +134,10 @@ public class CommentRepository
cmd.Parameters.AddWithValue("@announcementID", announcementId);
cmd.Parameters.AddWithValue("@commentID", comment.ID);
int writer = cmd.ExecuteNonQuery();
if (writer != 1)
{
throw new DatabaseOperationException("Database error: Announcement comment not created");
}
}
}
public void CreateResponseOnComment(User author, string description, string title, DateTime publishDate, int commentId)
@@ -140,10 +150,14 @@ public class CommentRepository
cmd.Parameters.AddWithValue("@commentID", commentId);
cmd.Parameters.AddWithValue("@responseID", response.ID);
int writer = cmd.ExecuteNonQuery();
if (writer != 1)
{
throw new DatabaseOperationException("Database error: Comment response not created");
}
}
}
private void DeleteComment(int id)
public void DeleteComment(int id)
{
using (SqlConnection connection = SqlConnectionHelper.CreateConnection())
{
@@ -152,6 +166,10 @@ public class CommentRepository
SqlCommand cmd = new SqlCommand(sql, connection);
cmd.Parameters.AddWithValue("@id", id);
int writer = cmd.ExecuteNonQuery();
if (writer != 1)
{
throw new DatabaseOperationException("Database error: Comment not deleted");
}
}
}
@@ -165,6 +183,10 @@ public class CommentRepository
cmd.Parameters.AddWithValue("@commentId", commentId);
cmd.Parameters.AddWithValue("@announcementId", announcementId);
int writer = cmd.ExecuteNonQuery();
if (writer != 1)
{
throw new DatabaseOperationException("Database error: Announcement comment not deleted");
}
}
DeleteComment(commentId);
}
@@ -178,6 +200,10 @@ public class CommentRepository
cmd.Parameters.AddWithValue("@commentId", commentId);
cmd.Parameters.AddWithValue("@responseId", responseId);
int writer = cmd.ExecuteNonQuery();
if (writer != 1)
{
throw new DatabaseOperationException("Database error: Announcement not created");
}
}
DeleteComment(commentId);
}

View File

@@ -11,6 +11,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Logic\Logic.csproj" />
<ProjectReference Include="..\Models\Models.csproj" />
</ItemGroup>

View File

@@ -1,4 +1,5 @@
using System;
using Logic.Exceptions;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
@@ -20,7 +21,6 @@ namespace Data
catch (SqlException e)
{
throw new DatabaseNetworkException("Unable to access FHICT VDI database", e);
// Console.WriteLine("Database connection error. Are you connected to the VDI VPN?");
}
return connection;

View File

@@ -7,10 +7,11 @@ using System.Data.SqlClient;
using Models;
using System.Data;
using System.Xml.Linq;
using Logic;
namespace Data
{
public class UserRepository
public class UserRepository : IUserRepository
{
public UserRepository() { }
public List<User> GetAllUsers()
@@ -54,17 +55,9 @@ namespace Data
(UserRole)reader["Role"]);
}
}
public List<User> GetUsersByPage(int? p, int? c)
public List<User> GetUsersByPage(int p, int c)
{
List<User> users = new List<User>();
if (c == null || c < 0)
{
c = 10;
}
if (p == null || p < 0)
{
p = 0;
}
using (SqlConnection conn = SqlConnectionHelper.CreateConnection())
{
string sql = "SELECT * FROM Users ORDER BY ID OFFSET @start ROWS FETCH NEXT @count ROWS ONLY;";
@@ -81,25 +74,20 @@ namespace Data
}
return users;
}
public bool CreateUser(string name, string password, UserRole role)
public User CreateUser(string name, string password, UserRole role)
{
using (SqlConnection conn = SqlConnectionHelper.CreateConnection())
{
string sql = "INSERT INTO Users (Name, Password, Role) VALUES (@name, @pass, @role);";
string sql = "INSERT INTO Users (Name, Password, Role) VALUES (@name, @pass, @role) " +
"SELECT SCOPE_IDENTITY();";
SqlCommand cmd = new SqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@name", name);
cmd.Parameters.AddWithValue("@pass", password);
cmd.Parameters.AddWithValue("@role", (int)role);
int writer = cmd.ExecuteNonQuery();
if (writer == 1)
{
return true;
}
else return false;
return GetUserById(Convert.ToInt32(cmd.ExecuteScalar()));
}
}
public bool UpdateUser(int id, string name, string password, UserRole role)
public void UpdateUser(int id, string name, string password, UserRole role)
{
using (SqlConnection conn = SqlConnectionHelper.CreateConnection())
{
@@ -111,16 +99,10 @@ namespace Data
cmd.Parameters.AddWithValue("@pass", password);
cmd.Parameters.AddWithValue("@role", (int)role);
cmd.Parameters.AddWithValue("@id", id);
int writer = cmd.ExecuteNonQuery();
if (writer == 1)
{
return true;
}
else return false;
cmd.ExecuteNonQuery();
}
}
public bool DisableUser(int id)
public void DisableUser(int id)
{
using (SqlConnection conn = SqlConnectionHelper.CreateConnection())
{
@@ -129,13 +111,21 @@ namespace Data
"WHERE ID = @id;";
SqlCommand cmd = new SqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@id", id.ToString());
int writer = cmd.ExecuteNonQuery();
cmd.ExecuteNonQuery();
}
}
if (writer == 1)
{
return true;
}
else return false;
public User GetUserByName(string userName)
{
using (SqlConnection conn = SqlConnectionHelper.CreateConnection())
{
string sql = "SELECT * FROM Users WHERE Name = @userName;";
SqlCommand cmd = new SqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@userName", userName);
var reader = cmd.ExecuteReader();
return new User(Convert.ToInt32(reader["ID"]), reader["Name"].ToString(),
reader["Password"].ToString(), (UserRole)reader["Role"]);
}
}
}

View File

@@ -1,20 +1,20 @@
using Models;
using Data;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Logic
{
public class AnnouncementManager
{
private AnnouncementRepository announcementRepository;
public AnnouncementManager()
private IAnnouncementRepository announcementRepository;
public AnnouncementManager(IAnnouncementRepository announcementRepository)
{
announcementRepository = new AnnouncementRepository();
this.announcementRepository = announcementRepository;
}
public List<Announcement> GetAllAnnouncements()
{
@@ -24,22 +24,44 @@ namespace Logic
{
return announcementRepository.GetAnnouncementById(id);
}
public List<Announcement> GetAnnouncementsByPage(int? p, int? c)
public List<Announcement> GetAnnouncementsByPage(int p = 0, int c = 10)
{
return announcementRepository.GetAnnouncementsByPage(p, c);
}
public bool CreateAnnouncement(string title, string description, User author, DateTime publishDate, bool isImportant, bool isSticky)
public void CreateAnnouncement(string title, string description, User author, DateTime publishDate, bool isImportant, bool isSticky)
{
return announcementRepository.CreateAnnouncement(title, description, author, publishDate, isImportant, isSticky);
announcementRepository.CreateAnnouncement(title, description, author, publishDate, isImportant, isSticky);
}
public bool UpdateAnnouncement(int id, string title, string description, bool isImportant, bool isSticky)
public void UpdateAnnouncement(int id, string title, string description, bool isImportant, bool isSticky)
{
description += $"{Environment.NewLine}{Environment.NewLine}Updated: {DateTime.Now.ToString("g")}";
return announcementRepository.UpdateAnnouncement(id, title, description, isImportant, isSticky);
announcementRepository.UpdateAnnouncement(id, title, description, isImportant, isSticky);
}
public bool DeleteAnnouncement(int id)
public void DeleteAnnouncement(int id)
{
return announcementRepository.DeleteAnnouncement(id);
announcementRepository.DeleteAnnouncement(id);
}
public List<Announcement> SearchAnnouncements(string query)
{
if (string.IsNullOrEmpty(query))
{
return new List<Announcement>();
}
else
{
var match = Regex.Match(query, "(?<=date:)[0-9]{4}-[0-9]{2}-[0-9]{2}");
DateTime date;
if (DateTime.TryParse(match.Groups[0].Value, out date))
{
query = Regex.Replace(query, "date:[0-9]{4}-[0-9]{2}-[0-9]{2}", "");
if (string.IsNullOrEmpty(query))
{
return announcementRepository.GetAllAnnouncements().Where(x => x.PublishDate.Date == date.Date).ToList();
}
else return announcementRepository.SearchAnnouncement(query).Where(x => x.PublishDate.Date == date.Date).ToList();
}
else return announcementRepository.SearchAnnouncement(query);
}
}
}
}

View File

@@ -1,5 +1,4 @@
using Models;
using Data;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -10,10 +9,10 @@ namespace Logic
{
public class CommentManager
{
private CommentRepository commentRepository;
public CommentManager()
private ICommentRepository commentRepository;
public CommentManager(ICommentRepository commentRepository)
{
commentRepository = new CommentRepository();
this.commentRepository = commentRepository;
}
public void CreateCommentToAnnouncement(User author, string description, string title, DateTime publishDate, int announcementId)

View File

@@ -5,7 +5,7 @@ using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace Data
namespace Logic.Exceptions
{
public class DatabaseNetworkException : WebException
{

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Logic.Exceptions
{
public class DatabaseOperationException : ApplicationException
{
public DatabaseOperationException(string? message) : base(message)
{
}
public DatabaseOperationException(string? message, Exception? innerException) : base(message, innerException)
{
}
}
}

View File

@@ -0,0 +1,21 @@
using Models;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Logic
{
public interface IAnnouncementRepository
{
public List<Announcement> GetAllAnnouncements();
public Announcement GetAnnouncementById(int id);
public List<Announcement> GetAnnouncementsByPage(int p, int c);
public void CreateAnnouncement(string title, string description, User author, DateTime publishDate, bool isImportant, bool isSticky);
public void UpdateAnnouncement(int id, string title, string description, bool isImportant, bool isSticky);
public void DeleteAnnouncement(int id);
public List<Announcement> SearchAnnouncement(string query);
}
}

View File

@@ -0,0 +1,19 @@
using System.ComponentModel.Design;
using System.Data.SqlClient;
using Models;
namespace Logic;
public interface ICommentRepository
{
public List<Comment> GetAllCommentsOnAnnouncement(int announcementId);
public List<Comment> GetAllCommentResponses(int commentId);
public Comment GetCommentById(int id);
public void UpdateComment(int id, string description);
public Comment CreateComment(User author, string description, string title, DateTime publishDate);
public void CreateCommentOnAnnouncement(User author, string description, string title, DateTime publishDate, int announcementId);
public void CreateResponseOnComment(User author, string description, string title, DateTime publishDate, int commentId);
public void DeleteComment(int id);
public void DeleteCommentOnAnnouncement(int commentId, int announcementId);
public void DeleteResponseOnComment(int responseId, int commentId);
}

View File

@@ -0,0 +1,20 @@
using Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Logic
{
public interface IUserRepository
{
public List<User> GetAllUsers();
public User GetUserById(int id);
public User GetUserByName(string userName);
public List<User> GetUsersByPage(int p, int c);
public User CreateUser(string name, string password, UserRole role);
public void UpdateUser(int id, string name, string password, UserRole role);
public void DisableUser(int id);
}
}

View File

@@ -11,7 +11,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Data\Data.csproj" />
<ProjectReference Include="..\Models\Models.csproj" />
</ItemGroup>

View File

@@ -1,6 +1,5 @@
using BCrypt.Net;
using Models;
using Data;
using System;
using System.Collections.Generic;
using System.Data;
@@ -15,10 +14,10 @@ namespace Logic
{
public class UserManager
{
private UserRepository userRepository;
public UserManager()
private readonly IUserRepository userRepository;
public UserManager(IUserRepository userRepository)
{
userRepository = new UserRepository();
this.userRepository = userRepository;
}
public List<User> GetAllUsers()
{
@@ -28,29 +27,38 @@ namespace Logic
{
return userRepository.GetUserById(id);
}
public List<User> GetUsersByPage(int? p, int? c)
public List<User> GetUsersByPage(int p = 0, int c = 10)
{
return userRepository.GetUsersByPage(p, c);
}
public User? AuthenticatedUser(string name, string password)
{
List<User> users = userRepository.GetAllUsers();
User user = users.Find(x => x.Name == name);
if (user == null)
User? user = users.Find(x => x.Name == name);
if (name == null || password == null)
{
return null;
throw new ArgumentNullException();
}
else
if (user != null && BCrypt.Net.BCrypt.Verify(password, user.Password))
{
if (BCrypt.Net.BCrypt.Verify(password, user.Password))
{
return user;
}
else return null;
return user;
}
return null;
}
public bool CreateUser(string name, string password, UserRole role)
public bool UserExists(string name)
{
return userRepository.GetUserByName(name) != null;
}
public User CreateUser(string name, string password, UserRole role)
{
if (UserExists(name))
{
throw new ArgumentException("User with given username already exists!");
}
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(password))
{
throw new ArgumentException("Name or password should not be empty");
}
return userRepository.CreateUser(name, password, role);
}
public void UpdateUser(int id, string name, string password, UserRole role)

View File

@@ -5,7 +5,7 @@ using System.Text;
namespace Models
{
public class Announcement : GenericMessage, IVotable
public class Announcement : GenericMessage
{
public Announcement(int id, User author, string description, string title, DateTime publishDate, bool isImportant, bool isSticky) : base(id, author, description, title, publishDate)
{
@@ -31,16 +31,6 @@ namespace Models
{
get; set;
}
public void DownVote()
{
throw new NotImplementedException();
}
public void UpVote()
{
throw new NotImplementedException();
}
public override string ToString()
{
return $"{Title} ({PublishDate.ToString("g")} - {Author.Name})";

View File

@@ -5,7 +5,7 @@ using System.Text;
namespace Models
{
public class Comment : GenericMessage, IVotable
public class Comment : GenericMessage
{
public Comment(int id, User author, string description, string title, DateTime publishDate) : base(id, author, description, title, publishDate)
{
@@ -13,15 +13,6 @@ namespace Models
}
public List<Comment> Responses { get; set; }
public void DownVote()
{
throw new NotImplementedException();
}
public void UpVote()
{
throw new NotImplementedException();
}
public override string ToString()
{
return $"{Author.Name} ({PublishDate.ToString("g")}) - {Description.PadRight(100).Trim()}";

View File

@@ -7,11 +7,6 @@ namespace Models
{
public abstract class GenericMessage
{
private User author;
private string description;
private string title;
private DateTime publishDate;
protected GenericMessage(int id, User author, string description, string title, DateTime publishDate)
{
ID = id;
@@ -31,23 +26,19 @@ namespace Models
public User Author
{
get => author;
set => author = value;
get;set;
}
public string Description
{
get => description;
set => description = value;
get;set;
}
public string Title
{
get => title;
set => title = value;
get;set;
}
public DateTime PublishDate
{
get => publishDate;
set => publishDate = value;
get; set;
}
}
}

View File

@@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Models
{
public interface IVotable
{
void UpVote()
{
throw new NotImplementedException();
}
void DownVote()
{
throw new NotImplementedException();
}
}
}

View File

@@ -9,10 +9,6 @@ namespace Models
{
public class User
{
private int id;
private string name;
private string password;
private UserRole role;
public User(int id, string name, string password, UserRole role)
{
@@ -23,29 +19,26 @@ namespace Models
}
public User()
{
}
public int ID
{
get; private set;
get; set;
}
public string Name
{
get => name;
set => name = value;
get; set;
}
[DataType(DataType.Password)]
public string Password
{
get => password;
set => password = value;
get; set;
}
public UserRole Role
{
get => role;
set => role = value;
get; set;
}
public override string ToString()
{

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Tests
{
[TestClass]
public class AnnouncementManagerTest
{
}
}

View File

@@ -0,0 +1,60 @@
using Logic;
using Models;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace Tests.Mocks
{
public class AnnouncementRepositoryFake : IAnnouncementRepository
{
private List<Announcement> announcements;
private int currentId;
public AnnouncementRepositoryFake()
{
announcements = new List<Announcement>();
currentId = 1;
}
public void CreateAnnouncement(string title, string description, User author, DateTime publishDate, bool isImportant, bool isSticky)
{
announcements.Add(new Announcement(currentId, author, description, title, publishDate, isImportant, isSticky));
currentId++;
}
public void DeleteAnnouncement(int id)
{
announcements.RemoveAt(id--);
}
public List<Announcement> GetAllAnnouncements()
{
return announcements;
}
public Announcement GetAnnouncementById(int id)
{
return announcements.FirstOrDefault(x => x.ID == id);
}
public List<Announcement> GetAnnouncementsByPage(int p, int c)
{
return announcements.GetRange(p + c, c);
}
public void UpdateAnnouncement(int id, string title, string description, bool isImportant, bool isSticky)
{
Announcement announcement = announcements.First(x => x.ID == id);
announcement.Title = title;
announcement.Description = description;
announcement.IsImportant = isImportant;
announcement.IsSticky = isSticky;
}
}
}

View File

@@ -0,0 +1,64 @@
using Models;
using Logic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Tests.Mocks
{
public class UserRepositoryFake : IUserRepository
{
private List<User> users;
private int currentId;
public UserRepositoryFake()
{
users = new List<User>();
currentId = 1;
}
public User CreateUser(string name, string password, UserRole role)
{
User user = new User(currentId, name, password, role);
users.Add(user);
currentId++;
return user;
}
public void DisableUser(int id)
{
User user = users.First(x => x.ID == id);
user.Name = $"Deleted User {user.ID}";
user.Password = "0";
}
public List<User> GetAllUsers()
{
return users;
}
public User GetUserById(int id)
{
return users.First(x => x.ID == id);
}
public User GetUserByName(string userName)
{
return users.FirstOrDefault(x => x.Name == userName);
}
public List<User> GetUsersByPage(int p, int c)
{
return users.GetRange(p + c, c);
}
public void UpdateUser(int id, string name, string password, UserRole role)
{
User user = users.First(x => x.ID == id);
user.Name = name;
user.Password = password;
user.Role = role;
}
}
}

View File

@@ -16,4 +16,9 @@
<PackageReference Include="coverlet.collector" Version="3.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Logic\Logic.csproj" />
<ProjectReference Include="..\Models\Models.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,11 +0,0 @@
namespace Tests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
}
}
}

View File

@@ -0,0 +1,153 @@
using BCrypt.Net;
using Logic;
using Models;
using Tests.Mocks;
namespace Tests
{
[TestClass]
public class UserManagerTest
{
// constants
readonly string USER_NAME = "user";
readonly string USER_PASSWORD = "password";
[TestMethod]
public void AuthenticatedUserWrongPasswordTest()
{
// Arrange
UserManager userManager = new UserManager(new UserRepositoryFake());
// Act
userManager.CreateUser(USER_NAME, BCrypt.Net.BCrypt.HashPassword(USER_PASSWORD), UserRole.TENANT);
User? result = userManager.AuthenticatedUser(USER_NAME, "incorrect");
// Assert
Assert.IsNull(result);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void AuthenticatedUserNullPasswordTest()
{
// Arrange
UserManager userManager = new UserManager(new UserRepositoryFake());
// Act
userManager.CreateUser(USER_NAME, BCrypt.Net.BCrypt.HashPassword(USER_PASSWORD), UserRole.TENANT);
userManager.AuthenticatedUser(USER_NAME, null);
// Assert
// ArgumentNullException expected
}
[TestMethod]
public void AuthenticatedUserWrongNameTest()
{
// Arrange
UserManager userManager = new UserManager(new UserRepositoryFake());
userManager.CreateUser(USER_NAME, USER_PASSWORD, UserRole.TENANT);
// Act
User? result = userManager.AuthenticatedUser("incorrect", USER_PASSWORD);
// Assert
Assert.IsNull(result);
}
[TestMethod]
public void AuthenticatedUserCorrectDetailsTest()
{
// Arrange
UserManager userManager = new UserManager(new UserRepositoryFake());
User user = userManager.CreateUser(USER_NAME, BCrypt.Net.BCrypt.HashPassword(USER_PASSWORD), UserRole.TENANT);
// Act
User? result = userManager.AuthenticatedUser(USER_NAME, USER_PASSWORD);
// Assert
Assert.AreEqual(user, result);
}
[TestMethod]
public void CreateUserCorrectDetailsTest()
{
// Arrange
UserManager userManager = new UserManager(new UserRepositoryFake());
int userId = 1;
string hashedPassword = BCrypt.Net.BCrypt.HashPassword(USER_PASSWORD);
User user = new User(userId, USER_NAME, hashedPassword, UserRole.TENANT);
// Act
User result = userManager.CreateUser(USER_NAME, hashedPassword, UserRole.TENANT);
// Assert
Assert.AreEqual(user.ID, result.ID);
Assert.AreEqual(user.Name, result.Name);
Assert.AreEqual(user.Password, result.Password);
Assert.AreEqual(user.Role, result.Role);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void CreateUserDuplicateNameTest()
{
// Arrange
UserManager userManager = new UserManager(new UserRepositoryFake());
// Act
userManager.CreateUser(USER_NAME, BCrypt.Net.BCrypt.HashPassword(USER_PASSWORD), UserRole.TENANT);
userManager.CreateUser(USER_NAME, BCrypt.Net.BCrypt.HashPassword(USER_PASSWORD), UserRole.TENANT);
// Assert
// ArgumentException expected
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void CreateUserEmptyNameTest()
{
// Arrange
UserManager userManager = new UserManager(new UserRepositoryFake());
// Act
userManager.CreateUser("", BCrypt.Net.BCrypt.HashPassword(USER_PASSWORD), UserRole.TENANT);
// Assert
// ArgumentException expected
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void CreateUserEmptyPasswordTest()
{
// Arrange
UserManager userManager = new UserManager(new UserRepositoryFake());
// Act
userManager.CreateUser(USER_NAME, "", UserRole.TENANT);
// Assert
// ArgumentException expected
}
[TestMethod]
public void DisableUserTest()
{
// Arrange
UserManager userManager = new UserManager(new UserRepositoryFake());
User user = userManager.CreateUser(USER_NAME, BCrypt.Net.BCrypt.HashPassword(USER_PASSWORD), UserRole.TENANT);
// Act
userManager.DisableUser(user.ID);
// Assert
Assert.AreEqual(user.Name, $"Deleted User {user.ID}");
Assert.AreEqual(user.Password, "0");
}
}
}

View File

@@ -3,16 +3,23 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Logic;
using Models;
using Data;
namespace WebApp.Pages
{
[Authorize]
public class AnnouncementModel : PageModel
{
private readonly IAnnouncementRepository _announcementRepository;
public AnnouncementModel(IAnnouncementRepository announcementRepository)
{
_announcementRepository = announcementRepository;
}
public void OnGet(int id)
{
AnnouncementManager announcementManager = new AnnouncementManager();
ViewData.Add("announcement", announcementManager.GetAllAnnouncements().Where(x => x.ID == id).First());
AnnouncementManager announcementManager = new AnnouncementManager(_announcementRepository);
ViewData.Add("announcement", announcementManager.GetAllAnnouncements().First(x => x.ID == id));
}
}
}

View File

@@ -4,12 +4,26 @@
@model WebApp.Pages.AnnouncementsModel
@{
ViewData["Title"] = "Announcements";
List<Announcement> announcements = ((List<Announcement>)ViewData["announcements"]).OrderByDescending(x => x.PublishDate).OrderByDescending(x => x.IsSticky).ToList();
int currentPage = @Convert.ToInt32(ViewData["page"]);
List<Announcement> announcements = ((List<Announcement>)ViewData["announcements"]).OrderByDescending(x => x.IsSticky).ToList();
int currentPage = 0;
if (ViewData["page"] != null)
{
currentPage = Convert.ToInt32(ViewData["page"]);
}
}
<a href="./CreateAnnouncement" class="btn btn-primary">Create new announcement</a>
<form method="get">
<div class="form-actions no-color">
<p>
<input type="hidden" name="handler" value="search" />
<input type="text" name="s" />
<input type="submit" asp-page-handler="Search" value="Search" class="btn btn-default" />
</p>
</div>
</form>
<div class="mb-3 mt-3">
@foreach (Announcement announcement in announcements)
{
@@ -27,7 +41,9 @@
}
</div>
<nav aria-label="Page navigation">
@if (ViewData["page"] != null)
{
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
@if (currentPage <= 1)
{
@@ -57,4 +73,5 @@
@: </li>
}
</ul>
</nav>
</nav>
}

View File

@@ -11,21 +11,33 @@ namespace WebApp.Pages
public class AnnouncementsModel : PageModel
{
public AnnouncementManager AnnouncementManager { get; set; }
public void OnGet(int? p, int? c)
private readonly IAnnouncementRepository _announcementRepository;
public AnnouncementsModel(IAnnouncementRepository announcementRepository)
{
AnnouncementManager = new AnnouncementManager();
if (p == null || p < 1)
_announcementRepository = announcementRepository;
}
public void OnGet(int? p, int? c) // page, count
{
AnnouncementManager = new AnnouncementManager(_announcementRepository);
if (!(p < 0))
{
p = 1;
}
if (c == null || c < 1)
if (!(c < 1))
{
c = 10;
}
ViewData.Add("announcements", AnnouncementManager.GetAnnouncementsByPage(p - 1, c));
ViewData.Add("announcements", AnnouncementManager.GetAnnouncementsByPage(p.Value - 1, c.Value));
ViewData.Add("page", p);
ViewData.Add("count", c);
ViewData.Add("allCount", AnnouncementManager.GetAllAnnouncements().Count());
ViewData.Add("allCount", AnnouncementManager.GetAllAnnouncements().Count);
}
public void OnGetSearch(string s) // search
{
AnnouncementManager = new AnnouncementManager(_announcementRepository);
ViewData.Add("announcements", AnnouncementManager.SearchAnnouncements(s));
}
}
}

View File

@@ -1,3 +1,4 @@
using Data;
using Logic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -29,7 +30,7 @@ namespace WebApp.Pages
}
public void OnPost()
{
UserManager userManager = new UserManager();
UserManager userManager = new UserManager(new UserRepository());
User user = userManager.GetUserById(int.Parse(User.FindFirstValue("id")));
if (NewPassword == null)
{

View File

@@ -1,3 +1,4 @@
using Data;
using Logic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -17,8 +18,8 @@ namespace WebApp.Pages
}
public IActionResult OnPost()
{
AnnouncementManager announcementManager = new AnnouncementManager();
UserManager userManager = new UserManager();
AnnouncementManager announcementManager = new AnnouncementManager(new AnnouncementRepository());
UserManager userManager = new UserManager(new UserRepository());
User user = userManager.GetUserById(int.Parse(User.FindFirstValue("id")));
announcementManager.CreateAnnouncement(Announcement.Title, Announcement.Description, user, DateTime.Now, Announcement.IsImportant, Announcement.IsSticky);
return RedirectToPage("Announcements");

View File

@@ -10,11 +10,17 @@ namespace WebApp.Pages
[Authorize]
public class DeleteAnnouncementModel : PageModel
{
private readonly IAnnouncementRepository _announcementRepository;
public DeleteAnnouncementModel(IAnnouncementRepository announcementRepository)
{
_announcementRepository = announcementRepository;
}
[BindProperty]
public int AnnouncementId { get; set; }
public void OnGet(int id)
{
AnnouncementManager announcementManager = new AnnouncementManager();
AnnouncementManager announcementManager = new AnnouncementManager(_announcementRepository);
if (id != null)
{
Announcement announcement = announcementManager.GetAnnouncementById(id);
@@ -26,7 +32,7 @@ namespace WebApp.Pages
}
public IActionResult OnPost()
{
AnnouncementManager announcementManager = new AnnouncementManager();
AnnouncementManager announcementManager = new AnnouncementManager(_announcementRepository);
announcementManager.DeleteAnnouncement(AnnouncementId);
return RedirectToPage("Announcements");
}

View File

@@ -10,11 +10,17 @@ namespace WebApp.Pages
[Authorize]
public class EditAnnouncementModel : PageModel
{
private readonly IAnnouncementRepository _announcementRepository;
public EditAnnouncementModel(IAnnouncementRepository announcementRepository)
{
_announcementRepository = announcementRepository;
}
[BindProperty]
public Announcement Announcement { get; set; }
public void OnGet(int id)
{
AnnouncementManager announcementManager = new AnnouncementManager();
AnnouncementManager announcementManager = new AnnouncementManager(_announcementRepository);
if (id != null)
{
Announcement announcement = announcementManager.GetAnnouncementById(id);
@@ -26,7 +32,7 @@ namespace WebApp.Pages
}
public IActionResult OnPost()
{
AnnouncementManager announcementManager = new AnnouncementManager();
AnnouncementManager announcementManager = new AnnouncementManager(_announcementRepository);
announcementManager.UpdateAnnouncement(Announcement.ID, Announcement.Title, Announcement.Description, Announcement.IsImportant, Announcement.IsSticky);
return RedirectToPage("Announcements");
}

View File

@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication.Cookies;
using Models;
using Logic;
using System.Security.Claims;
using Data;
namespace WebApp.Pages
{
@@ -19,7 +20,7 @@ namespace WebApp.Pages
public IActionResult OnPost(string? returnUrl)
{
var userManager = new UserManager();
var userManager = new UserManager(new UserRepository());
User? user = userManager.AuthenticatedUser(MyUser.Name, MyUser.Password);
if (user != null)
{

View File

@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Logic;
using Models;
using Data;
namespace WebApp.Pages
{
@@ -14,8 +15,8 @@ namespace WebApp.Pages
}
public void OnPost()
{
var userManager = new UserManager();
if (userManager.CreateUser(MyUser.Name, BCrypt.Net.BCrypt.HashPassword(MyUser.Password), MyUser.Role))
var userManager = new UserManager(new UserRepository());
if (userManager.CreateUser(MyUser.Name, BCrypt.Net.BCrypt.HashPassword(MyUser.Password), MyUser.Role) != null)
{
ViewData["confirm"] = $"Successfully registered {MyUser.Name}!";
}

View File

@@ -1,3 +1,5 @@
using Data;
using Logic;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace WebApp
@@ -16,6 +18,10 @@ namespace WebApp
options.AccessDeniedPath = new PathString("/Error/401");
});
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<ICommentRepository, CommentRepository>();
builder.Services.AddScoped<IAnnouncementRepository, AnnouncementRepository>();
var app = builder.Build();
// Configure the HTTP request pipeline.

View File

@@ -22,6 +22,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Data\Data.csproj" />
<ProjectReference Include="..\Logic\Logic.csproj" />
<ProjectReference Include="..\Models\Models.csproj" />
</ItemGroup>

View File

@@ -1,4 +1,5 @@
using Logic;
using Data;
using Logic;
using Models;
using System;
using System.Collections.Generic;
@@ -56,7 +57,7 @@ namespace WinForms
private void btnSave_Click(object sender, EventArgs e)
{
AnnouncementManager announcementManager = new AnnouncementManager();
AnnouncementManager announcementManager = new AnnouncementManager(new AnnouncementRepository());
if (string.IsNullOrEmpty(tbTitle.Text))
{
MessageBox.Show("Please enter a title");
@@ -114,7 +115,7 @@ namespace WinForms
if (MessageBox.Show($"Are you sure you want to delete\n{currentComment.Title}\nCreated at {currentComment.PublishDate.ToString("g")} by {currentComment.Author.Name}",
"Delete announcement", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
CommentManager commentManager = new CommentManager();
CommentManager commentManager = new CommentManager(new CommentRepository());
commentManager.DeleteCommentOnAnnouncement(currentComment.ID, announcement.ID);
}
RefreshComments();

View File

@@ -1,4 +1,5 @@
using Logic;
using Data;
using Logic;
using Models;
using System;
using System.Collections.Generic;
@@ -43,13 +44,10 @@ namespace WinForms
btnViewComment.Enabled = false;
}
if (comment != null && currentUser != null)
if (comment != null && currentUser != null && currentUser.ID != comment.Author.ID)
{
if (currentUser.ID != comment.Author.ID)
{
// restriction: only edit personal comments
readOnly = true;
}
// restriction: only edit personal comments
readOnly = true;
}
if (readOnly)
@@ -71,7 +69,7 @@ namespace WinForms
}
private void btnSave_Click(object sender, EventArgs e)
{
CommentManager commentManager = new CommentManager();
CommentManager commentManager = new CommentManager(new CommentRepository());
if (string.IsNullOrEmpty(tbTitle.Text) || string.IsNullOrEmpty(tbDescription.Text))
{
MessageBox.Show("Please enter a title and comment text");
@@ -135,7 +133,7 @@ namespace WinForms
if (MessageBox.Show($"Are you sure you want to delete\n{currentResponse.Title}\nCreated at {currentResponse.PublishDate.ToString("g")} by {currentResponse.Author.Name}",
"Delete announcement", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
CommentManager commentManager = new CommentManager();
CommentManager commentManager = new CommentManager(new CommentRepository());
commentManager.DeleteResponseOnComment(currentResponse.ID, comment.ID);
}
RefreshComments();

View File

@@ -1,4 +1,5 @@
using Logic;
using Data;
using Logic;
using Models;
using System;
using System.Collections.Generic;
@@ -70,7 +71,7 @@ namespace WinForms
User currentUser = (User)lbUsers.SelectedItem;
if (MessageBox.Show($"Are you sure you want to delete\n{currentUser.Name}\n{currentUser.Role}", "Delete user", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
UserManager userManager = new UserManager();
UserManager userManager = new UserManager(new UserRepository());
userManager.DisableUser(currentUser.ID);
}
}
@@ -107,13 +108,13 @@ namespace WinForms
private void RefreshLists()
{
UserManager userManager = new UserManager();
UserManager userManager = new UserManager(new UserRepository());
lbUsers.Items.Clear();
foreach (User _user in userManager.GetAllUsers())
{
lbUsers.Items.Add(_user);
}
AnnouncementManager announcementManager = new AnnouncementManager();
AnnouncementManager announcementManager = new AnnouncementManager(new AnnouncementRepository());
lbAnnouncements.Items.Clear();
foreach (Announcement announcement in announcementManager.GetAllAnnouncements())
{
@@ -145,7 +146,7 @@ namespace WinForms
if (MessageBox.Show($"Are you sure you want to delete\n{currentAnnouncement.Title}\nCreated at {currentAnnouncement.PublishDate.ToString("g")} by {currentAnnouncement.Author.Name}",
"Delete announcement", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
AnnouncementManager announcementManager = new AnnouncementManager();
AnnouncementManager announcementManager = new AnnouncementManager(new AnnouncementRepository());
announcementManager.DeleteAnnouncement(currentAnnouncement.ID);
}
RefreshLists();

View File

@@ -1,3 +1,4 @@
using Data;
using Logic;
using Models;
@@ -12,7 +13,7 @@ namespace WinForms
private void btnLogin_Click(object sender, EventArgs e)
{
UserManager userManager = new UserManager();
UserManager userManager = new UserManager(new UserRepository());
User? user = userManager.AuthenticatedUser(tbUsername.Text, tbPassword.Text);
if (user == null)
{

View File

@@ -1,4 +1,5 @@
using Logic;
using Data;
using Logic;
using Models;
using System;
using System.Collections.Generic;
@@ -41,7 +42,7 @@ namespace WinForms
private void btnSave_Click(object sender, EventArgs e)
{
UserManager userManager = new UserManager();
UserManager userManager = new UserManager(new UserRepository());
if (string.IsNullOrEmpty(tbUsername.Text) || string.IsNullOrEmpty(tbPassword.Text) || cbUserRole.SelectedIndex == -1)
{
MessageBox.Show("Please enter data in all fields");

View File

@@ -10,6 +10,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Data\Data.csproj" />
<ProjectReference Include="..\Logic\Logic.csproj" />
<ProjectReference Include="..\Models\Models.csproj" />
</ItemGroup>