package oltp
import (
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/xfer"
)
// tokenDB is a forge that can seal (a real data directory) and can
// answer for a repository's project code.
func tokenDB(t *testing.T, repos ...string) *DB {
t.Helper()
dir := t.TempDir()
db, err := Open(filepath.Join(dir, "oltp.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
for _, r := range repos {
if err := db.AddRepo(r, filepath.Join(dir, r), "", "public"); err != nil {
t.Fatal(err)
}
}
db.SyncProjectCode = func(repo string) (string, bool) { return "CODE-" + repo, true }
return db
}
func writerWithToken(t *testing.T, db *DB, user string, repos ...string) string {
t.Helper()
if err := db.AddAccount(user, "irrelevant to pushing", false); err != nil {
t.Fatal(err)
}
for _, r := range repos {
if err := db.Grant(user, r, Write); err != nil {
t.Fatal(err)
}
}
token, err := db.MintSyncToken(user)
if err != nil {
t.Fatal(err)
}
return token
}
// The push credential is the token, and nothing else. In particular it
// is NOT the account password: that was the previous design, and it
// planted a fast-hash derivative of every password in a database whose
// whole point is that argon2id makes a stolen copy worthless.
func TestTheSecretComesFromTheTokenNotThePassword(t *testing.T) {
db := tokenDB(t, "sandbox")
token := writerWithToken(t, db, "jan", "sandbox")
got, ok := db.Secret("sandbox", "jan")
if !ok {
t.Fatal("a writer holding a token has no push secret")
}
if want := xfer.SharedSecret("CODE-sandbox", "jan", token); got != want {
t.Errorf("secret is not derived from the token:\n got %q\nwant %q", got, want)
}
if bad := xfer.SharedSecret("CODE-sandbox", "jan", "irrelevant to pushing"); got == bad {
t.Error("the account password still derives the push secret")
}
}
// THE REASON FOR SEALING RATHER THAN STORING PER REPOSITORY.
//
// A repository created after the token was minted needs no re-issue, no
// second sign-in and no admin. The previous design stored a row per
// repository at sign-in, so a repository made five minutes later had no
// credential and its own creator could not push to it — the same shape
// of staleness that locked twenty-three repositories away from their
// owners.
func TestARepositoryMadeAfterTheTokenJustWorks(t *testing.T) {
db := tokenDB(t, "sandbox")
token := writerWithToken(t, db, "jan", "sandbox")
if err := db.AddRepo("later", t.TempDir(), "", "public"); err != nil {
t.Fatal(err)
}
if err := db.Grant("jan", "later", Write); err != nil {
t.Fatal(err)
}
got, ok := db.Secret("later", "jan")
if !ok {
t.Fatal("a repository created after the token was minted has no credential for its writer")
}
if want := xfer.SharedSecret("CODE-later", "jan", token); got != want {
t.Errorf("wrong derivation for the later repository:\n got %q\nwant %q", got, want)
}
}
// The letters are consulted on every call, so revoking write stops a
// push immediately rather than at some next mint.
func TestRevokingWriteStopsThePushAtOnce(t *testing.T) {
db := tokenDB(t, "sandbox")
writerWithToken(t, db, "jan", "sandbox")
if _, ok := db.Secret("sandbox", "jan"); !ok {
t.Fatal("premise broken: the writer should have a secret")
}
if err := db.Revoke("jan", "sandbox"); err != nil {
t.Fatal(err)
}
if _, ok := db.Secret("sandbox", "jan"); ok {
t.Error("a revoked writer could still prove itself to the sync protocol")
}
}
// Revoking the token stops every repository at once, which is what
// makes it a credential somebody can actually withdraw.
func TestRevokingTheTokenStopsEverything(t *testing.T) {
db := tokenDB(t, "sandbox", "other")
writerWithToken(t, db, "jan", "sandbox", "other")
if err := db.RevokeSyncToken("jan"); err != nil {
t.Fatal(err)
}
for _, r := range []string{"sandbox", "other"} {
if _, ok := db.Secret(r, "jan"); ok {
t.Errorf("%s still accepted a revoked token", r)
}
}
}
// A hand-issued sync_user row keeps working. Somebody is relying on one
// right now, and a credential change that silently breaks the people
// already using it is the exact mistake this landing exists to undo.
func TestAHandIssuedRowStillWins(t *testing.T) {
db := tokenDB(t, "sandbox")
writerWithToken(t, db, "jan", "sandbox")
if err := db.SetSyncUser("sandbox", "jan", "issued-by-hand"); err != nil {
t.Fatal(err)
}
if got, _ := db.Secret("sandbox", "jan"); got != "issued-by-hand" {
t.Errorf("the hand-issued row was overridden: %q", got)
}
}
// The token is never readable back out of the store: a page may say one
// exists and when it was made, never what it is.
func TestTheTokenIsShownOnceAndNeverAgain(t *testing.T) {
db := tokenDB(t, "sandbox")
token := writerWithToken(t, db, "jan", "sandbox")
created, ok := db.SyncTokenIssued("jan")
if !ok || created == "" {
t.Fatal("no issue date for a minted token")
}
if strings.Contains(created, token) {
t.Error("the issue record carries the token itself")
}
var sealed string
if err := db.db.QueryRow(`SELECT verifier FROM token WHERE account = ?`, "jan").Scan(&sealed); err != nil {
t.Fatal(err)
}
if strings.Contains(sealed, token) {
t.Error("the token is stored in the clear")
}
}
// An account with no token cannot push, even holding every letter.
func TestNoTokenMeansNoPush(t *testing.T) {
db := tokenDB(t, "sandbox")
if err := db.AddAccount("jan", "irrelevant", false); err != nil {
t.Fatal(err)
}
if err := db.Grant("jan", "sandbox", Write); err != nil {
t.Fatal(err)
}
if _, ok := db.Secret("sandbox", "jan"); ok {
t.Error("an account with no token was admitted")
}
// And the account DOES hold the letter, which is what makes this the
// state behind the push refusal's "holds no push credential"
// sentence rather than its "has not granted the check-in letter" one.
//
// A tester could not reach that sentence and reasonably concluded
// it was unreachable, since both clients refuse to push without a
// password and so the wire always carries some secret (G-134). But
// the sentence is chosen by what the SERVER holds, not by what the
// client sent: an account with the letter and no minted token lands
// here whatever it presents. Their accounts had both minted tokens.
if !db.MayWrite("sandbox", "jan") {
t.Error("an account granted Write does not hold the letter — the refusal would " +
"send it to an admin for a letter it already has")
}
}