|
| 1 | +package cli |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "context" |
| 6 | + "errors" |
| 7 | + "io" |
| 8 | + "reflect" |
| 9 | + "strings" |
| 10 | + "testing" |
| 11 | + |
| 12 | + "github.com/acmore/okdev/internal/kube" |
| 13 | + "github.com/acmore/okdev/internal/workload" |
| 14 | +) |
| 15 | + |
| 16 | +func TestParsePortForwardMappings(t *testing.T) { |
| 17 | + got, err := parsePortForwardMappings([]string{"8080:8080", "9000:9001"}) |
| 18 | + if err != nil { |
| 19 | + t.Fatalf("parsePortForwardMappings: %v", err) |
| 20 | + } |
| 21 | + want := []string{"8080:8080", "9000:9001"} |
| 22 | + if !reflect.DeepEqual(want, got) { |
| 23 | + t.Fatalf("unexpected mappings: got=%v want=%v", got, want) |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +func TestParsePortForwardMappingsRejectsInvalidValues(t *testing.T) { |
| 28 | + for _, tc := range [][]string{ |
| 29 | + nil, |
| 30 | + {}, |
| 31 | + {"8080"}, |
| 32 | + {"abc:8080"}, |
| 33 | + {"8080:def"}, |
| 34 | + {"0:8080"}, |
| 35 | + {"8080:0"}, |
| 36 | + {"8080:8080:8080"}, |
| 37 | + } { |
| 38 | + if _, err := parsePortForwardMappings(tc); err == nil { |
| 39 | + t.Fatalf("expected error for %v", tc) |
| 40 | + } |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +func TestSelectSinglePortForwardPodRejectsAmbiguousMatch(t *testing.T) { |
| 45 | + pods := []kube.PodSummary{ |
| 46 | + {Name: "master-0", Phase: "Running"}, |
| 47 | + {Name: "master-1", Phase: "Running"}, |
| 48 | + } |
| 49 | + _, err := selectSinglePortForwardPod(pods) |
| 50 | + if err == nil || !strings.Contains(err.Error(), "exactly one pod") { |
| 51 | + t.Fatalf("expected ambiguity error, got %v", err) |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +func TestSelectSinglePortForwardPodRejectsEmpty(t *testing.T) { |
| 56 | + _, err := selectSinglePortForwardPod(nil) |
| 57 | + if err == nil || !strings.Contains(err.Error(), "exactly one pod") { |
| 58 | + t.Fatalf("expected empty error, got %v", err) |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +func TestSelectSinglePortForwardPodReturnsSingle(t *testing.T) { |
| 63 | + pods := []kube.PodSummary{{Name: "worker-0", Phase: "Running"}} |
| 64 | + got, err := selectSinglePortForwardPod(pods) |
| 65 | + if err != nil { |
| 66 | + t.Fatalf("selectSinglePortForwardPod: %v", err) |
| 67 | + } |
| 68 | + if got.Name != "worker-0" { |
| 69 | + t.Fatalf("expected worker-0, got %q", got.Name) |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +func TestSplitPortForwardArgs(t *testing.T) { |
| 74 | + for _, tc := range []struct { |
| 75 | + name string |
| 76 | + args []string |
| 77 | + wantSession []string |
| 78 | + wantMappings []string |
| 79 | + }{ |
| 80 | + {"empty", nil, nil, nil}, |
| 81 | + {"mapping only", []string{"8080:8080"}, nil, []string{"8080:8080"}}, |
| 82 | + {"session and mapping", []string{"my-sess", "8080:8080"}, []string{"my-sess"}, []string{"8080:8080"}}, |
| 83 | + {"single non-mapping stays in mappings", []string{"8080"}, nil, []string{"8080"}}, |
| 84 | + {"two mappings", []string{"8080:8080", "9000:9000"}, nil, []string{"8080:8080", "9000:9000"}}, |
| 85 | + } { |
| 86 | + t.Run(tc.name, func(t *testing.T) { |
| 87 | + gotSession, gotMappings := splitPortForwardArgs(tc.args) |
| 88 | + if !reflect.DeepEqual(tc.wantSession, gotSession) { |
| 89 | + t.Fatalf("session: got=%v want=%v", gotSession, tc.wantSession) |
| 90 | + } |
| 91 | + if !reflect.DeepEqual(tc.wantMappings, gotMappings) { |
| 92 | + t.Fatalf("mappings: got=%v want=%v", gotMappings, tc.wantMappings) |
| 93 | + } |
| 94 | + }) |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +type fakePortForwardClient struct { |
| 99 | + forwardedNamespace string |
| 100 | + forwardedPod string |
| 101 | + forwardedMappings []string |
| 102 | +} |
| 103 | + |
| 104 | +func (f *fakePortForwardClient) PortForward(ctx context.Context, namespace, pod string, forwards []string, stdout io.Writer, stderr io.Writer) error { |
| 105 | + f.forwardedNamespace = namespace |
| 106 | + f.forwardedPod = pod |
| 107 | + f.forwardedMappings = append([]string(nil), forwards...) |
| 108 | + return context.Canceled |
| 109 | +} |
| 110 | + |
| 111 | +func TestRunPortForwardUsesDirectKubePortForward(t *testing.T) { |
| 112 | + client := &fakePortForwardClient{} |
| 113 | + target := workload.TargetRef{PodName: "okdev-sess-master-0"} |
| 114 | + var out bytes.Buffer |
| 115 | + err := runPortForward(context.Background(), client, "demo", target, []string{"8080:8080"}, &out) |
| 116 | + if !errors.Is(err, context.Canceled) { |
| 117 | + t.Fatalf("expected context-canceled passthrough, got %v", err) |
| 118 | + } |
| 119 | + if client.forwardedNamespace != "demo" || client.forwardedPod != "okdev-sess-master-0" { |
| 120 | + t.Fatalf("unexpected forwarded target: ns=%q pod=%q", client.forwardedNamespace, client.forwardedPod) |
| 121 | + } |
| 122 | + wantMappings := []string{"8080:8080"} |
| 123 | + if !reflect.DeepEqual(wantMappings, client.forwardedMappings) { |
| 124 | + t.Fatalf("unexpected mappings: got=%v want=%v", client.forwardedMappings, wantMappings) |
| 125 | + } |
| 126 | + if !strings.Contains(out.String(), "Forwarding to pod=okdev-sess-master-0") { |
| 127 | + t.Fatalf("expected startup message, got %q", out.String()) |
| 128 | + } |
| 129 | +} |
| 130 | + |
| 131 | +func TestPortForwardCommandRejectsPodAndRoleTogether(t *testing.T) { |
| 132 | + cmd := newPortForwardCmd(&Options{}) |
| 133 | + cmd.SetArgs([]string{"--pod", "a", "--role", "worker", "8080:8080"}) |
| 134 | + err := cmd.Execute() |
| 135 | + if err == nil || !strings.Contains(err.Error(), "mutually exclusive") { |
| 136 | + t.Fatalf("expected mutual exclusion error, got %v", err) |
| 137 | + } |
| 138 | +} |
| 139 | + |
| 140 | +func TestPortForwardCommandRejectsMissingMappings(t *testing.T) { |
| 141 | + cmd := newPortForwardCmd(&Options{}) |
| 142 | + cmd.SetArgs(nil) |
| 143 | + err := cmd.Execute() |
| 144 | + if err == nil || !strings.Contains(err.Error(), "requires at least one") { |
| 145 | + t.Fatalf("expected missing mapping error, got %v", err) |
| 146 | + } |
| 147 | +} |
0 commit comments