419ea11b14
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m12s
User-facing Feedback screen (Settings -> Info, registered accounts only): a message (<=1024 runes) plus one optional attachment, an anti-spam gate (one unreviewed message at a time), and the operator's inline reply with a Settings/Info badge. Server-rendered admin console section (/_gm/feedback): unread/read/archived queue with per-user search, detail with read/reply/ archive/delete/delete-all, safe attachment serving (nosniff, images inline via <img>, others download-only). Introduces account_roles, the first per-account role table; feedback_banned blocks only feedback submission, granted/revoked from /users and the delete-with-block action. - migration 00004_feedback (feedback_messages + account_roles) + jetgen - backend internal/feedback (store+service), internal/account/roles.go - wire: FlatBuffers feedback.submit/get/unread; gateway guest gate (Op.NonGuest, is_guest via session resolve) -> guest_forbidden before any backend call - reply push reuses NotificationEvent with a new admin_reply sub-kind - UI: /feedback route + screen, attachment picker, badge, channel detection, i18n - tests: feedback unit (Go+UI), gateway guest-gate, inttest lifecycle, e2e - docs: PLAN stage 19, ARCHITECTURE s15, FUNCTIONAL(+ru), TESTING, READMEs
82 lines
2.2 KiB
Go
82 lines
2.2 KiB
Go
package feedback
|
|
|
|
import "testing"
|
|
|
|
func TestAllowedAttachment(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
file string
|
|
want bool
|
|
}{
|
|
{"png image", "shot.png", true},
|
|
{"jpeg upper-case ext", "Photo.JPG", true},
|
|
{"pdf doc", "report.pdf", true},
|
|
{"archive 7z", "logs.7z", true},
|
|
{"doc with dotted name", "my.notes.docx", true},
|
|
{"disallowed exe", "evil.exe", false},
|
|
{"disallowed svg (xss vector)", "x.svg", false},
|
|
{"disallowed html", "x.html", false},
|
|
{"no extension", "README", false},
|
|
{"empty name", "", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := AllowedAttachment(tt.file); got != tt.want {
|
|
t.Errorf("AllowedAttachment(%q) = %v, want %v", tt.file, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsImageAndContentType(t *testing.T) {
|
|
tests := []struct {
|
|
file string
|
|
isImage bool
|
|
ctype string
|
|
}{
|
|
{"a.png", true, "image/png"},
|
|
{"a.jpg", true, "image/jpeg"},
|
|
{"a.jpeg", true, "image/jpeg"},
|
|
{"a.webp", true, "image/webp"},
|
|
{"a.gif", true, "image/gif"},
|
|
{"a.pdf", false, "application/octet-stream"},
|
|
{"a.zip", false, "application/octet-stream"},
|
|
{"a.svg", false, "application/octet-stream"}, // even if it slipped past, never image/svg+xml
|
|
{"noext", false, "application/octet-stream"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.file, func(t *testing.T) {
|
|
if got := IsImage(tt.file); got != tt.isImage {
|
|
t.Errorf("IsImage(%q) = %v, want %v", tt.file, got, tt.isImage)
|
|
}
|
|
if got := ContentType(tt.file); got != tt.ctype {
|
|
t.Errorf("ContentType(%q) = %q, want %q", tt.file, got, tt.ctype)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeChannel(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want string
|
|
}{
|
|
{"telegram", "telegram"},
|
|
{"ios", "ios"},
|
|
{"android", "android"},
|
|
{"web", "web"},
|
|
{" iOS ", "ios"}, // trimmed + lower-cased
|
|
{"TELEGRAM", "telegram"},
|
|
{"", "web"}, // unknown -> web
|
|
{"windows", "web"}, // unknown -> web
|
|
{"'; DROP", "web"}, // junk -> web
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.in, func(t *testing.T) {
|
|
if got := normalizeChannel(tt.in); got != tt.want {
|
|
t.Errorf("normalizeChannel(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|