@@ -30,6 +30,14 @@ type FTPTestContainer struct {
3030
3131// NewFTPTestContainer uses delfer/alpine-ftp-server, minimal env vars, fixed host port mapping syntax.
3232func NewFTPTestContainer (ctx context.Context , t * testing.T ) * FTPTestContainer {
33+ fc , err := NewFTPTestContainerE (ctx )
34+ require .NoError (t , err )
35+ return fc
36+ }
37+
38+ // NewFTPTestContainerE uses delfer/alpine-ftp-server, minimal env vars, fixed host port mapping syntax.
39+ // Returns error instead of using require.NoError, suitable for TestMain usage.
40+ func NewFTPTestContainerE (ctx context.Context ) (* FTPTestContainer , error ) {
3341 const (
3442 defaultUser = "ftpuser"
3543 defaultPassword = "ftppass"
@@ -38,9 +46,6 @@ func NewFTPTestContainer(ctx context.Context, t *testing.T) *FTPTestContainer {
3846 fixedHostControlPort = "2121"
3947 )
4048
41- // set up logging for testcontainers if the appropriate API is available
42- t .Logf ("Setting up FTP test container" )
43-
4449 pasvPortRangeContainer := fmt .Sprintf ("%s-%s" , pasvMinPort , pasvMaxPort )
4550 pasvPortRangeHost := fmt .Sprintf ("%s-%s" , pasvMinPort , pasvMaxPort ) // map 1:1
4651 exposedPortsWithBinding := []string {
@@ -49,7 +54,7 @@ func NewFTPTestContainer(ctx context.Context, t *testing.T) *FTPTestContainer {
4954 }
5055
5156 imageName := "delfer/alpine-ftp-server:latest"
52- t . Logf ( "Using FTP server image: %s" , imageName )
57+ fmt . Printf ( "Creating FTP container using %s (fixed host port %s)... \n " , imageName , fixedHostControlPort )
5358
5459 req := testcontainers.ContainerRequest {
5560 Image : imageName ,
@@ -60,57 +65,58 @@ func NewFTPTestContainer(ctx context.Context, t *testing.T) *FTPTestContainer {
6065 WaitingFor : wait .ForListeningPort (nat .Port ("21/tcp" )).WithStartupTimeout (2 * time .Minute ),
6166 }
6267
63- t .Logf ("creating FTP container using %s (minimal env vars, fixed host port %s)..." , imageName , fixedHostControlPort )
6468 container , err := testcontainers .GenericContainer (ctx , testcontainers.GenericContainerRequest {
6569 ContainerRequest : req ,
6670 Started : true ,
6771 })
68- // create the container instance to use its methods
69- ftpContainer := & FTPTestContainer {}
70-
71- // error handling with detailed logging for container startup issues
7272 if err != nil {
73- ftpContainer .logContainerError (ctx , t , container , err , imageName )
73+ logContainerLogs (ctx , container )
74+ return nil , fmt .Errorf ("failed to create ftp container: %w" , err )
7475 }
75- t . Logf ("FTP container created and started (ID: %s)" , container .GetContainerID ())
76+ fmt . Printf ("FTP container created and started (ID: %s)\n " , container .GetContainerID ())
7677
7778 host , err := container .Host (ctx )
78- require .NoError (t , err , "Failed to get container host" )
79+ if err != nil {
80+ _ = container .Terminate (ctx )
81+ return nil , fmt .Errorf ("failed to get container host: %w" , err )
82+ }
7983
8084 // since we requested a fixed port, construct the nat.Port struct directly
8185 // we still call MappedPort just to ensure the container is properly exposing *something* for port 21
82- _ , err = container .MappedPort (ctx , "21" )
83- require .NoError (t , err , "Failed to get mapped port info for container port 21/tcp (even though fixed)" )
86+ if _ , err = container .MappedPort (ctx , "21" ); err != nil {
87+ _ = container .Terminate (ctx )
88+ return nil , fmt .Errorf ("failed to get mapped port: %w" , err )
89+ }
8490
8591 // construct the Port struct based on our fixed request
8692 fixedHostNatPort , err := nat .NewPort ("tcp" , fixedHostControlPort )
87- require . NoError ( t , err , "Failed to create nat.Port for fixed host port" )
88-
89- t . Logf ( "FTP container should be accessible at: %s:%s (Control Plane) " , host , fixedHostControlPort )
90- t . Logf ( "FTP server using default config, passive ports %s mapped to host %s" , pasvPortRangeContainer , pasvPortRangeHost )
93+ if err != nil {
94+ _ = container . Terminate ( ctx )
95+ return nil , fmt . Errorf ( "failed to create nat.Port for fixed host port: %w " , err )
96+ }
9197
9298 time .Sleep (1 * time .Second )
9399
100+ fmt .Printf ("FTP container accessible at: %s:%s (passive ports %s)\n " , host , fixedHostControlPort , pasvPortRangeHost )
101+
94102 return & FTPTestContainer {
95103 Container : container ,
96104 Host : host ,
97105 Port : fixedHostNatPort , // use the manually constructed nat.Port for the fixed host port
98106 User : defaultUser ,
99107 Password : defaultPassword ,
100- }
108+ }, nil
101109}
102110
103- // connect function (Use default EPSV enabled)
111+ // connect establishes an FTP connection and logs in
104112func (fc * FTPTestContainer ) connect (ctx context.Context ) (* ftp.ServerConn , error ) {
105113 opts := []ftp.DialOption {
106114 ftp .DialWithTimeout (30 * time .Second ),
107115 ftp .DialWithContext (ctx ),
108- ftp .DialWithDebugOutput (os .Stdout ), // keep for debugging
109- // *** Use default (EPSV enabled) ***
110- // ftp.DialWithDisabledEPSV(true),
116+ ftp .DialWithDebugOutput (os .Stdout ),
111117 }
112118
113- connStr := fc .ConnectionString () // will use the fixed host port (e.g., 2121)
119+ connStr := fc .ConnectionString ()
114120 fmt .Printf ("Attempting FTP connection to: %s (User: %s)\n " , connStr , fc .User )
115121
116122 c , err := ftp .Dial (connStr , opts ... )
@@ -123,9 +129,7 @@ func (fc *FTPTestContainer) connect(ctx context.Context) (*ftp.ServerConn, error
123129 fmt .Printf ("Attempting FTP login with user: %s\n " , fc .User )
124130 if err := c .Login (fc .User , fc .Password ); err != nil {
125131 fmt .Printf ("FTP Login Error for user %s: %v\n " , fc .User , err )
126- if quitErr := c .Quit (); quitErr != nil {
127- fmt .Printf ("Warning: error closing FTP connection: %v\n " , quitErr )
128- }
132+ _ = c .Quit ()
129133 return nil , fmt .Errorf ("failed to login to FTP server with user %s: %w" , fc .User , err )
130134 }
131135 fmt .Printf ("FTP Login successful for user %s\n " , fc .User )
@@ -378,32 +382,25 @@ func splitPath(path string) []string {
378382 return strings .Split (cleanPath , "/" )
379383}
380384
381- // logContainerError handles container startup errors with detailed logging
382- func (fc * FTPTestContainer ) logContainerError (_ context.Context , t * testing.T , container testcontainers.Container , err error , imageName string ) {
383- logCtx , logCancel := context .WithTimeout (context .Background (), 10 * time .Second )
384- defer logCancel ()
385-
386- fc .logContainerLogs (logCtx , t , container )
387- require .NoError (t , err , "Failed to create or start FTP container %s" , imageName )
388- }
389-
390- // logContainerLogs attempts to fetch and log container logs
391- func (fc * FTPTestContainer ) logContainerLogs (ctx context.Context , t * testing.T , container testcontainers.Container ) {
385+ // logContainerLogs attempts to fetch and print container logs for debugging startup failures
386+ func logContainerLogs (ctx context.Context , container testcontainers.Container ) {
392387 if container == nil {
393- t . Logf ("Container object was nil after GenericContainer failure." )
388+ fmt . Printf ("Container object was nil after GenericContainer failure.\n " )
394389 return
395390 }
396391
397- logs , logErr := container .Logs (ctx )
398- if logErr != nil {
399- t .Logf ("Could not retrieve container logs after startup failure: %v" , logErr )
392+ logCtx , logCancel := context .WithTimeout (ctx , 10 * time .Second )
393+ defer logCancel ()
394+
395+ logs , err := container .Logs (logCtx )
396+ if err != nil {
397+ fmt .Printf ("Could not retrieve container logs after startup failure: %v\n " , err )
400398 return
401399 }
400+ defer logs .Close ()
402401
403402 logBytes , _ := io .ReadAll (logs )
404- if closeErr := logs . Close (); closeErr != nil {
405- t . Logf ( "warning: failed to close logs reader: %v " , closeErr )
403+ if len ( logBytes ) > 0 {
404+ fmt . Printf ( "Container logs: \n %s \n " , string ( logBytes ) )
406405 }
407-
408- t .Logf ("Container logs on startup failure:\n %s" , string (logBytes ))
409406}
0 commit comments