diff --git a/internal/handlers/analytics_test.go b/internal/handlers/analytics_test.go new file mode 100644 index 0000000..54f9440 --- /dev/null +++ b/internal/handlers/analytics_test.go @@ -0,0 +1,66 @@ +package handlers + +import ( + "testing" + + "github.com/jackc/pgx/v5/pgtype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseLimit_Valid(t *testing.T) { + tests := []struct { + name string + input string + expected int32 + }{ + {"string number", "10", 10}, + {"zero", "0", 0}, + {"large number", "1000", 1000}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parseLimit(tt.input) + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestParseLimit_Invalid(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"non-numeric", "abc"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := parseLimit(tt.input) + assert.Error(t, err) + }) + } +} + +func TestFormatUUID(t *testing.T) { + uuidBytes := [16]byte{ + 0xf4, 0x7a, 0xc1, 0x0b, + 0x58, 0xcc, + 0x43, 0x72, + 0xa5, 0x67, + 0x0e, 0x02, 0xb2, 0xc3, 0xd4, 0x79, + } + pgUUID := pgtype.UUID{Bytes: uuidBytes, Valid: true} + + result := formatUUID(pgUUID) + assert.Equal(t, "f47ac10b-58cc-4372-a567-0e02b2c3d479", result) +} + +func TestFormatUUID_Null(t *testing.T) { + pgUUID := pgtype.UUID{Valid: false} + + result := formatUUID(pgUUID) + assert.Equal(t, "", result) +} diff --git a/internal/handlers/auth_test.go b/internal/handlers/auth_test.go new file mode 100644 index 0000000..370d310 --- /dev/null +++ b/internal/handlers/auth_test.go @@ -0,0 +1,87 @@ +package handlers + +import ( + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateJWTWithAllClaims_UniqueTokens(t *testing.T) { + handler := &AuthHandler{jwtKey: []byte("test-secret-key")} + + token1, err := handler.generateJWTWithAllClaims("user-id-1", "admin", "test@test.com", "testuser") + require.NoError(t, err) + assert.NotEmpty(t, token1) + + token2, err := handler.generateJWTWithAllClaims("user-id-1", "admin", "test@test.com", "testuser") + require.NoError(t, err) + assert.NotEmpty(t, token2) + + assert.NotEqual(t, token1, token2, "Tokens generated at same time should be different due to jti") +} + +func TestGenerateJWTWithAllClaims_ValidClaims(t *testing.T) { + handler := &AuthHandler{jwtKey: []byte("test-secret-key")} + + token, err := handler.generateJWTWithAllClaims("user-id-123", "admin", "admin@test.com", "adminuser") + require.NoError(t, err) + assert.NotEmpty(t, token) + + parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + return []byte("test-secret-key"), nil + }) + require.NoError(t, err) + + claims := parsedToken.Claims.(jwt.MapClaims) + assert.Equal(t, "user-id-123", claims["user_id"]) + assert.Equal(t, "admin", claims["user_role"]) + assert.Equal(t, "admin@test.com", claims["user_email"]) + assert.Equal(t, "adminuser", claims["user_username"]) + assert.Contains(t, claims, "jti") + assert.Contains(t, claims, "exp") + assert.Contains(t, claims, "iat") + + exp := int64(claims["exp"].(float64)) + iat := int64(claims["iat"].(float64)) + assert.Greater(t, exp, iat, "exp should be greater than iat") + assert.Greater(t, exp, time.Now().Unix(), "exp should be in the future") +} + +func TestGenerateJWTWithRole(t *testing.T) { + handler := &AuthHandler{jwtKey: []byte("test-secret-key")} + + token, err := handler.generateJWTWithRole("user-id-456", "moderator") + require.NoError(t, err) + assert.NotEmpty(t, token) + + parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + return []byte("test-secret-key"), nil + }) + require.NoError(t, err) + + claims := parsedToken.Claims.(jwt.MapClaims) + assert.Equal(t, "user-id-456", claims["user_id"]) + assert.Equal(t, "moderator", claims["user_role"]) + assert.Equal(t, "", claims["user_email"]) + assert.Equal(t, "", claims["user_username"]) +} + +func TestGenerateJWT(t *testing.T) { + handler := &AuthHandler{jwtKey: []byte("test-secret-key")} + + token, err := handler.generateJWT("user-id-789") + require.NoError(t, err) + assert.NotEmpty(t, token) + + parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + return []byte("test-secret-key"), nil + }) + require.NoError(t, err) + + claims := parsedToken.Claims.(jwt.MapClaims) + assert.Equal(t, "user-id-789", claims["user_id"]) + assert.Equal(t, "user", claims["user_role"]) +} diff --git a/internal/handlers/kobo_test.go b/internal/handlers/kobo_test.go new file mode 100644 index 0000000..caff38a --- /dev/null +++ b/internal/handlers/kobo_test.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLooksLikeSHA256_Valid(t *testing.T) { + validSHA256 := "550e8400e29b41d4a716446655440000550e8400e29b41d4a716446655440000" + + result := looksLikeSHA256(validSHA256) + assert.True(t, result) +} + +func TestLooksLikeSHA256_Lowercase(t *testing.T) { + sha256 := "550e8400e29b41d4a716446655440000550e8400e29b41d4a716446655440000" + + result := looksLikeSHA256(sha256) + assert.True(t, result) +} + +func TestLooksLikeSHA256_Invalid(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"too short", "abc123"}, + {"too long", "550e8400e29b41d4a716446655440000550e8400e29b41d4a71644665544000000"}, + {"wrong characters", "550e8400e29b41d4a716446655440000550e8400e29b41d4a71644665544000gg"}, + {"empty", ""}, + {"with spaces", "550e8400 e29b 41d4 a716 446655440000"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := looksLikeSHA256(tt.input) + assert.False(t, result) + }) + } +} diff --git a/internal/handlers/library_test.go b/internal/handlers/library_test.go new file mode 100644 index 0000000..7a9fb1c --- /dev/null +++ b/internal/handlers/library_test.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseUUID_Library(t *testing.T) { + validUUID := "f47ac10b-58cc-4372-a567-0e02b2c3d479" + + result, err := parseUUID(validUUID) + require.NoError(t, err) + assert.True(t, result.Valid) +} + +func TestParseUUID_Invalid(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"not a uuid", "not-a-uuid"}, + {"empty", ""}, + {"wrong format", "f47ac10b-58cc-4372"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := parseUUID(tt.input) + assert.Error(t, err) + }) + } +} diff --git a/internal/handlers/progress_test.go b/internal/handlers/progress_test.go new file mode 100644 index 0000000..d80f641 --- /dev/null +++ b/internal/handlers/progress_test.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetDeviceIcon_Kobo(t *testing.T) { + result := getDeviceIcon("kobo") + assert.Equal(t, "📚", result) +} + +func TestGetDeviceIcon_KOReader(t *testing.T) { + result := getDeviceIcon("koreader") + assert.Equal(t, "📖", result) +} + +func TestGetDeviceIcon_Kindle(t *testing.T) { + result := getDeviceIcon("kindle") + assert.Equal(t, "📱", result) +} + +func TestGetDeviceIcon_Unknown(t *testing.T) { + result := getDeviceIcon("unknown") + assert.Equal(t, "📚", result) + + result = getDeviceIcon("") + assert.Equal(t, "📚", result) +} diff --git a/internal/handlers/refresh_token_test.go b/internal/handlers/refresh_token_test.go new file mode 100644 index 0000000..b2d7c4e --- /dev/null +++ b/internal/handlers/refresh_token_test.go @@ -0,0 +1,36 @@ +package handlers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseTokenUUID_ValidUUID(t *testing.T) { + validUUID := "550e8400-e29b-41d4-a716-446655440000" + + result, err := parseTokenUUID(validUUID) + + require.NoError(t, err) + assert.True(t, result.Valid) +} + +func TestParseTokenUUID_InvalidUUID(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"not a uuid", "not-a-valid-jwt-token"}, + {"empty string", ""}, + {"invalid format", "550e8400-e29b-41d4"}, + {"too short", "abc"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := parseTokenUUID(tt.input) + assert.Error(t, err) + }) + } +} diff --git a/internal/handlers/sidecar_test.go b/internal/handlers/sidecar_test.go new file mode 100644 index 0000000..0e5c717 --- /dev/null +++ b/internal/handlers/sidecar_test.go @@ -0,0 +1,68 @@ +package handlers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSanitizeFilename_NoChange(t *testing.T) { + tests := []struct { + name string + }{ + {"simple_filename"}, + {"file_name_123"}, + {"Document"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := sanitizeFilename(tt.name) + assert.Equal(t, tt.name, result) + }) + } +} + +func TestSanitizeFilename_ReplacesSpecialChars(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"slash", "file/name", "file_name"}, + {"backslash", "file\\name", "file_name"}, + {"colon", "file:name", "file_name"}, + {"asterisk", "file*name", "file_name"}, + {"question_mark", "file?name", "file_name"}, + {"quotes", "file\"name", "file_name"}, + {"less_than", "filename", "file_name"}, + {"pipe", "file|name", "file_name"}, + {"multiple_chars", "a/b\\c:d*e", "a_b_c_d_e"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := sanitizeFilename(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestSanitizeAll_Basic(t *testing.T) { + result := sanitizeAll("hello world", "o", "0") + assert.Equal(t, "hell0 w0rld", result) +} + +func TestSanitizeAll_NoMatch(t *testing.T) { + result := sanitizeAll("hello", "x", "y") + assert.Equal(t, "hello", result) +} + +func TestSanitizeAll_EmptyStrings(t *testing.T) { + result := sanitizeAll("", "a", "b") + assert.Equal(t, "", result) + + result = sanitizeAll("test", "", "x") + assert.Equal(t, "test", result) +}