package host import ( "context" "errors" "strings" "testing" ) // selfOK is the agent's own openat2 check passing, which is every box the // edge unit is right on. func selfOK() error { return nil } // TestNestedGateRefusesWhenTheAgentCannotOpenat2 pins the gate's first // question (#126, the edge unit blocker): when the agent's own process // cannot make the openat2 call, the gate refuses with that error before it // looks at any mount or asks /ready, so the first layout migration is // refused whole rather than failing on its first host's first open. func TestNestedGateRefusesWhenTheAgentCannotOpenat2(t *testing.T) { wantErr := errors.New("instancefs: openat2 is refused in this process (ENOSYS)") mountsCalled, readyCalled := false, false mounts := func(string) (bool, error) { mountsCalled = true; return true, nil } ready := func(context.Context) error { readyCalled = true; return nil } err := nestedGate(func() error { return wantErr }, mounts, ready)() if err != wantErr { t.Fatalf("nestedGate() = %v, want %v", err, wantErr) } if mountsCalled || readyCalled { t.Fatal("nestedGate went on to the mounts or /ready after the self check refused") } } // TestNestedGateRefusesOnAMissingMount pins the first half of the isolated // wake gate (design ยง8.5): the three nosymfollow mounts are checked in // order, and the first one that is missing (or errors) names itself in the // refusal rather than the gate silently moving on to /ready. func TestNestedGateRefusesOnAMissingMount(t *testing.T) { mounts := func(mountpoint string) (bool, error) { if mountpoint == "/run/carlos/instances" { return false, nil } return true, nil } readyCalled := false ready := func(context.Context) error { readyCalled = true; return nil } err := nestedGate(selfOK, mounts, ready)() if err == nil || !strings.Contains(err.Error(), "/run/carlos/instances") { t.Fatalf("nestedGate() = %v, want an error naming /run/carlos/instances", err) } if readyCalled { t.Fatal("nestedGate called /ready after a mount already refused the wake") } } // TestNestedGateRefusesOnAMountCheckError pins the same refusal for a // mounts func that itself errors (an unreadable /proc/self/mountinfo, in // production), rather than nestedGate treating an error as "not mounted" // silently or panicking on it. func TestNestedGateRefusesOnAMountCheckError(t *testing.T) { wantErr := errors.New("read mountinfo: permission denied") mounts := func(string) (bool, error) { return false, wantErr } err := nestedGate(selfOK, mounts, func(context.Context) error { return nil })() if err == nil || !strings.Contains(err.Error(), wantErr.Error()) { t.Fatalf("nestedGate() = %v, want it to carry %v", err, wantErr) } } // TestNestedGatePassesMountsThenChecksReady pins the second half: once // every mount checks out, the gate's answer is exactly whatever /ready // says, refused when the control service is not ready, allowed when it is. func TestNestedGatePassesMountsThenChecksReady(t *testing.T) { mounts := func(string) (bool, error) { return true, nil } wantErr := errors.New("instance control: box not ready") gate := nestedGate(selfOK, mounts, func(context.Context) error { return wantErr }) if err := gate(); err != wantErr { t.Fatalf("nestedGate() = %v, want the /ready error %v", err, wantErr) } gate = nestedGate(selfOK, mounts, func(context.Context) error { return nil }) if err := gate(); err != nil { t.Fatalf("nestedGate() = %v, want nil once every mount and /ready both pass", err) } }