package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"sync"
"testing"
"craftdesign.group/quarry/internal/oltp"
)
// The networked CLI is a forge door like the web: it can ask for a
// reset link to go to the owner, and it cannot set anybody's password.
func TestTheNetworkedCLIAsksForAResetAndSetsNoPassword(t *testing.T) {
var mu sync.Mutex
var hits []string
var bodies []map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var b map[string]any
_ = json.NewDecoder(r.Body).Decode(&b)
mu.Lock()
hits, bodies = append(hits, r.URL.Path), append(bodies, b)
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"name":"jo","emailed":true}`))
}))
defer srv.Close()
if err := accountAPICmd([]string{"passwd", "--server", srv.URL, "--as", "root", "--as-password", "pw",
"--user", "jo", "--password", "chosen-by-root"}); err == nil {
t.Error("FAIL: `quarry accounts passwd` still answers")
}
for _, h := range hits {
if strings.Contains(h, "passwd") {
t.Fatalf("FAIL: the CLI reached %s", h)
}
}
if err := accountAPICmd([]string{"reset", "--server", srv.URL, "--as", "root", "--as-password", "pw",
"--user", "jo"}); err != nil {
t.Fatal(err)
}
if len(hits) != 1 || hits[0] != "/api/v1/accounts/reset" {
t.Fatalf("FAIL: want one call to /api/v1/accounts/reset, got %v", hits)
}
if bodies[0]["name"] != "jo" || len(bodies[0]) != 1 {
t.Errorf("FAIL: reset body = %v", bodies[0])
}
t.Log("ok: the CLI asks for a reset link and has no passwd")
}
// Recovering an account on the box stays possible — it is the host
// operator's act, not the forge admin's — and it is journalled.
func TestTheHostOperatorsPasswdIsJournalled(t *testing.T) {
dir := t.TempDir()
if err := accountCmd([]string{"add", "--data", dir, "--user", "jo", "--password", "jos-password"}); err != nil {
t.Fatal(err)
}
if err := accountCmd([]string{"passwd", "--data", dir, "--user", "jo", "--password", "a-recovered-password"}); err != nil {
t.Fatal(err)
}
db, err := oltp.Open(filepath.Join(dir, "oltp.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
acts, err := db.AccountActs("jo", 10)
if err != nil {
t.Fatal(err)
}
if len(acts) != 1 || acts[0].Op != "account passwd" || acts[0].Owner != "jo" || !strings.HasPrefix(acts[0].Actor, "host:") {
t.Fatalf("FAIL: journal = %+v", acts)
}
if strings.Contains(acts[0].Actor+acts[0].Op+acts[0].Repo+acts[0].Owner, "a-recovered-password") {
t.Fatal("FAIL: the password is in the journal")
}
t.Log("ok: the host operator's recovery is journalled")
}