diff --git a/app/src/components/ComplaintCard.tsx b/app/src/components/ComplaintCard.tsx
index ed2ac8e..477d98a 100644
--- a/app/src/components/ComplaintCard.tsx
+++ b/app/src/components/ComplaintCard.tsx
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
import {
Zap, Hammer, Trash2, Pencil, X, Check, Calendar, MapPin, BedDouble,
MessageSquare, Wrench, GitBranch, ChevronRight, UserCircle, Clock,
+ Send, AlertCircle,
} from 'lucide-react';
import { POST_PLACES } from '../constants/models';
@@ -10,17 +11,11 @@ import { POST_PLACES } from '../constants/models';
export type Role = 'faculty' | 'warden' | 'centrehead';
-export interface CommentAuthor {
- id: number;
- email: string;
- position: string;
-}
-
export interface ComplaintComment {
id: number;
comment_text: string;
- author_id: number;
- Author?: CommentAuthor;
+ email: string;
+ role: string;
created_at: string;
}
@@ -57,6 +52,9 @@ interface ComplaintCardProps {
onCancelEdit: () => void;
onSaveEdit: (postId: number) => void;
onDelete: (postId: number) => void;
+ // Called after the author successfully posts a comment, so the parent can
+ // refetch and surface the new comment.
+ onCommentPosted?: () => void;
}
// ── Status config ──────────────────────────────────────────────────────────────
@@ -195,7 +193,7 @@ function CommentsList({ comments }: { comments: ComplaintComment[] }) {
return (
{comments.map((c, idx) => {
- const author = c.Author?.position ? roleLabel(c.Author.position) : `Admin #${c.author_id}`;
+ const author = c.role ? roleLabel(c.role) : 'Staff';
const initials = author.split(' ').map((w) => w[0]).slice(0, 2).join('').toUpperCase();
return (
@@ -208,7 +206,10 @@ function CommentsList({ comments }: { comments: ComplaintComment[] }) {
-
{author}
+
+ {author}
+ {c.email && {c.email}}
+
{formatDateTime(c.created_at)}
@@ -225,6 +226,74 @@ function CommentsList({ comments }: { comments: ComplaintComment[] }) {
);
}
+// ── Comment composer (post author only) ──────────────────────────────────────────
+
+function CommentComposer({ role, postId, onPosted }: { role: Role; postId: number; onPosted?: () => void }) {
+ const [text, setText] = useState('');
+ const [submitting, setBusy] = useState(false);
+ const [error, setError] = useState(null);
+
+ const disabled = submitting || !text.trim();
+
+ async function submit() {
+ const content = text.trim();
+ if (!content) return;
+ setBusy(true);
+ setError(null);
+ try {
+ const res = await fetch(`/api/post/${role}/comment/${postId}`, {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ Content: content }),
+ });
+ if (!res.ok) {
+ let msg = `Failed to post comment (${res.status})`;
+ try { const b = await res.json(); if (b?.error) msg = b.error; } catch {}
+ throw new Error(msg);
+ }
+ setText('');
+ onPosted?.();
+ } catch (err) {
+ setError((err as Error).message);
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ return (
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+}
+
// ── Modal ──────────────────────────────────────────────────────────────────────
interface ModalProps extends ComplaintCardProps {
@@ -233,7 +302,7 @@ interface ModalProps extends ComplaintCardProps {
function PostModal({
post, role, isEditing, isBusy, editForm,
- onEditFormChange, onStartEdit, onCancelEdit, onSaveEdit, onDelete, onClose,
+ onEditFormChange, onStartEdit, onCancelEdit, onSaveEdit, onDelete, onClose, onCommentPosted,
}: ModalProps) {
const isFaculty = role === 'faculty';
const isWarden = role === 'warden';
@@ -375,6 +444,7 @@ function PostModal({
)}
+
)}
@@ -388,7 +458,7 @@ function PostModal({
export function ComplaintCard({
post, role, isEditing, isBusy, editForm,
- onEditFormChange, onStartEdit, onCancelEdit, onSaveEdit, onDelete,
+ onEditFormChange, onStartEdit, onCancelEdit, onSaveEdit, onDelete, onCommentPosted,
}: ComplaintCardProps) {
const [modalOpen, setModalOpen] = useState(false);
@@ -489,6 +559,7 @@ export function ComplaintCard({
onSaveEdit={(id) => { onSaveEdit(id); }}
onDelete={(id) => { onDelete(id); setModalOpen(false); }}
onClose={() => { setModalOpen(false); if (isEditing) onCancelEdit(); }}
+ onCommentPosted={onCommentPosted}
/>
)}
>
diff --git a/app/src/pages/admin/AdminPostView.tsx b/app/src/pages/admin/AdminPostView.tsx
index a39aa2d..bf24890 100644
--- a/app/src/pages/admin/AdminPostView.tsx
+++ b/app/src/pages/admin/AdminPostView.tsx
@@ -44,17 +44,11 @@ interface CentreHeadAuthor {
phone_number: string;
}
-interface CommentAuthor {
- id: number;
- email: string;
- position: string;
-}
-
interface Comment {
id: number;
comment_text: string;
- author_id: number;
- Author?: CommentAuthor;
+ email: string;
+ role: string;
created_at: string;
}
@@ -110,6 +104,7 @@ type Post = FacultyPost | WardenPost | CentreHeadPost;
interface ApiResponse {
success: string;
post: Post;
+ position: string;
}
// ── Helpers ────────────────────────────────────────────────────────────────────
@@ -217,11 +212,13 @@ function Detail({ label, value }: { label: string; value?: string }) {
export function AdminPostView() {
const { role, post_id } = useParams<{ role: string; post_id: string }>();
- const adminPosition = sessionStorage.getItem('adminPosition') ?? '';
- const adminType = adminPosition.startsWith('XEN') ? 'xen' : adminPosition.startsWith('AE') ? 'ae' : adminPosition.startsWith('JE') ? 'je' : '';
const navigate = useNavigate();
const [post, setPost] = useState
(null);
+ // Admin position comes from the server (AdminGetPost response) rather than
+ // per-tab sessionStorage, so action gating survives new tabs / direct nav.
+ const [adminPosition, setAdminPosition] = useState('');
+ const adminType = adminPosition.startsWith('XEN') ? 'xen' : adminPosition.startsWith('AE') ? 'ae' : adminPosition.startsWith('JE') ? 'je' : '';
const [loading, setLoading] = useState(true);
const [error, setError] = useState<{ message: string; status?: number } | null>(null);
@@ -247,6 +244,7 @@ export function AdminPostView() {
})
.then((json: ApiResponse) => {
setPost(json.post);
+ setAdminPosition(json.position ?? '');
if (!silent) setLoading(false);
})
.catch((err: Error & { status?: number }) => {
@@ -463,11 +461,12 @@ export function AdminPostView() {
) : (
diff --git a/handlers/admin_comment.go b/handlers/admin_comment.go
index 6090d91..7c2026a 100644
--- a/handlers/admin_comment.go
+++ b/handlers/admin_comment.go
@@ -27,7 +27,7 @@ type CommentType struct {
// AdminPost comment allow any admin comment on any type of post.
// Common for all type of admins and posts.
-func (h *AdminHandler) AdminPostComment (c *gin.Context) {
+func (h *AdminHandler) AdminPostComment(c *gin.Context) {
// verify the admin
emailID, exists := c.Get(middleware.EmailKey)
if !exists {
@@ -89,7 +89,8 @@ func (h *AdminHandler) AdminPostComment (c *gin.Context) {
CommentableID: uint(postID),
CommentableType: postType,
Content : inputs.Content,
- AuthorID: admin.ID,
+ Email: admin.Email,
+ Role: string(admin.Position),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
diff --git a/handlers/admin_post.go b/handlers/admin_post.go
index 0a143a9..71ec26d 100644
--- a/handlers/admin_post.go
+++ b/handlers/admin_post.go
@@ -12,7 +12,7 @@ import (
// GetXENPosts fetch posts (all that were posted) where status of post is
// Pending_XEN PENDING_AE PENDING_JE Resolved_JE Resolved Closed
-func (h *AdminHandler) GetXENPosts (c *gin.Context) {
+func (h *AdminHandler) GetXENPosts(c *gin.Context) {
emailID, exists := c.Get(middleware.EmailKey)
if !exists {
@@ -86,7 +86,7 @@ func (h *AdminHandler) GetXENPosts (c *gin.Context) {
// GetAEPosts fetch posts where status of post is
// Pending_AE Pending_JE
-func (h* AdminHandler) GetAEPosts (c *gin.Context) {
+func (h* AdminHandler) GetAEPosts(c *gin.Context) {
// get email of user from gin context
email, exists := c.Get(middleware.EmailKey)
@@ -161,7 +161,7 @@ func (h* AdminHandler) GetAEPosts (c *gin.Context) {
// GetJEPosts fetch posts where status of Post is
// Pending_JE Resolved_JE
-func (h* AdminHandler) GetJEPosts (c *gin.Context) {
+func (h* AdminHandler) GetJEPosts(c *gin.Context) {
// get email of user from gin context
email, exists := c.Get(middleware.EmailKey)
@@ -271,11 +271,7 @@ func (h *AdminHandler) AdminGetPost(c *gin.Context) {
result := h.DB.Preload("Author", func (db *gorm.DB) (*gorm.DB) {
return db.Select("id, email, name, house_number, department, phone_number, block, type")
}).
- Preload("Comments", func(db *gorm.DB) (*gorm.DB) {
- return db.Preload("Author", func(d *gorm.DB) (*gorm.DB) {
- return d.Select("id, email, position")
- })
- }).
+ Preload("Comments").
Where("id = ?", postID).Take(&post)
if result.Error != nil {
c.JSON(404, gin.H{"error": "failed to fetch the post"})
@@ -287,11 +283,7 @@ func (h *AdminHandler) AdminGetPost(c *gin.Context) {
result := h.DB.Preload("Author", func (db *gorm.DB) (*gorm.DB) {
return db.Select("id, email, hostel, phone_number")
}).
- Preload("Comments", func(db *gorm.DB) (*gorm.DB) {
- return db.Preload("Author", func(d *gorm.DB) (*gorm.DB) {
- return d.Select("id, email, position")
- })
- }).
+ Preload("Comments").
Where("id = ?", postID).Take(&post)
if result.Error != nil {
c.JSON(404, gin.H{"error": "failed to fetch the post"})
@@ -303,11 +295,7 @@ func (h *AdminHandler) AdminGetPost(c *gin.Context) {
result := h.DB.Preload("Author", func (db *gorm.DB) (*gorm.DB) {
return db.Select("id, email, building, phone_number")
}).
- Preload("Comments", func(db *gorm.DB) (*gorm.DB) {
- return db.Preload("Author", func(d *gorm.DB) (*gorm.DB) {
- return d.Select("id, email, position")
- })
- }).
+ Preload("Comments").
Where("id = ?", postID).Take(&post)
if result.Error != nil {
c.JSON(404, gin.H{"error": "failed to fetch the post"})
@@ -319,5 +307,9 @@ func (h *AdminHandler) AdminGetPost(c *gin.Context) {
return
}
- c.JSON(200, gin.H{"success": "post fetched", "post": reqPost})
+ c.JSON(200, gin.H{
+ "success": "post fetched",
+ "post": reqPost,
+ "position": admin.Position,
+ })
}
diff --git a/handlers/centrehead_post.go b/handlers/centrehead_post.go
index dba8fc3..747b01d 100644
--- a/handlers/centrehead_post.go
+++ b/handlers/centrehead_post.go
@@ -1,10 +1,11 @@
package handlers
import (
+ "errors"
"fmt"
"log"
+ "strconv"
"time"
- "errors"
"github.com/ayush00git/cms-web/middleware"
"github.com/ayush00git/cms-web/models"
@@ -211,11 +212,7 @@ func (h *PostHandler) GetCentreheadPosts(c *gin.Context) {
var posts []models.CentreheadPost
result = h.DB.
- Preload("Comments", func(db *gorm.DB) (*gorm.DB) {
- return db.Preload("Author", func (d *gorm.DB) (*gorm.DB) {
- return d.Select("id, email, position")
- })
- }).
+ Preload("Comments").
Where("centrehead_id = ?", head.ID).
Find(&posts)
if result.Error != nil {
@@ -225,3 +222,75 @@ func (h *PostHandler) GetCentreheadPosts(c *gin.Context) {
c.JSON(200, gin.H{"success": "posts fetched successfully", "posts": posts})
}
+
+
+// CentreheadPostComment allows a user of type centrehead to post
+// comment as the post's author
+func (h *PostHandler) CentreheadPostComment(c *gin.Context) {
+ // get email of the logged in user from the gin context
+ email, _ := c.Get(middleware.EmailKey)
+
+ // check if the user is a type centrehead role
+ var head models.Centrehead
+ result := h.DB.Where("email = ?", email).Take(&head)
+ if result.Error != nil {
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ c.JSON(404, gin.H{"error": "user not found"})
+ return
+ }
+ c.JSON(500, gin.H{"error": "internal server error"})
+ return
+ }
+
+ // get post_id from path parameters
+ postIDString := c.Param("post_id")
+ postIDU64, err := strconv.ParseUint(postIDString, 10, 64)
+ if err != nil {
+ c.JSON(500, gin.H{"error": "failed to parse post_id at the moment"})
+ return
+ }
+
+ // read this post from the db
+ postID := uint(postIDU64)
+ var post models.CentreheadPost
+ result = h.DB.Where("id = ?", postID).Take(&post)
+ if result.Error != nil {
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ c.JSON(404, gin.H{"error": "post not found"})
+ return
+ }
+ c.JSON(500, gin.H{"error": "internal server error"})
+ return
+ }
+
+ // verify the centrehead(logged in user) is the author of the post
+ if head.ID != post.CentreheadID {
+ c.JSON(403, gin.H{"error": "you are not authorized to comment"})
+ return
+ }
+
+ // bind the input
+ var inputs CommentType
+ if err := c.ShouldBindJSON(&inputs); err != nil {
+ c.JSON(401, gin.H{"error": "invalid request body"})
+ return
+ }
+
+ doc := models.Comment{
+ CommentableID: postID,
+ CommentableType: "centrehead_posts",
+ Content: inputs.Content,
+ Email: head.Email,
+ Role: "centrehead",
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ }
+
+ result = h.DB.Create(&doc)
+ if result.Error != nil {
+ c.JSON(500, gin.H{"error": "failed to comment at the moment"})
+ return
+ }
+
+ c.JSON(201, gin.H{"success": "comment posted!"})
+}
diff --git a/handlers/faculty_post.go b/handlers/faculty_post.go
index 2eb29cf..17cc34f 100644
--- a/handlers/faculty_post.go
+++ b/handlers/faculty_post.go
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"log"
+ "strconv"
"time"
"github.com/ayush00git/cms-web/middleware"
@@ -223,11 +224,7 @@ func (h *PostHandler) GetFacultyPosts(c *gin.Context) {
// return posts where author is faculty (the logged in user)
var posts []models.FacultyPost
result = h.DB.
- Preload("Comments", func(db *gorm.DB) (*gorm.DB) {
- return db.Preload("Author", func(d *gorm.DB) (*gorm.DB) {
- return d.Select("id, email, position")
- })
- }).
+ Preload("Comments").
Where("faculty_id = ?", faculty.ID).
Find(&posts)
if result.Error != nil {
@@ -237,3 +234,74 @@ func (h *PostHandler) GetFacultyPosts(c *gin.Context) {
c.JSON(200, gin.H{"success": "posts fetched successfully", "posts": posts})
}
+
+
+// FacultyPostComment allows the author of the post to
+// comment on the post
+func (h *PostHandler) FacultyPostComment(c *gin.Context) {
+ // get email of the logged in user from gin context
+ email, _ := c.Get(middleware.EmailKey)
+
+ // find the user
+ var faculty models.Faculty
+ result := h.DB.Where("email = ?", email).Take(&faculty)
+ if result.Error != nil {
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ c.JSON(404, gin.H{"error": "user does not exists"})
+ return
+ }
+ c.JSON(500, gin.H{"error": "internal server error"});
+ return;
+ }
+
+ // get post id from path parameters
+ postIDString := c.Param("post_id")
+ postIDU64, err := strconv.ParseUint(postIDString, 10, 64)
+ if err != nil {
+ c.JSON(500, gin.H{"error": "failed to parse post id"})
+ return
+ }
+
+ // read database for this postIDU64
+ var post models.FacultyPost
+ result = h.DB.Where("id = ?", uint(postIDU64)).Take(&post)
+ if result.Error != nil {
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ c.JSON(404, gin.H{"error": "post unavailable"})
+ return
+ }
+ c.JSON(500, gin.H{"error": "internal server error"})
+ return
+ }
+
+ // validate the author of the post
+ if post.FacultyID != faculty.ID {
+ c.JSON(403, gin.H{"error": "you are not authorized to comment"})
+ return
+ }
+
+ // bind input to json
+ var inputs CommentType
+ if err := c.ShouldBindJSON(&inputs); err != nil {
+ c.JSON(401, gin.H{"error": "invalid request body"})
+ return
+ }
+
+ doc := models.Comment{
+ CommentableID: uint(postIDU64),
+ CommentableType: "faculty_posts",
+ Content: inputs.Content,
+ Email: faculty.Email,
+ Role: "faculty",
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ }
+
+ result = h.DB.Create(&doc)
+ if result.Error != nil {
+ c.JSON(500, gin.H{"error": "failed to comment at the moment"})
+ return
+ }
+
+ c.JSON(201, gin.H{"success": "comment posted!"})
+}
diff --git a/handlers/warden_post.go b/handlers/warden_post.go
index 51fede3..d598044 100644
--- a/handlers/warden_post.go
+++ b/handlers/warden_post.go
@@ -1,10 +1,11 @@
package handlers
import (
+ "errors"
"fmt"
"log"
+ "strconv"
"time"
- "errors"
"github.com/ayush00git/cms-web/middleware"
"github.com/ayush00git/cms-web/models"
@@ -215,11 +216,7 @@ func (h *PostHandler) GetWardenPosts(c *gin.Context) {
var posts []models.WardenPost
result = h.DB.
- Preload("Comments", func(db *gorm.DB) (*gorm.DB) {
- return db.Preload("Author", func(d *gorm.DB) (*gorm.DB) {
- return d.Select("id, email, position")
- })
- }).
+ Preload("Comments").
Where("warden_id = ?", warden.ID).
Find(&posts)
if result.Error != nil {
@@ -229,3 +226,75 @@ func (h *PostHandler) GetWardenPosts(c *gin.Context) {
c.JSON(200, gin.H{"success": "posts fetched successfully", "posts": posts})
}
+
+
+// WardenPostComment allows a user of type warden to post
+// comment as the post's author
+func (h *PostHandler) WardenPostComment(c *gin.Context) {
+ // get email of the logged in user from the gin context
+ email, _ := c.Get(middleware.EmailKey)
+
+ // check if the user is a type warden role
+ var warden models.Warden
+ result := h.DB.Where("email = ?", email).Take(&warden)
+ if result.Error != nil {
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ c.JSON(404, gin.H{"error": "user not found"})
+ return
+ }
+ c.JSON(500, gin.H{"error": "internal server error"})
+ return
+ }
+
+ // get post_id from path parameters
+ postIDString := c.Param("post_id")
+ postIDU64, err := strconv.ParseUint(postIDString, 10, 64)
+ if err != nil {
+ c.JSON(500, gin.H{"error": "failed to parse post_id at the moment"})
+ return
+ }
+
+ // read this post from the db
+ postID := uint(postIDU64)
+ var post models.WardenPost
+ result = h.DB.Where("id = ?", postID).Take(&post)
+ if result.Error != nil {
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ c.JSON(404, gin.H{"error": "post not found"})
+ return
+ }
+ c.JSON(500, gin.H{"error": "internal server error"})
+ return
+ }
+
+ // verify the warden(logged in user) is the author of the post
+ if warden.ID != post.WardenID {
+ c.JSON(403, gin.H{"error": "you are not authorized to comment"})
+ return
+ }
+
+ // bind the input
+ var inputs CommentType
+ if err := c.ShouldBindJSON(&inputs); err != nil {
+ c.JSON(401, gin.H{"error": "invalid request body"})
+ return
+ }
+
+ doc := models.Comment{
+ CommentableID: postID,
+ CommentableType: "warden_posts",
+ Content: inputs.Content,
+ Email: warden.Email,
+ Role: "warden",
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ }
+
+ result = h.DB.Create(&doc)
+ if result.Error != nil {
+ c.JSON(500, gin.H{"error": "failed to comment at the moment"})
+ return
+ }
+
+ c.JSON(201, gin.H{"success": "comment posted!"})
+}
diff --git a/models/post.go b/models/post.go
index 2f4718c..9acef3e 100644
--- a/models/post.go
+++ b/models/post.go
@@ -95,8 +95,8 @@ type Comment struct {
CommentableID uint `gorm:"not null"`
CommentableType string `gorm:"not null"`
Content string `gorm:"type:text;not null" json:"comment_text"`
- AuthorID uint `gorm:"not null" json:"author_id"`
- Author Admin `gorm:"foreignKey:AuthorID"`
+ Email string `gorm:"not null" json:"email"`
+ Role string `gorm:"not null" json:"role"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
diff --git a/routes/post.go b/routes/post.go
index ea60f4c..16c9221 100644
--- a/routes/post.go
+++ b/routes/post.go
@@ -27,4 +27,9 @@ func PostRoute(e *gin.Engine, h *handlers.PostHandler) {
e.GET("/api/post/faculty", middleware.IsAuthenticated(), h.GetFacultyPosts)
e.GET("/api/post/warden", middleware.IsAuthenticated(), h.GetWardenPosts)
e.GET("/api/post/centrehead", middleware.IsAuthenticated(), h.GetCentreheadPosts)
+
+ // APIs for comments on the posts
+ e.POST("/api/post/faculty/comment/:post_id", middleware.IsAuthenticated() ,h.FacultyPostComment)
+ e.POST("/api/post/warden/comment/:post_id", middleware.IsAuthenticated(), h.WardenPostComment)
+ e.POST("/api/post/centrehead/comment/:post_id", middleware.IsAuthenticated(), h.CentreheadPostComment)
}
diff --git a/test/admin_comment_test.go b/test/admin_comment_test.go
index ec7b077..ad82ace 100644
--- a/test/admin_comment_test.go
+++ b/test/admin_comment_test.go
@@ -43,8 +43,11 @@ func TestAdminPostComment_Success(t *testing.T) {
if doc.Content != "looks good" {
t.Fatalf("expected content %q, got %q", "looks good", doc.Content)
}
- if doc.AuthorID != admin.ID {
- t.Fatalf("expected author %d, got %d", admin.ID, doc.AuthorID)
+ if doc.Email != admin.Email {
+ t.Fatalf("expected author email %q, got %q", admin.Email, doc.Email)
+ }
+ if doc.Role != string(admin.Position) {
+ t.Fatalf("expected role %q, got %q", string(admin.Position), doc.Role)
}
}
diff --git a/test/helpers_test.go b/test/helpers_test.go
index a0881de..ec9a487 100644
--- a/test/helpers_test.go
+++ b/test/helpers_test.go
@@ -25,6 +25,7 @@ import (
"github.com/glebarez/sqlite"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
+ "gorm.io/gorm/logger"
)
// testPassword is the cleartext password every seeded user is created with, so
@@ -74,7 +75,11 @@ func newTestDB(t *testing.T) *gorm.DB {
// The unique name keeps tests isolated from one another, while the shared
// cache keeps the schema alive across gorm's connection pool.
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
- db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
+ db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
+ // Silence GORM's logger so expected "record not found" lookups in the
+ // not-found test cases don't spam the test output.
+ Logger: logger.Default.LogMode(logger.Silent),
+ })
if err != nil {
t.Fatalf("failed to open in-memory sqlite: %v", err)
}
@@ -144,6 +149,10 @@ func newPostRouter(db *gorm.DB, auth gin.HandlerFunc) *gin.Engine {
e.GET("/api/post/warden", auth, h.GetWardenPosts)
e.GET("/api/post/centrehead", auth, h.GetCentreheadPosts)
+ e.POST("/api/post/faculty/comment/:post_id", auth, h.FacultyPostComment)
+ e.POST("/api/post/warden/comment/:post_id", auth, h.WardenPostComment)
+ e.POST("/api/post/centrehead/comment/:post_id", auth, h.CentreheadPostComment)
+
return e
}
diff --git a/test/post_comment_test.go b/test/post_comment_test.go
new file mode 100644
index 0000000..f22a84f
--- /dev/null
+++ b/test/post_comment_test.go
@@ -0,0 +1,232 @@
+package test
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/ayush00git/cms-web/handlers"
+ "github.com/ayush00git/cms-web/models"
+)
+
+// Tests for the post-author comment APIs: FacultyPostComment,
+// WardenPostComment and CentreheadPostComment. These let the author of a post
+// comment on their own post; non-authors are rejected.
+
+// --- FacultyPostComment -----------------------------------------------------
+
+func TestFacultyPostComment_Success(t *testing.T) {
+ db := newTestDB(t)
+ f := seedFaculty(t, db, "fac.cmt@iit.ac.in")
+ post := models.FacultyPost{FacultyID: f.ID, Place: models.PlaceDepartmental, TypeOfPost: models.TypeCivil, Title: "t", Description: "d"}
+ db.Create(&post)
+
+ e := newPostRouter(db, authAs(f.ID, f.Email))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/faculty/comment/1", handlers.CommentType{Content: "my issue persists"})
+ assertStatus(t, rec, 201)
+
+ var doc models.Comment
+ if err := db.Where("commentable_type = ? AND commentable_id = ?", "faculty_posts", post.ID).Take(&doc).Error; err != nil {
+ t.Fatalf("expected comment to be persisted: %v", err)
+ }
+ if doc.Content != "my issue persists" {
+ t.Fatalf("expected content %q, got %q", "my issue persists", doc.Content)
+ }
+ if doc.Email != f.Email {
+ t.Fatalf("expected email %q, got %q", f.Email, doc.Email)
+ }
+ if doc.Role != "faculty" {
+ t.Fatalf("expected role %q, got %q", "faculty", doc.Role)
+ }
+}
+
+func TestFacultyPostComment_NotAuthor(t *testing.T) {
+ db := newTestDB(t)
+ owner := seedFaculty(t, db, "fac.owner@iit.ac.in")
+ other := seedFaculty(t, db, "fac.other@iit.ac.in")
+ post := models.FacultyPost{FacultyID: owner.ID, Place: models.PlaceDepartmental, TypeOfPost: models.TypeCivil, Title: "t", Description: "d"}
+ db.Create(&post)
+
+ e := newPostRouter(db, authAs(other.ID, other.Email))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/faculty/comment/1", handlers.CommentType{Content: "x"})
+ assertStatus(t, rec, 403)
+}
+
+func TestFacultyPostComment_UserNotFound(t *testing.T) {
+ db := newTestDB(t)
+ e := newPostRouter(db, authAs(999, "ghost@iit.ac.in"))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/faculty/comment/1", handlers.CommentType{Content: "x"})
+ assertStatus(t, rec, 404)
+}
+
+func TestFacultyPostComment_PostNotFound(t *testing.T) {
+ db := newTestDB(t)
+ f := seedFaculty(t, db, "fac.pnf@iit.ac.in")
+ e := newPostRouter(db, authAs(f.ID, f.Email))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/faculty/comment/9999", handlers.CommentType{Content: "x"})
+ assertStatus(t, rec, 404)
+}
+
+func TestFacultyPostComment_BadPostID(t *testing.T) {
+ db := newTestDB(t)
+ f := seedFaculty(t, db, "fac.badpid@iit.ac.in")
+ e := newPostRouter(db, authAs(f.ID, f.Email))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/faculty/comment/not-a-number", handlers.CommentType{Content: "x"})
+ assertStatus(t, rec, 500)
+}
+
+func TestFacultyPostComment_InvalidBody(t *testing.T) {
+ db := newTestDB(t)
+ f := seedFaculty(t, db, "fac.badbody@iit.ac.in")
+ post := models.FacultyPost{FacultyID: f.ID, Place: models.PlaceDepartmental, TypeOfPost: models.TypeCivil, Title: "t", Description: "d"}
+ db.Create(&post)
+
+ e := newPostRouter(db, authAs(f.ID, f.Email))
+ rec := doRequestRaw(t, e, http.MethodPost, "/api/post/faculty/comment/1", "{not json")
+ assertStatus(t, rec, 401)
+}
+
+// --- WardenPostComment ------------------------------------------------------
+
+func TestWardenPostComment_Success(t *testing.T) {
+ db := newTestDB(t)
+ w := seedWarden(t, db, "war.cmt@iit.ac.in")
+ post := models.WardenPost{WardenID: w.ID, RoomNumber: "A-1", TypeOfPost: models.TypeCivil, Title: "t", Description: "d"}
+ db.Create(&post)
+
+ e := newPostRouter(db, authAs(w.ID, w.Email))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/warden/comment/1", handlers.CommentType{Content: "still broken"})
+ assertStatus(t, rec, 201)
+
+ var doc models.Comment
+ if err := db.Where("commentable_type = ? AND commentable_id = ?", "warden_posts", post.ID).Take(&doc).Error; err != nil {
+ t.Fatalf("expected comment to be persisted: %v", err)
+ }
+ if doc.Content != "still broken" {
+ t.Fatalf("expected content %q, got %q", "still broken", doc.Content)
+ }
+ if doc.Email != w.Email {
+ t.Fatalf("expected email %q, got %q", w.Email, doc.Email)
+ }
+ if doc.Role != "warden" {
+ t.Fatalf("expected role %q, got %q", "warden", doc.Role)
+ }
+}
+
+func TestWardenPostComment_NotAuthor(t *testing.T) {
+ db := newTestDB(t)
+ owner := seedWarden(t, db, "war.owner@iit.ac.in")
+ other := seedWarden(t, db, "war.other@iit.ac.in")
+ post := models.WardenPost{WardenID: owner.ID, RoomNumber: "A-1", TypeOfPost: models.TypeCivil, Title: "t", Description: "d"}
+ db.Create(&post)
+
+ e := newPostRouter(db, authAs(other.ID, other.Email))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/warden/comment/1", handlers.CommentType{Content: "x"})
+ assertStatus(t, rec, 403)
+}
+
+func TestWardenPostComment_UserNotFound(t *testing.T) {
+ db := newTestDB(t)
+ e := newPostRouter(db, authAs(999, "ghost@iit.ac.in"))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/warden/comment/1", handlers.CommentType{Content: "x"})
+ assertStatus(t, rec, 404)
+}
+
+func TestWardenPostComment_PostNotFound(t *testing.T) {
+ db := newTestDB(t)
+ w := seedWarden(t, db, "war.pnf@iit.ac.in")
+ e := newPostRouter(db, authAs(w.ID, w.Email))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/warden/comment/9999", handlers.CommentType{Content: "x"})
+ assertStatus(t, rec, 404)
+}
+
+func TestWardenPostComment_BadPostID(t *testing.T) {
+ db := newTestDB(t)
+ w := seedWarden(t, db, "war.badpid@iit.ac.in")
+ e := newPostRouter(db, authAs(w.ID, w.Email))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/warden/comment/not-a-number", handlers.CommentType{Content: "x"})
+ assertStatus(t, rec, 500)
+}
+
+func TestWardenPostComment_InvalidBody(t *testing.T) {
+ db := newTestDB(t)
+ w := seedWarden(t, db, "war.badbody@iit.ac.in")
+ post := models.WardenPost{WardenID: w.ID, RoomNumber: "A-1", TypeOfPost: models.TypeCivil, Title: "t", Description: "d"}
+ db.Create(&post)
+
+ e := newPostRouter(db, authAs(w.ID, w.Email))
+ rec := doRequestRaw(t, e, http.MethodPost, "/api/post/warden/comment/1", "{not json")
+ assertStatus(t, rec, 401)
+}
+
+// --- CentreheadPostComment --------------------------------------------------
+
+func TestCentreheadPostComment_Success(t *testing.T) {
+ db := newTestDB(t)
+ ch := seedCentrehead(t, db, "ch.cmt@iit.ac.in")
+ post := models.CentreheadPost{CentreheadID: ch.ID, TypeOfPost: models.TypeCivil, Title: "t", Description: "d"}
+ db.Create(&post)
+
+ e := newPostRouter(db, authAs(ch.ID, ch.Email))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead/comment/1", handlers.CommentType{Content: "needs urgent fix"})
+ assertStatus(t, rec, 201)
+
+ var doc models.Comment
+ if err := db.Where("commentable_type = ? AND commentable_id = ?", "centrehead_posts", post.ID).Take(&doc).Error; err != nil {
+ t.Fatalf("expected comment to be persisted: %v", err)
+ }
+ if doc.Content != "needs urgent fix" {
+ t.Fatalf("expected content %q, got %q", "needs urgent fix", doc.Content)
+ }
+ if doc.Email != ch.Email {
+ t.Fatalf("expected email %q, got %q", ch.Email, doc.Email)
+ }
+ if doc.Role != "centrehead" {
+ t.Fatalf("expected role %q, got %q", "centrehead", doc.Role)
+ }
+}
+
+func TestCentreheadPostComment_NotAuthor(t *testing.T) {
+ db := newTestDB(t)
+ owner := seedCentrehead(t, db, "ch.owner@iit.ac.in")
+ other := seedCentrehead(t, db, "ch.other@iit.ac.in")
+ post := models.CentreheadPost{CentreheadID: owner.ID, TypeOfPost: models.TypeCivil, Title: "t", Description: "d"}
+ db.Create(&post)
+
+ e := newPostRouter(db, authAs(other.ID, other.Email))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead/comment/1", handlers.CommentType{Content: "x"})
+ assertStatus(t, rec, 403)
+}
+
+func TestCentreheadPostComment_UserNotFound(t *testing.T) {
+ db := newTestDB(t)
+ e := newPostRouter(db, authAs(999, "ghost@iit.ac.in"))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead/comment/1", handlers.CommentType{Content: "x"})
+ assertStatus(t, rec, 404)
+}
+
+func TestCentreheadPostComment_PostNotFound(t *testing.T) {
+ db := newTestDB(t)
+ ch := seedCentrehead(t, db, "ch.pnf@iit.ac.in")
+ e := newPostRouter(db, authAs(ch.ID, ch.Email))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead/comment/9999", handlers.CommentType{Content: "x"})
+ assertStatus(t, rec, 404)
+}
+
+func TestCentreheadPostComment_BadPostID(t *testing.T) {
+ db := newTestDB(t)
+ ch := seedCentrehead(t, db, "ch.badpid@iit.ac.in")
+ e := newPostRouter(db, authAs(ch.ID, ch.Email))
+ rec := doRequest(t, e, http.MethodPost, "/api/post/centrehead/comment/not-a-number", handlers.CommentType{Content: "x"})
+ assertStatus(t, rec, 500)
+}
+
+func TestCentreheadPostComment_InvalidBody(t *testing.T) {
+ db := newTestDB(t)
+ ch := seedCentrehead(t, db, "ch.badbody@iit.ac.in")
+ post := models.CentreheadPost{CentreheadID: ch.ID, TypeOfPost: models.TypeCivil, Title: "t", Description: "d"}
+ db.Create(&post)
+
+ e := newPostRouter(db, authAs(ch.ID, ch.Email))
+ rec := doRequestRaw(t, e, http.MethodPost, "/api/post/centrehead/comment/1", "{not json")
+ assertStatus(t, rec, 401)
+}