• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

go-pkgz / testutils / 14371540395

10 Apr 2025 03:18AM UTC coverage: 70.031% (-2.2%) from 72.207%
14371540395

push

github

web-flow
Add file operations to all container types (#4)

* Add DeleteFile methods to containers and standardize file operations

- Add DeleteFile method to FTP container
- Add file operations (get, save, list, delete) to SSH container
- Add file operations (get, save, list, delete) to Localstack (S3) container
- Add comprehensive tests for all new methods
- Update README with examples for all file operations

* Increase test timeout to 300s for container tests

164 of 256 new or added lines in 3 files covered. (64.06%)

107 existing lines in 3 files now uncovered.

680 of 971 relevant lines covered (70.03%)

3.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.74
/containers/ssh.go
1
package containers
2

3
import (
4
        "context"
5
        "fmt"
6
        "io"
7
        "os"
8
        "path/filepath"
9
        "strings"
10
        "testing"
11
        "time"
12

13
        "github.com/docker/go-connections/nat"
14
        "github.com/pkg/sftp"
15
        "github.com/stretchr/testify/require"
16
        "github.com/testcontainers/testcontainers-go"
17
        "github.com/testcontainers/testcontainers-go/wait"
18
        "golang.org/x/crypto/ssh"
19
)
20

21
// SSHTestContainer is a wrapper around a testcontainers.Container that provides an SSH server
22
type SSHTestContainer struct {
23
        Container testcontainers.Container
24
        Host      string
25
        Port      nat.Port
26
        User      string
27
}
28

29
// NewSSHTestContainer creates a new SSH test container and returns an SSHTestContainer instance
30
func NewSSHTestContainer(ctx context.Context, t *testing.T) *SSHTestContainer {
5✔
31
        return NewSSHTestContainerWithUser(ctx, t, "test")
5✔
32
}
5✔
33

34
// NewSSHTestContainerWithUser creates a new SSH test container with a specific user
35
func NewSSHTestContainerWithUser(ctx context.Context, t *testing.T, user string) *SSHTestContainer {
6✔
36
        pubKey, err := os.ReadFile("testdata/test_ssh_key.pub")
6✔
37
        require.NoError(t, err)
6✔
38

6✔
39
        req := testcontainers.ContainerRequest{
6✔
40
                Image:        "lscr.io/linuxserver/openssh-server:latest",
6✔
41
                ExposedPorts: []string{"2222/tcp"},
6✔
42
                WaitingFor:   wait.NewLogStrategy("done.").WithStartupTimeout(time.Minute),
6✔
43
                Files: []testcontainers.ContainerFile{
6✔
44
                        {HostFilePath: "testdata/test_ssh_key.pub", ContainerFilePath: "/authorized_key"},
6✔
45
                },
6✔
46
                Env: map[string]string{
6✔
47
                        "PUBLIC_KEY":  string(pubKey),
6✔
48
                        "USER_NAME":   user,
6✔
49
                        "TZ":          "Etc/UTC",
6✔
50
                        "SUDO_ACCESS": "true",
6✔
51
                },
6✔
52
        }
6✔
53

6✔
54
        container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
6✔
55
                ContainerRequest: req,
6✔
56
                Started:          true,
6✔
57
        })
6✔
58
        require.NoError(t, err)
6✔
59

6✔
60
        host, err := container.Host(ctx)
6✔
61
        require.NoError(t, err)
6✔
62

6✔
63
        port, err := container.MappedPort(ctx, "2222")
6✔
64
        require.NoError(t, err)
6✔
65

6✔
66
        return &SSHTestContainer{
6✔
67
                Container: container,
6✔
68
                Host:      host,
6✔
69
                Port:      port,
6✔
70
                User:      user,
6✔
71
        }
6✔
72
}
6✔
73

74
// Address returns the SSH server address in host:port format
75
func (sc *SSHTestContainer) Address() string {
11✔
76
        return fmt.Sprintf("%s:%s", sc.Host, sc.Port.Port())
11✔
77
}
11✔
78

79
// connect establishes an SSH connection and returns a SFTP client
80
func (sc *SSHTestContainer) connect(_ context.Context) (sftpClient *sftp.Client, sshClient *ssh.Client, err error) {
9✔
81
        key, err := os.ReadFile("testdata/test_ssh_key")
9✔
82
        if err != nil {
9✔
NEW
UNCOV
83
                return nil, nil, fmt.Errorf("failed to read SSH private key: %w", err)
×
NEW
UNCOV
84
        }
×
85

86
        signer, err := ssh.ParsePrivateKey(key)
9✔
87
        if err != nil {
9✔
NEW
UNCOV
88
                return nil, nil, fmt.Errorf("failed to parse SSH private key: %w", err)
×
NEW
UNCOV
89
        }
×
90

91
        config := &ssh.ClientConfig{
9✔
92
                User: sc.User,
9✔
93
                Auth: []ssh.AuthMethod{
9✔
94
                        ssh.PublicKeys(signer),
9✔
95
                },
9✔
96
                // #nosec G106 -- InsecureIgnoreHostKey is acceptable for test containers
9✔
97
                HostKeyCallback: ssh.InsecureIgnoreHostKey(),
9✔
98
                Timeout:         30 * time.Second,
9✔
99
        }
9✔
100

9✔
101
        addr := sc.Address()
9✔
102
        sshClient, err = ssh.Dial("tcp", addr, config)
9✔
103
        if err != nil {
9✔
NEW
UNCOV
104
                return nil, nil, fmt.Errorf("failed to dial SSH server at %s: %w", addr, err)
×
NEW
UNCOV
105
        }
×
106

107
        sftpClient, err = sftp.NewClient(sshClient)
9✔
108
        if err != nil {
9✔
NEW
UNCOV
109
                if closeErr := sshClient.Close(); closeErr != nil {
×
NEW
UNCOV
110
                        return nil, nil, fmt.Errorf("failed to create SFTP client: %w and failed to close SSH client: %v", err, closeErr)
×
NEW
UNCOV
111
                }
×
NEW
UNCOV
112
                return nil, nil, fmt.Errorf("failed to create SFTP client: %w", err)
×
113
        }
114

115
        return sftpClient, sshClient, nil
9✔
116
}
117

118
// GetFile downloads a file from the SSH server
119
func (sc *SSHTestContainer) GetFile(ctx context.Context, remotePath, localPath string) error {
1✔
120
        sftpClient, sshClient, err := sc.connect(ctx)
1✔
121
        if err != nil {
1✔
NEW
UNCOV
122
                return fmt.Errorf("failed to connect to SSH server for GetFile: %w", err)
×
NEW
UNCOV
123
        }
×
124
        defer sftpClient.Close()
1✔
125
        defer sshClient.Close()
1✔
126

1✔
127
        localDir := filepath.Dir(localPath)
1✔
128
        if err := os.MkdirAll(localDir, 0o750); err != nil {
1✔
NEW
UNCOV
129
                return fmt.Errorf("failed to create local directory %s: %w", localDir, err)
×
NEW
UNCOV
130
        }
×
131

132
        if !strings.HasPrefix(filepath.Clean(localPath), filepath.Clean(localDir)) {
1✔
NEW
UNCOV
133
                return fmt.Errorf("localPath %s attempts to escape from directory %s", localPath, localDir)
×
NEW
UNCOV
134
        }
×
135

136
        // open remote file
137
        remoteFile, err := sftpClient.Open(remotePath)
1✔
138
        if err != nil {
1✔
NEW
UNCOV
139
                return fmt.Errorf("failed to open remote file %s: %w", remotePath, err)
×
NEW
UNCOV
140
        }
×
141
        defer remoteFile.Close()
1✔
142

1✔
143
        // create local file
1✔
144
        localFile, err := os.OpenFile(localPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
1✔
145
        if err != nil {
1✔
NEW
UNCOV
146
                return fmt.Errorf("failed to create local file %s: %w", localPath, err)
×
NEW
UNCOV
147
        }
×
148
        defer localFile.Close()
1✔
149

1✔
150
        // copy remote file to local file
1✔
151
        if _, err := io.Copy(localFile, remoteFile); err != nil {
1✔
NEW
UNCOV
152
                return fmt.Errorf("failed to copy file content from %s to %s: %w", remotePath, localPath, err)
×
NEW
UNCOV
153
        }
×
154

155
        return nil
1✔
156
}
157

158
// SaveFile uploads a file to the SSH server
159
func (sc *SSHTestContainer) SaveFile(ctx context.Context, localPath, remotePath string) error {
2✔
160
        sftpClient, sshClient, err := sc.connect(ctx)
2✔
161
        if err != nil {
2✔
NEW
UNCOV
162
                return fmt.Errorf("failed to connect to SSH server for SaveFile: %w", err)
×
NEW
UNCOV
163
        }
×
164
        defer sftpClient.Close()
2✔
165
        defer sshClient.Close()
2✔
166

2✔
167
        if !strings.HasPrefix(filepath.Clean(localPath), filepath.Clean(filepath.Dir(localPath))) {
2✔
NEW
UNCOV
168
                return fmt.Errorf("localPath %s attempts to escape from its directory", localPath)
×
NEW
UNCOV
169
        }
×
170

171
        // open local file
172
        localFile, err := os.Open(localPath)
2✔
173
        if err != nil {
2✔
NEW
UNCOV
174
                return fmt.Errorf("failed to open local file %s: %w", localPath, err)
×
NEW
UNCOV
175
        }
×
176
        defer localFile.Close()
2✔
177

2✔
178
        // create remote directory if it doesn't exist
2✔
179
        remoteDir := filepath.Dir(remotePath)
2✔
180
        if remoteDir != "." && remoteDir != "/" {
4✔
181
                if err := sc.createDirRecursive(sftpClient, remoteDir); err != nil {
2✔
NEW
UNCOV
182
                        return fmt.Errorf("failed to create remote directory %s: %w", remoteDir, err)
×
NEW
UNCOV
183
                }
×
184
        }
185

186
        // create remote file
187
        remoteFile, err := sftpClient.Create(remotePath)
2✔
188
        if err != nil {
2✔
NEW
UNCOV
189
                return fmt.Errorf("failed to create remote file %s: %w", remotePath, err)
×
NEW
UNCOV
190
        }
×
191
        defer remoteFile.Close()
2✔
192

2✔
193
        // copy local file to remote file
2✔
194
        if _, err := io.Copy(remoteFile, localFile); err != nil {
2✔
NEW
UNCOV
195
                return fmt.Errorf("failed to copy file content from %s to %s: %w", localPath, remotePath, err)
×
NEW
UNCOV
196
        }
×
197

198
        return nil
2✔
199
}
200

201
// create directories recursively
202
func (sc *SSHTestContainer) createDirRecursive(sftpClient *sftp.Client, remotePath string) error {
2✔
203
        parts := strings.Split(strings.Trim(filepath.ToSlash(remotePath), "/"), "/")
2✔
204
        if len(parts) == 0 {
2✔
NEW
UNCOV
205
                return nil
×
NEW
UNCOV
206
        }
×
207

208
        current := "/"
2✔
209
        for _, part := range parts {
6✔
210
                current = filepath.Join(current, part)
4✔
211
                info, err := sftpClient.Stat(current)
4✔
212
                if err == nil && info.IsDir() {
6✔
213
                        continue
2✔
214
                }
215
                if err := sftpClient.Mkdir(current); err != nil {
2✔
NEW
UNCOV
216
                        return fmt.Errorf("failed to create directory %s: %w", current, err)
×
NEW
UNCOV
217
                }
×
218
        }
219
        return nil
2✔
220
}
221

222
// ListFiles lists files in a directory on the SSH server
223
func (sc *SSHTestContainer) ListFiles(ctx context.Context, remotePath string) ([]os.FileInfo, error) {
4✔
224
        sftpClient, sshClient, err := sc.connect(ctx)
4✔
225
        if err != nil {
4✔
NEW
UNCOV
226
                return nil, fmt.Errorf("failed to connect to SSH server for ListFiles: %w", err)
×
NEW
UNCOV
227
        }
×
228
        defer sftpClient.Close()
4✔
229
        defer sshClient.Close()
4✔
230

4✔
231
        // use root directory if path is empty
4✔
232
        if remotePath == "" || remotePath == "." {
4✔
NEW
UNCOV
233
                remotePath = "/"
×
NEW
UNCOV
234
        }
×
235

236
        // get file info
237
        files, err := sftpClient.ReadDir(remotePath)
4✔
238
        if err != nil {
4✔
NEW
UNCOV
239
                return nil, fmt.Errorf("failed to list files in remote path '%s': %w", remotePath, err)
×
NEW
UNCOV
240
        }
×
241

242
        return files, nil
4✔
243
}
244

245
// DeleteFile deletes a file from the SSH server
246
func (sc *SSHTestContainer) DeleteFile(ctx context.Context, remotePath string) error {
2✔
247
        sftpClient, sshClient, err := sc.connect(ctx)
2✔
248
        if err != nil {
2✔
NEW
UNCOV
249
                return fmt.Errorf("failed to connect to SSH server for DeleteFile: %w", err)
×
NEW
UNCOV
250
        }
×
251
        defer sftpClient.Close()
2✔
252
        defer sshClient.Close()
2✔
253

2✔
254
        // delete file
2✔
255
        if err := sftpClient.Remove(remotePath); err != nil {
2✔
NEW
UNCOV
256
                return fmt.Errorf("failed to delete remote file %s: %w", remotePath, err)
×
NEW
UNCOV
257
        }
×
258

259
        return nil
2✔
260
}
261

262
// Close terminates the container
263
func (sc *SSHTestContainer) Close(ctx context.Context) error {
6✔
264
        return sc.Container.Terminate(ctx)
6✔
265
}
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc