File: tcp_test.go

package info (click to toggle)
wait4x 3.6.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 744 kB
  • sloc: makefile: 248; sh: 13
file content (449 lines) | stat: -rw-r--r-- 15,699 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
// Copyright 2019-2025 The Wait4X Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package cmd provides the command-line interface for the Wait4X application.
package cmd

import (
	"context"
	"net"
	"strconv"
	"testing"
	"time"

	"github.com/spf13/cobra"
	"github.com/stretchr/testify/suite"
	"wait4x.dev/v3/internal/test"
)

// TCPCommandSuite is a test suite for TCP command functionality
type TCPCommandSuite struct {
	suite.Suite

	// Shared resources for the test suite
	rootCmd    *cobra.Command
	tcpCmd     *cobra.Command
	listener   net.Listener
	port       int
	unusedPort int
	serverDone chan struct{}
}

// SetupSuite sets up test suite resources
func (s *TCPCommandSuite) SetupSuite() {
	// Set up a TCP server for tests that need an active connection
	var err error
	s.listener, err = net.Listen("tcp", "127.0.0.1:0")
	s.Require().NoError(err)

	// Parse the port
	_, portStr, err := net.SplitHostPort(s.listener.Addr().String())
	s.Require().NoError(err)
	s.port, err = strconv.Atoi(portStr)
	s.Require().NoError(err)

	// Find an unused port for connection refused tests
	s.unusedPort = s.port + 1

	// Set up a channel to track server completion
	s.serverDone = make(chan struct{})

	// Handle connections in a goroutine
	go func() {
		defer close(s.serverDone)
		for {
			conn, err := s.listener.Accept()
			if err != nil {
				return // listener closed
			}

			if conn != nil {
				s.Require().NoError(conn.Close())
			}
		}
	}()
}

// TearDownSuite tears down test suite resources
func (s *TCPCommandSuite) TearDownSuite() {
	// Close listener
	if s.listener != nil {
		s.Require().NoError(s.listener.Close())
		<-s.serverDone // Wait for server goroutine to complete
	}
}

// SetupTest sets up each test
func (s *TCPCommandSuite) SetupTest() {
	s.rootCmd = NewRootCommand()
	s.tcpCmd = NewTCPCommand()
	s.rootCmd.AddCommand(s.tcpCmd)
}

// TestNewTCPCommand tests the TCP command creation
func (s *TCPCommandSuite) TestNewTCPCommand() {
	cmd := NewTCPCommand()

	s.Equal("tcp ADDRESS... [flags] [-- command [args...]]", cmd.Use)
	s.Equal("Check TCP connection", cmd.Short)
	s.NotNil(cmd.Example)
	s.Contains(cmd.Example, "wait4x tcp 127.0.0.1:9090")

	// Test that the command has the expected flags
	flags := cmd.Flags()
	connectionTimeout, err := flags.GetDuration("connection-timeout")
	s.NoError(err)
	s.Equal(3*time.Second, connectionTimeout) // Default from tcp package
}

// TestTCPCommandInvalidArgument tests the TCP command with invalid arguments
func (s *TCPCommandSuite) TestTCPCommandInvalidArgument() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp")
	s.Error(err)
	s.Equal("ADDRESS is required argument for the tcp command", err.Error())
}

// TestTCPCommandEmptyArgs tests the TCP command with empty arguments
func (s *TCPCommandSuite) TestTCPCommandEmptyArgs() {
	err := s.tcpCmd.Args(s.tcpCmd, []string{})
	s.Error(err)
	s.Equal("ADDRESS is required argument for the tcp command", err.Error())
}

// TestTCPCommandValidArgs tests the TCP command with valid arguments
func (s *TCPCommandSuite) TestTCPCommandValidArgs() {
	err := s.tcpCmd.Args(s.tcpCmd, []string{"127.0.0.1:8080"})
	s.NoError(err)

	err = s.tcpCmd.Args(s.tcpCmd, []string{"127.0.0.1:8080", "192.168.1.1:9090"})
	s.NoError(err)
}

// TestTCPConnectionSuccess tests the TCP connection success
func (s *TCPCommandSuite) TestTCPConnectionSuccess() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53")
	s.NoError(err)
}

// TestTCPConnectionSuccessLocal tests the TCP connection success to local server
func (s *TCPCommandSuite) TestTCPConnectionSuccessLocal() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", s.listener.Addr().String())
	s.NoError(err)
}

// TestTCPConnectionFail tests the TCP connection failure
func (s *TCPCommandSuite) TestTCPConnectionFail() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "127.0.0.1:8080", "-t", "2s")
	s.Error(err)
	s.Equal(context.DeadlineExceeded, err)
}

// TestTCPConnectionFailUnusedPort tests the TCP connection failure on unused port
func (s *TCPCommandSuite) TestTCPConnectionFailUnusedPort() {
	address := net.JoinHostPort("127.0.0.1", strconv.Itoa(s.unusedPort))
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", address, "-t", "2s")
	s.Error(err)
	s.Equal(context.DeadlineExceeded, err)
}

// TestTCPConnectionTimeout tests the TCP connection timeout behavior
func (s *TCPCommandSuite) TestTCPConnectionTimeout() {
	// Use a black-hole IP that will cause timeout
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "240.0.0.1:12345", "-t", "1s")
	s.Error(err)
	s.Equal(context.DeadlineExceeded, err)
}

// TestTCPConnectionWithCustomTimeout tests the TCP connection with custom timeout
func (s *TCPCommandSuite) TestTCPConnectionWithCustomTimeout() {
	// Test with a very short connection timeout
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "240.0.0.1:12345", "--connection-timeout", "100ms", "-t", "2s")
	s.Error(err)
	s.Equal(context.DeadlineExceeded, err)
}

// TestTCPConnectionWithInvalidTimeout tests the TCP connection with invalid timeout
func (s *TCPCommandSuite) TestTCPConnectionWithInvalidTimeout() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "127.0.0.1:8080", "--connection-timeout", "invalid")
	s.Error(err)
	s.Contains(err.Error(), "invalid argument \"invalid\" for \"--connection-timeout\" flag")
}

// TestTCPMultipleAddresses tests the TCP command with multiple addresses
func (s *TCPCommandSuite) TestTCPMultipleAddresses() {
	// Test with multiple valid addresses
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "8.8.8.8:53")
	s.NoError(err)
}

// TestTCPMultipleAddressesMixed tests the TCP command with mixed valid/invalid addresses
func (s *TCPCommandSuite) TestTCPMultipleAddressesMixed() {
	// One valid, one invalid - should fail
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "127.0.0.1:8080", "-t", "2s")
	s.Error(err)
	s.Equal(context.DeadlineExceeded, err)
}

// TestTCPCommandWithDash tests the TCP command with dash separator for command execution
func (s *TCPCommandSuite) TestTCPCommandWithDash() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--", "echo", "success")
	s.NoError(err)
}

// TestTCPCommandWithInvertCheck tests the TCP command with invert check flag
func (s *TCPCommandSuite) TestTCPCommandWithInvertCheck() {
	// With invert check, we expect the command to fail when connection succeeds
	// Use a shorter timeout to make the test fail faster
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "-v", "-t", "1s")
	s.Error(err)
	s.Equal(context.DeadlineExceeded, err)
}

// TestTCPCommandWithInvertCheckFail tests the TCP command with invert check when connection fails
func (s *TCPCommandSuite) TestTCPCommandWithInvertCheckFail() {
	// With invert check, we expect the command to succeed when connection fails
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "127.0.0.1:8080", "-v", "-t", "2s")
	s.NoError(err)
}

// TestTCPCommandWithInterval tests the TCP command with custom interval
func (s *TCPCommandSuite) TestTCPCommandWithInterval() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "-i", "500ms")
	s.NoError(err)
}

// TestTCPCommandWithBackoffPolicy tests the TCP command with different backoff policies
func (s *TCPCommandSuite) TestTCPCommandWithBackoffPolicy() {
	// Test with exponential backoff
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--backoff-policy", "exponential")
	s.NoError(err)

	// Test with linear backoff
	_, err = test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--backoff-policy", "linear")
	s.NoError(err)
}

// TestTCPCommandWithInvalidBackoffPolicy tests the TCP command with invalid backoff policy
func (s *TCPCommandSuite) TestTCPCommandWithInvalidBackoffPolicy() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--backoff-policy", "invalid")
	s.Error(err)
	s.Contains(err.Error(), "--backoff-policy must be one of")
}

// TestTCPCommandWithBackoffCoefficient tests the TCP command with custom backoff coefficient
func (s *TCPCommandSuite) TestTCPCommandWithBackoffCoefficient() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--backoff-exponential-coefficient", "1.5")
	s.NoError(err)
}

// TestTCPCommandWithBackoffMaxInterval tests the TCP command with custom backoff max interval
func (s *TCPCommandSuite) TestTCPCommandWithBackoffMaxInterval() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--backoff-exponential-max-interval", "3s")
	s.NoError(err)
}

// TestTCPCommandWithInvalidBackoffMaxInterval tests the TCP command with invalid backoff max interval
func (s *TCPCommandSuite) TestTCPCommandWithInvalidBackoffMaxInterval() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--backoff-policy", "exponential", "--backoff-exponential-max-interval", "100ms", "-i", "200ms")
	s.Error(err)
	s.Contains(err.Error(), "--backoff-exponential-max-interval must be greater than --interval")
}

// TestTCPCommandWithQuietMode tests the TCP command with quiet mode
func (s *TCPCommandSuite) TestTCPCommandWithQuietMode() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "-q")
	s.NoError(err)
}

// TestTCPCommandWithNoColor tests the TCP command with no color flag
func (s *TCPCommandSuite) TestTCPCommandWithNoColor() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "--no-color")
	s.NoError(err)
}

// TestTCPCommandWithZeroTimeout tests the TCP command with zero timeout (unlimited)
func (s *TCPCommandSuite) TestTCPCommandWithZeroTimeout() {
	// This should work but take longer, so we'll use a short timeout for the test
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53", "-t", "0s")
	s.NoError(err)
}

// TestTCPCommandWithInvalidAddressFormat tests the TCP command with invalid address format
func (s *TCPCommandSuite) TestTCPCommandWithInvalidAddressFormat() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "invalid-address", "-t", "2s")
	s.Error(err)
	// The error should be either a connection error or timeout
	if err == context.DeadlineExceeded {
		// Timeout is acceptable for invalid addresses
		s.Equal(context.DeadlineExceeded, err)
	} else {
		// Or it should be a connection error
		s.Contains(err.Error(), "failed to establish a tcp connection")
	}
}

// TestTCPCommandWithIPv6Address tests the TCP command with IPv6 address
func (s *TCPCommandSuite) TestTCPCommandWithIPv6Address() {
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "[::1]:53", "-t", "2s")
	// This might fail if IPv6 is not available, but should not crash
	if err != nil {
		if err == context.DeadlineExceeded {
			// Timeout is acceptable for IPv6 if not available
			s.Equal(context.DeadlineExceeded, err)
		} else {
			s.Contains(err.Error(), "failed to establish a tcp connection")
		}
	}
}

// TestTCPCommandTableDriven defines table-driven tests for various scenarios
func (s *TCPCommandSuite) TestTCPCommandTableDriven() {
	tests := []struct {
		name        string
		args        []string
		shouldError bool
		errorType   string // "timeout", "validation", "connection", "connection_or_timeout", or "" if no error
	}{
		{
			name:        "Valid Address",
			args:        []string{"tcp", "1.1.1.1:53"},
			shouldError: false,
		},
		{
			name:        "No Arguments",
			args:        []string{"tcp"},
			shouldError: true,
			errorType:   "validation",
		},
		{
			name:        "Connection Refused",
			args:        []string{"tcp", "240.0.0.1:12345", "-t", "2s"},
			shouldError: true,
			errorType:   "timeout",
		},
		{
			name:        "Invalid Address Format",
			args:        []string{"tcp", "not-a-valid-address", "-t", "2s"},
			shouldError: true,
			errorType:   "connection_or_timeout",
		},
		{
			name:        "Multiple Valid Addresses",
			args:        []string{"tcp", "1.1.1.1:53", "8.8.8.8:53"},
			shouldError: false,
		},
		{
			name:        "With Custom Interval",
			args:        []string{"tcp", "1.1.1.1:53", "-i", "500ms"},
			shouldError: false,
		},
		{
			name:        "With Invert Check Success",
			args:        []string{"tcp", "240.0.0.1:12345", "-v", "-t", "2s"},
			shouldError: false, // Should succeed because connection fails and we're inverting
		},
		{
			name:        "With Invert Check Failure",
			args:        []string{"tcp", "1.1.1.1:53", "-v", "-t", "1s"},
			shouldError: true, // Should fail because connection succeeds and we're inverting
			errorType:   "timeout",
		},
	}

	for _, tt := range tests {
		s.Run(tt.name, func() {
			_, err := test.ExecuteCommand(s.rootCmd, tt.args...)

			if tt.shouldError {
				s.Error(err)
				if tt.errorType == "timeout" {
					s.Equal(context.DeadlineExceeded, err)
				} else if tt.errorType == "validation" {
					s.Contains(err.Error(), "ADDRESS is required argument for the tcp command")
				} else if tt.errorType == "connection" {
					s.Contains(err.Error(), "failed to establish a tcp connection")
				} else if tt.errorType == "connection_or_timeout" {
					if err == context.DeadlineExceeded {
						s.Equal(context.DeadlineExceeded, err)
					} else {
						s.Contains(err.Error(), "failed to establish a tcp connection")
					}
				}
			} else {
				s.NoError(err)
			}
		})
	}
}

// TestTCPCommandFlags tests the TCP command flags
func (s *TCPCommandSuite) TestTCPCommandFlags() {
	flags := s.tcpCmd.Flags()

	// Test connection-timeout flag
	connectionTimeout, err := flags.GetDuration("connection-timeout")
	s.NoError(err)
	s.Equal(3*time.Second, connectionTimeout)

	// Test that the flag is required
	s.True(flags.Lookup("connection-timeout") != nil)
}

// TestTCPCommandHelp tests the TCP command help
func (s *TCPCommandSuite) TestTCPCommandHelp() {
	output, err := test.ExecuteCommand(s.rootCmd, "tcp", "--help")
	s.NoError(err)
	s.Contains(output, "Check TCP connection")
	s.Contains(output, "connection-timeout")
}

// TestTCPCommandExample tests the TCP command example
func (s *TCPCommandSuite) TestTCPCommandExample() {
	// The example should be present in the command
	s.Contains(s.tcpCmd.Example, "wait4x tcp 127.0.0.1:9090")
}

// TestTCPCommandRunE tests the runTCP function directly
func (s *TCPCommandSuite) TestTCPCommandRunE() {
	// Test argument validation - this should work without logger context
	err := s.tcpCmd.Args(s.tcpCmd, []string{})
	s.Error(err)
	s.Equal("ADDRESS is required argument for the tcp command", err.Error())

	// Test with valid arguments - this should also work for argument validation
	err = s.tcpCmd.Args(s.tcpCmd, []string{"1.1.1.1:53"})
	s.NoError(err)

	// For testing the actual runTCP function, we need to use the full command execution
	// since runTCP requires a logger in the context
	_, err = test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53")
	s.NoError(err)
}

// TestTCPCommandWithContext tests the TCP command with context
func (s *TCPCommandSuite) TestTCPCommandWithContext() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	s.tcpCmd.SetContext(ctx)

	// Test that the command works with a valid context
	_, err := test.ExecuteCommand(s.rootCmd, "tcp", "1.1.1.1:53")
	s.NoError(err)
}

// TestTCPCommandSuite runs the test suite
func TestTCPCommandSuite(t *testing.T) {
	suite.Run(t, new(TCPCommandSuite))
}