package e2e import ( "bytes" "encoding/json" "fmt" "github.com/gorilla/websocket" "github.com/openHPI/poseidon/internal/api" "github.com/openHPI/poseidon/pkg/dto" "github.com/openHPI/poseidon/tests" "github.com/openHPI/poseidon/tests/helpers" "github.com/stretchr/testify/require" "io" "net/http" "net/url" "strings" "time" ) func (s *E2ETestSuite) TestProvideRunnerRoute() { for _, environmentID := range environmentIDs { s.Run(environmentID.ToString(), func() { runnerRequestByteString, err := json.Marshal(dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)}) s.Require().NoError(err) reader := bytes.NewReader(runnerRequestByteString) s.Run("valid request returns a runner", func() { resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", reader) s.Require().NoError(err) s.Equal(http.StatusOK, resp.StatusCode) runnerResponse := new(dto.RunnerResponse) err = json.NewDecoder(resp.Body).Decode(runnerResponse) s.Require().NoError(err) s.NotEmpty(runnerResponse.ID) }) s.Run("invalid request returns bad request", func() { resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", strings.NewReader("")) s.Require().NoError(err) s.Equal(http.StatusBadRequest, resp.StatusCode) }) s.Run("requesting runner of unknown execution environment returns not found", func() { runnerRequestByteString, err := json.Marshal(dto.RunnerRequest{ ExecutionEnvironmentID: tests.NonExistingIntegerID, }) s.Require().NoError(err) reader := bytes.NewReader(runnerRequestByteString) resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", reader) s.Require().NoError(err) s.Equal(http.StatusNotFound, resp.StatusCode) }) }) } } // ProvideRunner creates a runner with the given RunnerRequest via an external request. // It needs a running Poseidon instance to work. func ProvideRunner(request *dto.RunnerRequest) (string, error) { runnerURL := helpers.BuildURL(api.BasePath, api.RunnersPath) runnerRequestByteString, err := json.Marshal(request) if err != nil { return "", err } reader := strings.NewReader(string(runnerRequestByteString)) resp, err := http.Post(runnerURL, "application/json", reader) //nolint:gosec // runnerURL is not influenced by a user if err != nil { return "", err } if resp.StatusCode != http.StatusOK { //nolint:goerr113 // dynamic error is ok in here, as it is a test return "", fmt.Errorf("expected response code 200 when getting runner, got %v", resp.StatusCode) } runnerResponse := new(dto.RunnerResponse) err = json.NewDecoder(resp.Body).Decode(runnerResponse) if err != nil { return "", err } return runnerResponse.ID, nil } // CopyFiles sends the dto.UpdateFileSystemRequest to Poseidon. // It needs a running Poseidon instance to work. func CopyFiles(runnerID string, request *dto.UpdateFileSystemRequest) (*http.Response, error) { data, err := json.Marshal(request) if err != nil { return nil, err } r := bytes.NewReader(data) return helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), "application/json", r) } func (s *E2ETestSuite) TestDeleteRunnerRoute() { for _, environmentID := range environmentIDs { s.Run(environmentID.ToString(), func() { runnerID, err := ProvideRunner(&dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)}) s.NoError(err) s.Run("Deleting the runner returns NoContent", func() { resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID), nil) s.NoError(err) s.Equal(http.StatusNoContent, resp.StatusCode) }) s.Run("Deleting it again returns NotFound", func() { resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID), nil) s.NoError(err) s.Equal(http.StatusNotFound, resp.StatusCode) }) s.Run("Deleting non-existing runner returns NotFound", func() { resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, tests.NonExistingStringID), nil) s.NoError(err) s.Equal(http.StatusNotFound, resp.StatusCode) }) }) } } //nolint:funlen // there are a lot of tests for the files route, this function can be a little longer than 100 lines ;) func (s *E2ETestSuite) TestCopyFilesRoute() { for _, environmentID := range environmentIDs { s.Run(environmentID.ToString(), func() { runnerID, err := ProvideRunner(&dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)}) s.NoError(err) request := &dto.UpdateFileSystemRequest{ Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}}, } s.Run("File copy with valid payload succeeds", func() { resp, err := CopyFiles(runnerID, request) s.NoError(err) s.Equal(http.StatusNoContent, resp.StatusCode) s.Run("File content can be printed on runner", func() { s.assertFileContent(runnerID, tests.DefaultFileName, tests.DefaultFileContent) }) }) s.Run("Files are put in correct location", func() { relativeFilePath := "relative/file/path.txt" relativeFileContent := "Relative file content" absoluteFilePath := "/tmp/absolute/file/path.txt" absoluteFileContent := "Absolute file content" request = &dto.UpdateFileSystemRequest{ Copy: []dto.File{ {Path: dto.FilePath(relativeFilePath), Content: []byte(relativeFileContent)}, {Path: dto.FilePath(absoluteFilePath), Content: []byte(absoluteFileContent)}, }, } s.Require().NoError(err) resp, err := CopyFiles(runnerID, request) s.NoError(err) s.Equal(http.StatusNoContent, resp.StatusCode) s.Run("File content of file with relative path can be printed on runner", func() { // the print command is executed in the context of the default working directory of the container s.assertFileContent(runnerID, relativeFilePath, relativeFileContent) }) s.Run("File content of file with absolute path can be printed on runner", func() { s.assertFileContent(runnerID, absoluteFilePath, absoluteFileContent) }) }) s.Run("File deletion request deletes file on runner", func() { request = &dto.UpdateFileSystemRequest{ Delete: []dto.FilePath{tests.DefaultFileName}, } s.Require().NoError(err) resp, err := CopyFiles(runnerID, request) s.NoError(err) s.Equal(http.StatusNoContent, resp.StatusCode) s.Run("File content can no longer be printed", func() { stdout, stderr := s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName) s.Equal("", stdout) s.Contains(stderr, "No such file or directory") }) }) s.Run("File copy happens after file deletion", func() { request = &dto.UpdateFileSystemRequest{ Delete: []dto.FilePath{tests.DefaultFileName}, Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}}, } s.Require().NoError(err) resp, err := CopyFiles(runnerID, request) s.NoError(err) s.Equal(http.StatusNoContent, resp.StatusCode) _ = resp.Body.Close() s.Run("File content can be printed on runner", func() { s.assertFileContent(runnerID, tests.DefaultFileName, tests.DefaultFileContent) }) }) s.Run("File copy with invalid payload returns bad request", func() { resp, err := helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), "text/html", strings.NewReader("")) s.NoError(err) s.Equal(http.StatusBadRequest, resp.StatusCode) }) s.Run("Copying to non-existing runner returns NotFound", func() { resp, err := CopyFiles(tests.NonExistingStringID, request) s.NoError(err) s.Equal(http.StatusNotFound, resp.StatusCode) }) }) } } func (s *E2ETestSuite) TestCopyFilesRoute_PermissionDenied() { s.Run("Nomad/If one file produces permission denied error, others are still copied", func() { runnerID, err := ProvideRunner(&dto.RunnerRequest{ ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, }) s.NoError(err) newFileContent := []byte("New content") copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{ Copy: []dto.File{ {Path: "/proc/1/environ", Content: []byte(tests.DefaultFileContent)}, {Path: tests.DefaultFileName, Content: newFileContent}, }, }) s.Require().NoError(err) resp, err := helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), "application/json", bytes.NewReader(copyFilesRequestByteString)) s.NoError(err) s.Equal(http.StatusInternalServerError, resp.StatusCode) internalServerError := new(dto.InternalServerError) err = json.NewDecoder(resp.Body).Decode(internalServerError) s.NoError(err) s.Contains(internalServerError.Message, "Cannot open: ") _ = resp.Body.Close() s.Run("File content can be printed on runner", func() { s.assertFileContent(runnerID, tests.DefaultFileName, string(newFileContent)) }) }) s.Run("AWS/If one file produces permission denied error, others are still copied", func() { for _, environmentID := range environmentIDs { if environmentID == tests.DefaultEnvironmentIDAsInteger { continue } s.Run(environmentID.ToString(), func() { runnerID, err := ProvideRunner(&dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)}) s.NoError(err) newFileContent := []byte("New content") copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{ Copy: []dto.File{ {Path: "/proc/1/environ", Content: []byte(tests.DefaultFileContent)}, {Path: tests.DefaultFileName, Content: newFileContent}, }, }) s.Require().NoError(err) resp, err := helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), "application/json", bytes.NewReader(copyFilesRequestByteString)) s.NoError(err) s.Equal(http.StatusNoContent, resp.StatusCode) _ = resp.Body.Close() stdout, stderr := s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName) s.Equal(string(newFileContent), stdout) s.Contains(stderr, "Exception") }) } }) } func (s *E2ETestSuite) TestGetFileContent_Nomad() { runnerID, err := ProvideRunner(&dto.RunnerRequest{ ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, }) require.NoError(s.T(), err) s.Run("Not Found", func() { getFileURL, err := url.Parse(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.FileContentRawPath)) s.Require().NoError(err) getFileURL.RawQuery = fmt.Sprintf("%s=%s", api.PathKey, tests.DefaultFileName) response, err := http.Get(getFileURL.String()) s.Require().NoError(err) s.Equal(http.StatusNotFound, response.StatusCode) }) s.Run("Ok", func() { newFileContent := []byte("New content") resp, err := CopyFiles(runnerID, &dto.UpdateFileSystemRequest{ Copy: []dto.File{{Path: tests.DefaultFileName, Content: newFileContent}}, }) s.Require().NoError(err) s.Equal(http.StatusNoContent, resp.StatusCode) getFileURL, err := url.Parse(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.FileContentRawPath)) s.Require().NoError(err) getFileURL.RawQuery = fmt.Sprintf("%s=%s", api.PathKey, tests.DefaultFileName) response, err := http.Get(getFileURL.String()) s.Require().NoError(err) s.Equal(http.StatusOK, response.StatusCode) content, err := io.ReadAll(response.Body) s.Require().NoError(err) s.Equal(newFileContent, content) }) } func (s *E2ETestSuite) TestRunnerGetsDestroyedAfterInactivityTimeout() { for _, environmentID := range environmentIDs { s.Run(environmentID.ToString(), func() { inactivityTimeout := 2 // seconds runnerID, err := ProvideRunner(&dto.RunnerRequest{ ExecutionEnvironmentID: int(environmentID), InactivityTimeout: inactivityTimeout, }) s.Require().NoError(err) executionTerminated := make(chan bool) var lastMessage *dto.WebSocketMessage go func() { webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, &dto.ExecutionRequest{Command: "sleep infinity"}) s.Require().NoError(err) connection, err := ConnectToWebSocket(webSocketURL) s.Require().NoError(err) messages, err := helpers.ReceiveAllWebSocketMessages(connection) if !s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) { s.Fail("websocket abnormal closure") } controlMessages := helpers.WebSocketControlMessages(messages) s.Require().NotEmpty(controlMessages) lastMessage = controlMessages[len(controlMessages)-1] log.Warn("") executionTerminated <- true }() s.Require().True(tests.ChannelReceivesSomething(executionTerminated, time.Duration(inactivityTimeout+5)*time.Second)) s.Equal(dto.WebSocketMetaTimeout, lastMessage.Type) }) } } func (s *E2ETestSuite) assertFileContent(runnerID, fileName, expectedContent string) { stdout, stderr := s.PrintContentOfFileOnRunner(runnerID, fileName) s.Equal(expectedContent, stdout) s.Equal("", stderr) } func (s *E2ETestSuite) PrintContentOfFileOnRunner(runnerID, filename string) (stdout, stderr string) { webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, &dto.ExecutionRequest{ Command: fmt.Sprintf("cat %s", filename), TimeLimit: int(tests.DefaultTestTimeout.Seconds()), }) s.Require().NoError(err) connection, err := ConnectToWebSocket(webSocketURL) s.Require().NoError(err) messages, err := helpers.ReceiveAllWebSocketMessages(connection) s.Require().Error(err) s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) stdout, stderr, _ = helpers.WebSocketOutputMessages(messages) return stdout, stderr }