Keep read receipts for the other person only, and let photos, voice, and files stay cached until they are actually replaced.
331 lines
11 KiB
Go
331 lines
11 KiB
Go
package api_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestChatEditOwnMessage(t *testing.T) {
|
|
e := setup(t)
|
|
student, studentID := register(t, e, "Chat Editor", "chat-editor@test.dev")
|
|
admin := login(t, e, bootstrapAdminEmail)
|
|
code, me := e.do(t, "GET", "/api/auth/me", admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("me: %d %v", code, me)
|
|
}
|
|
adminID := int64(me["user"].(map[string]any)["id"].(float64))
|
|
|
|
code, body := e.do(t, "POST", "/api/chats/"+itoa(adminID)+"/messages", student, map[string]any{"body": "سلام"})
|
|
if code != 201 {
|
|
t.Fatalf("send: %d %v", code, body)
|
|
}
|
|
msg := body["message"].(map[string]any)
|
|
msgID := int64(msg["id"].(float64))
|
|
|
|
code, body = e.do(t, "PUT", "/api/chats/"+itoa(adminID)+"/messages/"+itoa(msgID), student, map[string]any{"body": "سلام ویرایش"})
|
|
if code != 200 {
|
|
t.Fatalf("edit: %d %v", code, body)
|
|
}
|
|
edited := body["message"].(map[string]any)
|
|
if edited["body"] != "سلام ویرایش" || edited["editedAt"] == nil {
|
|
t.Fatalf("expected edited body, got %v", edited)
|
|
}
|
|
|
|
code, _ = e.do(t, "PUT", "/api/chats/"+itoa(studentID)+"/messages/"+itoa(msgID), admin, map[string]any{"body": "نه"})
|
|
if code != 404 {
|
|
t.Fatalf("partner edit should be rejected: %d", code)
|
|
}
|
|
|
|
code, _ = e.do(t, "PUT", "/api/chats/"+itoa(adminID)+"/messages/"+itoa(msgID), student, map[string]any{"body": " "})
|
|
if code != 400 {
|
|
t.Fatalf("empty edit: %d", code)
|
|
}
|
|
|
|
code, body = e.do(t, "GET", "/api/chats/"+itoa(adminID)+"/messages", student, nil)
|
|
if code != 200 {
|
|
t.Fatalf("list: %d %v", code, body)
|
|
}
|
|
msgs := body["messages"].([]any)
|
|
last := msgs[len(msgs)-1].(map[string]any)
|
|
if last["body"] != "سلام ویرایش" || last["editedAt"] == nil {
|
|
t.Fatalf("listed message not edited: %v", last)
|
|
}
|
|
}
|
|
|
|
func TestChatReplaceAttachment(t *testing.T) {
|
|
e := setup(t)
|
|
student, studentID := register(t, e, "Media Editor", "media-editor@test.dev")
|
|
admin := login(t, e, bootstrapAdminEmail)
|
|
code, me := e.do(t, "GET", "/api/auth/me", admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("me: %d %v", code, me)
|
|
}
|
|
adminID := int64(me["user"].(map[string]any)["id"].(float64))
|
|
|
|
png := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00}
|
|
code, up := uploadChatFile(t, e, student, "one.png", png)
|
|
if code != 201 {
|
|
t.Fatalf("upload: %d %v", code, up)
|
|
}
|
|
first := up["attachment"].(map[string]any)
|
|
|
|
code, body := e.do(t, "POST", "/api/chats/"+itoa(adminID)+"/messages", student, map[string]any{
|
|
"body": "",
|
|
"attachment": first,
|
|
})
|
|
if code != 201 {
|
|
t.Fatalf("send: %d %v", code, body)
|
|
}
|
|
msg := body["message"].(map[string]any)
|
|
msgID := int64(msg["id"].(float64))
|
|
oldPath := mediaPath(msg["attachment"].(map[string]any)["url"])
|
|
|
|
png[8] = 0x01
|
|
code, up = uploadChatFile(t, e, student, "two.png", png)
|
|
if code != 201 {
|
|
t.Fatalf("upload replacement: %d %v", code, up)
|
|
}
|
|
next := up["attachment"].(map[string]any)
|
|
|
|
code, body = e.do(t, "PUT", "/api/chats/"+itoa(adminID)+"/messages/"+itoa(msgID), student, map[string]any{
|
|
"body": "",
|
|
"attachment": next,
|
|
})
|
|
if code != 200 {
|
|
t.Fatalf("replace: %d %v", code, body)
|
|
}
|
|
edited := body["message"].(map[string]any)
|
|
newPath := mediaPath(edited["attachment"].(map[string]any)["url"])
|
|
if edited["editedAt"] == nil || newPath == "" || newPath == oldPath {
|
|
t.Fatalf("expected a replaced attachment, old %s new %v", oldPath, edited)
|
|
}
|
|
if edited["attachment"].(map[string]any)["type"] != "image" {
|
|
t.Fatalf("type: %v", edited["attachment"])
|
|
}
|
|
|
|
wrong := map[string]any{"type": "video", "url": next["url"], "name": "clip.mp4", "size": next["size"]}
|
|
code, _ = e.do(t, "PUT", "/api/chats/"+itoa(adminID)+"/messages/"+itoa(msgID), student, map[string]any{
|
|
"body": "",
|
|
"attachment": wrong,
|
|
})
|
|
if code != 404 {
|
|
t.Fatalf("different kind should miss: %d", code)
|
|
}
|
|
|
|
code, _ = e.do(t, "PUT", "/api/chats/"+itoa(adminID)+"/messages/"+itoa(msgID), student, map[string]any{
|
|
"body": "",
|
|
"attachment": map[string]any{"type": "file", "url": next["url"], "name": "x.pdf", "size": 1},
|
|
})
|
|
if code != 400 {
|
|
t.Fatalf("file replace: %d", code)
|
|
}
|
|
|
|
code, _ = e.do(t, "PUT", "/api/chats/"+itoa(studentID)+"/messages/"+itoa(msgID), admin, map[string]any{
|
|
"body": "",
|
|
"attachment": next,
|
|
})
|
|
if code != 400 {
|
|
t.Fatalf("partner replace: %d", code)
|
|
}
|
|
}
|
|
|
|
func TestChatReadMarksOnlyIncoming(t *testing.T) {
|
|
e := setup(t)
|
|
student, studentID := register(t, e, "Seen Student", "seen-student@test.dev")
|
|
admin := login(t, e, bootstrapAdminEmail)
|
|
code, me := e.do(t, "GET", "/api/auth/me", admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("me: %d %v", code, me)
|
|
}
|
|
adminID := int64(me["user"].(map[string]any)["id"].(float64))
|
|
|
|
code, body := e.do(t, "POST", "/api/chats/"+itoa(adminID)+"/messages", student, map[string]any{"body": "از هنرجو"})
|
|
if code != 201 {
|
|
t.Fatalf("student send: %d %v", code, body)
|
|
}
|
|
studentMsg := int64(body["message"].(map[string]any)["id"].(float64))
|
|
|
|
code, body = e.do(t, "GET", "/api/chats/"+itoa(adminID)+"/messages", student, nil)
|
|
if code != 200 {
|
|
t.Fatalf("student list: %d %v", code, body)
|
|
}
|
|
if chatMsg(t, body, studentMsg)["readAt"] != nil {
|
|
t.Fatalf("opening my own thread marked it seen: %v", chatMsg(t, body, studentMsg))
|
|
}
|
|
|
|
code, body = e.do(t, "GET", "/api/chats/"+itoa(studentID)+"/messages", admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("admin list: %d %v", code, body)
|
|
}
|
|
if chatMsg(t, body, studentMsg)["readAt"] == nil {
|
|
t.Fatalf("partner open should mark the incoming message seen")
|
|
}
|
|
|
|
code, body = e.do(t, "POST", "/api/chats/"+itoa(studentID)+"/messages", admin, map[string]any{"body": "از ادمین"})
|
|
if code != 201 {
|
|
t.Fatalf("admin send: %d %v", code, body)
|
|
}
|
|
adminMsg := int64(body["message"].(map[string]any)["id"].(float64))
|
|
|
|
code, body = e.do(t, "GET", "/api/chats/"+itoa(studentID)+"/messages", admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("admin list again: %d %v", code, body)
|
|
}
|
|
if chatMsg(t, body, adminMsg)["readAt"] != nil {
|
|
t.Fatalf("admin own message was marked seen: %v", chatMsg(t, body, adminMsg))
|
|
}
|
|
if chatMsg(t, body, studentMsg)["readAt"] == nil {
|
|
t.Fatalf("already seen incoming message lost its receipt")
|
|
}
|
|
}
|
|
|
|
func chatMsg(t *testing.T, body map[string]any, id int64) map[string]any {
|
|
t.Helper()
|
|
for _, raw := range body["messages"].([]any) {
|
|
msg := raw.(map[string]any)
|
|
if int64(msg["id"].(float64)) == id {
|
|
return msg
|
|
}
|
|
}
|
|
t.Fatalf("message %d missing in %v", id, body["messages"])
|
|
return nil
|
|
}
|
|
|
|
func mediaPath(raw any) string {
|
|
s, _ := raw.(string)
|
|
if i := strings.IndexByte(s, '?'); i >= 0 {
|
|
return s[:i]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestLearningDetailMatchesActiveStudents(t *testing.T) {
|
|
e := setup(t)
|
|
admin := login(t, e, bootstrapAdminEmail)
|
|
|
|
code, body := e.do(t, "GET", "/api/admin/stats", admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("stats: %d %v", code, body)
|
|
}
|
|
if body["stats"].(map[string]any)["users"].(float64) != 0 {
|
|
t.Fatalf("overview should ignore staff, got %v", body["stats"])
|
|
}
|
|
|
|
_, uid := register(t, e, "Active Student", "active-student@test.dev")
|
|
|
|
code, body = e.do(t, "GET", "/api/admin/stats", admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("stats after student: %d %v", code, body)
|
|
}
|
|
if body["stats"].(map[string]any)["users"].(float64) != 1 {
|
|
t.Fatalf("expected 1 active student, got %v", body["stats"])
|
|
}
|
|
|
|
code, body = e.do(t, "GET", "/api/admin/learning/overview", admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("overview: %d %v", code, body)
|
|
}
|
|
if body["overview"].(map[string]any)["studentsActive"].(float64) != 1 {
|
|
t.Fatalf("learning active mismatch: %v", body["overview"])
|
|
}
|
|
|
|
code, body = e.do(t, "GET", "/api/admin/users/"+itoa(uid)+"/learning", admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("learning detail: %d %v", code, body)
|
|
}
|
|
user := body["learning"].(map[string]any)["user"].(map[string]any)
|
|
if user["name"] != "Active Student" {
|
|
t.Fatalf("detail user: %v", user)
|
|
}
|
|
|
|
code, body = e.do(t, "GET", "/api/admin/users/999999/learning", admin, nil)
|
|
if code != 404 {
|
|
t.Fatalf("missing user: %d %v", code, body)
|
|
}
|
|
|
|
code, body = e.do(t, "GET", "/api/admin/learning/students?page=1&pageSize=10", admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("students page: %d %v", code, body)
|
|
}
|
|
if body["total"].(float64) != 1 || len(body["items"].([]any)) != 1 {
|
|
t.Fatalf("expected one page row, got %v", body)
|
|
}
|
|
}
|
|
|
|
func TestInviteStatusFilter(t *testing.T) {
|
|
e := setup(t)
|
|
admin := login(t, e, bootstrapAdminEmail)
|
|
code, body := e.do(t, "POST", "/api/admin/invites", admin, map[string]any{"maxUses": 2, "label": "مهر"})
|
|
if code != 201 {
|
|
t.Fatalf("create invite: %d %v", code, body)
|
|
}
|
|
code, body = e.do(t, "GET", "/api/admin/invites?status=active", admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("active filter: %d %v", code, body)
|
|
}
|
|
if body["total"].(float64) < 1 {
|
|
t.Fatalf("expected active invites, got %v", body)
|
|
}
|
|
code, body = e.do(t, "GET", "/api/admin/invites?status=revoked", admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("revoked filter: %d %v", code, body)
|
|
}
|
|
if body["total"].(float64) != 0 {
|
|
t.Fatalf("expected no revoked invites, got %v", body)
|
|
}
|
|
code, body = e.do(t, "GET", "/api/admin/invites?status=nope", admin, nil)
|
|
if code != 400 {
|
|
t.Fatalf("bad status: %d %v", code, body)
|
|
}
|
|
}
|
|
|
|
func TestPublicProfileOmitsPrivateFields(t *testing.T) {
|
|
e := setup(t)
|
|
student, studentID := register(t, e, "Public Name", "public-name@test.dev")
|
|
admin := login(t, e, bootstrapAdminEmail)
|
|
code, me := e.do(t, "GET", "/api/auth/me", admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("me: %d %v", code, me)
|
|
}
|
|
adminID := int64(me["user"].(map[string]any)["id"].(float64))
|
|
|
|
code, body := e.do(t, "GET", "/api/users/"+itoa(adminID), student, nil)
|
|
if code != 200 {
|
|
t.Fatalf("profile: %d %v", code, body)
|
|
}
|
|
user := body["user"].(map[string]any)
|
|
if user["name"] == nil || user["name"] == "" {
|
|
t.Fatalf("missing name: %v", user)
|
|
}
|
|
if _, ok := user["email"]; ok {
|
|
t.Fatalf("email must not be public: %v", user)
|
|
}
|
|
if _, ok := user["phone"]; ok {
|
|
t.Fatalf("phone must not be public: %v", user)
|
|
}
|
|
|
|
code, adminView := e.do(t, "GET", "/api/admin/users/"+itoa(studentID), admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("admin user: %d %v", code, adminView)
|
|
}
|
|
managed := adminView["user"].(map[string]any)
|
|
if managed["email"] != "public-name@test.dev" {
|
|
t.Fatalf("admin should see email: %v", managed)
|
|
}
|
|
if _, ok := managed["passwordHash"]; ok {
|
|
t.Fatalf("password hash must stay hidden: %v", managed)
|
|
}
|
|
code, hidden := e.do(t, "GET", "/api/admin/users/"+itoa(adminID), student, nil)
|
|
if code != 403 {
|
|
t.Fatalf("student must not open admin user: %d %v", code, hidden)
|
|
}
|
|
|
|
code, body = e.do(t, "GET", "/api/users/"+itoa(studentID), admin, nil)
|
|
if code != 200 {
|
|
t.Fatalf("student profile: %d %v", code, body)
|
|
}
|
|
if body["user"].(map[string]any)["username"] != "public-name" {
|
|
t.Fatalf("username: %v", body["user"])
|
|
}
|
|
}
|