| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | // selfOK is the agent's own openat2 check passing, which is every box the |
| 11 | // edge unit is right on. |
| 12 | func selfOK() error { return nil } |
| 13 | |
| 14 | // TestNestedGateRefusesWhenTheAgentCannotOpenat2 pins the gate's first |
| 15 | // question (#126, the edge unit blocker): when the agent's own process |
| 16 | // cannot make the openat2 call, the gate refuses with that error before it |
| 17 | // looks at any mount or asks /ready, so the first layout migration is |
| 18 | // refused whole rather than failing on its first host's first open. |
| 19 | func TestNestedGateRefusesWhenTheAgentCannotOpenat2(t *testing.T) { |
| 20 | wantErr := errors.New("instancefs: openat2 is refused in this process (ENOSYS)") |
| 21 | mountsCalled, readyCalled := false, false |
| 22 | mounts := func(string) (bool, error) { mountsCalled = true; return true, nil } |
| 23 | ready := func(context.Context) error { readyCalled = true; return nil } |
| 24 | err := nestedGate(func() error { return wantErr }, mounts, ready)() |
| 25 | if err != wantErr { |
| 26 | t.Fatalf("nestedGate() = %v, want %v", err, wantErr) |
| 27 | } |
| 28 | if mountsCalled || readyCalled { |
| 29 | t.Fatal("nestedGate went on to the mounts or /ready after the self check refused") |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | // TestNestedGateRefusesOnAMissingMount pins the first half of the isolated |
| 34 | // wake gate (design §8.5): the three nosymfollow mounts are checked in |
| 35 | // order, and the first one that is missing (or errors) names itself in the |
| 36 | // refusal rather than the gate silently moving on to /ready. |
| 37 | func TestNestedGateRefusesOnAMissingMount(t *testing.T) { |
| 38 | mounts := func(mountpoint string) (bool, error) { |
| 39 | if mountpoint == "/run/carlos/instances" { |
| 40 | return false, nil |
| 41 | } |
| 42 | return true, nil |
| 43 | } |
| 44 | readyCalled := false |
| 45 | ready := func(context.Context) error { readyCalled = true; return nil } |
| 46 | err := nestedGate(selfOK, mounts, ready)() |
| 47 | if err == nil || !strings.Contains(err.Error(), "/run/carlos/instances") { |
| 48 | t.Fatalf("nestedGate() = %v, want an error naming /run/carlos/instances", err) |
| 49 | } |
| 50 | if readyCalled { |
| 51 | t.Fatal("nestedGate called /ready after a mount already refused the wake") |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | // TestNestedGateRefusesOnAMountCheckError pins the same refusal for a |
| 56 | // mounts func that itself errors (an unreadable /proc/self/mountinfo, in |
| 57 | // production), rather than nestedGate treating an error as "not mounted" |
| 58 | // silently or panicking on it. |
| 59 | func TestNestedGateRefusesOnAMountCheckError(t *testing.T) { |
| 60 | wantErr := errors.New("read mountinfo: permission denied") |
| 61 | mounts := func(string) (bool, error) { return false, wantErr } |
| 62 | err := nestedGate(selfOK, mounts, func(context.Context) error { return nil })() |
| 63 | if err == nil || !strings.Contains(err.Error(), wantErr.Error()) { |
| 64 | t.Fatalf("nestedGate() = %v, want it to carry %v", err, wantErr) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | // TestNestedGatePassesMountsThenChecksReady pins the second half: once |
| 69 | // every mount checks out, the gate's answer is exactly whatever /ready |
| 70 | // says, refused when the control service is not ready, allowed when it is. |
| 71 | func TestNestedGatePassesMountsThenChecksReady(t *testing.T) { |
| 72 | mounts := func(string) (bool, error) { return true, nil } |
| 73 | |
| 74 | wantErr := errors.New("instance control: box not ready") |
| 75 | gate := nestedGate(selfOK, mounts, func(context.Context) error { return wantErr }) |
| 76 | if err := gate(); err != wantErr { |
| 77 | t.Fatalf("nestedGate() = %v, want the /ready error %v", err, wantErr) |
| 78 | } |
| 79 | |
| 80 | gate = nestedGate(selfOK, mounts, func(context.Context) error { return nil }) |
| 81 | if err := gate(); err != nil { |
| 82 | t.Fatalf("nestedGate() = %v, want nil once every mount and /ready both pass", err) |
| 83 | } |
| 84 | } |
| 85 | |