Files
wehub-resource-sync 1b8708893a
Security Scan / tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:12:26 +08:00

101 lines
4.0 KiB
Go

package auth_test
import (
"strings"
"github.com/mudler/LocalAI/core/http/auth"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Password policy", func() {
Describe("ValidatePasswordStrength", func() {
// Anything below MinPasswordScore is rejected. zxcvbn scores are subject
// to the embedded dictionary; if the underlying dictionary changes we
// want the test to break loudly so we can re-baseline rather than
// silently accept weaker passwords.
DescribeTable("rejects weak inputs",
func(pw string) {
Expect(auth.ValidatePasswordStrength(pw)).ToNot(Succeed())
},
Entry("too short", "Tr0ub4dor"),
Entry("empty", ""),
Entry("common: password", "password1234"),
Entry("common: 12345", "12345678901234"),
Entry("common: qwerty", "qwertyuiopas"),
Entry("keyboard run", "qwertyuiop12"),
Entry("app branding only", "localailocalai"),
Entry("repeated word", "passwordpassword"),
)
DescribeTable("accepts strong inputs",
func(pw string) {
Expect(auth.ValidatePasswordStrength(pw)).To(Succeed())
},
Entry("diceware-style passphrase", "correct horse battery staple unicycle"),
Entry("random-ish 14-char with punctuation", "Th3-Quick~Br0wn-F0x.Jumps"),
Entry("random mixed", "q9V$mZ1pL7nB3w"),
)
Context("length boundaries", func() {
It("returns ErrPasswordEmpty for empty input", func() {
Expect(auth.ValidatePasswordStrength("")).To(MatchError(auth.ErrPasswordEmpty))
})
It("returns ErrPasswordTooShort just under the floor", func() {
short := strings.Repeat("a", auth.MinPasswordLength-1)
Expect(auth.ValidatePasswordStrength(short)).To(MatchError(auth.ErrPasswordTooShort))
})
It("returns ErrPasswordTooLong just over the ceiling", func() {
long := strings.Repeat("a", auth.MaxPasswordLength+1)
Expect(auth.ValidatePasswordStrength(long)).To(MatchError(auth.ErrPasswordTooLong))
})
})
It("rejects passwords containing NUL bytes", func() {
Expect(auth.ValidatePasswordStrength("abcdef\x00ghijklmnop")).To(MatchError(auth.ErrPasswordNullByte))
})
// AllowWeak skips the policy-level checks (length floor + entropy) but
// the technical invariants (empty / max-length / NUL byte) always apply.
Context("with AllowWeak override", func() {
weak := "password1234"
It("rejects the weak password by default", func() {
Expect(auth.ValidatePasswordStrength(weak)).To(MatchError(auth.ErrPasswordTooWeak))
})
It("accepts the weak password when AllowWeak is set", func() {
Expect(auth.ValidatePasswordStrength(weak, auth.PasswordPolicy{AllowWeak: true})).To(Succeed())
})
It("bypasses the length floor", func() {
Expect(auth.ValidatePasswordStrength("short", auth.PasswordPolicy{AllowWeak: true})).To(Succeed())
})
It("does not bypass the empty-input check", func() {
Expect(auth.ValidatePasswordStrength("", auth.PasswordPolicy{AllowWeak: true})).To(MatchError(auth.ErrPasswordEmpty))
})
It("does not bypass the NUL-byte check", func() {
Expect(auth.ValidatePasswordStrength("ok\x00password1234", auth.PasswordPolicy{AllowWeak: true})).To(MatchError(auth.ErrPasswordNullByte))
})
It("does not bypass the max-length check", func() {
long := strings.Repeat("a", auth.MaxPasswordLength+1)
Expect(auth.ValidatePasswordStrength(long, auth.PasswordPolicy{AllowWeak: true})).To(MatchError(auth.ErrPasswordTooLong))
})
})
})
Describe("PasswordError", func() {
DescribeTable("produces a structured response",
func(err error, code string, overridable bool) {
r := auth.PasswordError(err)
Expect(r.ErrorCode).To(Equal(code))
Expect(r.Overridable).To(Equal(overridable))
Expect(r.Error).ToNot(BeEmpty())
},
Entry("empty", auth.ErrPasswordEmpty, "password_empty", false),
Entry("too short", auth.ErrPasswordTooShort, "password_too_short", true),
Entry("too long", auth.ErrPasswordTooLong, "password_too_long", false),
Entry("null byte", auth.ErrPasswordNullByte, "password_null_byte", false),
Entry("too weak", auth.ErrPasswordTooWeak, "password_too_weak", true),
)
})
})