Files
Agent-System-Backend/internal/store/one_time_password_test.go
T

140 lines
3.9 KiB
Go

package store
import (
"errors"
"path/filepath"
"testing"
)
func TestIssueOneTimePasswordAndComplete(t *testing.T) {
st, err := OpenSQLite(filepath.Join(t.TempDir(), "otp.db"))
if err != nil {
t.Fatal(err)
}
defer st.Close()
if err := st.CreateAccount("admin", "", "admin-hash", true); err != nil {
t.Fatal(err)
}
hash, err := HashPassword("normal-pass-1")
if err != nil {
t.Fatal(err)
}
if err := st.CreateAccount("alice", "", hash, false); err != nil {
t.Fatal(err)
}
if err := st.ClearMustChangePassword("alice"); err != nil {
t.Fatal(err)
}
otpHash, err := HashPassword("otp-pass-1")
if err != nil {
t.Fatal(err)
}
meta, err := st.IssueOneTimePassword("admin", "alice", otpHash)
if err != nil || meta.ID == "" || meta.Purpose != LoginKeyPurposeOneTimePassword {
t.Fatalf("issue: %+v %v", meta, err)
}
tok, err := st.LoginWithOneTimePassword("alice", "otp-pass-1")
if err != nil || tok == "" {
t.Fatalf("otp login: %q %v", tok, err)
}
if _, err := st.LoginWithOneTimePassword("alice", "otp-pass-1"); err == nil {
t.Fatal("consumed otp reused")
}
newHash, err := HashPassword("new-pass-2")
if err != nil {
t.Fatal(err)
}
if _, err := st.CompleteOneTimePasswordChecked(tok, newHash); err != nil {
t.Fatalf("complete otp: %v", err)
}
if _, _, ok := st.SessionInfo(tok); ok {
t.Fatal("otp session survived completion")
}
if _, ok := st.VerifyLogin("alice", "new-pass-2"); !ok {
t.Fatal("new password cannot login")
}
if _, ok := st.VerifyLogin("alice", "normal-pass-1"); ok {
t.Fatal("old password still valid")
}
}
func TestOneTimePasswordNormalLoginInvalidatesUnused(t *testing.T) {
st, err := OpenSQLite(filepath.Join(t.TempDir(), "otp_invalidate.db"))
if err != nil {
t.Fatal(err)
}
defer st.Close()
if err := st.CreateAccount("admin", "", "admin-hash", true); err != nil {
t.Fatal(err)
}
hash, err := HashPassword("normal-pass-1")
if err != nil {
t.Fatal(err)
}
if err := st.CreateAccount("nora", "", hash, false); err != nil {
t.Fatal(err)
}
if err := st.ClearMustChangePassword("nora"); err != nil {
t.Fatal(err)
}
otpHash, err := HashPassword("otp-pass-1")
if err != nil {
t.Fatal(err)
}
if _, err := st.IssueOneTimePassword("admin", "nora", otpHash); err != nil {
t.Fatal(err)
}
if _, _, ok := st.VerifyLoginWithVersion("nora", "normal-pass-1"); !ok {
t.Fatal("normal password no longer accepted")
}
if err := st.InvalidateUnusedOneTimePasswords("nora"); err != nil {
t.Fatal(err)
}
if _, err := st.LoginWithOneTimePassword("nora", "otp-pass-1"); !errors.Is(err, ErrBadCredentials) {
t.Fatalf("otp remained valid: %v", err)
}
}
func TestOneTimePasswordInitializesHistoricalPasswordlessAccount(t *testing.T) {
st, err := OpenSQLite(filepath.Join(t.TempDir(), "otp_init.db"))
if err != nil {
t.Fatal(err)
}
defer st.Close()
if err := st.CreateAccount("admin", "", "admin-hash", true); err != nil {
t.Fatal(err)
}
if err := st.CreateAccount("legacy-empty", "", "", false); err != nil {
t.Fatal(err)
}
before, err := st.GetAccountByOwner("legacy-empty")
if err != nil || before.HasPassword || before.InitializationState != InitializationLegacy {
t.Fatalf("fixture: %+v %v", before, err)
}
otpHash, err := HashPassword("otp-pass-1")
if err != nil {
t.Fatal(err)
}
if _, err := st.IssueOneTimePassword("admin", "legacy-empty", otpHash); err != nil {
t.Fatal(err)
}
tok, err := st.LoginWithOneTimePassword("legacy-empty", "otp-pass-1")
if err != nil {
t.Fatal(err)
}
finalHash, err := HashPassword("final-pass-1")
if err != nil {
t.Fatal(err)
}
if _, err := st.CompleteOneTimePasswordChecked(tok, finalHash); err != nil {
t.Fatal(err)
}
after, err := st.GetAccountByOwner("legacy-empty")
if err != nil || !after.HasPassword || after.InitializationState != InitializationInitialized {
t.Fatalf("account not initialized: %+v %v", after, err)
}
if _, ok := st.VerifyLogin("legacy-empty", "final-pass-1"); !ok {
t.Fatal("initialized account cannot login")
}
}