refactor: use pinned golangci-lint Docker image for linting
All checks were successful
check / check (push) Successful in 1m37s
All checks were successful
check / check (push) Successful in 1m37s
Refactor Dockerfile to use a separate lint stage with a pinned golangci-lint v2.11.3 Docker image instead of installing golangci-lint via curl in the builder stage. This follows the pattern used by sneak/pixa. Changes: - Dockerfile: separate lint stage using golangci/golangci-lint:v2.11.3 (Debian-based, pinned by sha256) with COPY --from=lint dependency - Bump Go from 1.24 to 1.26.1 (golang:1.26.1-bookworm, pinned) - Bump golangci-lint from v1.64.8 to v2.11.3 - Migrate .golangci.yml from v1 to v2 format (same linters, format only) - All Docker images pinned by sha256 digest - Fix all lint issues from the v2 linter upgrade: - Add package comments to all packages - Add doc comments to all exported types, functions, and methods - Fix unchecked errors (errcheck) - Fix unused parameters (revive) - Fix gosec warnings (MaxBytesReader for form parsing) - Fix staticcheck suggestions (fmt.Fprintf instead of WriteString) - Rename DeliveryTask to Task to avoid stutter (delivery.Task) - Rename shadowed builtin 'max' parameter - Update README.md version requirements
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
package database
|
||||
package database_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/webhooker/internal/database"
|
||||
)
|
||||
|
||||
func TestGenerateRandomPassword(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
length int
|
||||
@@ -18,109 +22,172 @@ func TestGenerateRandomPassword(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
password, err := GenerateRandomPassword(tt.length)
|
||||
t.Parallel()
|
||||
|
||||
password, err := database.GenerateRandomPassword(
|
||||
tt.length,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateRandomPassword() error = %v", err)
|
||||
t.Fatalf(
|
||||
"GenerateRandomPassword() error = %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if len(password) != tt.length {
|
||||
t.Errorf("Password length = %v, want %v", len(password), tt.length)
|
||||
t.Errorf(
|
||||
"Password length = %v, want %v",
|
||||
len(password), tt.length,
|
||||
)
|
||||
}
|
||||
|
||||
// For passwords >= 4 chars, check complexity
|
||||
if tt.length >= 4 {
|
||||
hasUpper := false
|
||||
hasLower := false
|
||||
hasDigit := false
|
||||
hasSpecial := false
|
||||
|
||||
for _, char := range password {
|
||||
switch {
|
||||
case char >= 'A' && char <= 'Z':
|
||||
hasUpper = true
|
||||
case char >= 'a' && char <= 'z':
|
||||
hasLower = true
|
||||
case char >= '0' && char <= '9':
|
||||
hasDigit = true
|
||||
case strings.ContainsRune("!@#$%^&*()_+-=[]{}|;:,.<>?", char):
|
||||
hasSpecial = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasUpper || !hasLower || !hasDigit || !hasSpecial {
|
||||
t.Errorf("Password lacks required complexity: upper=%v, lower=%v, digit=%v, special=%v",
|
||||
hasUpper, hasLower, hasDigit, hasSpecial)
|
||||
}
|
||||
}
|
||||
checkPasswordComplexity(
|
||||
t, password, tt.length,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkPasswordComplexity(
|
||||
t *testing.T,
|
||||
password string,
|
||||
length int,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
// For passwords >= 4 chars, check complexity
|
||||
if length < 4 {
|
||||
return
|
||||
}
|
||||
|
||||
flags := classifyChars(password)
|
||||
|
||||
if !flags[0] || !flags[1] || !flags[2] || !flags[3] {
|
||||
t.Errorf(
|
||||
"Password lacks required complexity: "+
|
||||
"upper=%v, lower=%v, digit=%v, special=%v",
|
||||
flags[0], flags[1], flags[2], flags[3],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func classifyChars(s string) [4]bool {
|
||||
var flags [4]bool // upper, lower, digit, special
|
||||
|
||||
for _, char := range s {
|
||||
switch {
|
||||
case char >= 'A' && char <= 'Z':
|
||||
flags[0] = true
|
||||
case char >= 'a' && char <= 'z':
|
||||
flags[1] = true
|
||||
case char >= '0' && char <= '9':
|
||||
flags[2] = true
|
||||
case strings.ContainsRune(
|
||||
"!@#$%^&*()_+-=[]{}|;:,.<>?",
|
||||
char,
|
||||
):
|
||||
flags[3] = true
|
||||
}
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
func TestGenerateRandomPasswordUniqueness(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Generate multiple passwords and ensure they're different
|
||||
passwords := make(map[string]bool)
|
||||
|
||||
const numPasswords = 100
|
||||
|
||||
for i := 0; i < numPasswords; i++ {
|
||||
password, err := GenerateRandomPassword(16)
|
||||
for range numPasswords {
|
||||
password, err := database.GenerateRandomPassword(16)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateRandomPassword() error = %v", err)
|
||||
t.Fatalf(
|
||||
"GenerateRandomPassword() error = %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if passwords[password] {
|
||||
t.Errorf("Duplicate password generated: %s", password)
|
||||
t.Errorf(
|
||||
"Duplicate password generated: %s",
|
||||
password,
|
||||
)
|
||||
}
|
||||
|
||||
passwords[password] = true
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashPassword(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
password := "testPassword123!"
|
||||
|
||||
hash, err := HashPassword(password)
|
||||
hash, err := database.HashPassword(password)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword() error = %v", err)
|
||||
}
|
||||
|
||||
// Check that hash has correct format
|
||||
if !strings.HasPrefix(hash, "$argon2id$") {
|
||||
t.Errorf("Hash doesn't have correct prefix: %s", hash)
|
||||
t.Errorf(
|
||||
"Hash doesn't have correct prefix: %s",
|
||||
hash,
|
||||
)
|
||||
}
|
||||
|
||||
// Verify password
|
||||
valid, err := VerifyPassword(password, hash)
|
||||
valid, err := database.VerifyPassword(password, hash)
|
||||
if err != nil {
|
||||
t.Fatalf("VerifyPassword() error = %v", err)
|
||||
}
|
||||
|
||||
if !valid {
|
||||
t.Error("VerifyPassword() returned false for correct password")
|
||||
t.Error(
|
||||
"VerifyPassword() returned false " +
|
||||
"for correct password",
|
||||
)
|
||||
}
|
||||
|
||||
// Verify wrong password fails
|
||||
valid, err = VerifyPassword("wrongPassword", hash)
|
||||
valid, err = database.VerifyPassword(
|
||||
"wrongPassword", hash,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("VerifyPassword() error = %v", err)
|
||||
}
|
||||
|
||||
if valid {
|
||||
t.Error("VerifyPassword() returned true for wrong password")
|
||||
t.Error(
|
||||
"VerifyPassword() returned true " +
|
||||
"for wrong password",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashPasswordUniqueness(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
password := "testPassword123!"
|
||||
|
||||
// Same password should produce different hashes due to salt
|
||||
hash1, err := HashPassword(password)
|
||||
// Same password should produce different hashes
|
||||
hash1, err := database.HashPassword(password)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword() error = %v", err)
|
||||
}
|
||||
|
||||
hash2, err := HashPassword(password)
|
||||
hash2, err := database.HashPassword(password)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword() error = %v", err)
|
||||
}
|
||||
|
||||
if hash1 == hash2 {
|
||||
t.Error("Same password produced identical hashes (salt not working)")
|
||||
t.Error(
|
||||
"Same password produced identical hashes " +
|
||||
"(salt not working)",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user