Skip to content
quarry Artifact [bb2db8646b]
Login

Artifact [bb2db8646b]

Artifact bb2db8646b5ee8f8c23877035535d5dde2aca9e909b852043a9349b6bdb18478:


package oltp

import (
	"path/filepath"
	"testing"
)

func resetDB(t *testing.T) *DB {
	t.Helper()
	db, err := Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	return db
}

// The whole point: somebody who has forgotten their password chooses a
// new one themselves, and no operator ever holds it.
func TestAResetLetsSomebodyChooseTheirOwnPassword(t *testing.T) {
	db := resetDB(t)
	if err := db.AddAccount("jo", "the-old-password", false); err != nil {
		t.Fatal(err)
	}
	token, err := db.ResetPassword("jo")
	if err != nil {
		t.Fatalf("FAIL: could not mint a reset: %v", err)
	}
	if name, ok := db.InviteFor(token); !ok || name != "jo" {
		t.Fatalf("FAIL: the token names %q ok=%v", name, ok)
	}
	if k := db.TokenKind(token); k != "reset" {
		t.Errorf("FAIL: the token reads as %q, so the page says the wrong thing", k)
	} else {
		t.Log("ok: the token knows it is a reset")
	}
	// The old password still works until the link is spent: a reset
	// that locked the account would make a lost link worse than the
	// problem it was sent to solve.
	if !db.VerifyPassword("jo", "the-old-password") {
		t.Error("FAIL: minting a reset locked the account out")
	} else {
		t.Log("ok: the old password holds until the link is used")
	}
	if _, err := db.RedeemInvite(token, "a-brand-new-password"); err != nil {
		t.Fatalf("FAIL: redeeming: %v", err)
	}
	if !db.VerifyPassword("jo", "a-brand-new-password") {
		t.Error("FAIL: the new password does not work")
	} else {
		t.Log("ok: the new password works")
	}
	if db.VerifyPassword("jo", "the-old-password") {
		t.Error("FAIL: the old password still works after the reset")
	} else {
		t.Log("ok: the old password is gone")
	}
	// Spent, and spent once.
	if _, ok := db.InviteFor(token); ok {
		t.Error("FAIL: the link can be used twice")
	} else {
		t.Log("ok: the link is one-time")
	}
}

// Minting again supersedes, so "send it again" cannot leave two live
// credentials behind where the operator believes there is one.
func TestASecondResetKillsTheFirst(t *testing.T) {
	db := resetDB(t)
	if err := db.AddAccount("jo", "the-old-password", false); err != nil {
		t.Fatal(err)
	}
	first, err := db.ResetPassword("jo")
	if err != nil {
		t.Fatal(err)
	}
	second, err := db.ResetPassword("jo")
	if err != nil {
		t.Fatal(err)
	}
	if _, ok := db.InviteFor(first); ok {
		t.Error("FAIL: the superseded link is still live")
	} else {
		t.Log("ok: the older link is dead")
	}
	if _, ok := db.InviteFor(second); !ok {
		t.Error("FAIL: the new link is not live")
	} else {
		t.Log("ok: the newer link works")
	}
}

// An account that has never been claimed wants the invite it is already
// waiting on, not a reset of a password it does not have.
func TestAPendingAccountIsToldToUseItsInvite(t *testing.T) {
	db := resetDB(t)
	if _, err := db.InviteAccount("newbie", "", false); err != nil {
		t.Fatal(err)
	}
	if _, err := db.ResetPassword("newbie"); err == nil {
		t.Error("FAIL: a pending account was given a reset")
	} else {
		t.Logf("ok: refused — %v", err)
	}
	if _, err := db.ResetPassword("nobody"); err == nil {
		t.Error("FAIL: an account that does not exist was given a reset")
	} else {
		t.Logf("ok: refused — %v", err)
	}
}

// An invite is the way into an account that has no password. Once the
// account has one, an invite minted earlier (the admin's copy from
// before a re-send) must not be a second way to overwrite it (#319).
func TestAnInviteCannotOverwriteAPasswordOnceTheAccountIsClaimed(t *testing.T) {
	db := resetDB(t)
	first, err := db.InviteAccount("jo", "[email protected]", false)
	if err != nil {
		t.Fatal(err)
	}
	second, err := db.InviteAccount("jo", "[email protected]", false)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.RedeemInvite(second, "the-real-password"); err != nil {
		t.Fatalf("FAIL: redeeming the newest invite: %v", err)
	}
	if _, ok := db.InviteFor(first); ok {
		t.Error("FAIL: a leftover invite still names a claimed account")
	}
	if _, err := db.RedeemInvite(first, "attacker-password"); err == nil {
		t.Error("FAIL: a leftover invite overwrote a claimed account's password")
	}
	if !db.VerifyPassword("jo", "the-real-password") {
		t.Error("FAIL: the claimed account's password was replaced")
	}
}

// A reset is for an account that has a password; a token of that kind
// is refused for an account that is waiting on its invite.
func TestAResetTokenIsRefusedForAnAccountWithNoPassword(t *testing.T) {
	db := resetDB(t)
	if err := db.AddAccount("jo", "the-old-password", false); err != nil {
		t.Fatal(err)
	}
	token, err := db.ResetPassword("jo")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.w.Exec(`UPDATE account SET pwhash = '' WHERE name = 'jo'`); err != nil {
		t.Fatal(err)
	}
	if _, ok := db.InviteFor(token); ok {
		t.Error("FAIL: a reset token is live for a passwordless account")
	}
	if _, err := db.RedeemInvite(token, "x-new-password-1"); err == nil {
		t.Error("FAIL: a reset token claimed a passwordless account")
	}
}