Coverage priorities 1-5: test pure functions, extract interfaces, mock-based handler tests

- Priority 1: Test NewSendEmailTask + NewSendPushTask (5 tests)
- Priority 2: Test customHTTPErrorHandler — all 15+ branches (21 tests)
- Priority 3: Extract Enqueuer interface + payload builders in worker pkg (5 tests)
- Priority 4: Extract ClassifyFile/ComputeRelPath in migrate-encrypt (6 tests)
- Priority 5: Define Handler interfaces, refactor to accept them, mock-based tests (14 tests)
- Fix .gitignore: /worker instead of worker to stop ignoring internal/worker/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-01 20:30:09 -05:00
parent 00fd674b56
commit bec880886b
83 changed files with 19569 additions and 730 deletions

View File

@@ -0,0 +1,61 @@
package main
import (
"testing"
"time"
)
func TestClassifyCompletion_CompletedAfterDue_ReturnsOverdue(t *testing.T) {
due := time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC)
completed := time.Date(2025, 6, 5, 14, 0, 0, 0, time.UTC)
got := classifyCompletion(completed, due, 7)
if got != "overdue_tasks" {
t.Errorf("got %q, want overdue_tasks", got)
}
}
func TestClassifyCompletion_CompletedOnDueDate_ReturnsDueSoon(t *testing.T) {
due := time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC)
completed := time.Date(2025, 6, 1, 10, 0, 0, 0, time.UTC)
got := classifyCompletion(completed, due, 7)
if got != "due_soon_tasks" {
t.Errorf("got %q, want due_soon_tasks", got)
}
}
func TestClassifyCompletion_CompletedWithinThreshold_ReturnsDueSoon(t *testing.T) {
due := time.Date(2025, 6, 10, 0, 0, 0, 0, time.UTC)
completed := time.Date(2025, 6, 5, 0, 0, 0, 0, time.UTC) // 5 days before due, threshold 7
got := classifyCompletion(completed, due, 7)
if got != "due_soon_tasks" {
t.Errorf("got %q, want due_soon_tasks", got)
}
}
func TestClassifyCompletion_CompletedAtExactThreshold_ReturnsDueSoon(t *testing.T) {
due := time.Date(2025, 6, 10, 0, 0, 0, 0, time.UTC)
completed := time.Date(2025, 6, 3, 0, 0, 0, 0, time.UTC) // exactly 7 days before due
got := classifyCompletion(completed, due, 7)
if got != "due_soon_tasks" {
t.Errorf("got %q, want due_soon_tasks", got)
}
}
func TestClassifyCompletion_CompletedBeyondThreshold_ReturnsUpcoming(t *testing.T) {
due := time.Date(2025, 6, 30, 0, 0, 0, 0, time.UTC)
completed := time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC) // 29 days before due, threshold 7
got := classifyCompletion(completed, due, 7)
if got != "upcoming_tasks" {
t.Errorf("got %q, want upcoming_tasks", got)
}
}
func TestClassifyCompletion_TimeNormalization_SameDayDifferentTimes(t *testing.T) {
due := time.Date(2025, 6, 1, 23, 59, 59, 0, time.UTC)
completed := time.Date(2025, 6, 1, 0, 0, 1, 0, time.UTC) // same day, different times
got := classifyCompletion(completed, due, 7)
// Same day → daysBefore == 0 → within threshold → due_soon
if got != "due_soon_tasks" {
t.Errorf("got %q, want due_soon_tasks", got)
}
}

View File

@@ -0,0 +1,50 @@
package main
import (
"path/filepath"
"strings"
)
// isEncrypted checks if a file path ends with .enc
func isEncrypted(path string) bool {
return strings.HasSuffix(path, ".enc")
}
// encryptedPath appends .enc to the file path.
func encryptedPath(path string) string {
return path + ".enc"
}
// shouldProcessFile returns true if the file should be encrypted.
func shouldProcessFile(isDir bool, path string) bool {
return !isDir && !isEncrypted(path)
}
// FileAction represents the decision about what to do with a file during encryption migration.
type FileAction int
const (
ActionSkipDir FileAction = iota // Directory, skip
ActionSkipEncrypted // Already encrypted, skip
ActionDryRun // Would encrypt (dry run mode)
ActionEncrypt // Should encrypt
)
// ClassifyFile determines what action to take for a file during the walk.
func ClassifyFile(isDir bool, path string, dryRun bool) FileAction {
if isDir {
return ActionSkipDir
}
if isEncrypted(path) {
return ActionSkipEncrypted
}
if dryRun {
return ActionDryRun
}
return ActionEncrypt
}
// ComputeRelPath computes the relative path from base to path.
func ComputeRelPath(base, path string) (string, error) {
return filepath.Rel(base, path)
}

View File

@@ -0,0 +1,96 @@
package main
import "testing"
func TestIsEncrypted_EncFile_True(t *testing.T) {
if !isEncrypted("photo.jpg.enc") {
t.Error("expected true for .enc file")
}
}
func TestIsEncrypted_PdfFile_False(t *testing.T) {
if isEncrypted("doc.pdf") {
t.Error("expected false for .pdf file")
}
}
func TestIsEncrypted_DotEncOnly_True(t *testing.T) {
if !isEncrypted(".enc") {
t.Error("expected true for '.enc'")
}
}
func TestEncryptedPath_AppendsDotEnc(t *testing.T) {
got := encryptedPath("uploads/photo.jpg")
want := "uploads/photo.jpg.enc"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
func TestShouldProcessFile_RegularFile_True(t *testing.T) {
if !shouldProcessFile(false, "photo.jpg") {
t.Error("expected true for regular file")
}
}
func TestShouldProcessFile_Directory_False(t *testing.T) {
if shouldProcessFile(true, "uploads") {
t.Error("expected false for directory")
}
}
func TestShouldProcessFile_AlreadyEncrypted_False(t *testing.T) {
if shouldProcessFile(false, "photo.jpg.enc") {
t.Error("expected false for already encrypted file")
}
}
// --- ClassifyFile ---
func TestClassifyFile_Directory_SkipDir(t *testing.T) {
if got := ClassifyFile(true, "uploads", false); got != ActionSkipDir {
t.Errorf("got %d, want ActionSkipDir", got)
}
}
func TestClassifyFile_EncryptedFile_SkipEncrypted(t *testing.T) {
if got := ClassifyFile(false, "photo.jpg.enc", false); got != ActionSkipEncrypted {
t.Errorf("got %d, want ActionSkipEncrypted", got)
}
}
func TestClassifyFile_DryRun_DryRun(t *testing.T) {
if got := ClassifyFile(false, "photo.jpg", true); got != ActionDryRun {
t.Errorf("got %d, want ActionDryRun", got)
}
}
func TestClassifyFile_Normal_Encrypt(t *testing.T) {
if got := ClassifyFile(false, "photo.jpg", false); got != ActionEncrypt {
t.Errorf("got %d, want ActionEncrypt", got)
}
}
// --- ComputeRelPath ---
func TestComputeRelPath_Valid(t *testing.T) {
got, err := ComputeRelPath("/uploads", "/uploads/photo.jpg")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != "photo.jpg" {
t.Errorf("got %q, want %q", got, "photo.jpg")
}
}
func TestComputeRelPath_NestedPath(t *testing.T) {
got, err := ComputeRelPath("/uploads", "/uploads/2024/01/photo.jpg")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := "2024/01/photo.jpg"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}

View File

@@ -13,7 +13,6 @@ import (
"flag"
"os"
"path/filepath"
"strings"
"time"
"github.com/rs/zerolog"
@@ -87,13 +86,11 @@ func main() {
return nil
}
// Skip directories
if info.IsDir() {
action := ClassifyFile(info.IsDir(), path, *dryRun)
switch action {
case ActionSkipDir:
return nil
}
// Skip files already encrypted
if strings.HasSuffix(path, ".enc") {
case ActionSkipEncrypted:
skipped++
return nil
}
@@ -101,14 +98,14 @@ func main() {
totalFiles++
// Compute the relative path from upload dir
relPath, err := filepath.Rel(absUploadDir, path)
relPath, err := ComputeRelPath(absUploadDir, path)
if err != nil {
log.Warn().Err(err).Str("path", path).Msg("Failed to compute relative path")
errCount++
return nil
}
if *dryRun {
if action == ActionDryRun {
log.Info().Str("file", relPath).Msg("[DRY RUN] Would encrypt")
return nil
}

24
cmd/worker/startup.go Normal file
View File

@@ -0,0 +1,24 @@
package main
import "github.com/treytartt/honeydue-api/internal/worker/jobs"
// queuePriorities returns the Asynq queue priority map.
func queuePriorities() map[string]int {
return map[string]int{
"critical": 6,
"default": 3,
"low": 1,
}
}
// allJobTypes returns all registered job type strings.
func allJobTypes() []string {
return []string{
jobs.TypeSmartReminder,
jobs.TypeDailyDigest,
jobs.TypeSendEmail,
jobs.TypeSendPush,
jobs.TypeOnboardingEmails,
jobs.TypeReminderLogCleanup,
}
}

View File

@@ -0,0 +1,45 @@
package main
import (
"testing"
)
func TestQueuePriorities_CriticalHighest(t *testing.T) {
p := queuePriorities()
if p["critical"] <= p["default"] || p["critical"] <= p["low"] {
t.Errorf("critical (%d) should be highest", p["critical"])
}
}
func TestQueuePriorities_ThreeQueues(t *testing.T) {
p := queuePriorities()
if len(p) != 3 {
t.Errorf("len = %d, want 3", len(p))
}
}
func TestAllJobTypes_Count(t *testing.T) {
types := allJobTypes()
if len(types) != 6 {
t.Errorf("len = %d, want 6", len(types))
}
}
func TestAllJobTypes_NoDuplicates(t *testing.T) {
types := allJobTypes()
seen := make(map[string]bool)
for _, typ := range types {
if seen[typ] {
t.Errorf("duplicate job type: %q", typ)
}
seen[typ] = true
}
}
func TestAllJobTypes_AllNonEmpty(t *testing.T) {
for _, typ := range allJobTypes() {
if typ == "" {
t.Error("found empty job type")
}
}
}