diff --git a/tests/e2e/file_test.go b/tests/e2e/file_test.go new file mode 100644 index 0000000..8a03005 --- /dev/null +++ b/tests/e2e/file_test.go @@ -0,0 +1,194 @@ +package e2e + +import ( + "context" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" +) + +type FileSuite struct { + suite.Suite + ctx context.Context +} + +func (s *FileSuite) SetupSuite() { + s.ctx = context.Background() +} + +func (s *FileSuite) TearDownSuite() {} + +// TestFileGetExisting verifies that GET on an existing file returns +// the file content with HTTP 200. Covers the basic file serving path: +// file handler → http.FileServer → file read. +func (s *FileSuite) TestFileGetExisting() { + gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName, + "testdata/file/server.yaml", + []testcontainers.ContainerFile{ + {HostFilePath: "testdata/file/hello.txt", ContainerFilePath: "/srv/files/hello.txt", FileMode: 0644}, + }, + "8080/tcp") + s.Require().NoError(err) + defer gostC.Terminate(s.ctx) + + cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8080/hello.txt"} + code, out, err := gostC.Exec(s.ctx, cmd) + s.Require().NoError(err) + + body, err := io.ReadAll(out) + s.Require().NoError(err) + + if code != 0 || !strings.Contains(string(body), "hello-gost-file") { + DumpLogs(s.T(), s.ctx, "file-server logs", gostC) + } + s.Require().Equal(0, code) + s.Require().Contains(string(body), "hello-gost-file") +} + +// TestFileGetNotFound verifies that GET on a nonexistent file returns +// 404 Not Found. +func (s *FileSuite) TestFileGetNotFound() { + gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName, + "testdata/file/server.yaml", + []testcontainers.ContainerFile{ + {HostFilePath: "testdata/file/hello.txt", ContainerFilePath: "/srv/files/hello.txt", FileMode: 0644}, + }, + "8080/tcp") + s.Require().NoError(err) + defer gostC.Terminate(s.ctx) + + cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}", + "http://127.0.0.1:8080/nonexistent.txt"} + _, out, err := gostC.Exec(s.ctx, cmd) + s.Require().NoError(err) + body, _ := io.ReadAll(out) + s.Assert().Contains(string(body), "404") +} + +// TestFileGetIndexHtml verifies that GET / serves index.html when +// present in the served directory. Covers the default index document +// behavior of http.FileServer. +func (s *FileSuite) TestFileGetIndexHtml() { + gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName, + "testdata/file/server.yaml", + []testcontainers.ContainerFile{ + {HostFilePath: "testdata/file/index.html", ContainerFilePath: "/srv/files/index.html", FileMode: 0644}, + }, + "8080/tcp") + s.Require().NoError(err) + defer gostC.Terminate(s.ctx) + + cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8080/"} + code, out, err := gostC.Exec(s.ctx, cmd) + s.Require().NoError(err) + + body, err := io.ReadAll(out) + s.Require().NoError(err) + if code != 0 || !strings.Contains(string(body), "gost file index") { + DumpLogs(s.T(), s.ctx, "file-server index logs", gostC) + } + s.Require().Equal(0, code) + s.Require().Contains(string(body), "gost file index") +} + +// TestFilePutUpload verifies that PUT uploads a file when file.put is +// enabled. Uploads a file and then reads it back via GET to confirm +// the content was persisted. +func (s *FileSuite) TestFilePutUpload() { + gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName, + "testdata/file/server_put.yaml", + []testcontainers.ContainerFile{ + {HostFilePath: "testdata/file/.empty", ContainerFilePath: "/srv/files/.empty", FileMode: 0644}, + }, + "8080/tcp") + s.Require().NoError(err) + defer gostC.Terminate(s.ctx) + + // Create source content inside the container. + _, _, err = gostC.Exec(s.ctx, []string{ + "sh", "-c", "printf 'uploaded-content' > /tmp/upload_src.txt", + }) + s.Require().NoError(err) + + // Upload via PUT. + _, out, err := gostC.Exec(s.ctx, []string{ + "curl", "-v", "-s", "-T", "/tmp/upload_src.txt", + "http://127.0.0.1:8080/uploaded.txt", + }) + s.Require().NoError(err) + putBody, _ := io.ReadAll(out) + s.T().Logf("PUT response:\n%s", string(putBody)) + + // Read back the uploaded file via GET. + _, out2, err := gostC.Exec(s.ctx, []string{ + "curl", "-v", "-s", + "http://127.0.0.1:8080/uploaded.txt", + }) + s.Require().NoError(err) + body, _ := io.ReadAll(out2) + s.Assert().Contains(string(body), "uploaded-content") +} + +// TestFilePutNoPermission verifies that PUT returns 405 Method Not +// Allowed when file.put is not enabled (default: false). +func (s *FileSuite) TestFilePutNoPermission() { + gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName, + "testdata/file/server.yaml", + []testcontainers.ContainerFile{ + {HostFilePath: "testdata/file/.empty", ContainerFilePath: "/srv/files/.empty", FileMode: 0644}, + }, + "8080/tcp") + s.Require().NoError(err) + defer gostC.Terminate(s.ctx) + + cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}", + "-T", "/dev/null", "http://127.0.0.1:8080/test.txt"} + _, out, err := gostC.Exec(s.ctx, cmd) + s.Require().NoError(err) + body, _ := io.ReadAll(out) + s.Assert().Contains(string(body), "405") +} + +// TestFileAuth verifies authentication on the file handler. +// Without credentials the handler returns 401 Unauthorized, +// with valid credentials it serves the file. +func (s *FileSuite) TestFileAuth() { + gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName, + "testdata/file/server_auth.yaml", + []testcontainers.ContainerFile{ + {HostFilePath: "testdata/file/hello.txt", ContainerFilePath: "/srv/files/hello.txt", FileMode: 0644}, + }, + "8080/tcp") + s.Require().NoError(err) + defer gostC.Terminate(s.ctx) + + s.T().Run("no-auth-401", func(t *testing.T) { + cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}", + "http://127.0.0.1:8080/hello.txt"} + _, out, _ := gostC.Exec(s.ctx, cmd) + body, _ := io.ReadAll(out) + s.Assert().Contains(string(body), "401") + }) + + s.T().Run("with-auth-success", func(t *testing.T) { + cmd := []string{"curl", "-v", "-s", "-u", "user:pass", + "http://127.0.0.1:8080/hello.txt"} + code, out, err := gostC.Exec(s.ctx, cmd) + s.Require().NoError(err) + + body, err := io.ReadAll(out) + s.Require().NoError(err) + if code != 0 || !strings.Contains(string(body), "hello-gost-file") { + DumpLogs(s.T(), s.ctx, "file-server auth logs", gostC) + } + s.Require().Equal(0, code) + s.Require().Contains(string(body), "hello-gost-file") + }) +} + +func TestFileSuite(t *testing.T) { + suite.Run(t, new(FileSuite)) +} diff --git a/tests/e2e/testdata/file/.empty b/tests/e2e/testdata/file/.empty new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/testdata/file/index.html b/tests/e2e/testdata/file/index.html new file mode 100644 index 0000000..bdc7df3 --- /dev/null +++ b/tests/e2e/testdata/file/index.html @@ -0,0 +1 @@ +gost file index diff --git a/tests/e2e/testdata/file/server.yaml b/tests/e2e/testdata/file/server.yaml new file mode 100644 index 0000000..6193eef --- /dev/null +++ b/tests/e2e/testdata/file/server.yaml @@ -0,0 +1,9 @@ +services: +- name: file-server + addr: :8080 + handler: + type: file + metadata: + file.dir: /srv/files + listener: + type: tcp diff --git a/tests/e2e/testdata/file/server_auth.yaml b/tests/e2e/testdata/file/server_auth.yaml new file mode 100644 index 0000000..59d0f55 --- /dev/null +++ b/tests/e2e/testdata/file/server_auth.yaml @@ -0,0 +1,15 @@ +services: +- name: file-server-auth + addr: :8080 + handler: + type: file + auther: auther-0 + metadata: + file.dir: /srv/files + listener: + type: tcp +authers: +- name: auther-0 + auths: + - username: user + password: pass diff --git a/tests/e2e/testdata/file/server_put.yaml b/tests/e2e/testdata/file/server_put.yaml new file mode 100644 index 0000000..709a657 --- /dev/null +++ b/tests/e2e/testdata/file/server_put.yaml @@ -0,0 +1,10 @@ +services: +- name: file-server-put + addr: :8080 + handler: + type: file + metadata: + file.dir: /srv/files + file.put: true + listener: + type: tcp