add caddyauth tests

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
This commit is contained in:
Mohammed Al Sahaf
2026-06-05 18:00:44 +03:00
parent 4e047f5ca6
commit 8619b04834
2 changed files with 479 additions and 0 deletions

View File

@@ -0,0 +1,324 @@
package caddyauth
import (
"strings"
"testing"
"golang.org/x/crypto/argon2"
)
func TestArgon2idHashCaddyModule(t *testing.T) {
a := Argon2idHash{}
info := a.CaddyModule()
if info.ID != "http.authentication.hashes.argon2id" {
t.Errorf("CaddyModule().ID = %v, want 'http.authentication.hashes.argon2id'", info.ID)
}
}
func TestArgon2idDecodeHash(t *testing.T) {
tests := []struct {
name string
hash string
wantErr bool
wantErrStr string
}{
{
name: "valid hash",
hash: "$argon2id$v=19$m=47104,t=1,p=1$P2nzckEdTZ3bxCiBCkRTyA$xQL3Z32eo5jKl7u5tcIsnEKObYiyNZQQf5/4sAau6Pg",
},
{
name: "too few parts",
hash: "$argon2id$v=19$m=47104,t=1,p=1",
wantErr: true,
},
{
name: "wrong variant",
hash: "$argon2i$v=19$m=47104,t=1,p=1$c29tZXNhbHQ$c29tZWtleQ",
wantErr: true,
wantErrStr: "unsupported variant",
},
{
name: "invalid version",
hash: "$argon2id$v=abc$m=47104,t=1,p=1$c29tZXNhbHQ$c29tZWtleQ",
wantErr: true,
},
{
name: "incompatible version",
hash: "$argon2id$v=18$m=47104,t=1,p=1$c29tZXNhbHQ$c29tZWtleQ",
wantErr: true,
},
{
name: "invalid parameters - too few",
hash: "$argon2id$v=19$m=47104,t=1$c29tZXNhbHQ$c29tZWtleQ",
wantErr: true,
},
{
name: "invalid memory parameter",
hash: "$argon2id$v=19$m=abc,t=1,p=1$c29tZXNhbHQ$c29tZWtleQ",
wantErr: true,
},
{
name: "invalid iterations parameter",
hash: "$argon2id$v=19$m=47104,t=abc,p=1$c29tZXNhbHQ$c29tZWtleQ",
wantErr: true,
},
{
name: "invalid parallelism parameter",
hash: "$argon2id$v=19$m=47104,t=1,p=abc$c29tZXNhbHQ$c29tZWtleQ",
wantErr: true,
},
{
name: "invalid salt base64",
hash: "$argon2id$v=19$m=47104,t=1,p=1$!!!invalid!!!$c29tZWtleQ",
wantErr: true,
},
{
name: "invalid key base64",
hash: "$argon2id$v=19$m=47104,t=1,p=1$c29tZXNhbHQ$!!!invalid!!!",
wantErr: true,
},
{
name: "empty string",
hash: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
argHash, key, err := DecodeHash([]byte(tt.hash))
if tt.wantErr {
if err == nil {
t.Error("DecodeHash() should return error")
}
if tt.wantErrStr != "" && !strings.Contains(err.Error(), tt.wantErrStr) {
t.Errorf("error %q should contain %q", err.Error(), tt.wantErrStr)
}
return
}
if err != nil {
t.Fatalf("DecodeHash() error: %v", err)
}
if argHash == nil {
t.Fatal("DecodeHash() returned nil hash")
}
if key == nil {
t.Fatal("DecodeHash() returned nil key")
}
if argHash.time == 0 {
t.Error("decoded time is 0")
}
if argHash.memory == 0 {
t.Error("decoded memory is 0")
}
if argHash.threads == 0 {
t.Error("decoded threads is 0")
}
if len(argHash.salt) == 0 {
t.Error("decoded salt is empty")
}
})
}
}
func TestArgon2idDecodeHashParsesCorrectValues(t *testing.T) {
hash := "$argon2id$v=19$m=47104,t=1,p=1$P2nzckEdTZ3bxCiBCkRTyA$xQL3Z32eo5jKl7u5tcIsnEKObYiyNZQQf5/4sAau6Pg"
argHash, _, err := DecodeHash([]byte(hash))
if err != nil {
t.Fatalf("DecodeHash() error: %v", err)
}
if argHash.memory != 47104 {
t.Errorf("memory = %d, want 47104", argHash.memory)
}
if argHash.time != 1 {
t.Errorf("time = %d, want 1", argHash.time)
}
if argHash.threads != 1 {
t.Errorf("threads = %d, want 1", argHash.threads)
}
}
func TestArgon2idCompare(t *testing.T) {
hasher := Argon2idHash{
time: defaultArgon2idTime,
memory: defaultArgon2idMemory,
threads: defaultArgon2idThreads,
keyLen: defaultArgon2idKeylen,
}
plaintext := []byte("test-password")
hashed, err := hasher.Hash(plaintext)
if err != nil {
t.Fatalf("Hash() error: %v", err)
}
tests := []struct {
name string
plaintext []byte
wantMatch bool
}{
{name: "correct password", plaintext: plaintext, wantMatch: true},
{name: "wrong password", plaintext: []byte("wrong"), wantMatch: false},
{name: "empty password", plaintext: []byte(""), wantMatch: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
match, err := hasher.Compare(hashed, tt.plaintext)
if err != nil {
t.Fatalf("Compare() error: %v", err)
}
if match != tt.wantMatch {
t.Errorf("Compare() = %v, want %v", match, tt.wantMatch)
}
})
}
}
func TestArgon2idHashRoundTrip(t *testing.T) {
hasher := Argon2idHash{
time: defaultArgon2idTime,
memory: defaultArgon2idMemory,
threads: defaultArgon2idThreads,
keyLen: defaultArgon2idKeylen,
}
plaintext := []byte("round-trip-test")
hashed, err := hasher.Hash(plaintext)
if err != nil {
t.Fatalf("Hash() error: %v", err)
}
// Verify hash format starts with $argon2id$
if !strings.HasPrefix(string(hashed), "$argon2id$v=") {
t.Errorf("Hash() output %q doesn't start with '$argon2id$v='", hashed)
}
match, err := hasher.Compare(hashed, plaintext)
if err != nil {
t.Fatalf("Compare() error: %v", err)
}
if !match {
t.Error("Hash then Compare round-trip failed")
}
}
func TestArgon2idHashProducesDifferentHashes(t *testing.T) {
hasher := Argon2idHash{
time: defaultArgon2idTime,
memory: defaultArgon2idMemory,
threads: defaultArgon2idThreads,
keyLen: defaultArgon2idKeylen,
}
plaintext := []byte("same-password")
hash1, err := hasher.Hash(plaintext)
if err != nil {
t.Fatalf("Hash() first error: %v", err)
}
hash2, err := hasher.Hash(plaintext)
if err != nil {
t.Fatalf("Hash() second error: %v", err)
}
// Different salts should produce different hashes
if string(hash1) == string(hash2) {
t.Error("Hash() should produce different output on each call (different salt)")
}
// But both should verify correctly
match1, _ := hasher.Compare(hash1, plaintext)
match2, _ := hasher.Compare(hash2, plaintext)
if !match1 || !match2 {
t.Error("Both hashes should verify against original plaintext")
}
}
func TestArgon2idFakeHash(t *testing.T) {
hasher := Argon2idHash{}
fake := hasher.FakeHash()
if len(fake) == 0 {
t.Fatal("FakeHash() returned empty result")
}
// Should be a valid argon2id hash
_, _, err := DecodeHash(fake)
if err != nil {
t.Errorf("FakeHash() is not a valid argon2id hash: %v", err)
}
// Should match the known plaintext "antitiming"
match, err := hasher.Compare(fake, []byte("antitiming"))
if err != nil {
t.Fatalf("Compare() with FakeHash error: %v", err)
}
if !match {
t.Error("FakeHash() should match plaintext 'antitiming'")
}
}
func TestArgon2idCompareWithInvalidHash(t *testing.T) {
hasher := Argon2idHash{}
_, err := hasher.Compare([]byte("not-a-hash"), []byte("password"))
if err == nil {
t.Error("Compare() with invalid hash should return error")
}
}
func TestArgon2idHashVersionInOutput(t *testing.T) {
hasher := Argon2idHash{
time: defaultArgon2idTime,
memory: defaultArgon2idMemory,
threads: defaultArgon2idThreads,
keyLen: defaultArgon2idKeylen,
}
hashed, err := hasher.Hash([]byte("test"))
if err != nil {
t.Fatalf("Hash() error: %v", err)
}
// Verify version field matches current argon2 version
versionStr := "v=" + strings.Split(string(hashed), "$")[2]
if versionStr != "v=v=19" {
// argon2.Version is 0x13 = 19
expectedVersion := "$argon2id$v=19$"
if !strings.Contains(string(hashed), expectedVersion[:len(expectedVersion)-1]) {
t.Errorf("Hash output should contain version %d, got %q", argon2.Version, hashed)
}
}
}
func TestGenerateSalt(t *testing.T) {
salt1, err := generateSalt(16)
if err != nil {
t.Fatalf("generateSalt(16) error: %v", err)
}
if len(salt1) != 16 {
t.Errorf("generateSalt(16) length = %d, want 16", len(salt1))
}
salt2, err := generateSalt(16)
if err != nil {
t.Fatalf("generateSalt(16) second call error: %v", err)
}
// Two salts should be different (with overwhelming probability)
if string(salt1) == string(salt2) {
t.Error("generateSalt() should produce different values on each call")
}
}
func TestGenerateSaltLengths(t *testing.T) {
for _, length := range []int{8, 16, 32, 64} {
salt, err := generateSalt(length)
if err != nil {
t.Fatalf("generateSalt(%d) error: %v", length, err)
}
if len(salt) != length {
t.Errorf("generateSalt(%d) length = %d, want %d", length, len(salt), length)
}
}
}

View File

@@ -0,0 +1,155 @@
package caddyauth
import (
"testing"
"golang.org/x/crypto/bcrypt"
)
func TestBcryptHashCaddyModule(t *testing.T) {
b := BcryptHash{}
info := b.CaddyModule()
if info.ID != "http.authentication.hashes.bcrypt" {
t.Errorf("CaddyModule().ID = %v, want 'http.authentication.hashes.bcrypt'", info.ID)
}
}
func TestBcryptHashCompare(t *testing.T) {
hasher := BcryptHash{}
plaintext := []byte("correct-password")
hashed, err := bcrypt.GenerateFromPassword(plaintext, bcrypt.MinCost)
if err != nil {
t.Fatalf("failed to generate bcrypt hash: %v", err)
}
tests := []struct {
name string
hashed []byte
plaintext []byte
wantMatch bool
wantErr bool
}{
{
name: "correct password matches",
hashed: hashed,
plaintext: plaintext,
wantMatch: true,
},
{
name: "incorrect password does not match",
hashed: hashed,
plaintext: []byte("wrong-password"),
wantMatch: false,
},
{
name: "empty password does not match",
hashed: hashed,
plaintext: []byte(""),
wantMatch: false,
},
{
name: "invalid hash returns error",
hashed: []byte("not-a-bcrypt-hash"),
plaintext: plaintext,
wantMatch: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
match, err := hasher.Compare(tt.hashed, tt.plaintext)
if tt.wantErr {
if err == nil {
t.Error("Compare() should return error")
}
return
}
if err != nil {
t.Fatalf("Compare() error: %v", err)
}
if match != tt.wantMatch {
t.Errorf("Compare() = %v, want %v", match, tt.wantMatch)
}
})
}
}
func TestBcryptHashHash(t *testing.T) {
hasher := BcryptHash{}
plaintext := []byte("test-password")
hashed, err := hasher.Hash(plaintext)
if err != nil {
t.Fatalf("Hash() error: %v", err)
}
if len(hashed) == 0 {
t.Fatal("Hash() returned empty result")
}
// Verify the hash is valid bcrypt
err = bcrypt.CompareHashAndPassword(hashed, plaintext)
if err != nil {
t.Errorf("Hash() produced invalid bcrypt hash: %v", err)
}
// Verify re-hashing produces different output (different salt)
hashed2, err := hasher.Hash(plaintext)
if err != nil {
t.Fatalf("Hash() second call error: %v", err)
}
if string(hashed) == string(hashed2) {
t.Error("Hash() should produce different output on each call (different salt)")
}
}
func TestBcryptHashRoundTrip(t *testing.T) {
hasher := BcryptHash{}
plaintext := []byte("round-trip-test")
hashed, err := hasher.Hash(plaintext)
if err != nil {
t.Fatalf("Hash() error: %v", err)
}
match, err := hasher.Compare(hashed, plaintext)
if err != nil {
t.Fatalf("Compare() error: %v", err)
}
if !match {
t.Error("Hash then Compare round-trip failed: password should match")
}
}
func TestBcryptFakeHash(t *testing.T) {
hasher := BcryptHash{}
fake := hasher.FakeHash()
if len(fake) == 0 {
t.Fatal("FakeHash() returned empty result")
}
// FakeHash should be a valid bcrypt hash matching "antitiming"
err := bcrypt.CompareHashAndPassword(fake, []byte("antitiming"))
if err != nil {
t.Errorf("FakeHash() is not a valid bcrypt hash of 'antitiming': %v", err)
}
// Calling FakeHash multiple times should return the same value
fake2 := hasher.FakeHash()
if string(fake) != string(fake2) {
t.Error("FakeHash() should return constant value")
}
}
func TestBcryptHashWithCustomCost(t *testing.T) {
hasher := BcryptHash{cost: bcrypt.MinCost}
plaintext := []byte("low-cost-test")
hashed, err := hasher.Hash(plaintext)
if err != nil {
t.Fatalf("Hash() with MinCost error: %v", err)
}
match, err := hasher.Compare(hashed, plaintext)
if err != nil {
t.Fatalf("Compare() error: %v", err)
}
if !match {
t.Error("Hash/Compare with MinCost should match")
}
}