diff --git a/Chirp/src/Chirp.Core/AuthorService.cs b/Chirp/src/Chirp.Core/AuthorService.cs index c9bc4457..090f5e1b 100644 --- a/Chirp/src/Chirp.Core/AuthorService.cs +++ b/Chirp/src/Chirp.Core/AuthorService.cs @@ -2,7 +2,6 @@ namespace Chirp.Core; - public interface IAuthorService { public List GetFollows(string username); @@ -11,13 +10,14 @@ public interface IAuthorService public bool Unfollow(string currentUser, string userToUnFollow); public bool MakeFollowersUnfollow(string username); } + public class AuthorService(IAuthorRepository db) : IAuthorService { public List GetFollows(string username) { return db.GetAuthorFollows(username).Result; } - + public List GetFollowers(string username) { return db.GetAuthorFollowers(username).Result; @@ -27,12 +27,12 @@ public bool MakeFollowersUnfollow(string username) { return db.MakeFollowersUnfollow(username).Result; } - + public bool Follow(string currentUser, string userToFollow) { return db.Follow(currentUser, userToFollow).Result; } - + public bool Unfollow(string currentUser, string userToUnFollow) { return db.UnFollow(currentUser, userToUnFollow).Result; diff --git a/Chirp/src/Chirp.Core/CheepService.cs b/Chirp/src/Chirp.Core/CheepService.cs index 37c0a97f..980d8090 100644 --- a/Chirp/src/Chirp.Core/CheepService.cs +++ b/Chirp/src/Chirp.Core/CheepService.cs @@ -11,6 +11,10 @@ public interface ICheepService public int GetAmountOfCheepPages(int pageSize); public int GetAmountOfCheepPagesFromAuthors(IEnumerable authors, int pageSize); public bool CreateCheep(CheepDTO cheep); + public bool AddCommentToCheep(CommentDTO comment); + public int GetCommentAmountOnCheep(int? cheepId); + public CheepDTO GetCheepFromId(int cheepId); + public List GetCommentsFromCheep(int cheepId); } public class CheepService(ICheepRepository db) : ICheepService @@ -19,8 +23,9 @@ public List GetCheepsByPage(int page, int pageSize) { ArgumentOutOfRangeException.ThrowIfNegative(pageSize); - var result = db.GetCheepsByPage(page, pageSize).Result ?? throw new ArgumentNullException(nameof(db.GetCheepsByPage)); - + var result = db.GetCheepsByPage(page, pageSize).Result ?? + throw new ArgumentNullException(nameof(db.GetCheepsByPage)); + return result.ToList(); } @@ -33,7 +38,7 @@ public List GetCheepsFromAuthorByPage(string author, int page, int pag { return db.GetCheepsFromAuthorByPage(author, page, pageSize).Result.ToList(); } - + public List GetCheepsFromAuthorsByPage(IEnumerable authors, int page, int pageSize) { return db.GetCheepsFromAuthorsByPage(authors, page, pageSize).Result.ToList(); @@ -53,4 +58,24 @@ public bool CreateCheep(CheepDTO cheep) { return db.CreateCheep(cheep).Result; } + + public bool AddCommentToCheep(CommentDTO comment) + { + return db.AddCommentToCheep(comment).Result; + } + + public int GetCommentAmountOnCheep(int? cheepId) + { + return db.GetCommentAmountOnCheep(cheepId).Result; + } + + public CheepDTO GetCheepFromId(int cheepId) + { + return db.GetCheepById(cheepId).Result; + } + + public List GetCommentsFromCheep(int cheepId) + { + return db.GetCommentsForCheep(cheepId).Result.ToList(); + } } \ No newline at end of file diff --git a/Chirp/src/Chirp.Core/DTO/CheepDTO.cs b/Chirp/src/Chirp.Core/DTO/CheepDTO.cs index e3ab9614..84804c8b 100644 --- a/Chirp/src/Chirp.Core/DTO/CheepDTO.cs +++ b/Chirp/src/Chirp.Core/DTO/CheepDTO.cs @@ -1,7 +1,8 @@ namespace Chirp.Core.DTO; -public class CheepDTO(string author, string message, long unixTimestamp) +public class CheepDTO(int? id, string author, string message, long unixTimestamp) { + public int? Id { get; set; } = id; public string Author { get; set; } = author; public string Message { get; set; } = message; public long UnixTimestamp { get; set; } = unixTimestamp; diff --git a/Chirp/src/Chirp.Core/DTO/CommentDTO.cs b/Chirp/src/Chirp.Core/DTO/CommentDTO.cs new file mode 100644 index 00000000..9f4b9296 --- /dev/null +++ b/Chirp/src/Chirp.Core/DTO/CommentDTO.cs @@ -0,0 +1,9 @@ +namespace Chirp.Core.DTO; + +public class CommentDTO(string author, int cheepId, string message, long unixTimestamp) +{ + public string Author { get; set; } = author; + public int CheepId { get; set; } = cheepId; + public string Message { get; set; } = message; + public long UnixTimestamp { get; set; } = unixTimestamp; +} \ No newline at end of file diff --git a/Chirp/src/Chirp.Core/IAuthorRepository.cs b/Chirp/src/Chirp.Core/IAuthorRepository.cs index 8765441b..4a33404c 100644 --- a/Chirp/src/Chirp.Core/IAuthorRepository.cs +++ b/Chirp/src/Chirp.Core/IAuthorRepository.cs @@ -8,7 +8,7 @@ public interface IAuthorRepository public Task AddAuthor(AuthorDTO author); public Task> GetAuthorFollows(string username); public Task> GetAuthorFollowers(string username); - public Task Follow (string currentUser, string userToFollow); - public Task UnFollow (string currentUser, string userToUnFollow); + public Task Follow(string currentUser, string userToFollow); + public Task UnFollow(string currentUser, string userToUnFollow); public Task MakeFollowersUnfollow(string user); } \ No newline at end of file diff --git a/Chirp/src/Chirp.Core/ICheepRepository.cs b/Chirp/src/Chirp.Core/ICheepRepository.cs index ffd6b5ef..64f1a97e 100644 --- a/Chirp/src/Chirp.Core/ICheepRepository.cs +++ b/Chirp/src/Chirp.Core/ICheepRepository.cs @@ -1,4 +1,5 @@ -using Chirp.Core.DTO; +using System.Collections; +using Chirp.Core.DTO; namespace Chirp.Core; @@ -11,4 +12,8 @@ public interface ICheepRepository public Task CreateCheep(CheepDTO cheep); public Task GetAmountOfCheeps(); public Task GetAmountOfCheepsFromAuthors(IEnumerable authors); + public Task AddCommentToCheep(CommentDTO comment); + public Task GetCommentAmountOnCheep(int? cheepId); + public Task GetCheepById(int cheepId); + public Task> GetCommentsForCheep(int cheepId); } \ No newline at end of file diff --git a/Chirp/src/Chirp.Infrastructure/AuthorRepository.cs b/Chirp/src/Chirp.Infrastructure/AuthorRepository.cs index db43ed29..7fbb741d 100644 --- a/Chirp/src/Chirp.Infrastructure/AuthorRepository.cs +++ b/Chirp/src/Chirp.Infrastructure/AuthorRepository.cs @@ -43,7 +43,7 @@ public async Task> GetAuthorFollowers(string username) .Where(a => a.NormalizedUserName == username.ToUpper()) .Select(a => a.Followers) .FirstOrDefaultAsync() ?? new List(); - + return followers.Select(a => new AuthorDTO(a.UserName!, a.Email!)).ToList(); } @@ -53,10 +53,10 @@ public async Task Follow(string currentUser, string userToFollow) if (!await UserExists(currentUser)) throw new UserDoesNotExist(); if (!await UserExists(userToFollow)) throw new UserDoesNotExist("User to follow does not exist"); if (await DoesFollow(currentUser, userToFollow)) return false; - + var authorToFollow = await GetAuthor(userToFollow); GetAuthor(currentUser).Result.Following.Add(authorToFollow); - + await context.SaveChangesAsync(); return true; } @@ -69,17 +69,17 @@ public async Task UnFollow(string currentUser, string userToUnfollow) if (!await UserExists(currentUser)) throw new UserDoesNotExist(); if (!await UserExists(userToUnfollow)) throw new UserDoesNotExist("User to unfollow does not exist"); if (!await DoesFollow(currentUser, userToUnfollow)) return false; - + var authorToUnfollow = await GetAuthor(userToUnfollow); context.Authors .Where(a => a.NormalizedUserName == currentUser) .Include(a => a.Following) .FirstOrDefault()!.Following.Remove(authorToUnfollow); - + await context.SaveChangesAsync(); return true; } - + private async Task GetAuthor(string username) { username = username.ToUpper(); @@ -98,14 +98,14 @@ private async Task DoesFollow(string currentUser, string userToFollow) { currentUser = currentUser.ToUpper(); userToFollow = userToFollow.ToUpper(); - var list = await context.Authors - .Where(a => a.NormalizedUserName == currentUser) - .Select(a => a.Following).FirstOrDefaultAsync(); - - return list != null && list.Any(a => a.NormalizedUserName == userToFollow); + var list = await context.Authors + .Where(a => a.NormalizedUserName == currentUser) + .Select(a => a.Following).FirstOrDefaultAsync(); + + return list != null && list.Any(a => a.NormalizedUserName == userToFollow); } - private async Task> GetFollowing(string currentUser) + private async Task> GetFollowing(string currentUser) { currentUser = currentUser.ToUpper(); return await context.Authors diff --git a/Chirp/src/Chirp.Infrastructure/CheepRepository.cs b/Chirp/src/Chirp.Infrastructure/CheepRepository.cs index ab88a386..a47390fe 100644 --- a/Chirp/src/Chirp.Infrastructure/CheepRepository.cs +++ b/Chirp/src/Chirp.Infrastructure/CheepRepository.cs @@ -12,9 +12,9 @@ public class CheepRepository(ChirpDBContext context) : ICheepRepository public async Task?> GetCheepsByPage(int page, int pageSize) { if (pageSize < 0) return null; - + var query = context.Cheeps - .Select(cheep => new { cheep.Author.UserName, cheep.Message, cheep.TimeStamp }) + .Select(cheep => new { cheep.Id, cheep.Author.UserName, cheep.Message, cheep.TimeStamp }) .OrderByDescending(cheep => cheep.TimeStamp) .Skip((page - 1) * pageSize) .Take(pageSize); @@ -22,7 +22,8 @@ public class CheepRepository(ChirpDBContext context) : ICheepRepository var cheeps = await query.ToListAsync(); return cheeps.Select(cheep => - new CheepDTO(cheep.UserName!, cheep.Message, new DateTimeOffset(cheep.TimeStamp).ToUnixTimeSeconds())); + new CheepDTO(cheep.Id, cheep.UserName!, cheep.Message, + new DateTimeOffset(cheep.TimeStamp).ToUnixTimeSeconds())); } public async Task> GetCheepsFromAuthorByPage(string author, int page, int pageSize) @@ -35,20 +36,22 @@ public async Task> GetCheepsFromAuthor(string author) author = author.ToUpper(); var query = context.Cheeps .Where(cheep => cheep.Author.NormalizedUserName == author) - .Select(cheep => new { cheep.Author.UserName, cheep.Message, cheep.TimeStamp }) + .Select(cheep => new { cheep.Id, cheep.Author.UserName, cheep.Message, cheep.TimeStamp }) .OrderByDescending(cheep => cheep.TimeStamp); var cheeps = await query.ToListAsync(); return cheeps.Select(cheep => - new CheepDTO(cheep.UserName!, cheep.Message, new DateTimeOffset(cheep.TimeStamp).ToUnixTimeSeconds())); + new CheepDTO(cheep.Id, cheep.UserName!, cheep.Message, + new DateTimeOffset(cheep.TimeStamp).ToUnixTimeSeconds())); } - - public async Task> GetCheepsFromAuthorsByPage(IEnumerable authors, int page, int pageSize) + + public async Task> GetCheepsFromAuthorsByPage(IEnumerable authors, int page, + int pageSize) { authors = authors.Select(author => author.ToUpper()); var query = context.Cheeps .Where(cheep => authors.Contains(cheep.Author.NormalizedUserName!)) - .Select(cheep => new { cheep.Author.UserName, cheep.Message, cheep.TimeStamp }) + .Select(cheep => new { cheep.Id, cheep.Author.UserName, cheep.Message, cheep.TimeStamp }) .OrderByDescending(cheep => cheep.TimeStamp) .Skip((page - 1) * pageSize) .Take(pageSize); @@ -56,7 +59,8 @@ public async Task> GetCheepsFromAuthorsByPage(IEnumerable< var cheeps = await query.ToListAsync(); return cheeps.Select(cheep => - new CheepDTO(cheep.UserName!, cheep.Message, new DateTimeOffset(cheep.TimeStamp).ToUnixTimeSeconds())); + new CheepDTO(cheep.Id, cheep.UserName!, cheep.Message, + new DateTimeOffset(cheep.TimeStamp).ToUnixTimeSeconds())); } public Task GetAmountOfCheeps() @@ -77,9 +81,70 @@ public async Task CreateCheep(CheepDTO cheep) .Where(a => a.UserName == cheep.Author) .FirstOrDefaultAsync(); if (author == null) return false; - var cheep2 = new Cheep {Author = author, Message = cheep.Message, TimeStamp = DateTimeOffset.FromUnixTimeSeconds(cheep.UnixTimestamp).DateTime}; + var cheep2 = new Cheep + { + Author = author, Message = cheep.Message, + TimeStamp = DateTimeOffset.FromUnixTimeSeconds(cheep.UnixTimestamp).DateTime + }; context.Cheeps.Add(cheep2); await context.SaveChangesAsync(); return true; } + + public async Task AddCommentToCheep(CommentDTO comment) + { + var author = await context.Authors + .Where(a => a.UserName == comment.Author) + .FirstOrDefaultAsync(); + if (author == null) return false; + var cheep = await context.Cheeps + .Where(c => c.Id == comment.CheepId) + .Include(c => c.Comments) + .FirstOrDefaultAsync(); + if (cheep == null) return false; + var newComment = new Comment + { + Author = author, Cheep = cheep, Message = comment.Message, + TimeStamp = DateTimeOffset.FromUnixTimeSeconds(comment.UnixTimestamp).DateTime + }; + cheep.Comments.Add(newComment); + await context.SaveChangesAsync(); + return true; + } + + public async Task GetCommentAmountOnCheep(int? cheepId) + { + if (cheepId == null) return 0; + var query = await context.Cheeps + .Include(c => c.Comments) + .Where(c => c.Id == cheepId) + .FirstOrDefaultAsync(); + var commentCount = query!.Comments.Count; + return commentCount; + } + + public async Task GetCheepById(int cheepId) + { + var cheep = await context.Cheeps + .Where(c => c.Id == cheepId) + .Select(cheep => new { cheep.Id, cheep.Author.UserName, cheep.Message, cheep.TimeStamp }) + .FirstOrDefaultAsync(); + + return new CheepDTO(cheep!.Id, cheep.UserName!, cheep.Message, + new DateTimeOffset(cheep.TimeStamp).ToUnixTimeSeconds()); + ; + } + + public async Task> GetCommentsForCheep(int cheepId) + { + var query = await context.Cheeps + .Where(c => c.Id == cheepId) + .Include(c => c.Comments) + .ThenInclude(comment => comment.Author) + .FirstOrDefaultAsync(); + var comments = query!.Comments.OrderByDescending(c => c.TimeStamp); + return comments.Select(comment => + new CommentDTO(comment.Author.UserName!, comment.Id, comment.Message, + new DateTimeOffset(comment.TimeStamp).ToUnixTimeSeconds())); + } } \ No newline at end of file diff --git a/Chirp/src/Chirp.Infrastructure/Chirp.Infrastructure.csproj b/Chirp/src/Chirp.Infrastructure/Chirp.Infrastructure.csproj index 705e30cb..36b90add 100644 --- a/Chirp/src/Chirp.Infrastructure/Chirp.Infrastructure.csproj +++ b/Chirp/src/Chirp.Infrastructure/Chirp.Infrastructure.csproj @@ -7,12 +7,12 @@ - + - - + + diff --git a/Chirp/src/Chirp.Infrastructure/ChirpDbContext.cs b/Chirp/src/Chirp.Infrastructure/ChirpDbContext.cs index 8ce020b9..1915dca4 100644 --- a/Chirp/src/Chirp.Infrastructure/ChirpDbContext.cs +++ b/Chirp/src/Chirp.Infrastructure/ChirpDbContext.cs @@ -8,6 +8,7 @@ public class ChirpDBContext(DbContextOptions options) : Identity { public DbSet Cheeps { get; set; } public DbSet Authors { get; set; } + public DbSet Comments { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -51,6 +52,27 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .WithMany(a => a.Followers); }); - + //define comment entity constraints + modelBuilder.Entity(entity => + { + entity.HasKey((c => c.Id)); + entity.Property(c => c.Id) + .ValueGeneratedOnAdd(); + + entity.Property(c => c.Message) + .IsRequired() + .HasMaxLength(160); + entity.Property(c => c.TimeStamp) + .IsRequired(); + entity.HasOne(c => c.Author) + .WithMany(a => a.Comments) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(c => c.Cheep) + .WithMany(c => c.Comments) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + }); } } \ No newline at end of file diff --git a/Chirp/src/Chirp.Infrastructure/Migrations/20241127124139_AddedComments.Designer.cs b/Chirp/src/Chirp.Infrastructure/Migrations/20241127124139_AddedComments.Designer.cs new file mode 100644 index 00000000..a6a3d1df --- /dev/null +++ b/Chirp/src/Chirp.Infrastructure/Migrations/20241127124139_AddedComments.Designer.cs @@ -0,0 +1,406 @@ +// +using System; +using Chirp.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Chirp.Infrastructure.Migrations +{ + [DbContext(typeof(ChirpDBContext))] + [Migration("20241127124139_AddedComments")] + partial class AddedComments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("AuthorAuthor", b => + { + b.Property("FollowersId") + .HasColumnType("TEXT"); + + b.Property("FollowingId") + .HasColumnType("TEXT"); + + b.HasKey("FollowersId", "FollowingId"); + + b.HasIndex("FollowingId"); + + b.ToTable("AuthorAuthor"); + }); + + modelBuilder.Entity("Chirp.Infrastructure.Model.Author", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Chirp.Infrastructure.Model.Cheep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("TEXT"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Cheeps"); + }); + + modelBuilder.Entity("Chirp.Infrastructure.Model.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CheepId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("TEXT"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("CheepId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AuthorAuthor", b => + { + b.HasOne("Chirp.Infrastructure.Model.Author", null) + .WithMany() + .HasForeignKey("FollowersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Chirp.Infrastructure.Model.Author", null) + .WithMany() + .HasForeignKey("FollowingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Chirp.Infrastructure.Model.Cheep", b => + { + b.HasOne("Chirp.Infrastructure.Model.Author", "Author") + .WithMany("Cheeps") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Chirp.Infrastructure.Model.Comment", b => + { + b.HasOne("Chirp.Infrastructure.Model.Author", "Author") + .WithMany("Comments") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Chirp.Infrastructure.Model.Cheep", "Cheep") + .WithMany("Comments") + .HasForeignKey("CheepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Cheep"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Chirp.Infrastructure.Model.Author", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Chirp.Infrastructure.Model.Author", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Chirp.Infrastructure.Model.Author", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Chirp.Infrastructure.Model.Author", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Chirp.Infrastructure.Model.Author", b => + { + b.Navigation("Cheeps"); + + b.Navigation("Comments"); + }); + + modelBuilder.Entity("Chirp.Infrastructure.Model.Cheep", b => + { + b.Navigation("Comments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Chirp/src/Chirp.Infrastructure/Migrations/20241127124139_AddedComments.cs b/Chirp/src/Chirp.Infrastructure/Migrations/20241127124139_AddedComments.cs new file mode 100644 index 00000000..05b31ddb --- /dev/null +++ b/Chirp/src/Chirp.Infrastructure/Migrations/20241127124139_AddedComments.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Chirp.Infrastructure.Migrations +{ + /// + public partial class AddedComments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Comments", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AuthorId = table.Column(type: "TEXT", nullable: false), + CheepId = table.Column(type: "INTEGER", nullable: false), + TimeStamp = table.Column(type: "TEXT", nullable: false), + Message = table.Column(type: "TEXT", maxLength: 160, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Comments", x => x.Id); + table.ForeignKey( + name: "FK_Comments_AspNetUsers_AuthorId", + column: x => x.AuthorId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Comments_Cheeps_CheepId", + column: x => x.CheepId, + principalTable: "Cheeps", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Comments_AuthorId", + table: "Comments", + column: "AuthorId"); + + migrationBuilder.CreateIndex( + name: "IX_Comments_CheepId", + table: "Comments", + column: "CheepId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Comments"); + } + } +} diff --git a/Chirp/src/Chirp.Infrastructure/Migrations/ChirpDBContextModelSnapshot.cs b/Chirp/src/Chirp.Infrastructure/Migrations/ChirpDBContextModelSnapshot.cs index 886c3472..44c750b3 100644 --- a/Chirp/src/Chirp.Infrastructure/Migrations/ChirpDBContextModelSnapshot.cs +++ b/Chirp/src/Chirp.Infrastructure/Migrations/ChirpDBContextModelSnapshot.cs @@ -128,6 +128,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Cheeps"); }); + modelBuilder.Entity("Chirp.Infrastructure.Model.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CheepId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("TEXT"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("CheepId"); + + b.ToTable("Comments"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { b.Property("Id") @@ -286,6 +316,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Author"); }); + modelBuilder.Entity("Chirp.Infrastructure.Model.Comment", b => + { + b.HasOne("Chirp.Infrastructure.Model.Author", "Author") + .WithMany("Comments") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Chirp.Infrastructure.Model.Cheep", "Cheep") + .WithMany("Comments") + .HasForeignKey("CheepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Cheep"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) @@ -340,6 +389,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Chirp.Infrastructure.Model.Author", b => { b.Navigation("Cheeps"); + + b.Navigation("Comments"); + }); + + modelBuilder.Entity("Chirp.Infrastructure.Model.Cheep", b => + { + b.Navigation("Comments"); }); #pragma warning restore 612, 618 } diff --git a/Chirp/src/Chirp.Infrastructure/Model/Author.cs b/Chirp/src/Chirp.Infrastructure/Model/Author.cs index 069046d7..a7794260 100644 --- a/Chirp/src/Chirp.Infrastructure/Model/Author.cs +++ b/Chirp/src/Chirp.Infrastructure/Model/Author.cs @@ -8,6 +8,7 @@ public sealed class Author : IdentityUser public List Cheeps { get; set; } = []; public List Following { get; set; } = []; public List Followers { get; set; } = []; + public List Comments { get; set; } = []; public Author() { diff --git a/Chirp/src/Chirp.Infrastructure/Model/Cheep.cs b/Chirp/src/Chirp.Infrastructure/Model/Cheep.cs index 38771a42..a305ea57 100644 --- a/Chirp/src/Chirp.Infrastructure/Model/Cheep.cs +++ b/Chirp/src/Chirp.Infrastructure/Model/Cheep.cs @@ -4,14 +4,14 @@ namespace Chirp.Infrastructure.Model; public class Cheep { - public int Id { get; set; } - public required string Message { get; set; } - public DateTime TimeStamp { get; set; } + public int Id { get; set; } + public required string Message { get; set; } + public DateTime TimeStamp { get; set; } public required Author Author { get; set; } - + public List Comments { get; set; } = []; + public Cheep() { - } public Cheep(int id, string message, DateTime timeStamp, Author author) diff --git a/Chirp/src/Chirp.Infrastructure/Model/Comment.cs b/Chirp/src/Chirp.Infrastructure/Model/Comment.cs new file mode 100644 index 00000000..65d98398 --- /dev/null +++ b/Chirp/src/Chirp.Infrastructure/Model/Comment.cs @@ -0,0 +1,23 @@ +namespace Chirp.Infrastructure.Model; + +public class Comment +{ + public int Id { get; set; } + public required Author Author { get; set; } + public required Cheep Cheep { get; set; } + public DateTime TimeStamp { get; set; } + public required string Message { get; set; } + + public Comment() + { + } + + public Comment(int id, Author author, Cheep cheep, string message, DateTime timeStamp) + { + Id = id; + Author = author; + Cheep = cheep; + Message = message; + TimeStamp = timeStamp; + } +} \ No newline at end of file diff --git a/Chirp/src/Chirp.Web/Pages/Shared/Components/CheepList/Default.cshtml b/Chirp/src/Chirp.Web/Pages/Shared/Components/CheepList/Default.cshtml index 84d01615..cbc3d9dd 100644 --- a/Chirp/src/Chirp.Web/Pages/Shared/Components/CheepList/Default.cshtml +++ b/Chirp/src/Chirp.Web/Pages/Shared/Components/CheepList/Default.cshtml @@ -1,6 +1,9 @@ @using Chirp.Core @using Chirp.Core.DTO @inject IAuthorService service +@inject ICheepService CheepService + +private @{ var targetPage = ViewBag.TargetPage as string ?? ""; IEnumerable cheeps = ViewBag.Cheeps; @@ -11,6 +14,21 @@ } } + +
    @foreach (var cheep in cheeps) { @@ -32,7 +50,14 @@

    @cheep.Message

    - — @DateTimeOffset.FromUnixTimeSeconds(cheep.UnixTimestamp).DateTime.ToString("dd/MM/yy H:mm:ss") +
    + — @DateTimeOffset.FromUnixTimeSeconds(cheep.UnixTimestamp).DateTime.ToString("dd/MM/yy H:mm:ss") + + Comment Icon + + @CheepService.GetCommentAmountOnCheep(cheep.Id) +
    } -
\ No newline at end of file + diff --git a/Chirp/src/Chirp.Web/Pages/Shared/TimeLinePageModel.cs b/Chirp/src/Chirp.Web/Pages/Shared/TimeLinePageModel.cs index b26ef461..24b22295 100644 --- a/Chirp/src/Chirp.Web/Pages/Shared/TimeLinePageModel.cs +++ b/Chirp/src/Chirp.Web/Pages/Shared/TimeLinePageModel.cs @@ -16,24 +16,26 @@ public abstract class TimeLinePageModel(ICheepService cheepService) : PageModel public int LastPageNumber = 1; protected const int PageSize = 32; - [BindProperty] - public MessageModel MessageModel { get; set; } = new MessageModel(); - + [BindProperty] public MessageModel MessageModel { get; set; } = new MessageModel(); + public ActionResult OnPost(string? author, [FromQuery] int page) { - if (string.IsNullOrWhiteSpace(MessageModel.Message)) { + if (string.IsNullOrWhiteSpace(MessageModel.Message)) + { ModelState.AddModelError("Message", "Message cannot be empty"); } - - if (!ModelState.IsValid) { + + if (!ModelState.IsValid) + { LoadCheeps(page); return Page(); } - + var dt = (DateTimeOffset)DateTime.UtcNow; if (User.Identity != null) { - var cheep = new CheepDTO ( + var cheep = new CheepDTO( + null, User.Identity.Name ?? "no name", MessageModel.Message!, dt.ToUnixTimeSeconds() @@ -49,5 +51,24 @@ public ActionResult OnPost(string? author, [FromQuery] int page) return RedirectToPage(null); //redirects to the same page } + public IActionResult OnPostComment(string author, int cheepId, string comment) + { + var dt = DateTimeOffset.UtcNow; + var commentDTO = new CommentDTO + ( + author, + cheepId, + comment, + dt.ToUnixTimeSeconds() + ); + + if (!CheepService.AddCommentToCheep(commentDTO)) + { + throw new ApplicationException("Failed to add comment"); + } + + return RedirectToPage(null); + } + protected abstract void LoadCheeps(int page); } \ No newline at end of file diff --git a/Chirp/src/Chirp.Web/Pages/SpecificCheep.cshtml b/Chirp/src/Chirp.Web/Pages/SpecificCheep.cshtml new file mode 100644 index 00000000..98f8b697 --- /dev/null +++ b/Chirp/src/Chirp.Web/Pages/SpecificCheep.cshtml @@ -0,0 +1,60 @@ +@page +@model Chirp.Web.Pages.SpecificCheep + +@{ + ViewData["Title"] = "Chirp!"; + Layout = "Shared/_Layout"; + var cheep = Model.Cheep; + var comments = Model.Comments; +} + +
+ + @cheep.Author + +

@cheep.Message

+
+
+ Comment Icon + @Model.CommentCount +
+
+ @if (User.Identity!.IsAuthenticated) + { +
+
+ + + + + +
+
+ } + else + { +

Log in to comment on this Cheep

+ } + +
+
    + @foreach (var comment in comments) + { +
  • +
    + + @comment.Author + + - + @Model.TimeSinceComment(comment.UnixTimestamp) + +
    +

    @comment.Message

    +
  • + } +
+
+
diff --git a/Chirp/src/Chirp.Web/Pages/SpecificCheep.cshtml.cs b/Chirp/src/Chirp.Web/Pages/SpecificCheep.cshtml.cs new file mode 100644 index 00000000..8d0d116d --- /dev/null +++ b/Chirp/src/Chirp.Web/Pages/SpecificCheep.cshtml.cs @@ -0,0 +1,81 @@ +using Chirp.Core; +using Chirp.Core.DTO; +using Chirp.Web.Pages.BindingModels; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Chirp.Web.Pages; + +public class SpecificCheep(ICheepService cheepService) : PageModel +{ + public CheepDTO Cheep { get; set; } = null!; + public int CommentCount { get; set; } + public List Comments { get; set; } = []; + + [BindProperty] public MessageModel MessageModel { get; set; } = new MessageModel(); + + public void OnGet(int cheepId) + { + Cheep = cheepService.GetCheepFromId(cheepId); + CommentCount = cheepService.GetCommentAmountOnCheep(cheepId); + Comments = cheepService.GetCommentsFromCheep(cheepId); + } + + public string TimeSinceComment(long timeStamp) + { + var utcThen = DateTimeOffset.FromUnixTimeSeconds(timeStamp); + var localTimeZone = TimeZoneInfo.Local; + var then = TimeZoneInfo.ConvertTime(utcThen, localTimeZone); + var now = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, localTimeZone); + + var timesince = (now - then) switch + { + { TotalMinutes: < 60 } ts => $"{ts.Minutes} minutes ago", + { TotalHours: < 24 } ts => $"{ts.Hours} hours ago", + { TotalDays: < 2 } => $"yesterday", + { TotalDays: < 5 } => $"on {then.DayOfWeek}", + var ts => $"{ts.Days} days ago", + }; + return timesince; + } + + public IActionResult OnPostComment(string author, int cheepId) + { + if (string.IsNullOrWhiteSpace(MessageModel.Message)) + { + ModelState.AddModelError("Message", "Message cannot be empty"); + Reload(cheepId); + return Page(); + } + + if (!ModelState.IsValid) + { + Reload(cheepId); + return Page(); + } + + + var dt = DateTimeOffset.UtcNow; + var commentDTO = new CommentDTO + ( + author, + cheepId, + MessageModel.Message, + dt.ToUnixTimeSeconds() + ); + + if (!cheepService.AddCommentToCheep(commentDTO)) + { + throw new ApplicationException("Failed to add comment"); + } + + return RedirectToPage("/SpecificCheep", new { cheepId = cheepId }); + } + + private void Reload(int cheepId) + { + Cheep = cheepService.GetCheepFromId(cheepId); + CommentCount = cheepService.GetCommentAmountOnCheep(cheepId); + Comments = cheepService.GetCommentsFromCheep(cheepId); + } +} \ No newline at end of file diff --git a/Chirp/src/Chirp.Web/wwwroot/images/comment.png b/Chirp/src/Chirp.Web/wwwroot/images/comment.png new file mode 100644 index 00000000..f974e87e Binary files /dev/null and b/Chirp/src/Chirp.Web/wwwroot/images/comment.png differ diff --git a/Chirp/test/PlaywrightTests/UITests/CommentTests.cs b/Chirp/test/PlaywrightTests/UITests/CommentTests.cs new file mode 100644 index 00000000..16bd9406 --- /dev/null +++ b/Chirp/test/PlaywrightTests/UITests/CommentTests.cs @@ -0,0 +1,71 @@ +using Microsoft.Playwright; +using PlaywrightTests.Utils; +using PlaywrightTests.Utils.PageTests; + +namespace PlaywrightTests.UITests; + +public class CommentTests : PageTestWithRazorPlaywrightWebApplicationFactory +{ + private TestAuthor _testAuthor; + + [SetUp] + public async Task SetUp() + { + _testAuthor = new TestAuthorBuilder(RazorFactory.GetUserManager()) + .WithUsernameAndEmail("author") + .Create(); + await GenerateCheep(_testAuthor.author); + } + + [Test] + public async Task SpecificCheepViewExists() + { + await GenerateCheep(_testAuthor.author, "this is author"); + await Page.GotoAsync("/"); + await Page.Locator("#messagelist div").Filter(new() { HasText = "—" }).GetByRole(AriaRole.Link).First + .ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Link, new() { Name = "author" })).ToBeVisibleAsync(); + } + + [Test] + public async Task CommentCountUpdates() + { + await GenerateCheep(_testAuthor.author, "this is author"); + await RazorPageUtils.Login(_testAuthor); + await Page.GotoAsync("/"); + await Expect(Page.Locator("li").Filter(new() { HasText = "author" }).Locator("small").Nth(1)) + .ToHaveTextAsync("0"); + await Page.Locator("#messagelist div").Filter(new() { HasText = "—" }).GetByRole(AriaRole.Link).First + .ClickAsync(); + await Page.GetByPlaceholder("Write a comment").ClickAsync(); + await Page.GetByPlaceholder("Write a comment").FillAsync("This is a comment"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Post" }).ClickAsync(); + await Page.GetByText("This is a comment").ClickAsync(); + await Expect(Page.GetByText("1", new() { Exact = true })).ToHaveTextAsync("1"); + } + + [Test] + public async Task TestAddComment() + { + await GenerateCheep(_testAuthor.author, "this is author"); + await RazorPageUtils.Login(_testAuthor); + await Page.GotoAsync("/"); + await Page.Locator("#messagelist div").Filter(new() { HasText = "—" }).GetByRole(AriaRole.Link).First + .ClickAsync(); + await Page.GetByPlaceholder("Write a comment").ClickAsync(); + await Page.GetByPlaceholder("Write a comment").FillAsync("This is a comment"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Post" }).ClickAsync(); + await Expect(Page.GetByText("This is a comment")).ToBeVisibleAsync(); + } + + [Test] + public async Task CannotPostCommentsWhenNotLoggedIn() + { + await GenerateCheep(_testAuthor.author, "this is author"); + await Page.GotoAsync("/"); + await Page.Locator("#messagelist div").Filter(new() { HasText = "—" }).GetByRole(AriaRole.Link).First + .ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Log in to comment on this" })) + .ToBeVisibleAsync(); + } +} \ No newline at end of file diff --git a/Chirp/test/PlaywrightTests/Utils/PageTests/PageTestWithRazorPlaywrightWebApplicationFactory.cs b/Chirp/test/PlaywrightTests/Utils/PageTests/PageTestWithRazorPlaywrightWebApplicationFactory.cs index 147511c8..daa267cc 100644 --- a/Chirp/test/PlaywrightTests/Utils/PageTests/PageTestWithRazorPlaywrightWebApplicationFactory.cs +++ b/Chirp/test/PlaywrightTests/Utils/PageTests/PageTestWithRazorPlaywrightWebApplicationFactory.cs @@ -30,6 +30,7 @@ public override BrowserNewContextOptions ContextOptions() public void RazorSetup() { _razorClient = RazorFactory.CreateClient(); + // Note this does not make a new database it just wipes the data RazorFactory.ResetDB(); RazorPageUtils = new PageUtils(Page); } diff --git a/Chirp/test/RepositoryTests/CheepRepositoryUnitTest.cs b/Chirp/test/RepositoryTests/CheepRepositoryUnitTest.cs index fa75125c..573f7cd5 100644 --- a/Chirp/test/RepositoryTests/CheepRepositoryUnitTest.cs +++ b/Chirp/test/RepositoryTests/CheepRepositoryUnitTest.cs @@ -2,8 +2,6 @@ using Chirp.Core.DTO; using Chirp.Infrastructure; using Chirp.Infrastructure.Model; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; using RepositoryTests.Utils; using TestUtilities; @@ -14,7 +12,7 @@ public class CheepRepositoryUnitTest { protected ChirpDBContext Context { get; } protected ICheepRepository CheepRepository { get; } - + public CheepRepositoryUnitTest(InMemoryDBFixture fixture) { fixture.ResetDatabase(); @@ -60,7 +58,7 @@ public async Task GetCheepsByPage_NegativePageSize_ReturnsNull() //arrange var author = TestUtils.CreateTestAuthor("Mr. test"); var cheep = new Cheep { Author = author, Message = "test", TimeStamp = DateTime.Now }; - + Context.Cheeps.Add(cheep); await Context.SaveChangesAsync(); @@ -124,7 +122,7 @@ public async Task GetCheepsByPage_ReturnsCorrectSingleCheep() var author = TestUtils.CreateTestAuthor("Mr. test"); var timeStamp = new DateTime(2000, 01, 01); var cheep = new Cheep { Author = author, Message = "test", TimeStamp = timeStamp }; - + Context.Cheeps.Add(cheep); await Context.SaveChangesAsync(); @@ -135,7 +133,8 @@ public async Task GetCheepsByPage_ReturnsCorrectSingleCheep() Assert.NotNull(result); var resultList = result.ToList(); - var expected = new CheepDTO(author.UserName!, cheep.Message, ((DateTimeOffset)timeStamp).ToUnixTimeSeconds()); + var expected = new CheepDTO(null, author.UserName!, cheep.Message, + ((DateTimeOffset)timeStamp).ToUnixTimeSeconds()); Assert.Single(resultList); var singleCheep = resultList.ElementAt(0); @@ -159,7 +158,7 @@ public async Task GetCheepsFromAuthorByPage_ReturnsCorrectCheepWhenMultipleAutho { var author = TestUtils.CreateTestAuthor($"name{i}"); authors.Add(author); - + var cheep = new Cheep { Author = author, Message = $"test{i}", TimeStamp = DateTime.Now }; cheeps.Add(cheep); } @@ -167,7 +166,7 @@ public async Task GetCheepsFromAuthorByPage_ReturnsCorrectCheepWhenMultipleAutho Context.Authors.AddRange(authors); Context.Cheeps.AddRange(cheeps); await Context.SaveChangesAsync(); - + for (int i = 0; i < 5; i++) { //act @@ -188,13 +187,13 @@ public async Task GetCheepsFromAuthor_ReturnsAllCheeps() List cheeps = []; for (int i = 0; i < 5; i++) { - cheeps.Add(new Cheep{ Author = author, Message = "test" + i, TimeStamp = DateTime.Now }); + cheeps.Add(new Cheep { Author = author, Message = "test" + i, TimeStamp = DateTime.Now }); } Context.Authors.Add(author); Context.Cheeps.AddRange(cheeps); await Context.SaveChangesAsync(); - + var result = (await CheepRepository.GetCheepsFromAuthor(author.UserName!)).ToList(); cheeps.Reverse(); Assert.Equal(5, result.Count); @@ -202,8 +201,6 @@ public async Task GetCheepsFromAuthor_ReturnsAllCheeps() { Assert.Equal(cheeps.ElementAt(i).Message, result.ElementAt(i).Message); } - - } [Fact] @@ -217,7 +214,7 @@ public async Task GetCheepsFromAuthorByPage_ReturnsNoCheepsForNonexistentAuthor( { var author = TestUtils.CreateTestAuthor($"name{i}"); authors.Add(author); - + var cheep = new Cheep { Author = author, Message = $"test{i}", TimeStamp = DateTime.Now }; cheeps.Add(cheep); } @@ -239,7 +236,7 @@ public async Task GetCheepsFromAuthorByPage_ReturnsAllCheepsFromAuthorForLargeNu //arrange var authorA = TestUtils.CreateTestAuthor("Mr. test"); var authorB = TestUtils.CreateTestAuthor("Mr. fake"); - + var authTotal = 0; for (int i = 0; i < 100; i++) @@ -287,7 +284,7 @@ public async Task CreateCheep() await Context.SaveChangesAsync(); var unixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - var newCheep = new CheepDTO(author.UserName!, "message", unixTimestamp); + var newCheep = new CheepDTO(null, author.UserName!, "message", unixTimestamp); //act var result = await CheepRepository.CreateCheep(newCheep); @@ -308,11 +305,11 @@ public async Task GetCheepsFromAuthorsByPage_ReturnsAllCheepsFromAuthorsFromMult var author2 = TestUtils.CreateTestAuthor("Mr. Test2"); List authors = new(); - - + + authors.Add(author1.UserName!); authors.Add(author2.UserName!); - + for (var i = 0; i < 100; i++) { if (i % 2 == 0) @@ -326,12 +323,12 @@ public async Task GetCheepsFromAuthorsByPage_ReturnsAllCheepsFromAuthorsFromMult Context.Cheeps.Add(cheep); } } - + await Context.SaveChangesAsync(); - + int pageNo = 1; int totalCount = 0; - + //Act while (true) { @@ -342,7 +339,7 @@ public async Task GetCheepsFromAuthorsByPage_ReturnsAllCheepsFromAuthorsFromMult totalCount += count; pageNo++; } - + //Assert Assert.Equal(100, totalCount); } @@ -367,12 +364,12 @@ public async Task GetCheepsFromAuthorsByPage_ReturnsNoCheepsFromAuthorWithNoChee cheeps.Add(cheep1); cheeps.Add(cheep2); } - + await Context.SaveChangesAsync(); - + //Act var result = await CheepRepository.GetCheepsFromAuthorsByPage(authors, 1, 20); - + //Assert Assert.Empty(result.ToList()); } @@ -388,22 +385,124 @@ public async Task GetCheepsFromAuthorsByPage_ReturnsInChronologicalOrder() for (var i = 0; i < 5; i++) { - var cheep = new Cheep { Author = author1, Message = $"{i}", TimeStamp = DateTimeOffset.FromUnixTimeSeconds(i * 60).DateTime }; + var cheep = new Cheep + { Author = author1, Message = $"{i}", TimeStamp = DateTimeOffset.FromUnixTimeSeconds(i * 60).DateTime }; Context.Cheeps.Add(cheep); } - + await Context.SaveChangesAsync(); - + //Act var result = await CheepRepository.GetCheepsFromAuthorsByPage(authors, 1, 20); var resultArray = result.ToArray(); //Assert Assert.True(resultArray[0].UnixTimestamp >= resultArray[1].UnixTimestamp); - + for (var i = 1; i < resultArray.ToList().Count; i++) { - Assert.True(resultArray[i-1].UnixTimestamp >= resultArray[i].UnixTimestamp); + Assert.True(resultArray[i - 1].UnixTimestamp >= resultArray[i].UnixTimestamp); } } + + #region comments + + [Fact] + public async Task GetCheepById_ReturnsCorrectCheep() + { + // Arrange TEST nr 100 lez gooo + var author = TestUtils.CreateTestAuthor("mr. test"); + var cheep = new Cheep + { + Id = 0, + Message = "test", + TimeStamp = DateTime.Now, + Author = author + }; + + Context.Authors.Add(author); + Context.Cheeps.Add(cheep); + await Context.SaveChangesAsync(); + + var result = await CheepRepository.GetCheepById(cheep.Id); + Assert.NotNull(result); + Assert.Equal(cheep.Id, result.Id); + Assert.Equal(cheep.Message, result.Message); + } + + [Fact] + public async Task GetCommentAmountOnCheep_ReturnsCorrectAmount() + { + var author = TestUtils.CreateTestAuthor("mr. test"); + var author2 = TestUtils.CreateTestAuthor("mr. comment"); + var cheep = new Cheep + { + Id = 0, + Message = "test", + TimeStamp = DateTime.Now, + Author = author + }; + Context.Authors.Add(author); + Context.Authors.Add(author2); + Context.Cheeps.Add(cheep); + await Context.SaveChangesAsync(); + + var comment1 = new CommentDTO(author2.UserName!, cheep.Id, "test comment", 1234); + await CheepRepository.AddCommentToCheep(comment1); + var count = await CheepRepository.GetCommentAmountOnCheep(cheep.Id); + Assert.Equal(1, count); + + comment1 = new CommentDTO(author2.UserName!, cheep.Id, "test comment", 1234); + await CheepRepository.AddCommentToCheep(comment1); + count = await CheepRepository.GetCommentAmountOnCheep(cheep.Id); + Assert.Equal(2, count); + } + + [Fact] + public async Task AddCommentToCheep_AddsCommentToCheep() + { + var author = TestUtils.CreateTestAuthor("mr. test"); + var author2 = TestUtils.CreateTestAuthor("mr. comment"); + var cheep = new Cheep + { + Id = 0, + Message = "test", + TimeStamp = DateTime.Now, + Author = author + }; + Context.Authors.Add(author); + Context.Authors.Add(author2); + Context.Cheeps.Add(cheep); + await Context.SaveChangesAsync(); + var comment1 = new CommentDTO(author2.UserName!, cheep.Id, "test comment", 1234); + await CheepRepository.AddCommentToCheep(comment1); + Assert.True(Context.Comments.Count() == 1); + } + + [Fact] + public async Task GetCommentsFromCheep_ReturnsCorrectComments() + { + var author = TestUtils.CreateTestAuthor("mr. test"); + var author2 = TestUtils.CreateTestAuthor("mr. comment"); + var cheep = new Cheep + { + Id = 0, + Message = "test", + TimeStamp = DateTime.Now, + Author = author + }; + Context.Authors.Add(author); + Context.Authors.Add(author2); + Context.Cheeps.Add(cheep); + await Context.SaveChangesAsync(); + var comment1 = new CommentDTO(author2.UserName!, cheep.Id, "test comment", 1234); + await CheepRepository.AddCommentToCheep(comment1); + var comments = await CheepRepository.GetCommentsForCheep(cheep.Id); + var first = comments.First(); + Assert.Single(comments); + Assert.Equal(comment1.Message, first.Message); + Assert.Equal(comment1.Author, first.Author); + } + + #endregion } \ No newline at end of file diff --git a/Chirp/test/Web.UnitTests/PageModelUnitTests.cs b/Chirp/test/Web.UnitTests/PageModelUnitTests.cs index fe0c31a2..9c75c1dc 100644 --- a/Chirp/test/Web.UnitTests/PageModelUnitTests.cs +++ b/Chirp/test/Web.UnitTests/PageModelUnitTests.cs @@ -17,7 +17,9 @@ public void PublicTimeLine() for (int i = 0; i < 10; i++) { cheeps.Add( - new CheepDTO("mr. test", + new CheepDTO( + null, + "mr. test", "test", new DateTimeOffset(DateTime.Now.AddHours(i)).ToUnixTimeSeconds())); } diff --git a/Chirp/test/Web.UnitTests/RazorPageIntegrationTest.cs b/Chirp/test/Web.UnitTests/RazorPageIntegrationTest.cs index 51e0898d..a0af89b5 100644 --- a/Chirp/test/Web.UnitTests/RazorPageIntegrationTest.cs +++ b/Chirp/test/Web.UnitTests/RazorPageIntegrationTest.cs @@ -1,9 +1,7 @@ -using System.Globalization; -using Chirp.Core; +using Chirp.Core; using Chirp.Core.DTO; using Moq; using Web.UnitTests.Utils; -using Program = Chirp.Web.Program; namespace Web.UnitTests; @@ -19,7 +17,9 @@ public async void DisplayCheeps_On_PublicTimeline() for (int i = 0; i < 10; i++) { cheeps.Add( - new CheepDTO("mr. " + i + "test", + new CheepDTO( + null, + "mr. " + i + "test", "test: " + i + "test!", new DateTimeOffset(DateTime.Now.AddHours(i)).ToUnixTimeSeconds())); }