package main
import (
"net/http"
"net/http/httptest"
"testing"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// A SYNC TOKEN IN THE BASIC PASSWORD SLOT IDENTIFIES THE ACCOUNT ON THE
// SYNC LANE — and nowhere else.
//
// The read gate learned the login card, and a token on the card reads a
// private repository. But no client sends a card on a clone's first
// round: it cannot sign one before the server has told it the project
// code. fossil answers the 401 with HTTP Basic; zit retries with Basic;
// both put the one credential the client holds — the token — in the
// password slot. identityCtx read it as a wrong password, so the caller
// was "identified, unrecognised", told 404, and a private repository
// stayed unclonable by the very token minted for it. jva measured all
// six combinations on 16670a54f4ca (2bb5659e): card+token 200,
// Basic+password 200, Basic+token 404. The last one is this test.
func TestSyncTokenInBasicReadsAPrivateRepository(t *testing.T) {
rg := newTestRegistrar(t, 25)
const pw = "pw12345678"
if err := rg.odb.AddAccount("jan", pw, false); err != nil {
t.Fatal(err)
}
if err := rg.odb.AddRepo("vault", t.TempDir(), "", oltp.Private); err != nil {
t.Fatal(err)
}
if err := rg.odb.SetCaps("jan", "vault", "o"); err != nil {
t.Fatal(err)
}
rg.refreshPrivate()
token, err := rg.odb.MintSyncToken("jan")
if err != nil {
t.Fatal(err)
}
basic := func(user, secret string) *http.Request {
req := httptest.NewRequest(http.MethodGet, "/xfer/vault", nil)
req.SetBasicAuth(user, secret)
return req
}
if !rg.SyncReadGate(basic("jan", token), "vault", "") {
t.Error("Basic carrying the sync token was refused a read of a private repository")
}
if !rg.SyncReadGate(basic("jan", pw), "vault", "") {
t.Error("Basic carrying the password stopped working")
}
if rg.SyncReadGate(basic("jan", "not-the-token"), "vault", "") {
t.Error("a wrong secret in Basic was admitted")
}
if rg.SyncReadGate(basic("jan", ""), "vault", "") {
t.Error("an empty secret in Basic was admitted")
}
// The tally's reader identity shares the gate's answer (main.go
// wires SetIdentify to the same resolver), so a token clone is
// recorded to jan, not to nobody.
if got := ops.User(syncIdentityCtx(basic("jan", token), &tokenDoors{db: rg.odb})); got != "jan" {
t.Errorf("sync identity for a token in Basic = %q, want jan", got)
}
// The API's Basic lane is untouched: the token is the sync
// credential, not a second password.
if got := ops.User(identityCtx(basic("jan", token), rg.odb)); got != "" {
t.Errorf("the API lane identified %q from a sync token in Basic; only the password should", got)
}
// A revoked token stops reading at once, as it stops pushing.
if err := rg.odb.RevokeSyncToken("jan"); err != nil {
t.Fatal(err)
}
if rg.SyncReadGate(basic("jan", token), "vault", "") {
t.Error("a revoked sync token still reads")
}
}