680 lines
26 KiB
Go
680 lines
26 KiB
Go
package main
|
|
|
|
// The user-run context surface (contextui.go, uicontext.go).
|
|
//
|
|
// The property this file exists for is the FIRST test: a process a normal
|
|
// person started must not serve the machine-admin console, even to the person
|
|
// who started it. Everything else here is about the surface being honest — it
|
|
// asks, it never grants, and it says which of the two it did.
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const contextTestPort = "7861"
|
|
|
|
// contextTestServer builds the context surface over a home directory the test owns.
|
|
func contextTestServer(t *testing.T, home string) (*uiServer, string) {
|
|
t.Helper()
|
|
u, err := newUIServer(contextTestPort, "test-secret-aaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
|
if err != nil {
|
|
t.Fatalf("newUIServer: %v", err)
|
|
}
|
|
u.contextUI = true
|
|
u.contextHome = home
|
|
tok, err := u.sessions.mint()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return u, uiCookieName + "=" + tok
|
|
}
|
|
|
|
func contextReq(t *testing.T, u *uiServer, method, path, cookie string, form url.Values) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
var r *http.Request
|
|
if method == http.MethodPost {
|
|
r = httptest.NewRequest(method, path, strings.NewReader(form.Encode()))
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
r.Header.Set("Origin", "http://127.0.0.1:"+contextTestPort)
|
|
} else {
|
|
r = httptest.NewRequest(method, path, nil)
|
|
}
|
|
r.Host = "127.0.0.1:" + contextTestPort
|
|
r.Header.Set("Cookie", cookie)
|
|
w := httptest.NewRecorder()
|
|
u.handler().ServeHTTP(w, r)
|
|
return w
|
|
}
|
|
|
|
// ── the property the surface exists for ─────────────────────────────────────
|
|
|
|
// A CONTEXT PROCESS SERVES NO MACHINE-ADMIN ROUTE. Not "refuses with 403" —
|
|
// DOES NOT MOUNT. The person who started this process may well also be the
|
|
// machine's administrator, and that is beside the point: this surface is not
|
|
// where that authority is exercised, and a route that exists is a route that
|
|
// gets linked to, bookmarked and eventually relied on.
|
|
func TestTheContextSurfaceServesNoMachineAdminRoute(t *testing.T) {
|
|
uiTestConfigDir(t)
|
|
u, cookie := contextTestServer(t, t.TempDir())
|
|
|
|
// Every route the admin console mounts. Named individually rather than
|
|
// derived, because the list is the assertion: if one of these ever starts
|
|
// answering here, that is the finding.
|
|
for _, path := range []string{
|
|
"/fragments/status", "/given", "/fragments/given", "/approve", "/deny",
|
|
"/setup", "/setup/pair", "/setup/stepup/start", "/setup/stepup/qr",
|
|
"/setup/stepup/key", "/setup/stepup/confirm", "/setup/folder/check",
|
|
"/setup/folder/propose", "/setup/anchor/choose", "/setup/owner/name",
|
|
"/diskmap", "/fragments/diskmap", "/diskmap/scan", "/diskmap/stop",
|
|
"/agents", "/fragments/agents", "/agents/mark", "/agents/unmark",
|
|
"/policy", "/fragments/policy",
|
|
} {
|
|
w := contextReq(t, u, http.MethodGet, path, cookie, nil)
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("%s answered %d on the context surface. This process runs as an "+
|
|
"ordinary person; the pairing list, the anchor, the marks and the policy "+
|
|
"are the machine's own console and belong to whoever administers it.",
|
|
path, w.Code)
|
|
}
|
|
}
|
|
// And the check is not passing because everything 404s.
|
|
if w := contextReq(t, u, http.MethodGet, "/", cookie, nil); w.Code != http.StatusOK {
|
|
t.Fatalf("/ = %d — the context page itself does not serve, so the assertions "+
|
|
"above proved nothing", w.Code)
|
|
}
|
|
}
|
|
|
|
// The same list from the other side: the ADMIN console must not grow the files
|
|
// routes either. Two surfaces, and the separation has to hold both ways or the
|
|
// next person to add a route picks whichever mux they happened to open.
|
|
func TestTheAdminConsoleServesNoContextRoute(t *testing.T) {
|
|
uiTestConfigDir(t)
|
|
u := uiTestServer(t)
|
|
cookie := uiSignedIn(t, u)
|
|
for _, path := range []string{"/fragments/context", "/context/scan", "/context/stop", "/context/ask"} {
|
|
r := uiReq(http.MethodGet, path)
|
|
r.Header.Set("Cookie", cookie)
|
|
w := httptest.NewRecorder()
|
|
u.handler().ServeHTTP(w, r)
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("%s answered %d on the admin console, want 404", path, w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
// THE STYLESHEET THE PAGE LINKS IS A STYLESHEET THE SERVER HAS. The rename to
|
|
// context.* proved the negative case: the page linked /assets/files.css while
|
|
// the embedded file was already context.css, and the suite stayed green because
|
|
// nothing asked for the href the page actually carries. A page whose
|
|
// stylesheet 404s renders unstyled and says nothing.
|
|
// So ask for whatever the page links, rather than asserting a name.
|
|
func TestTheContextPageStylesheetActuallyServes(t *testing.T) {
|
|
uiTestConfigDir(t)
|
|
u, cookie := contextTestServer(t, t.TempDir())
|
|
|
|
w := contextReq(t, u, http.MethodGet, "/", cookie, nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("/ = %d — the context page itself does not serve", w.Code)
|
|
}
|
|
hrefs := regexp.MustCompile(`<link[^>]+href="(/assets/[^"]+)"`).FindAllStringSubmatch(w.Body.String(), -1)
|
|
if len(hrefs) == 0 {
|
|
t.Fatal("the context page links no stylesheet at all — this check would pass for ever")
|
|
}
|
|
for _, m := range hrefs {
|
|
if got := contextReq(t, u, http.MethodGet, m[1], cookie, nil).Code; got != http.StatusOK {
|
|
t.Errorf("the page links %s but the server answers %d for it", m[1], got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── it asks; it never grants ────────────────────────────────────────────────
|
|
|
|
// The word on the button and the words around it have to say that pressing it
|
|
// shares nothing. This is the one place on either surface where a person could
|
|
// believe they had just handed over a folder when they had not, or the reverse.
|
|
func TestTheAskSaysNothingIsSharedByPressingIt(t *testing.T) {
|
|
home := t.TempDir()
|
|
sub := filepath.Join(home, "Contracts")
|
|
if err := os.MkdirAll(sub, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(sub, "lease.pdf"), make([]byte, 4096), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
uiTestConfigDir(t)
|
|
dmTestInstall(t, dmScanTree(t, home))
|
|
u, cookie := contextTestServer(t, home)
|
|
|
|
body := contextReq(t, u, http.MethodGet, "/?p="+url.QueryEscape(sub), cookie, nil).Body.String()
|
|
if !strings.Contains(body, "/context/ask") {
|
|
t.Fatalf("there is no way to ask for a folder from inside one: %q", dmSnip(body, "ask"))
|
|
}
|
|
for _, want := range []string{
|
|
"does not share anything",
|
|
"has to approve it",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("the ask does not say %q. A button that reads as "+
|
|
"\"share\" when nothing is shared is the worst sentence on this page.", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A FOLDER OUTSIDE YOUR OWN IS REFUSED AT THE POINT OF ASKING. The kernel is
|
|
// the real control — this process cannot read what it cannot read — but a
|
|
// request that travels to a host, waits for a person to approve it and only
|
|
// then fails is a bad way to learn that.
|
|
func TestAskingForAFolderOutsideYourOwnIsRefused(t *testing.T) {
|
|
home := t.TempDir()
|
|
outside := t.TempDir() // a sibling: real, readable, and not theirs
|
|
uiTestConfigDir(t)
|
|
dmTestInstall(t, nil)
|
|
u, cookie := contextTestServer(t, home)
|
|
|
|
w := contextReq(t, u, http.MethodPost, "/context/ask", cookie, url.Values{
|
|
"path": {outside}, "name": {"not mine"},
|
|
})
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "outside") {
|
|
t.Errorf("a folder outside the person's own directory was not refused: %q",
|
|
dmSnip(body, "flash"))
|
|
}
|
|
// And it names where that authority does live, rather than just saying no.
|
|
if !strings.Contains(body, "privatae-bastion ui") {
|
|
t.Errorf("the refusal does not say who can share a machine-wide folder: %q",
|
|
dmSnip(body, "flash"))
|
|
}
|
|
if len(u.contextAsked()) != 0 {
|
|
t.Error("a refused ask was recorded as an ask")
|
|
}
|
|
}
|
|
|
|
// The home directory ITSELF is askable — the boundary is "outside", not
|
|
// "below". A person who wants to index their whole home directory is doing the
|
|
// ordinary thing, not an edge case.
|
|
func TestYourOwnHomeDirectoryIsAskable(t *testing.T) {
|
|
home := t.TempDir()
|
|
docs := filepath.Join(home, "Documents")
|
|
// The folder has to EXIST: a path you cannot open is not one you can hand
|
|
// over, and the check says so. The first version of this test skipped the
|
|
// mkdir and read that refusal as a containment failure.
|
|
if err := os.MkdirAll(docs, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
uiTestConfigDir(t)
|
|
dmTestInstall(t, nil)
|
|
u, _ := contextTestServer(t, home)
|
|
if why := u.contextRefuseOutsideHome(home); why != "" {
|
|
t.Errorf("the person's own home directory was refused: %s", why)
|
|
}
|
|
if why := u.contextRefuseOutsideHome(docs); why != "" {
|
|
t.Errorf("a folder inside the person's home was refused: %s", why)
|
|
}
|
|
// And a folder inside your home that does not exist is still refused, here,
|
|
// rather than travelling to a host to fail there.
|
|
if why := u.contextRefuseOutsideHome(filepath.Join(home, "no-such-folder")); why == "" {
|
|
t.Error("a folder that does not exist was accepted")
|
|
}
|
|
}
|
|
|
|
// A PREFIX IS NOT A PARENT. "/home/user-backup" starts with "/home/user" and
|
|
// is a different person's directory.
|
|
func TestAPathThatMerelySharesAPrefixIsNotYours(t *testing.T) {
|
|
base := t.TempDir()
|
|
home := filepath.Join(base, "user")
|
|
sneak := filepath.Join(base, "user-backup")
|
|
for _, d := range []string{home, sneak} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
uiTestConfigDir(t)
|
|
u, _ := contextTestServer(t, home)
|
|
if why := u.contextRefuseOutsideHome(sneak); why == "" {
|
|
t.Errorf("%q was accepted as being inside %q — string prefixes are not path "+
|
|
"containment, and the difference is another person's files", sneak, home)
|
|
}
|
|
}
|
|
|
|
// ── the map is the person's own ─────────────────────────────────────────────
|
|
|
|
// There is NO whole-machine option on this surface, and that is not a hidden
|
|
// button: the scope does not exist here. A user-run process could not read
|
|
// other people's files anyway, so offering it would promise something false.
|
|
func TestTheContextSurfaceOffersNoWholeMachineMap(t *testing.T) {
|
|
uiTestConfigDir(t)
|
|
dmTestInstall(t, nil)
|
|
u, cookie := contextTestServer(t, t.TempDir())
|
|
body := contextReq(t, u, http.MethodGet, "/", cookie, nil).Body.String()
|
|
|
|
if strings.Contains(body, `value="machine"`) {
|
|
t.Error("the context surface offers a whole-machine map. This process runs as one " +
|
|
"person and cannot read another's files; the button would produce a map full " +
|
|
"of refusals and tell them something false about what this is.")
|
|
}
|
|
if !strings.Contains(body, "/context/scan") {
|
|
t.Fatal("there is no way to count anything at all, so the assertion above " +
|
|
"passed for the wrong reason")
|
|
}
|
|
}
|
|
|
|
// And the walk it starts is the home scope, whatever anybody posts.
|
|
func TestTheContextScanIsAlwaysTheHomeScope(t *testing.T) {
|
|
uiTestConfigDir(t)
|
|
s := dmTestInstall(t, nil)
|
|
release := make(chan struct{})
|
|
t.Cleanup(func() { close(release) })
|
|
s.scan = func(context.Context, []string, int, func(dmProgress)) *dmResult {
|
|
<-release
|
|
return &dmResult{TakenAt: uiNow()}
|
|
}
|
|
u, cookie := contextTestServer(t, t.TempDir())
|
|
|
|
// A hand-written form asking for the machine. The route takes no scope at
|
|
// all, so this must change nothing.
|
|
contextReq(t, u, http.MethodPost, "/context/scan", cookie, url.Values{"scope": {"machine"}})
|
|
s.mu.Lock()
|
|
got := s.scope
|
|
s.mu.Unlock()
|
|
if got != dmScopeHome {
|
|
t.Errorf("a posted scope=machine started a %q walk. This route reads no scope "+
|
|
"from the request precisely so that it cannot be widened by one.", got)
|
|
}
|
|
}
|
|
|
|
// ── the reads are reads ─────────────────────────────────────────────────────
|
|
|
|
func TestEveryContextReadIsGETOnlyAndEveryWriteIsPOSTOnly(t *testing.T) {
|
|
uiTestConfigDir(t)
|
|
dmTestInstall(t, nil)
|
|
u, cookie := contextTestServer(t, t.TempDir())
|
|
|
|
for _, path := range []string{"/", "/fragments/context"} {
|
|
if w := contextReq(t, u, http.MethodPost, path, cookie, url.Values{}); w.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("POST %s = %d, want 405", path, w.Code)
|
|
}
|
|
}
|
|
for _, path := range []string{"/context/scan", "/context/stop", "/context/ask"} {
|
|
if w := contextReq(t, u, http.MethodGet, path, cookie, nil); w.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("GET %s = %d, want 405 — it changes something", path, w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The same three defences the admin console has. A cross-site POST must not
|
|
// start a walk of somebody's home directory or hand a folder to a host.
|
|
func TestTheContextSurfaceTakesTheOriginAndHostChecks(t *testing.T) {
|
|
uiTestConfigDir(t)
|
|
s := dmTestInstall(t, nil)
|
|
started := 0
|
|
s.scan = func(context.Context, []string, int, func(dmProgress)) *dmResult {
|
|
started++
|
|
return &dmResult{}
|
|
}
|
|
u, cookie := contextTestServer(t, t.TempDir())
|
|
|
|
for _, route := range []string{"/context/scan", "/context/ask"} {
|
|
r := httptest.NewRequest(http.MethodPost, route, strings.NewReader(""))
|
|
r.Host = "127.0.0.1:" + contextTestPort
|
|
r.Header.Set("Origin", "https://evil.example")
|
|
r.Header.Set("Cookie", cookie)
|
|
w := httptest.NewRecorder()
|
|
u.handler().ServeHTTP(w, r)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("cross-site POST %s = %d, want 403", route, w.Code)
|
|
}
|
|
|
|
// And a rebound host, which is the one SameSite cannot see.
|
|
r = httptest.NewRequest(http.MethodPost, route, strings.NewReader(""))
|
|
r.Host = "evil.example:" + contextTestPort
|
|
r.Header.Set("Origin", "http://127.0.0.1:"+contextTestPort)
|
|
r.Header.Set("Cookie", cookie)
|
|
w = httptest.NewRecorder()
|
|
u.handler().ServeHTTP(w, r)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("rebound-host POST %s = %d, want 403", route, w.Code)
|
|
}
|
|
}
|
|
if started != 0 {
|
|
t.Fatalf("%d walks were started by refused requests", started)
|
|
}
|
|
}
|
|
|
|
// Signed out, this page says nothing about anybody's files — including the
|
|
// name of the directory it would have shown.
|
|
func TestSignedOutTheContextPageNamesNothing(t *testing.T) {
|
|
home := filepath.Join(t.TempDir(), "very-particular-directory-name")
|
|
if err := os.MkdirAll(home, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
uiTestConfigDir(t)
|
|
dmTestInstall(t, nil)
|
|
u, _ := contextTestServer(t, home)
|
|
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
r.Host = "127.0.0.1:" + contextTestPort
|
|
w := httptest.NewRecorder()
|
|
u.handler().ServeHTTP(w, r)
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Fatalf("/ without a session = %d, want 401", w.Code)
|
|
}
|
|
if strings.Contains(w.Body.String(), "very-particular-directory-name") {
|
|
t.Errorf("the refusal named the directory: %q", w.Body.String())
|
|
}
|
|
if !strings.Contains(w.Body.String(), "privatae-bastion context") {
|
|
t.Errorf("the refusal does not say how to get in: %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// ── two refusal-wording findings ────────────────────────────────────────────
|
|
|
|
// A REFUSAL THAT NAMES THE WRONG COMMAND sends somebody to a page they cannot
|
|
// open, which is worse than no instruction. The verb switch reached root() and
|
|
// not guarded(), so three refusal bodies on this surface still said `ui`: the
|
|
// guarded-route 401, the expired-ticket 403, and the rebinding refusal that
|
|
// lives in uiRequestAllowed and knew nothing about surfaces at all.
|
|
//
|
|
// Every refusal a person can actually hit, on both surfaces, checked together —
|
|
// a per-route test would have missed the two that are not routes.
|
|
func TestEveryRefusalNamesTheVerbThatOpensThisSurface(t *testing.T) {
|
|
uiTestConfigDir(t)
|
|
dmTestInstall(t, nil)
|
|
|
|
for _, c := range []struct {
|
|
surface string
|
|
want string
|
|
wrong string
|
|
}{
|
|
{"context", "privatae-bastion context", "privatae-bastion ui"},
|
|
{"admin", "privatae-bastion ui", "privatae-bastion context"},
|
|
} {
|
|
t.Run(c.surface, func(t *testing.T) {
|
|
var u *uiServer
|
|
var port, guardedPath string
|
|
if c.surface == "context" {
|
|
u, _ = contextTestServer(t, t.TempDir())
|
|
port, guardedPath = contextTestPort, "/fragments/context"
|
|
} else {
|
|
u = uiTestServer(t)
|
|
port, guardedPath = uiTestPort, "/fragments/status"
|
|
}
|
|
h := u.handler()
|
|
|
|
bodies := map[string]string{}
|
|
|
|
// 1. A guarded route with no session.
|
|
r := httptest.NewRequest(http.MethodGet, guardedPath, nil)
|
|
r.Host = "127.0.0.1:" + port
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Fatalf("%s with no session = %d, want 401", guardedPath, w.Code)
|
|
}
|
|
bodies["the guarded-route 401"] = w.Body.String()
|
|
|
|
// 2. A ticket that has already been used.
|
|
tok, err := u.tickets.mint()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := 0; i < 2; i++ {
|
|
r = httptest.NewRequest(http.MethodGet, "/?k="+tok, nil)
|
|
r.Host = "127.0.0.1:" + port
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
}
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("a re-used ticket = %d, want 403", w.Code)
|
|
}
|
|
bodies["the spent-ticket 403"] = w.Body.String()
|
|
|
|
// 3. A rebound host — the refusal that lives in uiRequestAllowed.
|
|
r = httptest.NewRequest(http.MethodGet, "/", nil)
|
|
r.Host = "evil.example:" + port
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("a rebound host = %d, want 403", w.Code)
|
|
}
|
|
bodies["the rebinding 403"] = w.Body.String()
|
|
|
|
for what, body := range bodies {
|
|
if !strings.Contains(body, c.want) {
|
|
t.Errorf("%s does not name %q: %q", what, c.want, strings.TrimSpace(body))
|
|
}
|
|
if strings.Contains(body, c.wrong) {
|
|
t.Errorf("%s tells the person to run %q, which does not open this "+
|
|
"surface: %q", what, c.wrong, strings.TrimSpace(body))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// renderContext duplicates the four headers renderWith and renderDiskMap set, and
|
|
// its comment claimed a comparison test made that duplication safe. It did not:
|
|
// the comparison existed for the DISKMAP renderer only, so dropping the CSP
|
|
// line from renderContext turned nothing red. A comment
|
|
// that names a safety net which does not cover it is worse than no comment,
|
|
// because the next person reads it and believes the duplication is held.
|
|
func TestTheContextPagesCarryTheSameHeadersAsTheAdminPages(t *testing.T) {
|
|
uiTestConfigDir(t)
|
|
dmTestInstall(t, nil)
|
|
|
|
admin := uiTestServer(t)
|
|
adminCookie := uiSignedIn(t, admin)
|
|
r := uiReq(http.MethodGet, "/")
|
|
r.Header.Set("Cookie", adminCookie)
|
|
w := httptest.NewRecorder()
|
|
admin.handler().ServeHTTP(w, r)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("the admin page = %d, so there is nothing to compare against", w.Code)
|
|
}
|
|
want := w.Header()
|
|
|
|
u, cookie := contextTestServer(t, t.TempDir())
|
|
for _, path := range []string{"/", "/fragments/context"} {
|
|
got := contextReq(t, u, http.MethodGet, path, cookie, nil)
|
|
if got.Code != http.StatusOK {
|
|
t.Fatalf("%s = %d", path, got.Code)
|
|
}
|
|
for _, k := range []string{"Content-Type", "Cache-Control",
|
|
"Content-Security-Policy", "X-Content-Type-Options"} {
|
|
if got.Header().Get(k) != want.Get(k) {
|
|
t.Errorf("%s %s: files %q, admin %q", path, k,
|
|
got.Header().Get(k), want.Get(k))
|
|
}
|
|
}
|
|
}
|
|
if want.Get("Content-Security-Policy") == "" {
|
|
t.Fatal("neither surface sends a CSP, so this comparison proved nothing")
|
|
}
|
|
}
|
|
|
|
// ── a fixture with an expiry date on it ─────────────────────────────────────
|
|
|
|
// NO FIXTURE MAY ENCODE "NOW" AS A LITERAL.
|
|
//
|
|
// Three fixtures stamped a pending request with a literal `At:` date, and
|
|
// stepup.go:569 drops any request older than stepupRequestTTL (24h,
|
|
// stepup.go:62). So those tests passed for one day after the date was typed and
|
|
// then failed on every machine for ever. Measured 32 hours past the stamp: six
|
|
// approvals tests red with nothing whatsoever wrong in the code they were
|
|
// testing.
|
|
//
|
|
// That is the worst failure shape a suite can have: it accuses working code,
|
|
// and it arrives on a day nobody changed anything. The scan below is over the
|
|
// FIELD THAT IS AGE-CHECKED, not over dates in general — an EnrolledAt or a
|
|
// FirstSeen in the past is a legitimate fixture and this must not fight it.
|
|
func TestNoFixtureHardcodesAMomentInTime(t *testing.T) {
|
|
names, err := packageTestFiles()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(names) < 20 {
|
|
t.Fatalf("only %d _test.go files found — the scan has stopped finding them, "+
|
|
"which makes this check a green light that proves nothing", len(names))
|
|
}
|
|
// `At:` followed by a literal RFC3339 date. That field is the one compared
|
|
// against stepupRequestTTL.
|
|
bad := regexp.MustCompile(`\bAt:\s*"(\d{4})-\d{2}-\d{2}T`)
|
|
found := 0
|
|
for _, name := range names {
|
|
b, err := os.ReadFile(name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i, line := range strings.Split(string(b), "\n") {
|
|
// A COMMENT IS NOT A FIXTURE. A comment may spell out the literal
|
|
// this bans while explaining it, and a guard that fails on its own
|
|
// explanation teaches the next person to delete the explanation.
|
|
if strings.HasPrefix(strings.TrimSpace(line), "//") {
|
|
continue
|
|
}
|
|
if m := bad.FindStringSubmatch(line); m != nil {
|
|
found++
|
|
t.Errorf("%s:%d stamps a request with a literal date:\n %s\n"+
|
|
"stepup.go:569 drops requests older than stepupRequestTTL, so this "+
|
|
"fixture stops working 24 hours after the date in it and takes "+
|
|
"working code down with it. Use uiTestRecently().",
|
|
name, i+1, strings.TrimSpace(line))
|
|
}
|
|
}
|
|
}
|
|
if found == 0 {
|
|
// The scan must be capable of finding something, or it is decoration.
|
|
// Assembled rather than written out, so the probe cannot trip the very
|
|
// scan it is checking.
|
|
probe := "At:" + ` "2026-` + `09-13T09:00:00Z",`
|
|
if !bad.MatchString(probe) {
|
|
t.Fatal("the pattern no longer matches the shape it was written for, so a " +
|
|
"clean run proves nothing")
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── what the page is NAMED for ──────────────────────────────────────────────
|
|
|
|
// A PAGE CALLED "YOUR CONTEXT" HAS TO SHOW YOUR CONTEXT. It showed a file
|
|
// browser and a list of requests, which answers every question except the one
|
|
// in its title.
|
|
func TestTheContextPageShowsWhatIsAlreadyContext(t *testing.T) {
|
|
uiTestConfigDir(t)
|
|
dmTestInstall(t, nil)
|
|
// Reuse the fixtures the given page's own tests use, so this exercises the
|
|
// same record shape the admin console reads rather than a parallel one.
|
|
const host = "h1.host.privatae.ai"
|
|
dcn := givenPairing(t, host, pairingActive)
|
|
givenStoreGrants(t, dcn, map[string]map[string]any{
|
|
"1": {"root": "/home/user/Contracts", "mask": []any{"read", "index"},
|
|
"sweep": "nightly", "ttl_days": float64(30)},
|
|
})
|
|
u, cookie := contextTestServer(t, t.TempDir())
|
|
body := contextReq(t, u, http.MethodGet, "/", cookie, nil).Body.String()
|
|
|
|
if !strings.Contains(body, "What is context now") {
|
|
t.Error("the page has no section for context that already exists")
|
|
}
|
|
if !strings.Contains(body, "/home/user/Contracts") {
|
|
t.Errorf("a folder that IS context is not on the page named for it: %q",
|
|
dmSnip(body, "context now"))
|
|
}
|
|
if !strings.Contains(body, host) {
|
|
t.Error("the page does not say WHICH host holds that folder")
|
|
}
|
|
}
|
|
|
|
// And when this process cannot read the records, it says so rather than
|
|
// rendering an empty table — "you have no context" is a different claim from
|
|
// "this window cannot see".
|
|
func TestAnUnreadableStoreIsSaidRatherThanShownAsNothing(t *testing.T) {
|
|
uiTestConfigDir(t)
|
|
dmTestInstall(t, nil)
|
|
pd, err := pairingsDir()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(pd, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(pd, "broken.host.privatae.ai.json"),
|
|
[]byte("{not json"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := loadPairings(); err == nil {
|
|
t.Skip("loadPairings tolerates this; the fixture needs updating")
|
|
}
|
|
u, cookie := contextTestServer(t, t.TempDir())
|
|
body := contextReq(t, u, http.MethodGet, "/", cookie, nil).Body.String()
|
|
if !strings.Contains(body, "cannot be read") {
|
|
t.Errorf("an unreadable pairing store rendered as nothing at all: %q",
|
|
dmSnip(body, "context now"))
|
|
}
|
|
}
|
|
|
|
// ── the asks outlive the window ─────────────────────────────────────────────
|
|
|
|
// A request waiting on somebody else is a thing YOU DID. Closing the page used
|
|
// to erase the only evidence you had done it.
|
|
func TestAsksSurviveTheWindowClosing(t *testing.T) {
|
|
uiTestConfigDir(t)
|
|
first, _ := contextTestServer(t, t.TempDir())
|
|
first.contextRecord(uiContextAsk{
|
|
Path: "/home/user/Contracts", Name: "Contracts",
|
|
Host: "h1.host.privatae.ai", At: "14 Sep 2026, 20:10", Sent: true,
|
|
})
|
|
if len(first.contextAsked()) != 1 {
|
|
t.Fatal("the ask was not recorded at all")
|
|
}
|
|
|
|
// A NEW server object is the window being closed and reopened.
|
|
second, cookie := contextTestServer(t, t.TempDir())
|
|
if got := len(second.contextAsked()); got != 0 {
|
|
t.Fatalf("a fresh server started with %d asks before loading", got)
|
|
}
|
|
second.contextLoadAsks()
|
|
got := second.contextAsked()
|
|
if len(got) != 1 {
|
|
t.Fatalf("after reopening, %d asks — the record of what this person asked for "+
|
|
"is gone, and they cannot see that they already asked", len(got))
|
|
}
|
|
if got[0].Path != "/home/user/Contracts" {
|
|
t.Errorf("the reloaded ask is %+v", got[0])
|
|
}
|
|
body := contextReq(t, second, http.MethodGet, "/", cookie, nil).Body.String()
|
|
if !strings.Contains(body, "/home/user/Contracts") {
|
|
t.Error("the reloaded ask does not reach the page")
|
|
}
|
|
}
|
|
|
|
// The file names folders a person asked to share; it is written private.
|
|
func TestTheAskHistoryIsWrittenPrivate(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("unix permission bits do not carry the same meaning here")
|
|
}
|
|
uiTestConfigDir(t)
|
|
u, _ := contextTestServer(t, t.TempDir())
|
|
u.contextRecord(uiContextAsk{Path: "/home/user/Private", At: "now"})
|
|
p, err := contextAsksPath()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fi, err := os.Stat(p)
|
|
if err != nil {
|
|
t.Fatalf("no history was written: %v", err)
|
|
}
|
|
if got := fi.Mode().Perm(); got != 0o600 {
|
|
t.Errorf("context-asks.json is %o, want 600 — it names folders this person "+
|
|
"asked to share", got)
|
|
}
|
|
}
|