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

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:
2026-07-10 18:13:44 +01:00
parent c0442d4ebd
commit 3029fd5179
57 changed files with 12604 additions and 94 deletions
+23
View File
@@ -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)
}
+3 -3
View File
@@ -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))
}
}
}
+655
View File
@@ -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")
}
+272
View File
@@ -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]
}
+10 -1
View File
@@ -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())
+56
View File
@@ -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)
}
+197
View File
@@ -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)
})
}
}
+7 -3
View File
@@ -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) {
+71 -15
View File
@@ -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),
})
+156
View File
@@ -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")
}
+43 -40
View File
@@ -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)
+1 -1
View File
@@ -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)
}
+109
View File
@@ -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()
}