package authn import ( "errors" "path/filepath" "testing" "golang.org/x/crypto/bcrypt" ) func openTestStore(t *testing.T) (*Store, string) { t.Helper() path := filepath.Join(t.TempDir(), DatabaseFile) store, err := Open(path) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = store.Close() }) return store, path } func TestPasswordRecordPersistsAndAuthenticates(t *testing.T) { store, path := openTestStore(t) created, wasCreated, err := store.SetPassword("Kami", "correct horse battery") if err != nil || !wasCreated || created.Username != "Kami" { t.Fatalf("created=%+v new=%v err=%v", created, wasCreated, err) } if _, err := store.Authenticate("KAMI", "correct horse battery"); err != nil { t.Fatalf("authenticate: %v", err) } if _, err := store.Authenticate("Kami", "wrong password"); !errors.Is(err, ErrInvalidCredentials) { t.Fatalf("wrong password error = %v", err) } if err := store.Close(); err != nil { t.Fatal(err) } reopened, err := Open(path) if err != nil { t.Fatal(err) } defer reopened.Close() if _, err := reopened.Authenticate("kami", "correct horse battery"); err != nil { t.Fatalf("persisted authentication: %v", err) } } func TestUpdateRequiresCurrentPasswordAndMovesUsername(t *testing.T) { store, _ := openTestStore(t) if _, _, err := store.SetPassword("operator", "original password"); err != nil { t.Fatal(err) } if _, err := store.Update("operator", "wrong password", "kami", "replacement password"); !errors.Is(err, ErrInvalidCredentials) { t.Fatalf("wrong current password error = %v", err) } updated, err := store.Update("operator", "original password", "kami", "replacement password") if err != nil || updated.Username != "kami" { t.Fatalf("updated=%+v err=%v", updated, err) } if _, err := store.Authenticate("operator", "original password"); !errors.Is(err, ErrInvalidCredentials) { t.Fatalf("old credential error = %v", err) } if _, err := store.Authenticate("kami", "replacement password"); err != nil { t.Fatalf("new credential: %v", err) } } func TestUpdateRefusesExistingUsername(t *testing.T) { store, _ := openTestStore(t) if _, _, err := store.SetPassword("one", "password one"); err != nil { t.Fatal(err) } if _, _, err := store.SetPassword("two", "password two"); err != nil { t.Fatal(err) } if _, err := store.Update("one", "password one", "TWO", ""); !errors.Is(err, ErrUsernameExists) { t.Fatalf("collision error = %v", err) } } func TestLegacyHashImportsOnlyIntoEmptyDatabase(t *testing.T) { store, _ := openTestStore(t) hash, err := bcrypt.GenerateFromPassword([]byte("legacy password"), bcrypt.MinCost) if err != nil { t.Fatal(err) } if imported, err := store.ImportBcrypt("legacy", string(hash)); err != nil || !imported { t.Fatalf("imported=%v err=%v", imported, err) } if imported, err := store.ImportBcrypt("intruder", string(hash)); err != nil || imported { t.Fatalf("second import=%v err=%v", imported, err) } if _, err := store.Authenticate("legacy", "legacy password"); err != nil { t.Fatalf("imported credential: %v", err) } } func TestCredentialValidation(t *testing.T) { store, _ := openTestStore(t) if _, _, err := store.SetPassword("", "a sufficiently long password"); err == nil { t.Fatal("blank username accepted") } if _, _, err := store.SetPassword("operator", "short"); err == nil { t.Fatal("short password accepted") } if _, _, err := store.SetPassword("operator", string(make([]byte, maximumPassword+1))); err == nil { t.Fatal("oversized bcrypt password accepted") } }