[🤖] Add Unit-Tests
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m34s

This commit is contained in:
2026-04-27 10:46:08 +02:00
parent dad0e3240d
commit 02d6894ec6
116 changed files with 18795 additions and 1 deletions
+379
View File
@@ -0,0 +1,379 @@
package cryptext
import (
"encoding/json"
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"strings"
"testing"
)
func TestPassHashInvalidEmpty(t *testing.T) {
ph := PassHash("")
tst.AssertFalse(t, ph.Valid())
tst.AssertFalse(t, ph.HasTOTP())
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
}
func TestPassHashInvalidGarbage(t *testing.T) {
for _, raw := range []string{
"garbage",
"99|nope",
"abc|payload",
"3|onlytwo",
"4|onlytwo",
"5|onlytwo",
"2|notbase64!|notbase64!",
"1|!!!notbase64!!!",
"3|!!notb64|!!notb64|0",
"3|abc|!!notb64|0",
} {
ph := PassHash(raw)
if ph.Valid() {
t.Errorf("expected %q to be invalid", raw)
}
}
}
func TestPassHashVerifyInvalid(t *testing.T) {
ph := PassHash("garbage-value")
tst.AssertFalse(t, ph.Verify("anything", nil))
}
func TestPassHashUpgradeInvalid(t *testing.T) {
ph := PassHash("garbage-value")
_, err := ph.Upgrade("anything")
if err == nil {
t.Errorf("expected error for invalid PassHash upgrade")
}
}
func TestPassHashStringRoundtrip(t *testing.T) {
ph, err := HashPassword("hunter2", nil)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, ph.String(), string(ph))
}
func TestPassHashMarshalJSONEmpty(t *testing.T) {
ph := PassHash("")
data, err := json.Marshal(ph)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, string(data), `""`)
}
func TestPassHashMarshalJSONMasked(t *testing.T) {
ph, err := HashPassword("hunter2", nil)
tst.AssertNoErr(t, err)
data, err := json.Marshal(ph)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, string(data), `"*****"`)
}
func TestPassHashDataV0(t *testing.T) {
ph, err := HashPasswordV0("test123")
tst.AssertNoErr(t, err)
v, seed, payload, hastotp, totpsecret, valid := ph.Data()
tst.AssertTrue(t, valid)
tst.AssertEqual(t, v, 0)
tst.AssertEqual(t, len(seed), 0)
tst.AssertEqual(t, string(payload), "test123")
tst.AssertFalse(t, hastotp)
tst.AssertEqual(t, len(totpsecret), 0)
}
func TestPassHashDataV1(t *testing.T) {
ph, err := HashPasswordV1("test123")
tst.AssertNoErr(t, err)
v, seed, payload, hastotp, _, valid := ph.Data()
tst.AssertTrue(t, valid)
tst.AssertEqual(t, v, 1)
tst.AssertEqual(t, len(seed), 0)
tst.AssertEqual(t, len(payload), 32) // sha-256 is 32 bytes
tst.AssertFalse(t, hastotp)
}
func TestPassHashDataV2(t *testing.T) {
ph, err := HashPasswordV2("test123")
tst.AssertNoErr(t, err)
v, seed, payload, hastotp, _, valid := ph.Data()
tst.AssertTrue(t, valid)
tst.AssertEqual(t, v, 2)
tst.AssertEqual(t, len(seed), 32)
tst.AssertEqual(t, len(payload), 32)
tst.AssertFalse(t, hastotp)
}
func TestPassHashDataV3(t *testing.T) {
ph, err := HashPasswordV3("test123", nil)
tst.AssertNoErr(t, err)
v, seed, payload, hastotp, _, valid := ph.Data()
tst.AssertTrue(t, valid)
tst.AssertEqual(t, v, 3)
tst.AssertEqual(t, len(seed), 32)
tst.AssertEqual(t, len(payload), 32)
tst.AssertFalse(t, hastotp)
}
func TestPassHashDataV4(t *testing.T) {
ph, err := HashPasswordV4("test123", nil)
tst.AssertNoErr(t, err)
v, _, _, hastotp, _, valid := ph.Data()
tst.AssertTrue(t, valid)
tst.AssertEqual(t, v, 4)
tst.AssertFalse(t, hastotp)
}
func TestPassHashDataV5(t *testing.T) {
ph, err := HashPasswordV5("test123", nil)
tst.AssertNoErr(t, err)
v, _, _, hastotp, _, valid := ph.Data()
tst.AssertTrue(t, valid)
tst.AssertEqual(t, v, 5)
tst.AssertFalse(t, hastotp)
}
func TestPassHashLatestIsV5(t *testing.T) {
ph, err := HashPassword("test", nil)
tst.AssertNoErr(t, err)
v, _, _, _, _, valid := ph.Data()
tst.AssertTrue(t, valid)
tst.AssertEqual(t, v, LatestPassHashVersion)
tst.AssertEqual(t, v, 5)
}
func TestPassHashUpgradeLatestIsNoop(t *testing.T) {
ph, err := HashPassword("test", nil)
tst.AssertNoErr(t, err)
tst.AssertFalse(t, ph.NeedsPasswordUpgrade())
ph2, err := ph.Upgrade("test")
tst.AssertNoErr(t, err)
tst.AssertEqual(t, string(ph), string(ph2))
}
func TestPassHashClearTOTPInvalid(t *testing.T) {
_, err := PassHash("garbage").ClearTOTP()
if err == nil {
t.Errorf("expected error from ClearTOTP on invalid")
}
}
func TestPassHashClearTOTPV0V1V2Noop(t *testing.T) {
ph0, _ := HashPasswordV0("x")
r0, err := ph0.ClearTOTP()
tst.AssertNoErr(t, err)
tst.AssertEqual(t, string(r0), string(ph0))
ph1, _ := HashPasswordV1("x")
r1, err := ph1.ClearTOTP()
tst.AssertNoErr(t, err)
tst.AssertEqual(t, string(r1), string(ph1))
ph2, _ := HashPasswordV2("x")
r2, err := ph2.ClearTOTP()
tst.AssertNoErr(t, err)
tst.AssertEqual(t, string(r2), string(ph2))
}
func TestPassHashClearTOTPV3(t *testing.T) {
secret := []byte{0x01, 0x02, 0x03}
ph, err := HashPasswordV3("test123", secret)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.HasTOTP())
cleared, err := ph.ClearTOTP()
tst.AssertNoErr(t, err)
tst.AssertFalse(t, cleared.HasTOTP())
tst.AssertTrue(t, cleared.Valid())
tst.AssertTrue(t, cleared.Verify("test123", nil))
}
func TestPassHashClearTOTPV4(t *testing.T) {
secret := []byte{0x01, 0x02, 0x03}
ph, err := HashPasswordV4("test123", secret)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.HasTOTP())
cleared, err := ph.ClearTOTP()
tst.AssertNoErr(t, err)
tst.AssertFalse(t, cleared.HasTOTP())
tst.AssertTrue(t, cleared.Verify("test123", nil))
}
func TestPassHashClearTOTPV5(t *testing.T) {
secret := []byte{0x01, 0x02, 0x03}
ph, err := HashPasswordV5("test123", secret)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.HasTOTP())
cleared, err := ph.ClearTOTP()
tst.AssertNoErr(t, err)
tst.AssertFalse(t, cleared.HasTOTP())
tst.AssertTrue(t, cleared.Verify("test123", nil))
}
func TestPassHashWithTOTPInvalid(t *testing.T) {
_, err := PassHash("garbage").WithTOTP([]byte{0x01})
if err == nil {
t.Errorf("expected error for WithTOTP on invalid")
}
}
func TestPassHashWithTOTPV0V1V2Errors(t *testing.T) {
ph0, _ := HashPasswordV0("x")
if _, err := ph0.WithTOTP([]byte{0x01}); err == nil {
t.Errorf("expected v0 not to support TOTP")
}
ph1, _ := HashPasswordV1("x")
if _, err := ph1.WithTOTP([]byte{0x01}); err == nil {
t.Errorf("expected v1 not to support TOTP")
}
ph2, _ := HashPasswordV2("x")
if _, err := ph2.WithTOTP([]byte{0x01}); err == nil {
t.Errorf("expected v2 not to support TOTP")
}
}
func TestPassHashWithTOTPV3V4V5(t *testing.T) {
secret := []byte{0xDE, 0xAD, 0xBE, 0xEF}
ph3, _ := HashPasswordV3("pw", nil)
tst.AssertFalse(t, ph3.HasTOTP())
r3, err := ph3.WithTOTP(secret)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, r3.HasTOTP())
ph4, _ := HashPasswordV4("pw", nil)
tst.AssertFalse(t, ph4.HasTOTP())
r4, err := ph4.WithTOTP(secret)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, r4.HasTOTP())
ph5, _ := HashPasswordV5("pw", nil)
tst.AssertFalse(t, ph5.HasTOTP())
r5, err := ph5.WithTOTP(secret)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, r5.HasTOTP())
}
func TestPassHashChangeInvalid(t *testing.T) {
_, err := PassHash("garbage").Change("new-pw")
if err == nil {
t.Errorf("expected error from Change on invalid")
}
}
func TestPassHashChangeKeepsVersion(t *testing.T) {
cases := []struct {
name string
hashed func() (PassHash, error)
version int
}{
{"V0", func() (PassHash, error) { return HashPasswordV0("old") }, 0},
{"V1", func() (PassHash, error) { return HashPasswordV1("old") }, 1},
{"V2", func() (PassHash, error) { return HashPasswordV2("old") }, 2},
{"V3", func() (PassHash, error) { return HashPasswordV3("old", nil) }, 3},
{"V4", func() (PassHash, error) { return HashPasswordV4("old", nil) }, 4},
{"V5", func() (PassHash, error) { return HashPasswordV5("old", nil) }, 5},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
ph, err := c.hashed()
tst.AssertNoErr(t, err)
changed, err := ph.Change("new-pw")
tst.AssertNoErr(t, err)
v, _, _, _, _, valid := changed.Data()
tst.AssertTrue(t, valid)
tst.AssertEqual(t, v, c.version)
tst.AssertTrue(t, changed.Verify("new-pw", nil))
tst.AssertFalse(t, changed.Verify("old", nil))
})
}
}
func TestPassHashChangeKeepsTOTPV3(t *testing.T) {
secret := []byte{0xAB, 0xCD}
ph, err := HashPasswordV3("old", secret)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.HasTOTP())
changed, err := ph.Change("new")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, changed.HasTOTP())
}
func TestPassHashV0Format(t *testing.T) {
ph, err := HashPasswordV0("plaintext-pw")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, strings.HasPrefix(string(ph), "0|"))
tst.AssertEqual(t, string(ph), "0|plaintext-pw")
}
func TestPassHashV1Format(t *testing.T) {
ph, err := HashPasswordV1("test")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, strings.HasPrefix(string(ph), "1|"))
}
func TestPassHashV2Format(t *testing.T) {
ph, err := HashPasswordV2("test")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, strings.HasPrefix(string(ph), "2|"))
tst.AssertEqual(t, strings.Count(string(ph), "|"), 2)
}
func TestPassHashV3Format(t *testing.T) {
ph, err := HashPasswordV3("test", nil)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, strings.HasPrefix(string(ph), "3|"))
tst.AssertEqual(t, strings.Count(string(ph), "|"), 3)
tst.AssertTrue(t, strings.HasSuffix(string(ph), "|0"))
}
func TestPassHashV4Format(t *testing.T) {
ph, err := HashPasswordV4("test", nil)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, strings.HasPrefix(string(ph), "4|"))
tst.AssertTrue(t, strings.HasSuffix(string(ph), "|0"))
}
func TestPassHashV5Format(t *testing.T) {
ph, err := HashPasswordV5("test", nil)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, strings.HasPrefix(string(ph), "5|"))
tst.AssertTrue(t, strings.HasSuffix(string(ph), "|0"))
}
func TestPassHashV5VerifyLongPassword(t *testing.T) {
// V5 hashes via sha512 first → bcrypt's 72-byte limit shouldn't apply
longPw := strings.Repeat("a", 200)
ph, err := HashPasswordV5(longPw, nil)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, ph.Verify(longPw, nil))
tst.AssertFalse(t, ph.Verify(longPw+"x", nil))
}
func TestPassHashV5DifferentEachCall(t *testing.T) {
ph1, err := HashPasswordV5("samepw", nil)
tst.AssertNoErr(t, err)
ph2, err := HashPasswordV5("samepw", nil)
tst.AssertNoErr(t, err)
// Bcrypt salts internally — same password should produce different hashes
tst.AssertNotEqual(t, string(ph1), string(ph2))
// Both must verify
tst.AssertTrue(t, ph1.Verify("samepw", nil))
tst.AssertTrue(t, ph2.Verify("samepw", nil))
}