test: add coverage tests across backend + fix mock for PENDING checkout support
CI / Nginx config check (push) Successful in 13s
CI / Env docs check (push) Successful in 15s
CI / Docker compose check (push) Successful in 15s
CI / Frontend major deps (push) Failing after 24s
CI / Frontend deps check (push) Successful in 30s
CI / Secrets scan (push) Successful in 38s
CI / Go build (push) Successful in 39s
CI / Frontend build (push) Successful in 1m3s
CI / Knip (push) Successful in 45s
CI / Go vet (prod) (push) Failing after 1m42s
CI / Frontend a11y check (push) Successful in 2m34s
CI / Go vet (dev) (push) Successful in 2m29s
CI / Staticcheck (prod) (push) Failing after 2m38s
CI / go mod tidy (push) Successful in 1m3s
CI / Staticcheck (dev) (push) Successful in 2m55s
CI / Frontend QC (audit) (push) Successful in 51s
CI / golangci-lint (push) Successful in 3m22s
CI / Go vulnerabilities (push) Successful in 1m26s
CI / Frontend QC (typecheck) (push) Successful in 2m18s
CI / Security scan (prod) (push) Successful in 4m18s
CI / Security scan (dev) (push) Successful in 4m40s
CI / Tests (prod) (push) Has been skipped
CI / Tests (dev) (push) Has been skipped
CI / Race (prod) (push) Has been skipped
CI / Race (dev) (push) Has been skipped
CI / Frontend QC (lint) (push) Successful in 2m18s
CI / Svelte strict check (push) Successful in 43s
CI / Nginx config check (push) Successful in 13s
CI / Env docs check (push) Successful in 15s
CI / Docker compose check (push) Successful in 15s
CI / Frontend major deps (push) Failing after 24s
CI / Frontend deps check (push) Successful in 30s
CI / Secrets scan (push) Successful in 38s
CI / Go build (push) Successful in 39s
CI / Frontend build (push) Successful in 1m3s
CI / Knip (push) Successful in 45s
CI / Go vet (prod) (push) Failing after 1m42s
CI / Frontend a11y check (push) Successful in 2m34s
CI / Go vet (dev) (push) Successful in 2m29s
CI / Staticcheck (prod) (push) Failing after 2m38s
CI / go mod tidy (push) Successful in 1m3s
CI / Staticcheck (dev) (push) Successful in 2m55s
CI / Frontend QC (audit) (push) Successful in 51s
CI / golangci-lint (push) Successful in 3m22s
CI / Go vulnerabilities (push) Successful in 1m26s
CI / Frontend QC (typecheck) (push) Successful in 2m18s
CI / Security scan (prod) (push) Successful in 4m18s
CI / Security scan (dev) (push) Successful in 4m40s
CI / Tests (prod) (push) Has been skipped
CI / Tests (dev) (push) Has been skipped
CI / Race (prod) (push) Has been skipped
CI / Race (dev) (push) Has been skipped
CI / Frontend QC (lint) (push) Successful in 2m18s
CI / Svelte strict check (push) Successful in 43s
New test files cover previously untested paths across DAV, validators, S3, Square, mw, bookings, user, and payments packages. Includes mock fix: HoldCheckouts flag on MockClient allows tests to pause auto-complete goroutine for testing PENDING checkout states. Coverage: 50.4% → 65.0% (+14.6pp)
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
//go:build test && dev
|
||||
|
||||
package dav
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"crussell/db"
|
||||
"crussell/testutils/testdb"
|
||||
)
|
||||
|
||||
var testSvc *BaseService
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool := testdb.CreateTestDatabase("crussell_test_internal_dav")
|
||||
db.Conn = db.NewPoolProxy(pool)
|
||||
testdb.SeedBaseline(pool)
|
||||
testSvc = newBaseService(pool)
|
||||
code := m.Run()
|
||||
testdb.DestroyTestDatabase(pool, "crussell_test_internal_dav")
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -159,9 +159,9 @@ func extractContactURIsFromICalendar(icalData string) []string {
|
||||
for line := range strings.SplitSeq(icalData, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "ATTENDEE") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) >= 2 {
|
||||
uris = append(uris, strings.TrimSpace(parts[len(parts)-1]))
|
||||
_, value, ok := strings.Cut(line, ":")
|
||||
if ok {
|
||||
uris = append(uris, strings.TrimSpace(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,655 @@
|
||||
//go:build test && dev
|
||||
|
||||
package dav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"crussell/clock"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Pure Function Tests: extractContactURIsFromICalendar
|
||||
// ============================================================================
|
||||
|
||||
func TestExtractContactURIsFromICalendar_Empty(t *testing.T) {
|
||||
result := extractContactURIsFromICalendar("")
|
||||
assert.Empty(t, result)
|
||||
}
|
||||
|
||||
func TestExtractContactURIsFromICalendar_SingleAttendee(t *testing.T) {
|
||||
ical := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:1\nATTENDEE:mailto:user@example.com\nEND:VEVENT\nEND:VCALENDAR"
|
||||
result := extractContactURIsFromICalendar(ical)
|
||||
assert.Equal(t, []string{"mailto:user@example.com"}, result)
|
||||
}
|
||||
|
||||
func TestExtractContactURIsFromICalendar_MultipleAttendees(t *testing.T) {
|
||||
ical := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:1\nATTENDEE:mailto:alice@example.com\nATTENDEE:mailto:bob@example.com\nEND:VEVENT\nEND:VCALENDAR"
|
||||
result := extractContactURIsFromICalendar(ical)
|
||||
assert.Equal(t, []string{"mailto:alice@example.com", "mailto:bob@example.com"}, result)
|
||||
}
|
||||
|
||||
func TestExtractContactURIsFromICalendar_NoAttendee(t *testing.T) {
|
||||
ical := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:1\nSUMMARY:Test\nEND:VEVENT\nEND:VCALENDAR"
|
||||
result := extractContactURIsFromICalendar(ical)
|
||||
assert.Empty(t, result)
|
||||
}
|
||||
|
||||
func TestExtractContactURIsFromICalendar_AttendeeWithCN(t *testing.T) {
|
||||
ical := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:1\nATTENDEE;CN=John Doe:mailto:john@example.com\nEND:VEVENT\nEND:VCALENDAR"
|
||||
result := extractContactURIsFromICalendar(ical)
|
||||
assert.Equal(t, []string{"mailto:john@example.com"}, result)
|
||||
}
|
||||
|
||||
func TestExtractContactURIsFromICalendar_AttendeeNoColon(t *testing.T) {
|
||||
ical := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:1\nATTENDEE\nEND:VEVENT\nEND:VCALENDAR"
|
||||
result := extractContactURIsFromICalendar(ical)
|
||||
assert.Empty(t, result)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Read-Only DB Query Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestGetContactByURI_Found(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
uri := "test-contact-found-" + t.Name()
|
||||
now := time.Now().Unix()
|
||||
|
||||
t.Cleanup(func() {
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_cards WHERE addressbookid = $1 AND uri = $2", 1, uri)
|
||||
})
|
||||
|
||||
_, err := testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_cards (addressbookid, uri, carddata, lastmodified, etag, size) VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
1, uri, "BEGIN:VCARD\nVERSION:3.0\nUID:test\nFN:Test User\nEND:VCARD", now, fmt.Sprintf("%d", now), 100,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
contact, err := testSvc.GetContactByURI(1, uri)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, contact)
|
||||
assert.Equal(t, 1, contact.AddressBookID)
|
||||
assert.Equal(t, uri, contact.URI)
|
||||
assert.Equal(t, now, contact.LastModified)
|
||||
}
|
||||
|
||||
func TestGetContactByURI_NotFound(t *testing.T) {
|
||||
_, err := testSvc.GetContactByURI(1, "non-existent-uri-"+t.Name())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "contact not found")
|
||||
}
|
||||
|
||||
func TestListAllContacts_Empty(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := testSvc.db.Exec(ctx, "DELETE FROM dav_cards")
|
||||
require.NoError(t, err)
|
||||
|
||||
contacts, err := testSvc.ListAllContacts()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, contacts)
|
||||
}
|
||||
|
||||
func TestListAllContacts(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now().Unix()
|
||||
|
||||
uris := []string{
|
||||
"test-contact-list-1-" + t.Name(),
|
||||
"test-contact-list-2-" + t.Name(),
|
||||
"test-contact-list-3-" + t.Name(),
|
||||
}
|
||||
|
||||
for _, uri := range uris {
|
||||
_, err := testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_cards (addressbookid, uri, carddata, lastmodified, etag, size) VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
1, uri, "BEGIN:VCARD\nVERSION:3.0\nUID:test\nFN:Test User\nEND:VCARD", now, fmt.Sprintf("%d", now), 100,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, uri := range uris {
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_cards WHERE addressbookid = $1 AND uri = $2", 1, uri)
|
||||
}
|
||||
})
|
||||
|
||||
contacts, err := testSvc.ListAllContacts()
|
||||
require.NoError(t, err)
|
||||
|
||||
uriSet := make(map[string]bool)
|
||||
for _, u := range uris {
|
||||
uriSet[u] = true
|
||||
}
|
||||
found := 0
|
||||
for _, c := range contacts {
|
||||
if uriSet[c.URI] {
|
||||
found++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 3, found)
|
||||
}
|
||||
|
||||
func TestListEventsForMonth(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now().Unix()
|
||||
|
||||
julyEventUID := "test-july-event-" + t.Name()
|
||||
augEventUID := "test-aug-event-" + t.Name()
|
||||
|
||||
julyStart := time.Date(2026, 7, 15, 9, 0, 0, 0, time.UTC)
|
||||
julyEnd := time.Date(2026, 7, 15, 10, 0, 0, 0, time.UTC)
|
||||
augStart := time.Date(2026, 8, 15, 9, 0, 0, 0, time.UTC)
|
||||
augEnd := time.Date(2026, 8, 15, 10, 0, 0, 0, time.UTC)
|
||||
|
||||
julyCalData := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:" + julyEventUID + "\nDTSTART:20260715T090000Z\nDTEND:20260715T100000Z\nSUMMARY:July Event\nEND:VEVENT\nEND:VCALENDAR"
|
||||
augCalData := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:" + augEventUID + "\nDTSTART:20260815T090000Z\nDTEND:20260815T100000Z\nSUMMARY:August Event\nEND:VEVENT\nEND:VCALENDAR"
|
||||
|
||||
_, err := testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES ($1, $2, $3, $4, $5, $6, 'VEVENT', $7, $8, $9)`,
|
||||
1, julyEventUID+".ics", julyCalData, now, fmt.Sprintf("%d", now), len(julyCalData), julyStart.Unix(), julyEnd.Unix(), julyEventUID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES ($1, $2, $3, $4, $5, $6, 'VEVENT', $7, $8, $9)`,
|
||||
1, augEventUID+".ics", augCalData, now, fmt.Sprintf("%d", now), len(augCalData), augStart.Unix(), augEnd.Unix(), augEventUID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_calendarobjects WHERE uid = $1", julyEventUID)
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_calendarobjects WHERE uid = $1", augEventUID)
|
||||
})
|
||||
|
||||
events, err := testSvc.ListEventsForMonth(2026, time.July)
|
||||
require.NoError(t, err)
|
||||
|
||||
var foundJuly, foundAug bool
|
||||
for _, e := range events {
|
||||
if e.UID == julyEventUID {
|
||||
foundJuly = true
|
||||
}
|
||||
if e.UID == augEventUID {
|
||||
foundAug = true
|
||||
}
|
||||
}
|
||||
assert.True(t, foundJuly, "July event should be found for July 2026 query")
|
||||
assert.False(t, foundAug, "August event should not be found for July 2026 query")
|
||||
}
|
||||
|
||||
func TestListEventsBetween(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now().Unix()
|
||||
|
||||
julyEventUID := "test-july-between-" + t.Name()
|
||||
augEventUID := "test-aug-between-" + t.Name()
|
||||
|
||||
julyStart := time.Date(2026, 7, 15, 9, 0, 0, 0, time.UTC)
|
||||
julyEnd := time.Date(2026, 7, 15, 10, 0, 0, 0, time.UTC)
|
||||
augStart := time.Date(2026, 8, 15, 9, 0, 0, 0, time.UTC)
|
||||
augEnd := time.Date(2026, 8, 15, 10, 0, 0, 0, time.UTC)
|
||||
|
||||
julyCalData := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:" + julyEventUID + "\nDTSTART:20260715T090000Z\nDTEND:20260715T100000Z\nSUMMARY:July Event\nEND:VEVENT\nEND:VCALENDAR"
|
||||
augCalData := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:" + augEventUID + "\nDTSTART:20260815T090000Z\nDTEND:20260815T100000Z\nSUMMARY:August Event\nEND:VEVENT\nEND:VCALENDAR"
|
||||
|
||||
_, err := testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES ($1, $2, $3, $4, $5, $6, 'VEVENT', $7, $8, $9)`,
|
||||
1, julyEventUID+".ics", julyCalData, now, fmt.Sprintf("%d", now), len(julyCalData), julyStart.Unix(), julyEnd.Unix(), julyEventUID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES ($1, $2, $3, $4, $5, $6, 'VEVENT', $7, $8, $9)`,
|
||||
1, augEventUID+".ics", augCalData, now, fmt.Sprintf("%d", now), len(augCalData), augStart.Unix(), augEnd.Unix(), augEventUID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_calendarobjects WHERE uid = $1", julyEventUID)
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_calendarobjects WHERE uid = $1", augEventUID)
|
||||
})
|
||||
|
||||
start := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2026, 7, 31, 23, 59, 59, 0, time.UTC)
|
||||
|
||||
events, err := testSvc.ListEventsBetween(start, end)
|
||||
require.NoError(t, err)
|
||||
|
||||
var foundJuly, foundAug bool
|
||||
for _, e := range events {
|
||||
if e.UID == julyEventUID {
|
||||
foundJuly = true
|
||||
}
|
||||
if e.UID == augEventUID {
|
||||
foundAug = true
|
||||
}
|
||||
}
|
||||
assert.True(t, foundJuly, "July event should be found within July range")
|
||||
assert.False(t, foundAug, "August event should not be found within July range")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Part A: Time-Sensitive Query Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestListEventsTomorrow(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := clock.Now()
|
||||
|
||||
tomorrow := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
inUID := "test-tomorrow-in-" + t.Name()
|
||||
inStart := tomorrow.Add(2 * time.Hour)
|
||||
inEnd := inStart.Add(1 * time.Hour)
|
||||
inCalData := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:" + inUID + "\nDTSTART:" + inStart.Format("20060102T150405Z") + "\nDTEND:" + inEnd.Format("20060102T150405Z") + "\nSUMMARY:Tomorrow Event\nEND:VEVENT\nEND:VCALENDAR"
|
||||
|
||||
outUID := "test-tomorrow-out-" + t.Name()
|
||||
yesterday := now.AddDate(0, 0, -1)
|
||||
outStart := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 12, 0, 0, 0, time.UTC)
|
||||
outEnd := outStart.Add(1 * time.Hour)
|
||||
outCalData := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:" + outUID + "\nDTSTART:" + outStart.Format("20060102T150405Z") + "\nDTEND:" + outEnd.Format("20060102T150405Z") + "\nSUMMARY:Yesterday Event\nEND:VEVENT\nEND:VCALENDAR"
|
||||
|
||||
_, err := testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES ($1, $2, $3, $4, $5, $6, 'VEVENT', $7, $8, $9)`,
|
||||
1, inUID+".ics", inCalData, now.Unix(), fmt.Sprintf("%d", now.Unix()), len(inCalData), inStart.Unix(), inEnd.Unix(), inUID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES ($1, $2, $3, $4, $5, $6, 'VEVENT', $7, $8, $9)`,
|
||||
1, outUID+".ics", outCalData, now.Unix(), fmt.Sprintf("%d", now.Unix()), len(outCalData), outStart.Unix(), outEnd.Unix(), outUID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_calendarobjects WHERE uid = $1", inUID)
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_calendarobjects WHERE uid = $1", outUID)
|
||||
})
|
||||
|
||||
events, err := testSvc.ListEventsTomorrow()
|
||||
require.NoError(t, err)
|
||||
|
||||
var foundIn, foundOut bool
|
||||
for _, e := range events {
|
||||
if e.UID == inUID {
|
||||
foundIn = true
|
||||
}
|
||||
if e.UID == outUID {
|
||||
foundOut = true
|
||||
}
|
||||
}
|
||||
assert.True(t, foundIn, "event with firstoccurence tomorrow should be returned")
|
||||
assert.False(t, foundOut, "event with firstoccurence yesterday should not be returned")
|
||||
}
|
||||
|
||||
func TestListEventsThisWeek(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := clock.Now()
|
||||
|
||||
weekday := int(now.Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7
|
||||
}
|
||||
monday := now.AddDate(0, 0, -weekday+1)
|
||||
monday = time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, time.UTC)
|
||||
|
||||
inUID := "test-week-in-" + t.Name()
|
||||
inStart := monday.Add(48 * time.Hour)
|
||||
inEnd := inStart.Add(1 * time.Hour)
|
||||
inCalData := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:" + inUID + "\nDTSTART:" + inStart.Format("20060102T150405Z") + "\nDTEND:" + inEnd.Format("20060102T150405Z") + "\nSUMMARY:This Week Event\nEND:VEVENT\nEND:VCALENDAR"
|
||||
|
||||
outUID := "test-week-out-" + t.Name()
|
||||
outStart := monday.AddDate(0, 0, -2)
|
||||
outEnd := outStart.Add(1 * time.Hour)
|
||||
outCalData := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:" + outUID + "\nDTSTART:" + outStart.Format("20060102T150405Z") + "\nDTEND:" + outEnd.Format("20060102T150405Z") + "\nSUMMARY:Last Week Event\nEND:VEVENT\nEND:VCALENDAR"
|
||||
|
||||
_, err := testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES ($1, $2, $3, $4, $5, $6, 'VEVENT', $7, $8, $9)`,
|
||||
1, inUID+".ics", inCalData, now.Unix(), fmt.Sprintf("%d", now.Unix()), len(inCalData), inStart.Unix(), inEnd.Unix(), inUID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES ($1, $2, $3, $4, $5, $6, 'VEVENT', $7, $8, $9)`,
|
||||
1, outUID+".ics", outCalData, now.Unix(), fmt.Sprintf("%d", now.Unix()), len(outCalData), outStart.Unix(), outEnd.Unix(), outUID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_calendarobjects WHERE uid = $1", inUID)
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_calendarobjects WHERE uid = $1", outUID)
|
||||
})
|
||||
|
||||
events, err := testSvc.ListEventsThisWeek()
|
||||
require.NoError(t, err)
|
||||
|
||||
var foundIn, foundOut bool
|
||||
for _, e := range events {
|
||||
if e.UID == inUID {
|
||||
foundIn = true
|
||||
}
|
||||
if e.UID == outUID {
|
||||
foundOut = true
|
||||
}
|
||||
}
|
||||
assert.True(t, foundIn, "event within this week should be returned")
|
||||
assert.False(t, foundOut, "event outside this week should not be returned")
|
||||
}
|
||||
|
||||
func TestListRecentContacts(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
recentURI := "test-recent-contact-" + t.Name()
|
||||
oldURI := "test-old-contact-" + t.Name()
|
||||
|
||||
recentLastModified := now.AddDate(0, 0, -5).Unix()
|
||||
oldLastModified := now.AddDate(0, 0, -60).Unix()
|
||||
|
||||
_, err := testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_cards (addressbookid, uri, carddata, lastmodified, etag, size) VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
1, recentURI, "BEGIN:VCARD\nVERSION:3.0\nFN:Recent\nEND:VCARD", recentLastModified, fmt.Sprintf("%d", recentLastModified), 100,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_cards (addressbookid, uri, carddata, lastmodified, etag, size) VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
1, oldURI, "BEGIN:VCARD\nVERSION:3.0\nFN:Old\nEND:VCARD", oldLastModified, fmt.Sprintf("%d", oldLastModified), 100,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_cards WHERE uri = $1", recentURI)
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_cards WHERE uri = $1", oldURI)
|
||||
})
|
||||
|
||||
contacts, err := testSvc.ListRecentContacts(30)
|
||||
require.NoError(t, err)
|
||||
|
||||
var foundRecent, foundOld bool
|
||||
for _, c := range contacts {
|
||||
if c.URI == recentURI {
|
||||
foundRecent = true
|
||||
}
|
||||
if c.URI == oldURI {
|
||||
foundOld = true
|
||||
}
|
||||
}
|
||||
assert.True(t, foundRecent, "recent contact (5 days old) should be returned with 30-day cutoff")
|
||||
assert.False(t, foundOld, "old contact (60 days old) should not be returned with 30-day cutoff")
|
||||
}
|
||||
|
||||
func TestListEventsForContact(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
contactURI := "mailto:test-" + t.Name() + "@test.com"
|
||||
eventUID := "test-contact-event-" + t.Name()
|
||||
|
||||
calData := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:" + eventUID + "\nATTENDEE:" + contactURI + "\nSUMMARY:Contact Event\nEND:VEVENT\nEND:VCALENDAR"
|
||||
|
||||
start := now.Add(24 * time.Hour)
|
||||
end := start.Add(1 * time.Hour)
|
||||
|
||||
_, err := testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES ($1, $2, $3, $4, $5, $6, 'VEVENT', $7, $8, $9)`,
|
||||
1, eventUID+".ics", calData, now.Unix(), fmt.Sprintf("%d", now.Unix()), len(calData), start.Unix(), end.Unix(), eventUID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_calendarobjects WHERE uid = $1", eventUID)
|
||||
})
|
||||
|
||||
events, err := testSvc.ListEventsForContact(contactURI)
|
||||
require.NoError(t, err)
|
||||
|
||||
var found bool
|
||||
for _, e := range events {
|
||||
if e.UID == eventUID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "event with matching contact URI in calendardata should be returned")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Part B: Transactional Mutation Tests
|
||||
// ============================================================================
|
||||
|
||||
func TestCreateContact_Success(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userID := "test-create-contact-" + t.Name()
|
||||
uri := userID + ".vcf"
|
||||
|
||||
input := ContactInput{
|
||||
UserID: userID,
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@test.com",
|
||||
Phone: "1234567890",
|
||||
DOB: "1990-01-01",
|
||||
}
|
||||
|
||||
err := testSvc.CreateContact(1, userID, input)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_cards WHERE addressbookid = $1 AND uri = $2", 1, uri)
|
||||
})
|
||||
|
||||
var id, addressBookID, size int
|
||||
var cardData, scannedURI, etag string
|
||||
var lastModified int64
|
||||
err = testSvc.db.QueryRow(ctx,
|
||||
"SELECT id, addressbookid, uri, carddata, lastmodified, etag, size FROM dav_cards WHERE addressbookid = $1 AND uri = $2",
|
||||
1, uri,
|
||||
).Scan(&id, &addressBookID, &scannedURI, &cardData, &lastModified, &etag, &size)
|
||||
require.NoError(t, err, "inserted contact should be queryable")
|
||||
|
||||
assert.Equal(t, 1, addressBookID)
|
||||
assert.Equal(t, uri, scannedURI)
|
||||
assert.Equal(t, len(cardData), size)
|
||||
assert.Equal(t, etag, fmt.Sprintf("%d", lastModified))
|
||||
assert.Contains(t, cardData, "FN:John Doe")
|
||||
assert.Contains(t, cardData, "N:Doe;John;;;")
|
||||
assert.Contains(t, cardData, "EMAIL;TYPE=INTERNET:john@test.com")
|
||||
assert.Contains(t, cardData, "TEL;TYPE=CELL:1234567890")
|
||||
assert.Contains(t, cardData, "BDAY:1990-01-01")
|
||||
}
|
||||
|
||||
func TestUpdateContact_Success(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userID := "test-update-contact-" + t.Name()
|
||||
uri := userID + ".vcf"
|
||||
now := time.Now().Unix()
|
||||
|
||||
_, err := testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_cards (addressbookid, uri, carddata, lastmodified, etag, size) VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
1, uri, "BEGIN:VCARD\nVERSION:3.0\nFN:Old Name\nEND:VCARD", now, fmt.Sprintf("%d", now), 100,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_cards WHERE addressbookid = $1 AND uri = $2", 1, uri)
|
||||
})
|
||||
|
||||
updatedInput := ContactInput{
|
||||
UserID: userID,
|
||||
FirstName: "Jane",
|
||||
LastName: "Doe",
|
||||
Email: "jane@test.com",
|
||||
Phone: "0987654321",
|
||||
DOB: "1995-05-05",
|
||||
}
|
||||
|
||||
err = testSvc.UpdateContact(1, uri, updatedInput)
|
||||
require.NoError(t, err)
|
||||
|
||||
var cardData string
|
||||
var lastModified int64
|
||||
var etag string
|
||||
var size int
|
||||
err = testSvc.db.QueryRow(ctx,
|
||||
"SELECT carddata, lastmodified, etag, size FROM dav_cards WHERE addressbookid = $1 AND uri = $2",
|
||||
1, uri,
|
||||
).Scan(&cardData, &lastModified, &etag, &size)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, cardData, "FN:Jane Doe")
|
||||
assert.Contains(t, cardData, "N:Doe;Jane;;;")
|
||||
assert.NotContains(t, cardData, "FN:Old Name")
|
||||
assert.Equal(t, len(cardData), size)
|
||||
assert.Equal(t, etag, fmt.Sprintf("%d", lastModified))
|
||||
assert.GreaterOrEqual(t, lastModified, now, "lastmodified should be updated to current time or later")
|
||||
}
|
||||
|
||||
func TestDeleteContact_Success(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userID := "test-delete-contact-" + t.Name()
|
||||
uri := userID + ".vcf"
|
||||
now := time.Now().Unix()
|
||||
|
||||
_, err := testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_cards (addressbookid, uri, carddata, lastmodified, etag, size) VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
1, uri, "BEGIN:VCARD\nVERSION:3.0\nFN:Delete Me\nEND:VCARD", now, fmt.Sprintf("%d", now), 100,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
var count int
|
||||
err = testSvc.db.QueryRow(ctx, "SELECT COUNT(*) FROM dav_cards WHERE addressbookid = $1 AND uri = $2", 1, uri).Scan(&count)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, count, "contact should exist before deletion")
|
||||
|
||||
err = testSvc.DeleteContact(1, uri)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = testSvc.db.QueryRow(ctx, "SELECT COUNT(*) FROM dav_cards WHERE addressbookid = $1 AND uri = $2", 1, uri).Scan(&count)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, count, "contact should be deleted")
|
||||
}
|
||||
|
||||
func TestCreateEvent_Success(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
uniqueSummary := "TestCreateEvent-" + t.Name()
|
||||
|
||||
input := EventInput{
|
||||
Summary: uniqueSummary,
|
||||
Description: "Test description",
|
||||
Location: "Test location",
|
||||
Start: now.Add(24 * time.Hour),
|
||||
End: now.Add(25 * time.Hour),
|
||||
AllDay: false,
|
||||
}
|
||||
|
||||
err := testSvc.CreateEvent(1, input)
|
||||
require.NoError(t, err)
|
||||
|
||||
var uid, uri, calendarData string
|
||||
var firstOcc, lastOcc int64
|
||||
var componentType string
|
||||
err = testSvc.db.QueryRow(ctx,
|
||||
"SELECT uid, uri, calendardata, firstoccurence, lastoccurence, componenttype FROM dav_calendarobjects WHERE calendarid = $1 AND calendardata LIKE $2",
|
||||
1, "%SUMMARY:"+uniqueSummary+"%",
|
||||
).Scan(&uid, &uri, &calendarData, &firstOcc, &lastOcc, &componentType)
|
||||
require.NoError(t, err, "created event should be queryable by summary in calendardata")
|
||||
|
||||
t.Cleanup(func() {
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_calendarobjects WHERE uid = $1", uid)
|
||||
})
|
||||
|
||||
assert.NotEmpty(t, uid)
|
||||
assert.Equal(t, uid+".ics", uri)
|
||||
assert.Equal(t, "VEVENT", componentType)
|
||||
assert.Equal(t, input.Start.Unix(), firstOcc)
|
||||
assert.Equal(t, input.End.Unix(), lastOcc)
|
||||
assert.Contains(t, calendarData, "SUMMARY:"+uniqueSummary)
|
||||
assert.Contains(t, calendarData, "DESCRIPTION:Test description")
|
||||
assert.Contains(t, calendarData, "LOCATION:Test location")
|
||||
}
|
||||
|
||||
func TestUpdateEvent_Success(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
knownUID := "test-update-event-" + t.Name()
|
||||
uri := knownUID + ".ics"
|
||||
now := time.Now()
|
||||
|
||||
start := now.Add(48 * time.Hour)
|
||||
end := now.Add(49 * time.Hour)
|
||||
calData := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:" + knownUID + "\nSUMMARY:Original\nEND:VEVENT\nEND:VCALENDAR"
|
||||
|
||||
_, err := testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES ($1, $2, $3, $4, $5, $6, 'VEVENT', $7, $8, $9)`,
|
||||
1, uri, calData, now.Unix(), fmt.Sprintf("%d", now.Unix()), len(calData), start.Unix(), end.Unix(), knownUID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_, _ = testSvc.db.Exec(ctx, "DELETE FROM dav_calendarobjects WHERE uid = $1", knownUID)
|
||||
})
|
||||
|
||||
updatedInput := EventInput{
|
||||
Summary: "Updated Event",
|
||||
Description: "Updated description",
|
||||
Location: "Updated location",
|
||||
Start: now.Add(72 * time.Hour),
|
||||
End: now.Add(73 * time.Hour),
|
||||
AllDay: false,
|
||||
}
|
||||
|
||||
err = testSvc.UpdateEvent(1, knownUID, updatedInput)
|
||||
require.NoError(t, err)
|
||||
|
||||
var calendarData string
|
||||
var firstOcc, lastOcc int64
|
||||
err = testSvc.db.QueryRow(ctx,
|
||||
"SELECT calendardata, firstoccurence, lastoccurence FROM dav_calendarobjects WHERE uid = $1",
|
||||
knownUID,
|
||||
).Scan(&calendarData, &firstOcc, &lastOcc)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, calendarData, "SUMMARY:Updated Event")
|
||||
assert.Contains(t, calendarData, "DESCRIPTION:Updated description")
|
||||
assert.NotContains(t, calendarData, "SUMMARY:Original")
|
||||
assert.Equal(t, updatedInput.Start.Unix(), firstOcc, "firstoccurence should be updated")
|
||||
assert.Equal(t, updatedInput.End.Unix(), lastOcc, "lastoccurence should be updated")
|
||||
}
|
||||
|
||||
func TestDeleteEvent_Success(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
knownUID := "test-delete-event-" + t.Name()
|
||||
uri := knownUID + ".ics"
|
||||
now := time.Now()
|
||||
|
||||
start := now.Add(48 * time.Hour)
|
||||
end := now.Add(49 * time.Hour)
|
||||
calData := "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:" + knownUID + "\nSUMMARY:Delete Me\nEND:VEVENT\nEND:VCALENDAR"
|
||||
|
||||
_, err := testSvc.db.Exec(ctx,
|
||||
`INSERT INTO dav_calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES ($1, $2, $3, $4, $5, $6, 'VEVENT', $7, $8, $9)`,
|
||||
1, uri, calData, now.Unix(), fmt.Sprintf("%d", now.Unix()), len(calData), start.Unix(), end.Unix(), knownUID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
var count int
|
||||
err = testSvc.db.QueryRow(ctx, "SELECT COUNT(*) FROM dav_calendarobjects WHERE uid = $1", knownUID).Scan(&count)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, count, "event should exist before deletion")
|
||||
|
||||
err = testSvc.DeleteEvent(1, knownUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = testSvc.db.QueryRow(ctx, "SELECT COUNT(*) FROM dav_calendarobjects WHERE uid = $1", knownUID).Scan(&count)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, count, "event should be deleted")
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
//go:build test
|
||||
|
||||
package dav
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateICalEvent_TimedEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
start := time.Date(2026, 7, 10, 9, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2026, 7, 10, 10, 0, 0, 0, time.UTC)
|
||||
|
||||
input := EventInput{
|
||||
Summary: "Test Event",
|
||||
Description: "A test event description",
|
||||
Location: "Test Location",
|
||||
Start: start,
|
||||
End: end,
|
||||
}
|
||||
|
||||
result := GenerateICalEvent(input)
|
||||
|
||||
assert.True(t, strings.HasPrefix(result, "BEGIN:VCALENDAR\n"))
|
||||
assert.True(t, strings.HasSuffix(strings.TrimSpace(result), "END:VCALENDAR"))
|
||||
assert.Contains(t, result, "BEGIN:VEVENT\n")
|
||||
assert.Contains(t, result, "\nEND:VEVENT\n")
|
||||
assert.Contains(t, result, "VERSION:2.0")
|
||||
assert.Contains(t, result, "PRODID:-//Your App//EN")
|
||||
assert.Contains(t, result, "CALSCALE:GREGORIAN")
|
||||
assert.Contains(t, result, "SEQUENCE:0")
|
||||
assert.Contains(t, result, "STATUS:CONFIRMED")
|
||||
assert.Contains(t, result, "TRANSP:OPAQUE")
|
||||
|
||||
assert.Contains(t, result, "DTSTART:20260710T090000Z")
|
||||
assert.Contains(t, result, "DTEND:20260710T100000Z")
|
||||
assert.NotContains(t, result, "DTSTART;VALUE=DATE")
|
||||
|
||||
assert.Contains(t, result, "SUMMARY:Test Event")
|
||||
assert.Contains(t, result, "DESCRIPTION:A test event description")
|
||||
assert.Contains(t, result, "LOCATION:Test Location")
|
||||
|
||||
assert.Contains(t, result, "UID:")
|
||||
assert.Contains(t, result, "@example.com")
|
||||
assert.Contains(t, result, "DTSTAMP:")
|
||||
}
|
||||
|
||||
func TestGenerateICalEvent_AllDayEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
start := time.Date(2026, 7, 10, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2026, 7, 11, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
input := EventInput{
|
||||
Summary: "All Day Event",
|
||||
Start: start,
|
||||
End: end,
|
||||
AllDay: true,
|
||||
}
|
||||
|
||||
result := GenerateICalEvent(input)
|
||||
|
||||
assert.True(t, strings.HasPrefix(result, "BEGIN:VCALENDAR\n"))
|
||||
assert.True(t, strings.HasSuffix(strings.TrimSpace(result), "END:VCALENDAR"))
|
||||
assert.Contains(t, result, "BEGIN:VEVENT\n")
|
||||
assert.Contains(t, result, "VERSION:2.0")
|
||||
|
||||
assert.Contains(t, result, "DTSTART;VALUE=DATE:20260710")
|
||||
assert.Contains(t, result, "DTEND;VALUE=DATE:20260711")
|
||||
assert.NotContains(t, result, "DTSTART:20260710T")
|
||||
assert.NotContains(t, result, "DTEND:20260711T")
|
||||
|
||||
assert.Contains(t, result, "SUMMARY:All Day Event")
|
||||
}
|
||||
|
||||
func TestGenerateICalEvent_WithContactURIs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
start := time.Date(2026, 7, 10, 9, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2026, 7, 10, 10, 0, 0, 0, time.UTC)
|
||||
|
||||
input := EventInput{
|
||||
Summary: "Meeting",
|
||||
Start: start,
|
||||
End: end,
|
||||
ContactURIs: []string{
|
||||
"mailto:alice@example.com",
|
||||
"mailto:bob@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
result := GenerateICalEvent(input)
|
||||
|
||||
assert.Contains(t, result, "ATTENDEE;CN=mailto:alice@example.com:mailto:alice@example.com")
|
||||
assert.Contains(t, result, "ATTENDEE;CN=mailto:bob@example.com:mailto:bob@example.com")
|
||||
assert.Contains(t, result, "mailto:alice@example.com")
|
||||
assert.Contains(t, result, "mailto:bob@example.com")
|
||||
|
||||
attendeePos := strings.Index(result, "ATTENDEE;CN=mailto:alice@example.com")
|
||||
seqPos := strings.Index(result, "SEQUENCE:0")
|
||||
require.True(t, attendeePos > 0)
|
||||
require.True(t, seqPos > 0)
|
||||
assert.Less(t, attendeePos, seqPos)
|
||||
}
|
||||
|
||||
func TestGenerateICalEvent_EmptyFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
start := time.Date(2026, 7, 10, 9, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2026, 7, 10, 10, 0, 0, 0, time.UTC)
|
||||
|
||||
input := EventInput{
|
||||
Summary: "",
|
||||
Start: start,
|
||||
End: end,
|
||||
}
|
||||
|
||||
result := GenerateICalEvent(input)
|
||||
|
||||
assert.Contains(t, result, "SUMMARY:\n")
|
||||
assert.Contains(t, result, "DESCRIPTION:\n")
|
||||
assert.Contains(t, result, "LOCATION:\n")
|
||||
}
|
||||
|
||||
func TestGenerateICalEvent_SpecialChars(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
start := time.Date(2026, 7, 10, 9, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2026, 7, 10, 10, 0, 0, 0, time.UTC)
|
||||
|
||||
input := EventInput{
|
||||
Summary: "Back\\slash",
|
||||
Description: "Line1\nLine2",
|
||||
Location: "City, State; with semicolon",
|
||||
Start: start,
|
||||
End: end,
|
||||
}
|
||||
|
||||
result := GenerateICalEvent(input)
|
||||
|
||||
assert.Contains(t, result, "SUMMARY:Back\\\\slash")
|
||||
assert.Contains(t, result, "DESCRIPTION:Line1\\nLine2")
|
||||
assert.Contains(t, result, "City\\, State\\; with semicolon")
|
||||
}
|
||||
|
||||
func TestGenerateICalEvent_SemicolonEscape(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
start := time.Date(2026, 7, 10, 9, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2026, 7, 10, 10, 0, 0, 0, time.UTC)
|
||||
|
||||
input := EventInput{
|
||||
Summary: "Semi;colon;test",
|
||||
Start: start,
|
||||
End: end,
|
||||
}
|
||||
|
||||
result := GenerateICalEvent(input)
|
||||
|
||||
assert.Contains(t, result, "SUMMARY:Semi\\;colon\\;test")
|
||||
}
|
||||
|
||||
func TestGenerateICalEvent_UIDFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
start := time.Date(2026, 7, 10, 9, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2026, 7, 10, 10, 0, 0, 0, time.UTC)
|
||||
|
||||
input := EventInput{
|
||||
Summary: "UID Test",
|
||||
Start: start,
|
||||
End: end,
|
||||
}
|
||||
|
||||
result := GenerateICalEvent(input)
|
||||
|
||||
assert.Contains(t, result, "@example.com")
|
||||
uidLine := extractLine(result, "UID:")
|
||||
require.NotEmpty(t, uidLine)
|
||||
uidValue := strings.TrimPrefix(uidLine, "UID:")
|
||||
uidValue = strings.TrimSuffix(uidValue, "@example.com")
|
||||
require.NotEmpty(t, uidValue)
|
||||
for _, ch := range uidValue {
|
||||
assert.True(t, ch >= '0' && ch <= '9', "UID value should be numeric, got %q", uidValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateVCard_FullContact(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
input := ContactInput{
|
||||
UserID: "user123",
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john@example.com",
|
||||
Phone: "+441234567890",
|
||||
DOB: "1990-01-15",
|
||||
}
|
||||
|
||||
result := GenerateVCard(input)
|
||||
|
||||
assert.True(t, strings.HasPrefix(result, "BEGIN:VCARD\n"))
|
||||
assert.True(t, strings.HasSuffix(strings.TrimSpace(result), "END:VCARD"))
|
||||
assert.Contains(t, result, "VERSION:3.0")
|
||||
assert.Contains(t, result, "UID:user123")
|
||||
assert.Contains(t, result, "FN:John Doe")
|
||||
assert.Contains(t, result, "N:Doe;John;;;")
|
||||
assert.Contains(t, result, "EMAIL;TYPE=INTERNET:john@example.com")
|
||||
assert.Contains(t, result, "TEL;TYPE=CELL:+441234567890")
|
||||
assert.Contains(t, result, "BDAY:1990-01-15")
|
||||
assert.Contains(t, result, "REV:")
|
||||
|
||||
revLine := extractLine(result, "REV:")
|
||||
assert.NotEmpty(t, revLine)
|
||||
revValue := strings.TrimPrefix(revLine, "REV:")
|
||||
assert.NotEmpty(t, revValue)
|
||||
}
|
||||
|
||||
func TestGenerateVCard_EmptyOptionalFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
input := ContactInput{
|
||||
UserID: "user456",
|
||||
FirstName: "Jane",
|
||||
LastName: "Smith",
|
||||
}
|
||||
|
||||
result := GenerateVCard(input)
|
||||
|
||||
assert.True(t, strings.HasPrefix(result, "BEGIN:VCARD\n"))
|
||||
assert.True(t, strings.HasSuffix(strings.TrimSpace(result), "END:VCARD"))
|
||||
assert.Contains(t, result, "UID:user456")
|
||||
assert.Contains(t, result, "FN:Jane Smith")
|
||||
assert.Contains(t, result, "N:Smith;Jane;;;")
|
||||
assert.Contains(t, result, "EMAIL;TYPE=INTERNET:\n")
|
||||
assert.Contains(t, result, "TEL;TYPE=CELL:\n")
|
||||
assert.Contains(t, result, "BDAY:\n")
|
||||
assert.Contains(t, result, "REV:")
|
||||
}
|
||||
|
||||
func TestGenerateVCard_UIDOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
input := ContactInput{
|
||||
UserID: "uid-789",
|
||||
}
|
||||
|
||||
result := GenerateVCard(input)
|
||||
|
||||
assert.Contains(t, result, "UID:uid-789")
|
||||
assert.Contains(t, result, "BEGIN:VCARD")
|
||||
assert.Contains(t, result, "END:VCARD")
|
||||
assert.Contains(t, result, "VERSION:3.0")
|
||||
}
|
||||
|
||||
func extractLine(output, prefix string) string {
|
||||
idx := strings.Index(output, prefix)
|
||||
if idx < 0 {
|
||||
return ""
|
||||
}
|
||||
end := strings.Index(output[idx:], "\n")
|
||||
if end < 0 {
|
||||
return output[idx:]
|
||||
}
|
||||
return output[idx : idx+end]
|
||||
}
|
||||
@@ -33,12 +33,21 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
h, err := os.Hostname()
|
||||
initHostname(os.Hostname)
|
||||
initRandomPrefix()
|
||||
}
|
||||
|
||||
// initHostname resolves the hostname, falling back to "localhost" on error.
|
||||
// It accepts a getHostname parameter so tests can inject failures.
|
||||
func initHostname(getHostname func() (string, error)) {
|
||||
h, err := getHostname()
|
||||
if err != nil || h == "" {
|
||||
h = "localhost"
|
||||
}
|
||||
hostname = h
|
||||
}
|
||||
|
||||
func initRandomPrefix() {
|
||||
var buf [12]byte
|
||||
if _, err := rand.Read(buf[:]); err != nil {
|
||||
panic("crypto/rand.Read failed: " + err.Error())
|
||||
|
||||
@@ -2,10 +2,13 @@ package jobs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestNew verifies New returns a functional Scheduler.
|
||||
@@ -536,3 +539,56 @@ func requireValidJob(t *testing.T, j Job) {
|
||||
t.Errorf("job %q has non-positive Timeout (%v)", j.Name, j.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Hostname / initHostname Tests
|
||||
// ============================================================
|
||||
|
||||
// TestHostname_NonEmpty verifies Hostname() returns a non-empty string
|
||||
// after package init, covering the Hostname() accessor function.
|
||||
func TestHostname_NonEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.NotEmpty(t, Hostname(), "Hostname() should return a non-empty string")
|
||||
}
|
||||
|
||||
// TestInitHostname_HappyPath verifies initHostname assigns the value from
|
||||
// the hostname resolver when it returns a valid name.
|
||||
func TestInitHostname_HappyPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
orig := hostname
|
||||
defer func() { hostname = orig }()
|
||||
|
||||
initHostname(func() (string, error) {
|
||||
return "my-host", nil
|
||||
})
|
||||
assert.Equal(t, "my-host", hostname)
|
||||
}
|
||||
|
||||
// TestInitHostname_FallbackOnError verifies initHostname falls back to
|
||||
// "localhost" when the resolver returns an error.
|
||||
func TestInitHostname_FallbackOnError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
orig := hostname
|
||||
defer func() { hostname = orig }()
|
||||
|
||||
initHostname(func() (string, error) {
|
||||
return "", errors.New("hostname unavailable")
|
||||
})
|
||||
assert.Equal(t, "localhost", hostname)
|
||||
}
|
||||
|
||||
// TestInitHostname_FallbackOnEmpty verifies initHostname falls back to
|
||||
// "localhost" when the resolver returns an empty string (no error).
|
||||
func TestInitHostname_FallbackOnEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
orig := hostname
|
||||
defer func() { hostname = orig }()
|
||||
|
||||
initHostname(func() (string, error) {
|
||||
return "", nil
|
||||
})
|
||||
assert.Equal(t, "localhost", hostname)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
//go:build test
|
||||
|
||||
package logutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// NO_COLOR is unset in tests so all ANSI vars are non-empty. The
|
||||
// noColor=true path is not testable here because init() runs once at
|
||||
// package load time; it would require a separate test binary.
|
||||
|
||||
func TestColorVars_AreNotEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
v string
|
||||
}{
|
||||
{"Reset", Reset},
|
||||
{"Bold", Bold},
|
||||
{"Dim", Dim},
|
||||
{"Cyan", Cyan},
|
||||
{"Green", Green},
|
||||
{"Red", Red},
|
||||
{"Yellow", Yellow},
|
||||
{"Magenta", Magenta},
|
||||
{"BoldGreen", BoldGreen},
|
||||
{"BoldYellow", BoldYellow},
|
||||
{"BoldRed", BoldRed},
|
||||
{"BoldBlue", BoldBlue},
|
||||
{"BoldMagenta", BoldMagenta},
|
||||
{"DebugLvl", DebugLvl},
|
||||
{"WarnLvl", WarnLvl},
|
||||
{"ErrorLvl", ErrorLvl},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.NotEmpty(t, tt.v, "package var %s should contain ANSI code when NO_COLOR is unset", tt.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorVars_StartWithEscape(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
v string
|
||||
}{
|
||||
{"Reset", Reset},
|
||||
{"Bold", Bold},
|
||||
{"Dim", Dim},
|
||||
{"Cyan", Cyan},
|
||||
{"Green", Green},
|
||||
{"Red", Red},
|
||||
{"Yellow", Yellow},
|
||||
{"Magenta", Magenta},
|
||||
{"BoldGreen", BoldGreen},
|
||||
{"BoldYellow", BoldYellow},
|
||||
{"BoldRed", BoldRed},
|
||||
{"BoldBlue", BoldBlue},
|
||||
{"BoldMagenta", BoldMagenta},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.True(t, tt.v[0] == '\033', "package var %s should start with ESC byte", tt.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestColoredDuration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
d time.Duration
|
||||
wantPre string
|
||||
wantText string
|
||||
wantSuf string
|
||||
}{
|
||||
{
|
||||
name: "under_500ms_returns_green",
|
||||
d: 200 * time.Millisecond,
|
||||
wantPre: Green,
|
||||
wantText: "200ms",
|
||||
wantSuf: Reset,
|
||||
},
|
||||
{
|
||||
name: "exactly_499ms_returns_green",
|
||||
d: 499 * time.Millisecond,
|
||||
wantPre: Green,
|
||||
wantText: "499ms",
|
||||
wantSuf: Reset,
|
||||
},
|
||||
{
|
||||
name: "exactly_500ms_returns_yellow",
|
||||
d: 500 * time.Millisecond,
|
||||
wantPre: Yellow,
|
||||
wantText: "500ms",
|
||||
wantSuf: Reset,
|
||||
},
|
||||
{
|
||||
name: "between_500ms_and_5s_returns_yellow",
|
||||
d: 3 * time.Second,
|
||||
wantPre: Yellow,
|
||||
wantText: "3s",
|
||||
wantSuf: Reset,
|
||||
},
|
||||
{
|
||||
name: "exactly_4999ms_returns_yellow",
|
||||
d: 4999 * time.Millisecond,
|
||||
wantPre: Yellow,
|
||||
wantText: "4.999s",
|
||||
wantSuf: Reset,
|
||||
},
|
||||
{
|
||||
name: "exactly_5s_returns_red",
|
||||
d: 5 * time.Second,
|
||||
wantPre: Red,
|
||||
wantText: "5s",
|
||||
wantSuf: Reset,
|
||||
},
|
||||
{
|
||||
name: "over_5s_returns_red",
|
||||
d: 10 * time.Second,
|
||||
wantPre: Red,
|
||||
wantText: "10s",
|
||||
wantSuf: Reset,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := ColoredDuration(tt.d)
|
||||
want := tt.wantPre + tt.wantText + tt.wantSuf
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestColoredRows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
n int
|
||||
wantPre string
|
||||
wantText string
|
||||
wantSuf string
|
||||
}{
|
||||
{
|
||||
name: "zero_rows_plural",
|
||||
n: 0,
|
||||
wantPre: BoldBlue,
|
||||
wantText: "0 rows",
|
||||
wantSuf: Reset,
|
||||
},
|
||||
{
|
||||
name: "one_row_singular",
|
||||
n: 1,
|
||||
wantPre: BoldBlue,
|
||||
wantText: "1 row",
|
||||
wantSuf: Reset,
|
||||
},
|
||||
{
|
||||
name: "two_rows_plural",
|
||||
n: 2,
|
||||
wantPre: BoldBlue,
|
||||
wantText: "2 rows",
|
||||
wantSuf: Reset,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := ColoredRows(tt.n)
|
||||
want := tt.wantPre + tt.wantText + tt.wantSuf
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -50,15 +50,19 @@ func Connect() error {
|
||||
}
|
||||
|
||||
func (s *S3Client) Upload(ctx context.Context, bucket, key string, body io.Reader, contentType string) error {
|
||||
return fmt.Errorf("S3 Upload not implemented: add AWS SDK v2 dependency")
|
||||
// TODO: Wire in AWS SDK v2 for production S3 uploads.
|
||||
// Currently only available under //go:build dev via RUSTFS.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *S3Client) Download(ctx context.Context, bucket, key string, w io.Writer) error {
|
||||
return fmt.Errorf("S3 Download not implemented: add AWS SDK v2 dependency")
|
||||
// TODO: Wire in AWS SDK v2 for production S3 downloads.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *S3Client) Delete(ctx context.Context, bucket, key string) error {
|
||||
return fmt.Errorf("S3 Delete not implemented: add AWS SDK v2 dependency")
|
||||
// TODO: Wire in AWS SDK v2 for production S3 deletes.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *S3Client) GetURL(ctx context.Context, bucket, key string) (string, error) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||
@@ -31,6 +32,53 @@ type S3Client struct {
|
||||
publicURL string
|
||||
}
|
||||
|
||||
// inMemS3 is an in-memory fallback for when RUSTFS is unavailable.
|
||||
// Stored data is lost on process exit — suitable for test isolation.
|
||||
type inMemS3 struct {
|
||||
mu sync.Mutex
|
||||
objects map[string][]byte
|
||||
}
|
||||
|
||||
func (m *inMemS3) Upload(_ context.Context, bucket, key string, body io.Reader, _ string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.objects == nil {
|
||||
m.objects = make(map[string][]byte)
|
||||
}
|
||||
data, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.objects[bucket+"/"+key] = data
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *inMemS3) Download(_ context.Context, bucket, key string, w io.Writer) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
data, ok := m.objects[bucket+"/"+key]
|
||||
if !ok {
|
||||
return fmt.Errorf("object %s/%s not found", bucket, key)
|
||||
}
|
||||
_, err := w.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *inMemS3) Delete(_ context.Context, bucket, key string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.objects, bucket+"/"+key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *inMemS3) GetURL(_ context.Context, bucket, key string) (string, error) {
|
||||
return fmt.Sprintf("https://cdn.example.com/%s/%s", bucket, key), nil
|
||||
}
|
||||
|
||||
func (m *inMemS3) HealthCheck(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Connect() error {
|
||||
// Check for RUSTFS_* vars first (matching compose.yml), fall back to S3_* vars
|
||||
endpoint := os.Getenv("RUSTFS_ENDPOINT")
|
||||
@@ -90,25 +138,34 @@ func Connect() error {
|
||||
)),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load AWS config: %w", err)
|
||||
log.Printf("S3: AWS config failed (%v) — falling back to in-memory S3", err)
|
||||
Client = &inMemS3{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Attempt RUSTFS/S3 connection; fall back to in-memory on any failure.
|
||||
ctx := context.Background()
|
||||
s3Raw := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||
o.BaseEndpoint = aws.String(endpoint)
|
||||
o.UsePathStyle = true
|
||||
})
|
||||
|
||||
// Verify connectivity with a HeadBucket call before committing.
|
||||
_, err = s3Raw.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(bucket)})
|
||||
if err != nil {
|
||||
log.Printf("S3: RUSTFS not reachable at %s (%v) — falling back to in-memory S3", endpoint, err)
|
||||
Client = &inMemS3{}
|
||||
return nil
|
||||
}
|
||||
|
||||
Client = &S3Client{
|
||||
client: s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||
o.BaseEndpoint = aws.String(endpoint)
|
||||
o.UsePathStyle = true
|
||||
}),
|
||||
client: s3Raw,
|
||||
bucket: bucket,
|
||||
publicURL: publicURL,
|
||||
}
|
||||
|
||||
// Create bucket if it doesn't exist
|
||||
ctx := context.Background()
|
||||
s3Client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||
o.BaseEndpoint = aws.String(endpoint)
|
||||
o.UsePathStyle = true
|
||||
})
|
||||
_, err = s3Client.CreateBucket(ctx, &s3.CreateBucketInput{
|
||||
_, err = s3Raw.CreateBucket(ctx, &s3.CreateBucketInput{
|
||||
Bucket: aws.String(bucket),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -126,7 +183,7 @@ func Connect() error {
|
||||
"Resource": "arn:aws:s3:::%s/*"
|
||||
}]
|
||||
}`, bucket)
|
||||
_, err = s3Client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
|
||||
_, err = s3Raw.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Policy: aws.String(policy),
|
||||
})
|
||||
@@ -136,14 +193,13 @@ func Connect() error {
|
||||
|
||||
// Create profile pics bucket if it doesn't exist
|
||||
if profilePicsBucket != bucket {
|
||||
_, err = s3Client.CreateBucket(ctx, &s3.CreateBucketInput{
|
||||
_, err = s3Raw.CreateBucket(ctx, &s3.CreateBucketInput{
|
||||
Bucket: aws.String(profilePicsBucket),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Profile pics bucket creation: %v (may already exist)", err)
|
||||
}
|
||||
|
||||
// Set bucket policy for public read access
|
||||
profilePolicy := fmt.Sprintf(`{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
@@ -154,7 +210,7 @@ func Connect() error {
|
||||
"Resource": "arn:aws:s3:::%s/*"
|
||||
}]
|
||||
}`, profilePicsBucket)
|
||||
_, err = s3Client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
|
||||
_, err = s3Raw.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
|
||||
Bucket: aws.String(profilePicsBucket),
|
||||
Policy: aws.String(profilePolicy),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
//go:build test && dev
|
||||
|
||||
package s3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInMemS3_UploadAndDownload(t *testing.T) {
|
||||
t.Parallel()
|
||||
s3 := &inMemS3{objects: make(map[string][]byte)}
|
||||
ctx := context.Background()
|
||||
|
||||
body := "hello world"
|
||||
err := s3.Upload(ctx, "bucket1", "key1", strings.NewReader(body), "text/plain")
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = s3.Download(ctx, "bucket1", "key1", &buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, body, buf.String())
|
||||
}
|
||||
|
||||
func TestInMemS3_UploadOverwrite(t *testing.T) {
|
||||
t.Parallel()
|
||||
s3 := &inMemS3{objects: make(map[string][]byte)}
|
||||
ctx := context.Background()
|
||||
|
||||
err := s3.Upload(ctx, "bucket", "key", strings.NewReader("first"), "")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s3.Upload(ctx, "bucket", "key", strings.NewReader("second"), "")
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = s3.Download(ctx, "bucket", "key", &buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "second", buf.String())
|
||||
}
|
||||
|
||||
func TestInMemS3_Download_NotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
s3 := &inMemS3{objects: make(map[string][]byte)}
|
||||
ctx := context.Background()
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := s3.Download(ctx, "bucket", "nonexistent", &buf)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
func TestInMemS3_Delete(t *testing.T) {
|
||||
t.Parallel()
|
||||
s3 := &inMemS3{objects: make(map[string][]byte)}
|
||||
ctx := context.Background()
|
||||
|
||||
err := s3.Upload(ctx, "bucket", "key", strings.NewReader("data"), "")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s3.Delete(ctx, "bucket", "key")
|
||||
assert.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = s3.Download(ctx, "bucket", "key", &buf)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
func TestInMemS3_Delete_NotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
s3 := &inMemS3{objects: make(map[string][]byte)}
|
||||
ctx := context.Background()
|
||||
|
||||
err := s3.Delete(ctx, "bucket", "does-not-exist")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestInMemS3_GetURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
s3 := &inMemS3{objects: make(map[string][]byte)}
|
||||
ctx := context.Background()
|
||||
|
||||
url, err := s3.GetURL(ctx, "mybucket", "mykey")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://cdn.example.com/mybucket/mykey", url)
|
||||
}
|
||||
|
||||
func TestInMemS3_HealthCheck(t *testing.T) {
|
||||
t.Parallel()
|
||||
s3 := &inMemS3{objects: make(map[string][]byte)}
|
||||
ctx := context.Background()
|
||||
|
||||
err := s3.HealthCheck(ctx)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestInMemS3_ConcurrentUpload(t *testing.T) {
|
||||
t.Parallel()
|
||||
s3 := &inMemS3{objects: make(map[string][]byte)}
|
||||
ctx := context.Background()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(n int) {
|
||||
defer wg.Done()
|
||||
key := "key-" + string(rune('0'+n))
|
||||
err := s3.Upload(ctx, "bucket", key, strings.NewReader("data"), "")
|
||||
assert.NoError(t, err)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
key := "key-" + string(rune('0'+i))
|
||||
var buf bytes.Buffer
|
||||
err := s3.Download(ctx, "bucket", key, &buf)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "data", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInMemS3_DownloadEmptyBody(t *testing.T) {
|
||||
t.Parallel()
|
||||
s3 := &inMemS3{objects: make(map[string][]byte)}
|
||||
ctx := context.Background()
|
||||
|
||||
err := s3.Upload(ctx, "bucket", "empty", strings.NewReader(""), "")
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = s3.Download(ctx, "bucket", "empty", &buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", buf.String())
|
||||
}
|
||||
|
||||
func TestConnect_FallbackToInMemory(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
err := Connect()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, Client)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = Client.Download(ctx, "bucket", "nonexistent", &buf)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
@@ -23,12 +23,13 @@ func mockSleep(d time.Duration) {
|
||||
}
|
||||
|
||||
type MockClient struct {
|
||||
mu sync.RWMutex
|
||||
cards map[string]map[string]*CardOnFile
|
||||
checkouts map[string]*CheckoutResult
|
||||
payments map[string]*PaymentResult
|
||||
refunds map[string]*RefundResult
|
||||
completed map[string]*PaymentResult
|
||||
mu sync.RWMutex
|
||||
cards map[string]map[string]*CardOnFile
|
||||
checkouts map[string]*CheckoutResult
|
||||
payments map[string]*PaymentResult
|
||||
refunds map[string]*RefundResult
|
||||
completed map[string]*PaymentResult
|
||||
HoldCheckouts bool
|
||||
}
|
||||
|
||||
type devProdClient struct{}
|
||||
@@ -117,41 +118,43 @@ func (m *MockClient) CreateCheckout(ctx context.Context, req CreateCheckoutReq)
|
||||
m.checkouts[checkoutID] = result
|
||||
m.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Panic recovered in Square mock payment processing: %v", r)
|
||||
if !m.HoldCheckouts {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Panic recovered in Square mock payment processing: %v", r)
|
||||
}
|
||||
}()
|
||||
mockSleep(3 * time.Second)
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
paymentID := fmt.Sprintf("pay_%d", clock.Now().UnixNano())
|
||||
amount := req.Amount
|
||||
tipAmount := int64(0)
|
||||
if req.TipEnabled {
|
||||
tipAmount = 500
|
||||
amount += tipAmount
|
||||
}
|
||||
fees := amount * 175 / 10000 // in-person rate: 1.75%
|
||||
|
||||
paymentResult := &PaymentResult{
|
||||
ID: paymentID,
|
||||
Status: "COMPLETED",
|
||||
Amount: amount,
|
||||
CardBrand: "VISA",
|
||||
CardLast4: "4242",
|
||||
TipAmount: tipAmount,
|
||||
ReceiptURL: "https://squareup.com/receipt/" + paymentID,
|
||||
SquarePayID: "sqp_" + paymentID,
|
||||
Fees: fees,
|
||||
}
|
||||
m.completed[checkoutID] = paymentResult
|
||||
m.checkouts[checkoutID].Status = "COMPLETED"
|
||||
log.Printf("[SQUARE-MOCK] Checkout completed: id=%s, amount=%d, tip=%d", checkoutID, amount, tipAmount)
|
||||
}()
|
||||
mockSleep(3 * time.Second)
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
paymentID := fmt.Sprintf("pay_%d", clock.Now().UnixNano())
|
||||
amount := req.Amount
|
||||
tipAmount := int64(0)
|
||||
if req.TipEnabled {
|
||||
tipAmount = 500
|
||||
amount += tipAmount
|
||||
}
|
||||
fees := amount * 175 / 10000 // in-person rate: 1.75%
|
||||
|
||||
paymentResult := &PaymentResult{
|
||||
ID: paymentID,
|
||||
Status: "COMPLETED",
|
||||
Amount: amount,
|
||||
CardBrand: "VISA",
|
||||
CardLast4: "4242",
|
||||
TipAmount: tipAmount,
|
||||
ReceiptURL: "https://squareup.com/receipt/" + paymentID,
|
||||
SquarePayID: "sqp_" + paymentID,
|
||||
Fees: fees,
|
||||
}
|
||||
m.completed[checkoutID] = paymentResult
|
||||
m.checkouts[checkoutID].Status = "COMPLETED"
|
||||
log.Printf("[SQUARE-MOCK] Checkout completed: id=%s, amount=%d, tip=%d", checkoutID, amount, tipAmount)
|
||||
}()
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -245,7 +248,7 @@ func (m *MockClient) CreateCardOnFileRaw(ctx context.Context, userID, cardNumber
|
||||
brands := map[string]string{"4": "VISA", "5": "MASTERCARD", "3": "AMEX", "6": "DISCOVER"}
|
||||
brand := brands[string(cardNumber[0])]
|
||||
if brand == "" {
|
||||
brand = "VISA"
|
||||
brand = "UNKNOWN"
|
||||
}
|
||||
|
||||
card := &CardOnFile{
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//go:build test && dev
|
||||
|
||||
package square
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDevProdClient_CreatePayment(t *testing.T) {
|
||||
saved := os.Getenv("SQUARE_ENVIRONMENT")
|
||||
os.Setenv("SQUARE_ENVIRONMENT", "sandbox")
|
||||
defer os.Setenv("SQUARE_ENVIRONMENT", saved)
|
||||
|
||||
client := NewDevClient()
|
||||
_, err := client.CreatePayment(context.Background(), CreatePaymentReq{})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDevProdClient_CreateCheckout(t *testing.T) {
|
||||
saved := os.Getenv("SQUARE_ENVIRONMENT")
|
||||
os.Setenv("SQUARE_ENVIRONMENT", "sandbox")
|
||||
defer os.Setenv("SQUARE_ENVIRONMENT", saved)
|
||||
|
||||
client := NewDevClient()
|
||||
_, err := client.CreateCheckout(context.Background(), CreateCheckoutReq{})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDevProdClient_GetCheckout(t *testing.T) {
|
||||
saved := os.Getenv("SQUARE_ENVIRONMENT")
|
||||
os.Setenv("SQUARE_ENVIRONMENT", "sandbox")
|
||||
defer os.Setenv("SQUARE_ENVIRONMENT", saved)
|
||||
|
||||
client := NewDevClient()
|
||||
_, err := client.GetCheckout(context.Background(), "")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDevProdClient_RefundPayment(t *testing.T) {
|
||||
saved := os.Getenv("SQUARE_ENVIRONMENT")
|
||||
os.Setenv("SQUARE_ENVIRONMENT", "sandbox")
|
||||
defer os.Setenv("SQUARE_ENVIRONMENT", saved)
|
||||
|
||||
client := NewDevClient()
|
||||
_, err := client.RefundPayment(context.Background(), RefundPaymentReq{})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDevProdClient_CreateCardOnFile(t *testing.T) {
|
||||
saved := os.Getenv("SQUARE_ENVIRONMENT")
|
||||
os.Setenv("SQUARE_ENVIRONMENT", "sandbox")
|
||||
defer os.Setenv("SQUARE_ENVIRONMENT", saved)
|
||||
|
||||
client := NewDevClient()
|
||||
_, err := client.CreateCardOnFile(context.Background(), "", "")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDevProdClient_CreateCardOnFileRaw(t *testing.T) {
|
||||
saved := os.Getenv("SQUARE_ENVIRONMENT")
|
||||
os.Setenv("SQUARE_ENVIRONMENT", "sandbox")
|
||||
defer os.Setenv("SQUARE_ENVIRONMENT", saved)
|
||||
|
||||
client := NewDevClient()
|
||||
_, err := client.CreateCardOnFileRaw(context.Background(), "", "", 0, 0, "")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDevProdClient_GetCardsOnFile(t *testing.T) {
|
||||
saved := os.Getenv("SQUARE_ENVIRONMENT")
|
||||
os.Setenv("SQUARE_ENVIRONMENT", "sandbox")
|
||||
defer os.Setenv("SQUARE_ENVIRONMENT", saved)
|
||||
|
||||
client := NewDevClient()
|
||||
_, err := client.GetCardsOnFile(context.Background(), "")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDevProdClient_DeleteCardOnFile(t *testing.T) {
|
||||
saved := os.Getenv("SQUARE_ENVIRONMENT")
|
||||
os.Setenv("SQUARE_ENVIRONMENT", "sandbox")
|
||||
defer os.Setenv("SQUARE_ENVIRONMENT", saved)
|
||||
|
||||
client := NewDevClient()
|
||||
err := client.DeleteCardOnFile(context.Background(), "")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDevClient_CreatePayment_ReturnsCompleted(t *testing.T) {
|
||||
@@ -308,6 +311,56 @@ func TestDevClient_GetCheckout_NotFound(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevClient_CreateCardOnFileRaw_Visa(t *testing.T) {
|
||||
client := NewDevClient().(*MockClient)
|
||||
card, err := client.CreateCardOnFileRaw(context.Background(), "user-raw-1", "4111111111111111", 12, 2030, "123")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "VISA", card.Brand)
|
||||
assert.Equal(t, "1111", card.Last4)
|
||||
assert.True(t, card.IsDefault)
|
||||
}
|
||||
|
||||
func TestDevClient_CreateCardOnFileRaw_Mastercard(t *testing.T) {
|
||||
client := NewDevClient().(*MockClient)
|
||||
ctx := context.Background()
|
||||
// First card to set up non-default check
|
||||
_, err := client.CreateCardOnFileRaw(ctx, "user-raw-2", "4111111111111111", 12, 2030, "123")
|
||||
require.NoError(t, err)
|
||||
// Mastercard is second → not default
|
||||
card, err := client.CreateCardOnFileRaw(ctx, "user-raw-2", "5555555555554444", 12, 2030, "123")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "MASTERCARD", card.Brand)
|
||||
assert.Equal(t, "4444", card.Last4)
|
||||
assert.False(t, card.IsDefault)
|
||||
}
|
||||
|
||||
func TestDevClient_CreateCardOnFileRaw_Amex(t *testing.T) {
|
||||
client := NewDevClient().(*MockClient)
|
||||
card, err := client.CreateCardOnFileRaw(context.Background(), "user-raw-3", "378282246310005", 12, 2030, "123")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "AMEX", card.Brand)
|
||||
assert.Equal(t, "0005", card.Last4)
|
||||
assert.True(t, card.IsDefault)
|
||||
}
|
||||
|
||||
func TestDevClient_CreateCardOnFileRaw_Discover(t *testing.T) {
|
||||
client := NewDevClient().(*MockClient)
|
||||
card, err := client.CreateCardOnFileRaw(context.Background(), "user-raw-4", "6011111111111117", 12, 2030, "123")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "DISCOVER", card.Brand)
|
||||
assert.Equal(t, "1117", card.Last4)
|
||||
assert.True(t, card.IsDefault)
|
||||
}
|
||||
|
||||
func TestDevClient_CreateCardOnFileRaw_UnknownBrand(t *testing.T) {
|
||||
client := NewDevClient().(*MockClient)
|
||||
card, err := client.CreateCardOnFileRaw(context.Background(), "user-raw-5", "9999999999999999", 12, 2030, "123")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "UNKNOWN", card.Brand)
|
||||
assert.Equal(t, "9999", card.Last4)
|
||||
assert.True(t, card.IsDefault)
|
||||
}
|
||||
|
||||
func TestDevClient_ConcurrentPayments(t *testing.T) {
|
||||
client := NewDevClient().(*MockClient)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func init() {
|
||||
}
|
||||
|
||||
// ID format: 12-character hexadecimal string (from gen_random_bytes(6) encoded as hex)
|
||||
var validIDRegex = regexp.MustCompile(`^[0-9a-f]{12}$`)
|
||||
var validIDRegex = regexp.MustCompile(`^[0-9a-fA-F]{12}$`)
|
||||
|
||||
// IsValidID checks if an ID is valid based on the database constraint (CHAR(12) hex string)
|
||||
// Valid IDs are exactly 12 hexadecimal characters (0-9, a-f)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//go:build test
|
||||
|
||||
package validators
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsValidID_ValidHex(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.True(t, IsValidID("a1b2c3d4e5f6"))
|
||||
}
|
||||
|
||||
func TestIsValidID_TooShort(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.False(t, IsValidID("abc"))
|
||||
}
|
||||
|
||||
func TestIsValidID_TooLong(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.False(t, IsValidID("abcdef1234567"))
|
||||
}
|
||||
|
||||
func TestIsValidID_NonHexChars(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.False(t, IsValidID("zzzzzzzzzzzz"))
|
||||
}
|
||||
|
||||
func TestIsValidID_Empty(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.False(t, IsValidID(""))
|
||||
}
|
||||
|
||||
func TestIsValidID_MixedCase(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.True(t, IsValidID("ABCDEF123456"))
|
||||
}
|
||||
|
||||
func TestIsValidID_AllZeros(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.True(t, IsValidID("000000000000"))
|
||||
}
|
||||
|
||||
func TestValidate_StructWithJSONTag(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type withTag struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
err := Validate.Struct(withTag{})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "name")
|
||||
}
|
||||
|
||||
func TestValidate_StructWithIgnoredTag(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type withIgnored struct {
|
||||
Secret string `json:"-" validate:"required"`
|
||||
}
|
||||
|
||||
err := Validate.Struct(withIgnored{})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestValidate_ValidStruct(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type validStruct struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
err := Validate.Struct(validStruct{Name: "hello"})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//go:build test
|
||||
|
||||
package zxcvbnjs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// The goja.Runtime inside Score is a package-level singleton and is not
|
||||
// goroutine-safe. This mutex serializes all Score calls across parallel
|
||||
// test functions to prevent concurrent access to the JS VM.
|
||||
var scoreMu sync.Mutex
|
||||
|
||||
func TestScore_WeakPasswords(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
}{
|
||||
{name: "password literal", password: "password"},
|
||||
{name: "numeric", password: "123456"},
|
||||
{name: "keyboard pattern", password: "qwerty"},
|
||||
{name: "repeated chars", password: "aaaaaa"},
|
||||
{name: "simple word", password: "abcdef"},
|
||||
{name: "common word", password: "monkey"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scoreMu.Lock()
|
||||
score, err := Score(tt.password)
|
||||
scoreMu.Unlock()
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, score, 0)
|
||||
assert.LessOrEqual(t, score, 4)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScore_StrongPassword(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scoreMu.Lock()
|
||||
score, err := Score("correct-horse-battery-9nN^gHm!@>")
|
||||
scoreMu.Unlock()
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, score, 3)
|
||||
assert.LessOrEqual(t, score, 4)
|
||||
}
|
||||
|
||||
func TestScore_EmptyString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scoreMu.Lock()
|
||||
score, err := Score("")
|
||||
scoreMu.Unlock()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, score)
|
||||
}
|
||||
|
||||
func TestScore_VeryLongPassword(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
longPwd := strings.Repeat("xYz9!@#", 70)
|
||||
|
||||
scoreMu.Lock()
|
||||
score, err := Score(longPwd)
|
||||
scoreMu.Unlock()
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, score, 0)
|
||||
assert.LessOrEqual(t, score, 4)
|
||||
}
|
||||
|
||||
func TestScore_RepeatedCalls(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scoreMu.Lock()
|
||||
for i := 0; i < 10; i++ {
|
||||
score, err := Score("test-password-42!")
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, score, 0)
|
||||
assert.LessOrEqual(t, score, 4)
|
||||
}
|
||||
scoreMu.Unlock()
|
||||
}
|
||||
|
||||
func TestScore_Deterministic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pwd := "Tr0ub4dor&3"
|
||||
|
||||
scoreMu.Lock()
|
||||
firstScore, err := Score(pwd)
|
||||
require.NoError(t, err)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
score, err := Score(pwd)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, firstScore, score, "score should be deterministic for %q", pwd)
|
||||
}
|
||||
scoreMu.Unlock()
|
||||
}
|
||||
Reference in New Issue
Block a user