Skip to content
quarry Artifact [1eb77487f1]
Login

Artifact [1eb77487f1]

Artifact 1eb77487f1d0b86ec4b6aec70ed600f16382fe4cff1c36ec596a6453aaddea5b:


package oltp

import (
	"testing"
	"time"
)

// Re-inviting supersedes, as ResetPassword does: one live link per
// pending account, the newest.
func TestReinviteSupersedesTheOlderLink(t *testing.T) {
	db := testDB(t)
	first, err := db.InviteAccount("bob", "", false)
	if err != nil {
		t.Fatal(err)
	}
	second, err := db.InviteAccount("bob", "", false)
	if err != nil {
		t.Fatal(err)
	}
	if _, ok := db.InviteFor(first); ok {
		t.Error("FAIL: the superseded invite is still live")
	}
	if name, ok := db.InviteFor(second); !ok || name != "bob" {
		t.Errorf("FAIL: the newest invite is not live: %q %v", name, ok)
	}
}

// mintRaw writes a token row the way a build before the supersede did,
// so a database carrying such rows across the upgrade is covered too.
func mintRaw(t *testing.T, db *DB, token, account, kind string) {
	t.Helper()
	now := time.Now().UTC()
	if _, err := db.w.Exec(`INSERT INTO invite (token, account, created, expires, kind) VALUES (?, ?, ?, ?, ?)`,
		hashToken(token), account, now.Format(time.RFC3339), now.Add(InviteTTL).Format(time.RFC3339), kind); err != nil {
		t.Fatal(err)
	}
}

// An invite token is only ever good while the account is unclaimed,
// whatever else is true of it.
func TestAnInviteForAClaimedAccountIsRefused(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("bob", "bobs-own-pw", false); err != nil {
		t.Fatal(err)
	}
	mintRaw(t, db, "left-over", "bob", "invite")
	if _, err := db.RedeemInvite("left-over", "admins-pw"); err == nil {
		t.Error("FAIL: an invite set a claimed account's password")
	}
	if _, ok := db.Authenticate("bob", "bobs-own-pw"); !ok {
		t.Error("FAIL: bob's own password stopped working")
	}
}

// Redeeming any token spends every other unspent one the account has,
// invite or reset alike.
func TestRedeemingSpendsTheAccountsOtherTokens(t *testing.T) {
	db := testDB(t)
	if _, err := db.InviteAccount("bob", "", false); err != nil {
		t.Fatal(err)
	}
	mintRaw(t, db, "inv-a", "bob", "invite")
	mintRaw(t, db, "inv-b", "bob", "invite")
	if _, err := db.RedeemInvite("inv-a", "bobs-own-pw"); err != nil {
		t.Fatal(err)
	}
	if _, ok := db.InviteFor("inv-b"); ok {
		t.Error("FAIL: a second invite survived the claim")
	}
	mintRaw(t, db, "rst-a", "bob", "reset")
	mintRaw(t, db, "rst-b", "bob", "reset")
	if _, err := db.RedeemInvite("rst-a", "bobs-next-pw"); err != nil {
		t.Fatal(err)
	}
	if _, ok := db.InviteFor("rst-b"); ok {
		t.Error("FAIL: a second reset survived the redemption")
	}
}

// Setting a password retires an imported fossil credential: left in
// place, the old fossil password still signs in and Authenticate's
// upgrade then writes it over the password just set.
func TestSetPasswordRetiresTheLegacyCredential(t *testing.T) {
	for _, self := range []bool{false, true} {
		db := testDB(t)
		const code, login, old = "code", "alice", "fossil-pw"
		if _, _, err := db.ImportUsers("repo", code, []ImportedUser{
			{Login: login, PWHash: legacySecret(code, login, old)},
		}); err != nil {
			t.Fatal(err)
		}
		set := db.SetPassword
		if self {
			set = func(name, pw string) error { return db.SetPasswordSelf(name, pw, "") }
		}
		if err := set(login, "brand-new-pw"); err != nil {
			t.Fatal(err)
		}
		if _, _, ok := db.LegacyCredential(login); ok {
			t.Errorf("FAIL (self=%v): the legacy credential survived a password change", self)
		}
		if _, ok := db.Authenticate(login, old); ok {
			t.Errorf("FAIL (self=%v): the old fossil password still signs in", self)
		}
		if _, ok := db.Authenticate(login, "brand-new-pw"); !ok {
			t.Errorf("FAIL (self=%v): the new password does not sign in", self)
		}
	}
}

// The host operator's password change and its journal entry land
// together or not at all: a change the journal did not take must not
// happen.
func TestAJournalledPasswordChangeNeedsItsJournal(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("jo", "jos-password", false); err != nil {
		t.Fatal(err)
	}
	if err := db.SetPasswordJournalled("jo", "recovered-pw", "host:op", "account passwd"); err != nil {
		t.Fatal(err)
	}
	acts, err := db.AccountActs("jo", 10)
	if err != nil || len(acts) != 1 || acts[0].Op != "account passwd" || acts[0].Actor != "host:op" {
		t.Fatalf("FAIL: journal = %+v %v", acts, err)
	}
	if _, ok := db.Authenticate("jo", "recovered-pw"); !ok {
		t.Fatal("FAIL: the journalled change did not take")
	}
	if _, err := db.w.Exec(`DROP TABLE forge_act`); err != nil {
		t.Fatal(err)
	}
	if err := db.SetPasswordJournalled("jo", "unjournalled-pw", "host:op", "account passwd"); err == nil {
		t.Error("FAIL: the change went through with no journal")
	}
	if _, ok := db.Authenticate("jo", "recovered-pw"); !ok {
		t.Error("FAIL: the unjournalled change replaced the password")
	}
	if err := db.SetPasswordJournalled("nobody", "pw", "host:op", "account passwd"); err == nil {
		t.Error("FAIL: a missing account took a password")
	}
}