1package ports
2
3import (
4 "bytes"
5 "errors"
6 "testing"
7
8 "github.com/bnema/vev/internal/domain"
9)
10
11func testRemoteTarget(stopped bool) *domain.RemoteSessionTarget {
12 var lifecycle domain.SessionLifecycleID
13 for i := range lifecycle {
14 lifecycle[i] = byte(i + 1)
15 }
16 target := &domain.RemoteSessionTarget{
17 Endpoint: "build@mule:2222",
18 DisplayOrigin: "mule:2222",
19 LifecycleID: lifecycle,
20 SessionName: "work",
21 Stopped: stopped,
22 }
23 if stopped {
24 target.StoppedTab = domain.NewOrdinalTabSelector(2, "", 4)
25 } else {
26 target.LiveTabID = "tab-3"
27 }
28 return target
29}
30
31func TestRemoteTargetWireRoundTripPreservesExactSelector(t *testing.T) {
32 for _, stopped := range []bool{false, true} {
33 name := "live"
34 if stopped {
35 name = "stopped"
36 }
37 t.Run(name, func(t *testing.T) {
38 target := testRemoteTarget(stopped)
39 msg := Hello{
40 Version: ProtocolVersion,
41 Intent: IntentAttach,
42 Name: target.SessionName,
43 Size: domain.Size{Cols: 80, Rows: 24},
44 RemoteTarget: target,
45 EnvironmentPolicy: EnvironmentPolicyDaemonOwned,
46 }
47 payload := MarshalHello(msg)
48 if payload == nil {
49 t.Fatal("MarshalHello returned nil")
50 }
51 got, err := UnmarshalHello(payload)
52 if err != nil {
53 t.Fatal(err)
54 }
55 if got.RemoteTarget == nil || *got.RemoteTarget != *target {
56 t.Fatalf("remote target = %#v, want %#v", got.RemoteTarget, target)
57 }
58 if got.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {
59 t.Fatalf("environment policy = %d, want daemon-owned", got.EnvironmentPolicy)
60 }
61
62 attachPayload := MarshalAttachTarget(AttachTarget{
63 Endpoint: target.Endpoint,
64 Session: target.SessionName,
65 Intent: IntentAttach,
66 RemoteTarget: target,
67 EnvironmentPolicy: EnvironmentPolicyDaemonOwned,
68 })
69 if attachPayload == nil {
70 t.Fatal("MarshalAttachTarget returned nil")
71 }
72 attach, err := UnmarshalAttachTarget(attachPayload)
73 if err != nil {
74 t.Fatal(err)
75 }
76 if attach.RemoteTarget == nil || *attach.RemoteTarget != *target {
77 t.Fatalf("attach target = %#v, want %#v", attach.RemoteTarget, target)
78 }
79 })
80 }
81}
82
83func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) {
84 target := testRemoteTarget(false)
85 tests := []struct {
86 name string
87 validate func() error
88 marshalBad func() []byte
89 marshalGood func() []byte
90 unmarshal func([]byte) error
91 policyOffset int
92 wantErr error
93 }{
94 {
95 name: "hello",
96 validate: func() error {
97 return ValidateHello(Hello{
98 Version: ProtocolVersion, Intent: IntentAttach,
99 Name: target.SessionName, Size: domain.Size{Cols: 80, Rows: 24},
100 RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyClientOwned,
101 })
102 },
103 marshalBad: func() []byte {
104 return MarshalHello(Hello{
105 Version: ProtocolVersion, Intent: IntentAttach,
106 Name: target.SessionName, Size: domain.Size{Cols: 80, Rows: 24},
107 RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyClientOwned,
108 })
109 },
110 marshalGood: func() []byte {
111 return MarshalHello(Hello{
112 Version: ProtocolVersion, Intent: IntentAttach,
113 Name: target.SessionName, Size: domain.Size{Cols: 80, Rows: 24},
114 RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned,
115 })
116 },
117 unmarshal: func(payload []byte) error { _, err := UnmarshalHello(payload); return err },
118 policyOffset: 8,
119 wantErr: ErrInvalidHello,
120 },
121 {
122 name: "attach target",
123 validate: func() error {
124 return ValidateAttachTarget(AttachTarget{
125 Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach,
126 RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyClientOwned,
127 })
128 },
129 marshalBad: func() []byte {
130 return MarshalAttachTarget(AttachTarget{
131 Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach,
132 RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyClientOwned,
133 })
134 },
135 marshalGood: func() []byte {
136 return MarshalAttachTarget(AttachTarget{
137 Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach,
138 RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned,
139 })
140 },
141 unmarshal: func(payload []byte) error { _, err := UnmarshalAttachTarget(payload); return err },
142 policyOffset: 1,
143 wantErr: ErrInvalidAttachTarget,
144 },
145 }
146
147 for _, tt := range tests {
148 t.Run(tt.name, func(t *testing.T) {
149 if err := tt.validate(); !errors.Is(err, tt.wantErr) {
150 t.Fatalf("direct validation error = %v, want %v", err, tt.wantErr)
151 }
152 if payload := tt.marshalBad(); payload != nil {
153 t.Fatalf("marshal accepted client-owned policy: %x", payload)
154 }
155 payload := tt.marshalGood()
156 if payload == nil {
157 t.Fatal("failed to marshal daemon-owned control payload")
158 }
159 for i := range len(payload) {
160 if err := tt.unmarshal(payload[:i]); err == nil {
161 t.Fatalf("prefix %d unexpectedly decoded", i)
162 }
163 }
164 trailing := append(append([]byte(nil), payload...), 0xff)
165 if err := tt.unmarshal(trailing); err == nil {
166 t.Fatal("trailing garbage unexpectedly decoded")
167 }
168 payload[len(payload)-tt.policyOffset] = byte(EnvironmentPolicyClientOwned)
169 if err := tt.unmarshal(payload); !errors.Is(err, tt.wantErr) {
170 t.Fatalf("mutated policy error = %v, want %v", err, tt.wantErr)
171 }
172 })
173 }
174}
175
176func TestAttachTargetRejectsResumeIntent(t *testing.T) {
177 tests := []AttachTarget{
178 {Endpoint: "host", Session: "work", Intent: IntentResume},
179 {
180 Endpoint: "build@mule:2222",
181 Session: "work",
182 Intent: IntentResume,
183 RemoteTarget: testRemoteTarget(false),
184 EnvironmentPolicy: EnvironmentPolicyDaemonOwned,
185 },
186 }
187 for _, target := range tests {
188 if err := ValidateAttachTarget(target); !errors.Is(err, ErrInvalidAttachTarget) {
189 t.Fatalf("ValidateAttachTarget(%#v) error = %v, want %v", target, err, ErrInvalidAttachTarget)
190 }
191 if got := MarshalAttachTarget(target); got != nil {
192 t.Fatalf("MarshalAttachTarget(%#v) = %x, want nil", target, got)
193 }
194 }
195}
196
197func TestTargetWireIncludesRequiredV27Section(t *testing.T) {
198 want := []byte{0, 4, 'h', 'o', 's', 't', 0, 4, 'w', 'o', 'r', 'k', IntentAttach, 0, 0}
199 got := MarshalAttachTarget(AttachTarget{Endpoint: "host", Session: "work", Intent: IntentAttach})
200 if !bytes.Equal(got, want) {
201 t.Fatalf("target bytes = %x, want %x", got, want)
202 }
203}