1
16
17 package portforward
18
19 import (
20 "context"
21 "fmt"
22 "net/http"
23 "net/url"
24 "reflect"
25 "testing"
26
27 "github.com/spf13/cobra"
28
29 corev1 "k8s.io/api/core/v1"
30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/apimachinery/pkg/runtime/schema"
32 "k8s.io/apimachinery/pkg/util/intstr"
33 "k8s.io/cli-runtime/pkg/genericiooptions"
34 "k8s.io/client-go/rest/fake"
35 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
36 "k8s.io/kubectl/pkg/scheme"
37 )
38
39 type fakePortForwarder struct {
40 method string
41 url *url.URL
42 pfErr error
43 }
44
45 func (f *fakePortForwarder) ForwardPorts(method string, url *url.URL, opts PortForwardOptions) error {
46 f.method = method
47 f.url = url
48 return f.pfErr
49 }
50
51 func testPortForward(t *testing.T, flags map[string]string, args []string) {
52 version := "v1"
53
54 tests := []struct {
55 name string
56 podPath, pfPath string
57 pod *corev1.Pod
58 pfErr bool
59 }{
60 {
61 name: "pod portforward",
62 podPath: "/api/" + version + "/namespaces/test/pods/foo",
63 pfPath: "/api/" + version + "/namespaces/test/pods/foo/portforward",
64 pod: execPod(),
65 },
66 {
67 name: "pod portforward error",
68 podPath: "/api/" + version + "/namespaces/test/pods/foo",
69 pfPath: "/api/" + version + "/namespaces/test/pods/foo/portforward",
70 pod: execPod(),
71 pfErr: true,
72 },
73 }
74 for _, test := range tests {
75 t.Run(test.name, func(t *testing.T) {
76 var err error
77 tf := cmdtesting.NewTestFactory().WithNamespace("test")
78 defer tf.Cleanup()
79
80 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
81 ns := scheme.Codecs.WithoutConversion()
82
83 tf.Client = &fake.RESTClient{
84 VersionedAPIPath: "/api/v1",
85 GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
86 NegotiatedSerializer: ns,
87 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
88 switch p, m := req.URL.Path, req.Method; {
89 case p == test.podPath && m == "GET":
90 body := cmdtesting.ObjBody(codec, test.pod)
91 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
92 default:
93 t.Errorf("%s: unexpected request: %#v\n%#v", test.name, req.URL, req)
94 return nil, nil
95 }
96 }),
97 }
98 tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
99 ff := &fakePortForwarder{}
100 if test.pfErr {
101 ff.pfErr = fmt.Errorf("pf error")
102 }
103
104 opts := &PortForwardOptions{}
105 ctx, cancel := context.WithCancel(context.Background())
106 defer cancel()
107 cmd := NewCmdPortForward(tf, genericiooptions.NewTestIOStreamsDiscard())
108 cmd.Run = func(cmd *cobra.Command, args []string) {
109 if err = opts.Complete(tf, cmd, args); err != nil {
110 return
111 }
112 opts.PortForwarder = ff
113 if err = opts.Validate(); err != nil {
114 return
115 }
116 err = opts.RunPortForwardContext(ctx)
117 }
118
119 for name, value := range flags {
120 cmd.Flags().Set(name, value)
121 }
122 cmd.Run(cmd, args)
123
124 if test.pfErr && err != ff.pfErr {
125 t.Errorf("%s: Unexpected port-forward error: %v", test.name, err)
126 }
127 if !test.pfErr && err != nil {
128 t.Errorf("%s: Unexpected error: %v", test.name, err)
129 }
130 if test.pfErr {
131 return
132 }
133
134 if ff.url == nil || ff.url.Path != test.pfPath {
135 t.Errorf("%s: Did not get expected path for portforward request", test.name)
136 }
137 if ff.method != "POST" {
138 t.Errorf("%s: Did not get method for attach request: %s", test.name, ff.method)
139 }
140 })
141 }
142 }
143
144 func TestPortForward(t *testing.T) {
145 testPortForward(t, nil, []string{"foo", ":5000", ":1000"})
146 }
147
148 func TestTranslateServicePortToTargetPort(t *testing.T) {
149 cases := []struct {
150 name string
151 svc corev1.Service
152 pod corev1.Pod
153 ports []string
154 translated []string
155 err bool
156 }{
157 {
158 name: "test success 1 (int port)",
159 svc: corev1.Service{
160 Spec: corev1.ServiceSpec{
161 Ports: []corev1.ServicePort{
162 {
163 Port: 80,
164 TargetPort: intstr.FromInt32(8080),
165 },
166 },
167 },
168 },
169 pod: corev1.Pod{
170 Spec: corev1.PodSpec{
171 Containers: []corev1.Container{
172 {
173 Ports: []corev1.ContainerPort{
174 {
175 Name: "http",
176 ContainerPort: int32(8080)},
177 },
178 },
179 },
180 },
181 },
182 ports: []string{"80"},
183 translated: []string{"80:8080"},
184 err: false,
185 },
186 {
187 name: "test success 1 (int port with random local port)",
188 svc: corev1.Service{
189 Spec: corev1.ServiceSpec{
190 Ports: []corev1.ServicePort{
191 {
192 Port: 80,
193 TargetPort: intstr.FromInt32(8080),
194 },
195 },
196 },
197 },
198 pod: corev1.Pod{
199 Spec: corev1.PodSpec{
200 Containers: []corev1.Container{
201 {
202 Ports: []corev1.ContainerPort{
203 {
204 Name: "http",
205 ContainerPort: int32(8080)},
206 },
207 },
208 },
209 },
210 },
211 ports: []string{":80"},
212 translated: []string{":8080"},
213 err: false,
214 },
215 {
216 name: "test success 1 (int port with explicit local port)",
217 svc: corev1.Service{
218 Spec: corev1.ServiceSpec{
219 Ports: []corev1.ServicePort{
220 {
221 Port: 8080,
222 TargetPort: intstr.FromInt32(8080),
223 },
224 },
225 },
226 },
227 pod: corev1.Pod{
228 Spec: corev1.PodSpec{
229 Containers: []corev1.Container{
230 {
231 Ports: []corev1.ContainerPort{
232 {
233 Name: "http",
234 ContainerPort: int32(8080)},
235 },
236 },
237 },
238 },
239 },
240 ports: []string{"8000:8080"},
241 translated: []string{"8000:8080"},
242 err: false,
243 },
244 {
245 name: "test success 2 (clusterIP: None)",
246 svc: corev1.Service{
247 Spec: corev1.ServiceSpec{
248 ClusterIP: "None",
249 Ports: []corev1.ServicePort{
250 {
251 Port: 80,
252 TargetPort: intstr.FromInt32(8080),
253 },
254 },
255 },
256 },
257 pod: corev1.Pod{
258 Spec: corev1.PodSpec{
259 Containers: []corev1.Container{
260 {
261 Ports: []corev1.ContainerPort{
262 {
263 Name: "http",
264 ContainerPort: int32(8080)},
265 },
266 },
267 },
268 },
269 },
270 ports: []string{"80"},
271 translated: []string{"80"},
272 err: false,
273 },
274 {
275 name: "test success 2 (clusterIP: None with random local port)",
276 svc: corev1.Service{
277 Spec: corev1.ServiceSpec{
278 ClusterIP: "None",
279 Ports: []corev1.ServicePort{
280 {
281 Port: 80,
282 TargetPort: intstr.FromInt32(8080),
283 },
284 },
285 },
286 },
287 pod: corev1.Pod{
288 Spec: corev1.PodSpec{
289 Containers: []corev1.Container{
290 {
291 Ports: []corev1.ContainerPort{
292 {
293 Name: "http",
294 ContainerPort: int32(8080)},
295 },
296 },
297 },
298 },
299 },
300 ports: []string{":80"},
301 translated: []string{":80"},
302 err: false,
303 },
304 {
305 name: "test success 3 (named target port)",
306 svc: corev1.Service{
307 Spec: corev1.ServiceSpec{
308 Ports: []corev1.ServicePort{
309 {
310 Port: 80,
311 TargetPort: intstr.FromString("http"),
312 },
313 {
314 Port: 443,
315 TargetPort: intstr.FromString("https"),
316 },
317 },
318 },
319 },
320 pod: corev1.Pod{
321 Spec: corev1.PodSpec{
322 Containers: []corev1.Container{
323 {
324 Ports: []corev1.ContainerPort{
325 {
326 Name: "http",
327 ContainerPort: int32(8080)},
328 {
329 Name: "https",
330 ContainerPort: int32(8443)},
331 },
332 },
333 },
334 },
335 },
336 ports: []string{"80", "443"},
337 translated: []string{"80:8080", "443:8443"},
338 err: false,
339 },
340 {
341 name: "test success 3 (named target port with random local port)",
342 svc: corev1.Service{
343 Spec: corev1.ServiceSpec{
344 Ports: []corev1.ServicePort{
345 {
346 Port: 80,
347 TargetPort: intstr.FromString("http"),
348 },
349 {
350 Port: 443,
351 TargetPort: intstr.FromString("https"),
352 },
353 },
354 },
355 },
356 pod: corev1.Pod{
357 Spec: corev1.PodSpec{
358 Containers: []corev1.Container{
359 {
360 Ports: []corev1.ContainerPort{
361 {
362 Name: "http",
363 ContainerPort: int32(8080)},
364 {
365 Name: "https",
366 ContainerPort: int32(8443)},
367 },
368 },
369 },
370 },
371 },
372 ports: []string{":80", ":443"},
373 translated: []string{":8080", ":8443"},
374 err: false,
375 },
376 {
377 name: "test success 4 (named service port)",
378 svc: corev1.Service{
379 Spec: corev1.ServiceSpec{
380 Ports: []corev1.ServicePort{
381 {
382 Port: 80,
383 Name: "http",
384 TargetPort: intstr.FromInt32(8080),
385 },
386 {
387 Port: 443,
388 Name: "https",
389 TargetPort: intstr.FromInt32(8443),
390 },
391 },
392 },
393 },
394 pod: corev1.Pod{
395 Spec: corev1.PodSpec{
396 Containers: []corev1.Container{
397 {
398 Ports: []corev1.ContainerPort{
399 {
400 ContainerPort: int32(8080)},
401 {
402 ContainerPort: int32(8443)},
403 },
404 },
405 },
406 },
407 },
408 ports: []string{"http", "https"},
409 translated: []string{"80:8080", "443:8443"},
410 err: false,
411 },
412 {
413 name: "test success 4 (named service port with random local port)",
414 svc: corev1.Service{
415 Spec: corev1.ServiceSpec{
416 Ports: []corev1.ServicePort{
417 {
418 Port: 80,
419 Name: "http",
420 TargetPort: intstr.FromInt32(8080),
421 },
422 {
423 Port: 443,
424 Name: "https",
425 TargetPort: intstr.FromInt32(8443),
426 },
427 },
428 },
429 },
430 pod: corev1.Pod{
431 Spec: corev1.PodSpec{
432 Containers: []corev1.Container{
433 {
434 Ports: []corev1.ContainerPort{
435 {
436 ContainerPort: int32(8080)},
437 {
438 ContainerPort: int32(8443)},
439 },
440 },
441 },
442 },
443 },
444 ports: []string{":http", ":https"},
445 translated: []string{":8080", ":8443"},
446 err: false,
447 },
448 {
449 name: "test success 4 (named service port and named pod container port)",
450 svc: corev1.Service{
451 Spec: corev1.ServiceSpec{
452 Ports: []corev1.ServicePort{
453 {
454 Port: 80,
455 Name: "http",
456 TargetPort: intstr.FromString("http"),
457 },
458 },
459 },
460 },
461 pod: corev1.Pod{
462 Spec: corev1.PodSpec{
463 Containers: []corev1.Container{
464 {
465 Ports: []corev1.ContainerPort{
466 {
467 Name: "http",
468 ContainerPort: int32(80)},
469 },
470 },
471 },
472 },
473 },
474 ports: []string{"http"},
475 translated: []string{"80"},
476 err: false,
477 },
478 {
479 name: "test success (targetPort omitted)",
480 svc: corev1.Service{
481 Spec: corev1.ServiceSpec{
482 Ports: []corev1.ServicePort{
483 {
484 Port: 80,
485 },
486 },
487 },
488 },
489 pod: corev1.Pod{
490 Spec: corev1.PodSpec{
491 Containers: []corev1.Container{
492 {
493 Ports: []corev1.ContainerPort{
494 {
495 Name: "http",
496 ContainerPort: int32(80)},
497 },
498 },
499 },
500 },
501 },
502 ports: []string{"80"},
503 translated: []string{"80"},
504 err: false,
505 },
506 {
507 name: "test success (targetPort omitted with random local port)",
508 svc: corev1.Service{
509 Spec: corev1.ServiceSpec{
510 Ports: []corev1.ServicePort{
511 {
512 Port: 80,
513 },
514 },
515 },
516 },
517 pod: corev1.Pod{
518 Spec: corev1.PodSpec{
519 Containers: []corev1.Container{
520 {
521 Ports: []corev1.ContainerPort{
522 {
523 Name: "http",
524 ContainerPort: int32(80)},
525 },
526 },
527 },
528 },
529 },
530 ports: []string{":80"},
531 translated: []string{":80"},
532 err: false,
533 },
534 {
535 name: "test failure 1 (named target port lookup failure)",
536 svc: corev1.Service{
537 Spec: corev1.ServiceSpec{
538 Ports: []corev1.ServicePort{
539 {
540 Port: 80,
541 TargetPort: intstr.FromString("http"),
542 },
543 },
544 },
545 },
546 pod: corev1.Pod{
547 Spec: corev1.PodSpec{
548 Containers: []corev1.Container{
549 {
550 Ports: []corev1.ContainerPort{
551 {
552 Name: "https",
553 ContainerPort: int32(443)},
554 },
555 },
556 },
557 },
558 },
559 ports: []string{"80"},
560 translated: []string{},
561 err: true,
562 },
563 {
564 name: "test failure 1 (named service port lookup failure)",
565 svc: corev1.Service{
566 Spec: corev1.ServiceSpec{
567 Ports: []corev1.ServicePort{
568 {
569 Port: 80,
570 TargetPort: intstr.FromString("http"),
571 },
572 },
573 },
574 },
575 pod: corev1.Pod{
576 Spec: corev1.PodSpec{
577 Containers: []corev1.Container{
578 {
579 Ports: []corev1.ContainerPort{
580 {
581 Name: "http",
582 ContainerPort: int32(8080)},
583 },
584 },
585 },
586 },
587 },
588 ports: []string{"https"},
589 translated: []string{},
590 err: true,
591 },
592 {
593 name: "test failure 2 (service port not declared)",
594 svc: corev1.Service{
595 Spec: corev1.ServiceSpec{
596 Ports: []corev1.ServicePort{
597 {
598 Port: 80,
599 TargetPort: intstr.FromString("http"),
600 },
601 },
602 },
603 },
604 pod: corev1.Pod{
605 Spec: corev1.PodSpec{
606 Containers: []corev1.Container{
607 {
608 Ports: []corev1.ContainerPort{
609 {
610 Name: "https",
611 ContainerPort: int32(443)},
612 },
613 },
614 },
615 },
616 },
617 ports: []string{"443"},
618 translated: []string{},
619 err: true,
620 },
621 }
622
623 for _, tc := range cases {
624 translated, err := translateServicePortToTargetPort(tc.ports, tc.svc, tc.pod)
625 if err != nil {
626 if tc.err {
627 continue
628 }
629
630 t.Errorf("%v: unexpected error: %v", tc.name, err)
631 continue
632 }
633
634 if tc.err {
635 t.Errorf("%v: unexpected success", tc.name)
636 continue
637 }
638
639 if !reflect.DeepEqual(translated, tc.translated) {
640 t.Errorf("%v: expected %v; got %v", tc.name, tc.translated, translated)
641 }
642 }
643 }
644
645 func execPod() *corev1.Pod {
646 return &corev1.Pod{
647 ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"},
648 Spec: corev1.PodSpec{
649 RestartPolicy: corev1.RestartPolicyAlways,
650 DNSPolicy: corev1.DNSClusterFirst,
651 Containers: []corev1.Container{
652 {
653 Name: "bar",
654 },
655 },
656 },
657 Status: corev1.PodStatus{
658 Phase: corev1.PodRunning,
659 },
660 }
661 }
662
663 func TestConvertPodNamedPortToNumber(t *testing.T) {
664 cases := []struct {
665 name string
666 pod corev1.Pod
667 ports []string
668 converted []string
669 err bool
670 }{
671 {
672 name: "port number without local port",
673 pod: corev1.Pod{
674 Spec: corev1.PodSpec{
675 Containers: []corev1.Container{
676 {
677 Ports: []corev1.ContainerPort{
678 {
679 Name: "http",
680 ContainerPort: int32(80)},
681 },
682 },
683 },
684 },
685 },
686 ports: []string{"80"},
687 converted: []string{"80"},
688 err: false,
689 },
690 {
691 name: "port number with local port",
692 pod: corev1.Pod{
693 Spec: corev1.PodSpec{
694 Containers: []corev1.Container{
695 {
696 Ports: []corev1.ContainerPort{
697 {
698 Name: "http",
699 ContainerPort: int32(80)},
700 },
701 },
702 },
703 },
704 },
705 ports: []string{"8000:80"},
706 converted: []string{"8000:80"},
707 err: false,
708 },
709 {
710 name: "port number with random local port",
711 pod: corev1.Pod{
712 Spec: corev1.PodSpec{
713 Containers: []corev1.Container{
714 {
715 Ports: []corev1.ContainerPort{
716 {
717 Name: "http",
718 ContainerPort: int32(80)},
719 },
720 },
721 },
722 },
723 },
724 ports: []string{":80"},
725 converted: []string{":80"},
726 err: false,
727 },
728 {
729 name: "named port without local port",
730 pod: corev1.Pod{
731 Spec: corev1.PodSpec{
732 Containers: []corev1.Container{
733 {
734 Ports: []corev1.ContainerPort{
735 {
736 Name: "http",
737 ContainerPort: int32(80)},
738 },
739 },
740 },
741 },
742 },
743 ports: []string{"http"},
744 converted: []string{"80"},
745 err: false,
746 },
747 {
748 name: "named port with local port",
749 pod: corev1.Pod{
750 Spec: corev1.PodSpec{
751 Containers: []corev1.Container{
752 {
753 Ports: []corev1.ContainerPort{
754 {
755 Name: "http",
756 ContainerPort: int32(80)},
757 },
758 },
759 },
760 },
761 },
762 ports: []string{"8000:http"},
763 converted: []string{"8000:80"},
764 err: false,
765 },
766 {
767 name: "named port with random local port",
768 pod: corev1.Pod{
769 Spec: corev1.PodSpec{
770 Containers: []corev1.Container{
771 {
772 Ports: []corev1.ContainerPort{
773 {
774 Name: "http",
775 ContainerPort: int32(80)},
776 },
777 },
778 },
779 },
780 },
781 ports: []string{":http"},
782 converted: []string{":80"},
783 err: false,
784 },
785 {
786 name: "named port can not be found",
787 pod: corev1.Pod{
788 Spec: corev1.PodSpec{
789 Containers: []corev1.Container{
790 {
791 Ports: []corev1.ContainerPort{
792 {
793 Name: "https",
794 ContainerPort: int32(443)},
795 },
796 },
797 },
798 },
799 },
800 ports: []string{"http"},
801 err: true,
802 },
803 {
804 name: "one of the requested named ports can not be found",
805 pod: corev1.Pod{
806 Spec: corev1.PodSpec{
807 Containers: []corev1.Container{
808 {
809 Ports: []corev1.ContainerPort{
810 {
811 Name: "https",
812 ContainerPort: int32(443)},
813 },
814 },
815 },
816 },
817 },
818 ports: []string{"https", "http"},
819 err: true,
820 },
821 }
822
823 for _, tc := range cases {
824 converted, err := convertPodNamedPortToNumber(tc.ports, tc.pod)
825 if err != nil {
826 if tc.err {
827 continue
828 }
829
830 t.Errorf("%v: unexpected error: %v", tc.name, err)
831 continue
832 }
833
834 if tc.err {
835 t.Errorf("%v: unexpected success", tc.name)
836 continue
837 }
838
839 if !reflect.DeepEqual(converted, tc.converted) {
840 t.Errorf("%v: expected %v; got %v", tc.name, tc.converted, converted)
841 }
842 }
843 }
844
845 func TestCheckUDPPort(t *testing.T) {
846 tests := []struct {
847 name string
848 pod *corev1.Pod
849 service *corev1.Service
850 ports []string
851 expectError bool
852 }{
853 {
854 name: "forward to a UDP port in a Pod",
855 pod: &corev1.Pod{
856 Spec: corev1.PodSpec{
857 Containers: []corev1.Container{
858 {
859 Ports: []corev1.ContainerPort{
860 {Protocol: corev1.ProtocolUDP, ContainerPort: 53},
861 },
862 },
863 },
864 },
865 },
866 ports: []string{"53"},
867 expectError: true,
868 },
869 {
870 name: "forward to a named UDP port in a Pod",
871 pod: &corev1.Pod{
872 Spec: corev1.PodSpec{
873 Containers: []corev1.Container{
874 {
875 Ports: []corev1.ContainerPort{
876 {Protocol: corev1.ProtocolUDP, ContainerPort: 53, Name: "dns"},
877 },
878 },
879 },
880 },
881 },
882 ports: []string{"dns"},
883 expectError: true,
884 },
885 {
886 name: "Pod has ports with both TCP and UDP protocol (UDP first)",
887 pod: &corev1.Pod{
888 Spec: corev1.PodSpec{
889 Containers: []corev1.Container{
890 {
891 Ports: []corev1.ContainerPort{
892 {Protocol: corev1.ProtocolUDP, ContainerPort: 53},
893 {Protocol: corev1.ProtocolTCP, ContainerPort: 53},
894 },
895 },
896 },
897 },
898 },
899 ports: []string{":53"},
900 },
901 {
902 name: "Pod has ports with both TCP and UDP protocol (TCP first)",
903 pod: &corev1.Pod{
904 Spec: corev1.PodSpec{
905 Containers: []corev1.Container{
906 {
907 Ports: []corev1.ContainerPort{
908 {Protocol: corev1.ProtocolTCP, ContainerPort: 53},
909 {Protocol: corev1.ProtocolUDP, ContainerPort: 53},
910 },
911 },
912 },
913 },
914 },
915 ports: []string{":53"},
916 },
917
918 {
919 name: "forward to a UDP port in a Service",
920 service: &corev1.Service{
921 Spec: corev1.ServiceSpec{
922 Ports: []corev1.ServicePort{
923 {Protocol: corev1.ProtocolUDP, Port: 53},
924 },
925 },
926 },
927 ports: []string{"53"},
928 expectError: true,
929 },
930 {
931 name: "forward to a named UDP port in a Service",
932 service: &corev1.Service{
933 Spec: corev1.ServiceSpec{
934 Ports: []corev1.ServicePort{
935 {Protocol: corev1.ProtocolUDP, Port: 53, Name: "dns"},
936 },
937 },
938 },
939 ports: []string{"10053:dns"},
940 expectError: true,
941 },
942 {
943 name: "Service has ports with both TCP and UDP protocol (UDP first)",
944 service: &corev1.Service{
945 Spec: corev1.ServiceSpec{
946 Ports: []corev1.ServicePort{
947 {Protocol: corev1.ProtocolUDP, Port: 53},
948 {Protocol: corev1.ProtocolTCP, Port: 53},
949 },
950 },
951 },
952 ports: []string{"53"},
953 },
954 {
955 name: "Service has ports with both TCP and UDP protocol (TCP first)",
956 service: &corev1.Service{
957 Spec: corev1.ServiceSpec{
958 Ports: []corev1.ServicePort{
959 {Protocol: corev1.ProtocolTCP, Port: 53},
960 {Protocol: corev1.ProtocolUDP, Port: 53},
961 },
962 },
963 },
964 ports: []string{"53"},
965 },
966 }
967 for _, tc := range tests {
968 var err error
969 if tc.pod != nil {
970 err = checkUDPPortInPod(tc.ports, tc.pod)
971 } else if tc.service != nil {
972 err = checkUDPPortInService(tc.ports, tc.service)
973 }
974 if err != nil {
975 if tc.expectError {
976 continue
977 }
978 t.Errorf("%v: unexpected error: %v", tc.name, err)
979 continue
980 }
981 if tc.expectError {
982 t.Errorf("%v: unexpected success", tc.name)
983 }
984 }
985 }
986
View as plain text