Files
honeyDueAPI/internal/admin/handlers/admin_security_test.go
Trey t 4976eafc6c Rebrand from Casera/MyCrib to honeyDue
Total rebrand across all Go API source files:
- Go module path: casera-api -> honeydue-api
- All imports updated (130+ files)
- Docker: containers, images, networks renamed
- Email templates: support email, noreply, icon URL
- Domains: casera.app/mycrib.treytartt.com -> honeyDue.treytartt.com
- Bundle IDs: com.tt.casera -> com.tt.honeyDue
- IAP product IDs updated
- Landing page, admin panel, config defaults
- Seeds, CI workflows, Makefile, docs
- Database table names preserved (no migration needed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:33:38 -06:00

200 lines
4.8 KiB
Go

package handlers
import (
"html"
"testing"
"github.com/stretchr/testify/assert"
"github.com/treytartt/honeydue-api/internal/admin/dto"
)
func TestAdminSortBy_ValidColumn_Works(t *testing.T) {
tests := []struct {
name string
sortBy string
allowlist []string
defaultCol string
expected string
}{
{
name: "exact match returns column",
sortBy: "created_at",
allowlist: []string{"id", "created_at", "updated_at", "name"},
defaultCol: "created_at",
expected: "created_at",
},
{
name: "case insensitive match returns canonical column",
sortBy: "Created_At",
allowlist: []string{"id", "created_at", "updated_at", "name"},
defaultCol: "created_at",
expected: "created_at",
},
{
name: "different valid column",
sortBy: "name",
allowlist: []string{"id", "created_at", "updated_at", "name"},
defaultCol: "created_at",
expected: "name",
},
{
name: "date_joined for user handler",
sortBy: "date_joined",
allowlist: []string{"id", "username", "email", "date_joined", "last_login", "is_active"},
defaultCol: "date_joined",
expected: "date_joined",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := dto.PaginationParams{SortBy: tt.sortBy}
result := p.GetSafeSortBy(tt.allowlist, tt.defaultCol)
assert.Equal(t, tt.expected, result)
})
}
}
func TestAdminSortBy_SQLInjection_ReturnsDefault(t *testing.T) {
allowlist := []string{"id", "created_at", "updated_at", "name"}
defaultCol := "created_at"
tests := []struct {
name string
sortBy string
}{
{
name: "SQL injection with DROP TABLE",
sortBy: "created_at; DROP TABLE users; --",
},
{
name: "SQL injection with UNION SELECT",
sortBy: "id UNION SELECT password FROM auth_user",
},
{
name: "SQL injection with subquery",
sortBy: "(SELECT password FROM auth_user LIMIT 1)",
},
{
name: "SQL injection with comment",
sortBy: "created_at--",
},
{
name: "SQL injection with semicolon",
sortBy: "created_at;",
},
{
name: "SQL injection with OR 1=1",
sortBy: "created_at OR 1=1",
},
{
name: "column not in allowlist",
sortBy: "password",
},
{
name: "SQL injection with single quotes",
sortBy: "name'; DROP TABLE users; --",
},
{
name: "SQL injection with double dashes",
sortBy: "id -- comment",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := dto.PaginationParams{SortBy: tt.sortBy}
result := p.GetSafeSortBy(allowlist, defaultCol)
assert.Equal(t, defaultCol, result, "SQL injection attempt should return default column")
})
}
}
func TestAdminSortBy_EmptyString_ReturnsDefault(t *testing.T) {
tests := []struct {
name string
sortBy string
defaultCol string
}{
{
name: "empty string returns default",
sortBy: "",
defaultCol: "created_at",
},
{
name: "whitespace only returns default",
sortBy: " ",
defaultCol: "created_at",
},
{
name: "tab only returns default",
sortBy: "\t",
defaultCol: "date_joined",
},
{
name: "different default column",
sortBy: "",
defaultCol: "completed_at",
},
}
allowlist := []string{"id", "created_at", "updated_at", "name"}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := dto.PaginationParams{SortBy: tt.sortBy}
result := p.GetSafeSortBy(allowlist, tt.defaultCol)
assert.Equal(t, tt.defaultCol, result)
})
}
}
func TestSendEmail_XSSEscaped(t *testing.T) {
// SEC-22: Subject and Body must be HTML-escaped before insertion into email template.
// This tests the html.EscapeString behavior that the handler relies on.
tests := []struct {
name string
input string
expected string
}{
{
name: "script tag in subject",
input: `<script>alert("xss")</script>`,
expected: `&lt;script&gt;alert(&#34;xss&#34;)&lt;/script&gt;`,
},
{
name: "img onerror payload",
input: `<img src=x onerror=alert(1)>`,
expected: `&lt;img src=x onerror=alert(1)&gt;`,
},
{
name: "ampersand and angle brackets",
input: `Tom & Jerry <bros>`,
expected: `Tom &amp; Jerry &lt;bros&gt;`,
},
{
name: "plain text passes through",
input: "Hello World",
expected: "Hello World",
},
{
name: "single quotes",
input: `It's a 'test'`,
expected: `It&#39;s a &#39;test&#39;`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
escaped := html.EscapeString(tt.input)
assert.Equal(t, tt.expected, escaped)
// Verify the escaped output does NOT contain raw angle brackets from the input
if tt.input != tt.expected {
assert.NotContains(t, escaped, "<script>")
assert.NotContains(t, escaped, "<img")
}
})
}
}