Skip to content
quarry Artifact [8d284af88e]
Login

Artifact [8d284af88e]

Artifact 8d284af88e3ee4eaba6ba341a67266cd488d876cdf87c234511ad0aa10b13a01:


package oltp

import (
	"testing"
	"time"
)

func signinDB(t *testing.T, scopes ...string) (*DB, string, Token) {
	t.Helper()
	db := tokenDB(t, "pub")
	if err := db.AddAccount("bot", "irrelevant to signing in", false); err != nil {
		t.Fatal(err)
	}
	_, tok, err := db.MintToken("bot", TokenSpec{Name: "browser", Scopes: scopes, Ceiling: "o"})
	if err != nil {
		t.Fatal(err)
	}
	return db, "bot", tok
}

func sessionRowOf(t *testing.T, db *DB, cookie string) (tokenID, expires string, n int) {
	t.Helper()
	db.db.QueryRow(`SELECT count(*) FROM session WHERE token = ?`, hashToken(cookie)).Scan(&n)
	db.db.QueryRow(`SELECT token_id, expires FROM session WHERE token = ?`, hashToken(cookie)).Scan(&tokenID, &expires)
	return
}

// A LINK OPENS ONE SESSION BOUND TO ITS TOKEN, AND A SECOND OPEN IS SPENT.
func TestSigninLinkOpensOneBoundSession(t *testing.T) {
	db, acct, tok := signinDB(t, ScopeWeb)
	link, _, err := db.MintSigninLink(acct, tok.ID, "/r/pub")
	if err != nil {
		t.Fatal(err)
	}
	var stored string
	db.db.QueryRow(`SELECT token FROM signin_link`).Scan(&stored)
	if stored != hashToken(link) || stored == link {
		t.Errorf("FAIL: the link is stored as %q; want only its hash", stored)
	}
	cookie, expires, next, ok := db.RedeemSigninLink(link, "10.0.0.1", "agent-browser")
	if !ok || next != "/r/pub" {
		t.Fatalf("FAIL: first open ok=%v next=%q; want a session and /r/pub", ok, next)
	}
	id, _, n := sessionRowOf(t, db, cookie)
	if n != 1 || id != tok.ID {
		t.Errorf("FAIL: session rows=%d token_id=%q; want one bound to %q", n, id, tok.ID)
	}
	if life := time.Until(expires); life > TokenSessionTTL || life < TokenSessionTTL-time.Minute {
		t.Errorf("FAIL: session lasts %v; want about %v", life, TokenSessionTTL)
	}
	if a, ok := db.SessionAccount(cookie); !ok || a.Name != acct {
		t.Errorf("FAIL: the cookie resolves to %q ok=%v; want %q", a.Name, ok, acct)
	}
	bound, ok := db.SessionToken(cookie)
	if !ok || bound.ID != tok.ID || bound.Ceiling != "o" {
		t.Errorf("FAIL: SessionToken = %+v ok=%v; want the minting token and its ceiling", bound, ok)
	}
	if _, _, _, ok := db.RedeemSigninLink(link, "", ""); ok {
		t.Error("FAIL: a second open of the same link opened a session")
	}
	var sessions int
	db.db.QueryRow(`SELECT count(*) FROM session WHERE token_id = ?`, tok.ID).Scan(&sessions)
	if sessions != 1 {
		t.Errorf("FAIL: %d sessions after two opens; want 1", sessions)
	}
}

// A TOKEN WITHOUT WEB SCOPE CANNOT MINT.
func TestSigninLinkNeedsWebScope(t *testing.T) {
	db, acct, tok := signinDB(t, ScopeAPI)
	if _, _, err := db.MintSigninLink(acct, tok.ID, ""); err == nil {
		t.Error("FAIL: an api-only token minted a sign-in link")
	}
	if _, _, err := db.MintSigninLink(acct, "nosuchid", ""); err == nil {
		t.Error("FAIL: an unknown token minted a sign-in link")
	}
	var n int
	db.db.QueryRow(`SELECT count(*) FROM signin_link`).Scan(&n)
	if n != 0 {
		t.Errorf("FAIL: %d link rows after refusals; want 0", n)
	}
}

// A LINK PAST TWO MINUTES IS REFUSED AND NO SESSION IS OPENED.
func TestSigninLinkLapses(t *testing.T) {
	db, acct, tok := signinDB(t, ScopeWeb)
	link, _, err := db.MintSigninLink(acct, tok.ID, "")
	if err != nil {
		t.Fatal(err)
	}
	late := time.Now().UTC().Add(-time.Second).Format(time.RFC3339)
	if _, err := db.w.Exec(`UPDATE signin_link SET expires = ?`, late); err != nil {
		t.Fatal(err)
	}
	if _, _, _, ok := db.RedeemSigninLink(link, "", ""); ok {
		t.Error("FAIL: a lapsed link opened a session")
	}
	if _, _, _, ok := db.RedeemSigninLink("not a link", "", ""); ok {
		t.Error("FAIL: an unknown link opened a session")
	}
}

// REVOKING THE TOKEN ENDS ITS SESSIONS IN THE SAME TRANSACTION, AND A
// LINK ALREADY MINTED FOR IT IS SPENT BUT OPENS NOTHING.
func TestRevokingTheTokenEndsItsSessions(t *testing.T) {
	db, acct, tok := signinDB(t, ScopeWeb)
	link, _, _ := db.MintSigninLink(acct, tok.ID, "")
	cookie, _, _, ok := db.RedeemSigninLink(link, "", "")
	if !ok {
		t.Fatal("FAIL: could not open a session")
	}
	pending, _, _ := db.MintSigninLink(acct, tok.ID, "")
	if err := db.RevokeToken(acct, tok.ID); err != nil {
		t.Fatal(err)
	}
	if _, _, n := sessionRowOf(t, db, cookie); n != 0 {
		t.Errorf("FAIL: %d session rows survive the revoke; want 0", n)
	}
	if _, ok := db.SessionAccount(cookie); ok {
		t.Error("FAIL: the cookie of a revoked token's session still resolves")
	}
	if _, _, _, ok := db.RedeemSigninLink(pending, "", ""); ok {
		t.Error("FAIL: a link minted before the revoke opened a session")
	}
	var used string
	db.db.QueryRow(`SELECT used FROM signin_link WHERE token = ?`, hashToken(pending)).Scan(&used)
	if used == "" {
		t.Error("FAIL: the refused link was not spent")
	}
}

// A SESSION NEVER OUTLIVES ITS TOKEN, EVEN WHEN THE TOKEN MERELY LAPSES.
func TestTokenSessionEndsWhenTheTokenExpires(t *testing.T) {
	db, acct, tok := signinDB(t, ScopeWeb)
	link, _, _ := db.MintSigninLink(acct, tok.ID, "")
	cookie, _, _, _ := db.RedeemSigninLink(link, "", "")
	if _, err := db.w.Exec(`UPDATE token SET expires = ? WHERE id = ?`,
		time.Now().UTC().Add(-time.Second).Format(time.RFC3339), tok.ID); err != nil {
		t.Fatal(err)
	}
	if _, ok := db.SessionAccount(cookie); ok {
		t.Error("FAIL: a session outlived its expired token")
	}
}

// A SIGN-IN LINK IS UNKNOWN TO THE INVITE PATH: IT CANNOT SET A PASSWORD.
func TestSigninLinkIsNotAnInvite(t *testing.T) {
	db, acct, tok := signinDB(t, ScopeWeb)
	link, _, _ := db.MintSigninLink(acct, tok.ID, "")
	if _, ok := db.InviteFor(link); ok {
		t.Error("FAIL: a sign-in link is a live invite")
	}
	if _, err := db.RedeemInvite(link, "a password of eight"); err == nil {
		t.Error("FAIL: a sign-in link redeemed as an invite")
	}
}

// A SESSION OPENED BY A PERSON IS BOUND TO NO TOKEN.
func TestPasswordSessionHasNoToken(t *testing.T) {
	db := tokenDB(t)
	if err := db.AddAccount("pat", "a long password", false); err != nil {
		t.Fatal(err)
	}
	cookie, _, ok := db.NewSession("pat", "a long password")
	if !ok {
		t.Fatal("FAIL: could not sign in")
	}
	if _, ok := db.SessionToken(cookie); ok {
		t.Error("FAIL: a password session reports a token")
	}
}