added k8s stub adapter for execution environment
This commit is contained in:
@ -1,85 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/internal/environment"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mockHTTPHandler(writer http.ResponseWriter, _ *http.Request) {
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type MainTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
}
|
||||
|
||||
func TestMainTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MainTestSuite))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestNewRouterV1WithAuthenticationDisabled() {
|
||||
config.Config.Server.Token = ""
|
||||
router := mux.NewRouter()
|
||||
m := &environment.ManagerHandlerMock{}
|
||||
m.On("Statistics").Return(make(map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData))
|
||||
configureV1Router(router, nil, m)
|
||||
|
||||
s.Run("health route is accessible", func() {
|
||||
request, err := http.NewRequest(http.MethodGet, "/api/v1/health", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNoContent, recorder.Code)
|
||||
})
|
||||
|
||||
s.Run("added route is accessible", func() {
|
||||
router.HandleFunc("/api/v1/test", mockHTTPHandler)
|
||||
request, err := http.NewRequest(http.MethodGet, "/api/v1/test", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestNewRouterV1WithAuthenticationEnabled() {
|
||||
config.Config.Server.Token = "TestToken"
|
||||
router := mux.NewRouter()
|
||||
m := &environment.ManagerHandlerMock{}
|
||||
m.On("Statistics").Return(make(map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData))
|
||||
configureV1Router(router, nil, m)
|
||||
|
||||
s.Run("health route is accessible", func() {
|
||||
request, err := http.NewRequest(http.MethodGet, "/api/v1/health", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNoContent, recorder.Code)
|
||||
})
|
||||
|
||||
s.Run("protected route is not accessible", func() {
|
||||
// request an available API route that should be guarded by authentication.
|
||||
// (which one, in particular, does not matter here)
|
||||
request, err := http.NewRequest(http.MethodPost, "/api/v1/runners", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusUnauthorized, recorder.Code)
|
||||
})
|
||||
config.Config.Server.Token = ""
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testToken = "C0rr3ctT0k3n"
|
||||
|
||||
type AuthenticationMiddlewareTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
request *http.Request
|
||||
recorder *httptest.ResponseRecorder
|
||||
httpAuthenticationMiddleware http.Handler
|
||||
}
|
||||
|
||||
func (s *AuthenticationMiddlewareTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
correctAuthenticationToken = []byte(testToken)
|
||||
s.recorder = httptest.NewRecorder()
|
||||
request, err := http.NewRequest(http.MethodGet, "/api/v1/test", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
s.request = request
|
||||
s.httpAuthenticationMiddleware = HTTPAuthenticationMiddleware(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *AuthenticationMiddlewareTestSuite) TearDownTest() {
|
||||
defer s.MemoryLeakTestSuite.TearDownTest()
|
||||
correctAuthenticationToken = []byte(nil)
|
||||
}
|
||||
|
||||
func (s *AuthenticationMiddlewareTestSuite) TestReturns401WhenHeaderUnset() {
|
||||
s.httpAuthenticationMiddleware.ServeHTTP(s.recorder, s.request)
|
||||
assert.Equal(s.T(), http.StatusUnauthorized, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *AuthenticationMiddlewareTestSuite) TestReturns401WhenTokenWrong() {
|
||||
s.request.Header.Set(TokenHeader, "Wr0ngT0k3n")
|
||||
s.httpAuthenticationMiddleware.ServeHTTP(s.recorder, s.request)
|
||||
assert.Equal(s.T(), http.StatusUnauthorized, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *AuthenticationMiddlewareTestSuite) TestWarnsWhenUnauthorized() {
|
||||
var hook *test.Hook
|
||||
logger, hook := test.NewNullLogger()
|
||||
log = logger.WithField("pkg", "api/auth")
|
||||
|
||||
s.request.Header.Set(TokenHeader, "Wr0ngT0k3n")
|
||||
s.httpAuthenticationMiddleware.ServeHTTP(s.recorder, s.request)
|
||||
|
||||
assert.Equal(s.T(), http.StatusUnauthorized, s.recorder.Code)
|
||||
assert.Equal(s.T(), logrus.WarnLevel, hook.LastEntry().Level)
|
||||
assert.Equal(s.T(), hook.LastEntry().Data["token"], "Wr0ngT0k3n")
|
||||
}
|
||||
|
||||
func (s *AuthenticationMiddlewareTestSuite) TestPassesWhenTokenCorrect() {
|
||||
s.request.Header.Set(TokenHeader, testToken)
|
||||
s.httpAuthenticationMiddleware.ServeHTTP(s.recorder, s.request)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, s.recorder.Code)
|
||||
}
|
||||
|
||||
func TestHTTPAuthenticationMiddleware(t *testing.T) {
|
||||
suite.Run(t, new(AuthenticationMiddlewareTestSuite))
|
||||
}
|
||||
|
||||
func TestInitializeAuthentication(t *testing.T) {
|
||||
t.Run("if token unset", func(t *testing.T) {
|
||||
config.Config.Server.Token = ""
|
||||
initialized := InitializeAuthentication()
|
||||
assert.Equal(t, false, initialized)
|
||||
assert.Equal(t, []byte(nil), correctAuthenticationToken, "it should not set correctAuthenticationToken")
|
||||
})
|
||||
t.Run("if token set", func(t *testing.T) {
|
||||
config.Config.Server.Token = testToken
|
||||
initialized := InitializeAuthentication()
|
||||
assert.Equal(t, true, initialized)
|
||||
assert.Equal(t, []byte(testToken), correctAuthenticationToken, "it should set correctAuthenticationToken")
|
||||
config.Config.Server.Token = ""
|
||||
correctAuthenticationToken = []byte(nil)
|
||||
})
|
||||
}
|
@ -1,308 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/openHPI/poseidon/internal/environment"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const jobHCLBasicFormat = "job \"%s\" {}"
|
||||
|
||||
type EnvironmentControllerTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
manager *environment.ManagerHandlerMock
|
||||
router *mux.Router
|
||||
}
|
||||
|
||||
func TestEnvironmentControllerTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(EnvironmentControllerTestSuite))
|
||||
}
|
||||
|
||||
func (s *EnvironmentControllerTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
s.manager = &environment.ManagerHandlerMock{}
|
||||
s.router = NewRouter(nil, s.manager)
|
||||
}
|
||||
|
||||
func (s *EnvironmentControllerTestSuite) TestList() {
|
||||
call := s.manager.On("List", mock.AnythingOfType("bool"))
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{[]runner.ExecutionEnvironment{}, nil}
|
||||
})
|
||||
path, err := s.router.Get(listRouteName).URL()
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("with no Environments", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
var environmentsResponse ExecutionEnvironmentsResponse
|
||||
err = json.NewDecoder(recorder.Result().Body).Decode(&environmentsResponse)
|
||||
s.Require().NoError(err)
|
||||
_ = recorder.Result().Body.Close()
|
||||
|
||||
s.Empty(environmentsResponse.ExecutionEnvironments)
|
||||
})
|
||||
s.manager.Calls = []mock.Call{}
|
||||
|
||||
s.Run("with fetch", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
query := path.Query()
|
||||
query.Set("fetch", "true")
|
||||
path.RawQuery = query.Encode()
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
s.manager.AssertCalled(s.T(), "List", true)
|
||||
})
|
||||
s.manager.Calls = []mock.Call{}
|
||||
|
||||
s.Run("with bad fetch", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
query := path.Query()
|
||||
query.Set("fetch", "YouDecide")
|
||||
path.RawQuery = query.Encode()
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusBadRequest, recorder.Code)
|
||||
s.manager.AssertNotCalled(s.T(), "List")
|
||||
})
|
||||
|
||||
s.Run("returns multiple environments", func() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
|
||||
var firstEnvironment, secondEnvironment *environment.NomadEnvironment
|
||||
call.Run(func(args mock.Arguments) {
|
||||
firstEnvironment, err = environment.NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, apiMock,
|
||||
fmt.Sprintf(jobHCLBasicFormat, nomad.TemplateJobID(tests.DefaultEnvironmentIDAsInteger)))
|
||||
s.Require().NoError(err)
|
||||
secondEnvironment, err = environment.NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, apiMock,
|
||||
fmt.Sprintf(jobHCLBasicFormat, nomad.TemplateJobID(tests.DefaultEnvironmentIDAsInteger)))
|
||||
s.Require().NoError(err)
|
||||
call.ReturnArguments = mock.Arguments{[]runner.ExecutionEnvironment{firstEnvironment, secondEnvironment}, nil}
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
paramMap := make(map[string]interface{})
|
||||
err := json.NewDecoder(recorder.Result().Body).Decode(¶mMap)
|
||||
s.Require().NoError(err)
|
||||
environmentsInterface, ok := paramMap["executionEnvironments"]
|
||||
s.Require().True(ok)
|
||||
environments, ok := environmentsInterface.([]interface{})
|
||||
s.Require().True(ok)
|
||||
s.Equal(2, len(environments))
|
||||
|
||||
err = firstEnvironment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.NoError(err)
|
||||
err = secondEnvironment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.NoError(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *EnvironmentControllerTestSuite) TestGet() {
|
||||
call := s.manager.On("Get", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("bool"))
|
||||
path, err := s.router.Get(getRouteName).URL(executionEnvironmentIDKey, tests.DefaultEnvironmentIDAsString)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("with unknown environment", func() {
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{nil, runner.ErrUnknownExecutionEnvironment}
|
||||
})
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNotFound, recorder.Code)
|
||||
s.manager.AssertCalled(s.T(), "Get", dto.EnvironmentID(0), false)
|
||||
})
|
||||
s.manager.Calls = []mock.Call{}
|
||||
|
||||
s.Run("not found with fetch", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
query := path.Query()
|
||||
query.Set("fetch", "true")
|
||||
path.RawQuery = query.Encode()
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{nil, runner.ErrUnknownExecutionEnvironment}
|
||||
})
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNotFound, recorder.Code)
|
||||
s.manager.AssertCalled(s.T(), "Get", dto.EnvironmentID(0), true)
|
||||
})
|
||||
s.manager.Calls = []mock.Call{}
|
||||
|
||||
s.Run("returns environment", func() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
|
||||
var testEnvironment *environment.NomadEnvironment
|
||||
call.Run(func(args mock.Arguments) {
|
||||
testEnvironment, err = environment.NewNomadEnvironment(tests.DefaultEnvironmentIDAsInteger, apiMock,
|
||||
fmt.Sprintf(jobHCLBasicFormat, nomad.TemplateJobID(tests.DefaultEnvironmentIDAsInteger)))
|
||||
s.Require().NoError(err)
|
||||
call.ReturnArguments = mock.Arguments{testEnvironment, nil}
|
||||
})
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
var environmentParams map[string]interface{}
|
||||
err := json.NewDecoder(recorder.Result().Body).Decode(&environmentParams)
|
||||
s.Require().NoError(err)
|
||||
idInterface, ok := environmentParams["id"]
|
||||
s.Require().True(ok)
|
||||
idFloat, ok := idInterface.(float64)
|
||||
s.Require().True(ok)
|
||||
s.Equal(tests.DefaultEnvironmentIDAsInteger, int(idFloat))
|
||||
|
||||
err = testEnvironment.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.NoError(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *EnvironmentControllerTestSuite) TestDelete() {
|
||||
call := s.manager.On("Delete", mock.AnythingOfType("dto.EnvironmentID"))
|
||||
path, err := s.router.Get(deleteRouteName).URL(executionEnvironmentIDKey, tests.DefaultEnvironmentIDAsString)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodDelete, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("environment not found", func() {
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{false, nil}
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNotFound, recorder.Code)
|
||||
})
|
||||
|
||||
s.Run("environment deleted", func() {
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{true, nil}
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNoContent, recorder.Code)
|
||||
})
|
||||
|
||||
s.manager.Calls = []mock.Call{}
|
||||
s.Run("with bad environment id", func() {
|
||||
_, err := s.router.Get(deleteRouteName).URL(executionEnvironmentIDKey, "MagicNonNumberID")
|
||||
s.Error(err)
|
||||
})
|
||||
}
|
||||
|
||||
type CreateOrUpdateEnvironmentTestSuite struct {
|
||||
EnvironmentControllerTestSuite
|
||||
path string
|
||||
id dto.EnvironmentID
|
||||
body []byte
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateEnvironmentTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(CreateOrUpdateEnvironmentTestSuite))
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) SetupTest() {
|
||||
s.EnvironmentControllerTestSuite.SetupTest()
|
||||
s.id = tests.DefaultEnvironmentIDAsInteger
|
||||
testURL, err := s.router.Get(createOrUpdateRouteName).URL(executionEnvironmentIDKey, strconv.Itoa(int(s.id)))
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
s.path = testURL.String()
|
||||
s.body, err = json.Marshal(dto.ExecutionEnvironmentRequest{})
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) recordRequest() *httptest.ResponseRecorder {
|
||||
recorder := httptest.NewRecorder()
|
||||
request, err := http.NewRequest(http.MethodPut, s.path, bytes.NewReader(s.body))
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
return recorder
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsBadRequestWhenBadBody() {
|
||||
s.body = []byte{}
|
||||
recorder := s.recordRequest()
|
||||
s.Equal(http.StatusBadRequest, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsInternalServerErrorWhenManagerReturnsError() {
|
||||
testError := tests.ErrDefault
|
||||
s.manager.
|
||||
On("CreateOrUpdate", s.id, mock.AnythingOfType("dto.ExecutionEnvironmentRequest"), mock.Anything).
|
||||
Return(false, testError)
|
||||
|
||||
recorder := s.recordRequest()
|
||||
s.Equal(http.StatusInternalServerError, recorder.Code)
|
||||
s.Contains(recorder.Body.String(), testError.Error())
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsCreatedIfNewEnvironment() {
|
||||
s.manager.
|
||||
On("CreateOrUpdate", s.id, mock.AnythingOfType("dto.ExecutionEnvironmentRequest"), mock.Anything).
|
||||
Return(true, nil)
|
||||
|
||||
recorder := s.recordRequest()
|
||||
s.Equal(http.StatusCreated, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsNoContentIfNotNewEnvironment() {
|
||||
s.manager.
|
||||
On("CreateOrUpdate", s.id, mock.AnythingOfType("dto.ExecutionEnvironmentRequest"), mock.Anything).
|
||||
Return(false, nil)
|
||||
|
||||
recorder := s.recordRequest()
|
||||
s.Equal(http.StatusNoContent, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) TestReturnsNotFoundOnNonIntegerID() {
|
||||
s.path = strings.Join([]string{BasePath, EnvironmentsPath, "/", "invalid-id"}, "")
|
||||
recorder := s.recordRequest()
|
||||
s.Equal(http.StatusNotFound, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *CreateOrUpdateEnvironmentTestSuite) TestFailsOnTooLargeID() {
|
||||
tooLargeIntStr := strconv.Itoa(math.MaxInt64) + "0"
|
||||
s.path = strings.Join([]string{BasePath, EnvironmentsPath, "/", tooLargeIntStr}, "")
|
||||
recorder := s.recordRequest()
|
||||
s.Equal(http.StatusBadRequest, recorder.Code)
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/internal/environment"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
)
|
||||
|
||||
func (s *MainTestSuite) TestHealth() {
|
||||
s.Run("returns StatusNoContent as default", func() {
|
||||
request, err := http.NewRequest(http.MethodGet, "/health", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
manager := &environment.ManagerHandlerMock{}
|
||||
manager.On("Statistics").Return(map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData{})
|
||||
|
||||
Health(manager).ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNoContent, recorder.Code)
|
||||
})
|
||||
s.Run("returns InternalServerError for warnings and errors", func() {
|
||||
s.Run("Prewarming Pool Alert", func() {
|
||||
request, err := http.NewRequest(http.MethodGet, "/health", http.NoBody)
|
||||
if err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
manager := &environment.ManagerHandlerMock{}
|
||||
manager.On("Statistics").Return(map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData{
|
||||
tests.DefaultEnvironmentIDAsInteger: {
|
||||
ID: tests.DefaultEnvironmentIDAsInteger,
|
||||
PrewarmingPoolSize: 3,
|
||||
IdleRunners: 1,
|
||||
},
|
||||
})
|
||||
config.Config.Server.Alert.PrewarmingPoolThreshold = 0.5
|
||||
|
||||
Health(manager).ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusServiceUnavailable, recorder.Code)
|
||||
|
||||
b, err := io.ReadAll(recorder.Body)
|
||||
s.Require().NoError(err)
|
||||
var details dto.InternalServerError
|
||||
err = json.Unmarshal(b, &details)
|
||||
s.Require().NoError(err)
|
||||
s.Contains(details.Message, ErrorPrewarmingPoolDepleting.Error())
|
||||
})
|
||||
})
|
||||
}
|
@ -1,501 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/pkg/monitoring"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const invalidID = "some-invalid-runner-id"
|
||||
|
||||
type MiddlewareTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
manager *runner.ManagerMock
|
||||
router *mux.Router
|
||||
runner runner.Runner
|
||||
capturedRunner runner.Runner
|
||||
runnerRequest func(string) *http.Request
|
||||
}
|
||||
|
||||
func (s *MiddlewareTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
s.manager = &runner.ManagerMock{}
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
s.runner = runner.NewNomadJob(tests.DefaultRunnerID, nil, apiMock, nil)
|
||||
s.capturedRunner = nil
|
||||
s.runnerRequest = func(runnerId string) *http.Request {
|
||||
path, err := s.router.Get("test-runner-id").URL(RunnerIDKey, runnerId)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPost, path.String(), http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
return request
|
||||
}
|
||||
runnerRouteHandler := func(writer http.ResponseWriter, request *http.Request) {
|
||||
var ok bool
|
||||
s.capturedRunner, ok = runner.FromContext(request.Context())
|
||||
if ok {
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
s.router = mux.NewRouter()
|
||||
runnerController := &RunnerController{s.manager, s.router}
|
||||
s.router.Use(monitoring.InfluxDB2Middleware)
|
||||
s.router.Use(runnerController.findRunnerMiddleware)
|
||||
s.router.HandleFunc(fmt.Sprintf("/test/{%s}", RunnerIDKey), runnerRouteHandler).Name("test-runner-id")
|
||||
}
|
||||
|
||||
func (s *MiddlewareTestSuite) TearDownTest() {
|
||||
defer s.MemoryLeakTestSuite.TearDownTest()
|
||||
err := s.runner.Destroy(nil)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func TestMiddlewareTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MiddlewareTestSuite))
|
||||
}
|
||||
|
||||
func (s *MiddlewareTestSuite) TestFindRunnerMiddlewareIfRunnerExists() {
|
||||
s.manager.On("Get", s.runner.ID()).Return(s.runner, nil)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, s.runnerRequest(s.runner.ID()))
|
||||
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
s.Equal(s.runner, s.capturedRunner)
|
||||
}
|
||||
|
||||
func (s *MiddlewareTestSuite) TestFindRunnerMiddlewareIfRunnerDoesNotExist() {
|
||||
s.manager.On("Get", invalidID).Return(nil, runner.ErrRunnerNotFound)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, s.runnerRequest(invalidID))
|
||||
|
||||
s.Equal(http.StatusGone, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *MiddlewareTestSuite) TestFindRunnerMiddlewareDoesNotEarlyRespond() {
|
||||
body := strings.NewReader(strings.Repeat("A", 798968))
|
||||
|
||||
path, err := s.router.Get("test-runner-id").URL(RunnerIDKey, invalidID)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPost, path.String(), body)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.manager.On("Get", mock.AnythingOfType("string")).Return(nil, runner.ErrRunnerNotFound)
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
|
||||
s.Equal(http.StatusGone, recorder.Code)
|
||||
s.Equal(0, body.Len()) // No data should be unread
|
||||
}
|
||||
|
||||
func TestRunnerRouteTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(RunnerRouteTestSuite))
|
||||
}
|
||||
|
||||
type RunnerRouteTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
runnerManager *runner.ManagerMock
|
||||
router *mux.Router
|
||||
runner runner.Runner
|
||||
executionID string
|
||||
}
|
||||
|
||||
func (s *RunnerRouteTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
s.runnerManager = &runner.ManagerMock{}
|
||||
s.router = NewRouter(s.runnerManager, nil)
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
s.runner = runner.NewNomadJob("some-id", nil, apiMock, func(_ runner.Runner) error { return nil })
|
||||
s.executionID = "execution"
|
||||
s.runner.StoreExecution(s.executionID, &dto.ExecutionRequest{})
|
||||
s.runnerManager.On("Get", s.runner.ID()).Return(s.runner, nil)
|
||||
}
|
||||
|
||||
func (s *RunnerRouteTestSuite) TearDownTest() {
|
||||
defer s.MemoryLeakTestSuite.TearDownTest()
|
||||
s.Require().NoError(s.runner.Destroy(nil))
|
||||
}
|
||||
|
||||
func TestProvideRunnerTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ProvideRunnerTestSuite))
|
||||
}
|
||||
|
||||
type ProvideRunnerTestSuite struct {
|
||||
RunnerRouteTestSuite
|
||||
defaultRequest *http.Request
|
||||
path string
|
||||
}
|
||||
|
||||
func (s *ProvideRunnerTestSuite) SetupTest() {
|
||||
s.RunnerRouteTestSuite.SetupTest()
|
||||
|
||||
path, err := s.router.Get(ProvideRoute).URL()
|
||||
s.Require().NoError(err)
|
||||
s.path = path.String()
|
||||
|
||||
runnerRequest := dto.RunnerRequest{ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger}
|
||||
body, err := json.Marshal(runnerRequest)
|
||||
s.Require().NoError(err)
|
||||
s.defaultRequest, err = http.NewRequest(http.MethodPost, s.path, bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *ProvideRunnerTestSuite) TestValidRequestReturnsRunner() {
|
||||
s.runnerManager.
|
||||
On("Claim", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("int")).
|
||||
Return(s.runner, nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
s.router.ServeHTTP(recorder, s.defaultRequest)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
s.Run("response contains runnerId", func() {
|
||||
var runnerResponse dto.RunnerResponse
|
||||
err := json.NewDecoder(recorder.Result().Body).Decode(&runnerResponse)
|
||||
s.Require().NoError(err)
|
||||
_ = recorder.Result().Body.Close()
|
||||
s.Equal(s.runner.ID(), runnerResponse.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ProvideRunnerTestSuite) TestInvalidRequestReturnsBadRequest() {
|
||||
badRequest, err := http.NewRequest(http.MethodPost, s.path, strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
s.router.ServeHTTP(recorder, badRequest)
|
||||
s.Equal(http.StatusBadRequest, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *ProvideRunnerTestSuite) TestWhenExecutionEnvironmentDoesNotExistReturnsNotFound() {
|
||||
s.runnerManager.
|
||||
On("Claim", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("int")).
|
||||
Return(nil, runner.ErrUnknownExecutionEnvironment)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
s.router.ServeHTTP(recorder, s.defaultRequest)
|
||||
s.Equal(http.StatusNotFound, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *ProvideRunnerTestSuite) TestWhenNoRunnerAvailableReturnsNomadOverload() {
|
||||
s.runnerManager.
|
||||
On("Claim", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("int")).
|
||||
Return(nil, runner.ErrNoRunnersAvailable)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
s.router.ServeHTTP(recorder, s.defaultRequest)
|
||||
s.Equal(http.StatusInternalServerError, recorder.Code)
|
||||
|
||||
var internalServerError dto.InternalServerError
|
||||
err := json.NewDecoder(recorder.Result().Body).Decode(&internalServerError)
|
||||
s.Require().NoError(err)
|
||||
_ = recorder.Result().Body.Close()
|
||||
s.Equal(dto.ErrorNomadOverload, internalServerError.ErrorCode)
|
||||
}
|
||||
|
||||
func (s *RunnerRouteTestSuite) TestExecuteRoute() {
|
||||
path, err := s.router.Get(ExecutePath).URL(RunnerIDKey, s.runner.ID())
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("valid request", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
executionRequest := dto.ExecutionRequest{
|
||||
Command: "command",
|
||||
TimeLimit: 10,
|
||||
Environment: nil,
|
||||
}
|
||||
body, err := json.Marshal(executionRequest)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPost, path.String(), bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
|
||||
var webSocketResponse dto.ExecutionResponse
|
||||
err = json.NewDecoder(recorder.Result().Body).Decode(&webSocketResponse)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
s.Run("creates an execution request for the runner", func() {
|
||||
webSocketURL, err := url.Parse(webSocketResponse.WebSocketURL)
|
||||
s.Require().NoError(err)
|
||||
executionID := webSocketURL.Query().Get(ExecutionIDKey)
|
||||
ok := s.runner.ExecutionExists(executionID)
|
||||
|
||||
s.True(ok, "No execution request with this id: ", executionID)
|
||||
})
|
||||
})
|
||||
|
||||
s.Run("invalid request", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
body := ""
|
||||
request, err := http.NewRequest(http.MethodPost, path.String(), strings.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
|
||||
s.Equal(http.StatusBadRequest, recorder.Code)
|
||||
})
|
||||
|
||||
s.Run("forbidden characters in command", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
executionRequest := dto.ExecutionRequest{
|
||||
Command: "echo 'forbidden'",
|
||||
TimeLimit: 10,
|
||||
}
|
||||
body, err := json.Marshal(executionRequest)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPost, path.String(), bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusBadRequest, recorder.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateFileSystemRouteTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(UpdateFileSystemRouteTestSuite))
|
||||
}
|
||||
|
||||
type UpdateFileSystemRouteTestSuite struct {
|
||||
RunnerRouteTestSuite
|
||||
path string
|
||||
recorder *httptest.ResponseRecorder
|
||||
runnerMock *runner.RunnerMock
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) SetupTest() {
|
||||
s.RunnerRouteTestSuite.SetupTest()
|
||||
routeURL, err := s.router.Get(UpdateFileSystemPath).URL(RunnerIDKey, tests.DefaultMockID)
|
||||
s.Require().NoError(err)
|
||||
s.path = routeURL.String()
|
||||
s.runnerMock = &runner.RunnerMock{}
|
||||
s.runnerMock.On("ID").Return(tests.DefaultMockID)
|
||||
s.runnerMock.On("Environment").Return(dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger))
|
||||
s.runnerManager.On("Get", tests.DefaultMockID).Return(s.runnerMock, nil)
|
||||
s.recorder = httptest.NewRecorder()
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemReturnsNoContentOnValidRequest() {
|
||||
s.runnerMock.On("UpdateFileSystem", mock.AnythingOfType("*dto.UpdateFileSystemRequest"), mock.Anything).
|
||||
Return(nil)
|
||||
|
||||
copyRequest := dto.UpdateFileSystemRequest{}
|
||||
body, err := json.Marshal(copyRequest)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPatch, s.path, bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusNoContent, s.recorder.Code)
|
||||
s.runnerMock.AssertCalled(s.T(), "UpdateFileSystem",
|
||||
mock.AnythingOfType("*dto.UpdateFileSystemRequest"), mock.Anything)
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemReturnsBadRequestOnInvalidRequestBody() {
|
||||
request, err := http.NewRequest(http.MethodPatch, s.path, strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusBadRequest, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemToNonExistingRunnerReturnsGone() {
|
||||
s.runnerManager.On("Get", invalidID).Return(nil, runner.ErrRunnerNotFound)
|
||||
path, err := s.router.Get(UpdateFileSystemPath).URL(RunnerIDKey, invalidID)
|
||||
s.Require().NoError(err)
|
||||
copyRequest := dto.UpdateFileSystemRequest{}
|
||||
body, err := json.Marshal(copyRequest)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPatch, path.String(), bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusGone, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemReturnsInternalServerErrorWhenCopyFailed() {
|
||||
s.runnerMock.
|
||||
On("UpdateFileSystem", mock.AnythingOfType("*dto.UpdateFileSystemRequest"), mock.Anything).
|
||||
Return(runner.ErrorFileCopyFailed)
|
||||
|
||||
copyRequest := dto.UpdateFileSystemRequest{}
|
||||
body, err := json.Marshal(copyRequest)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPatch, s.path, bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusInternalServerError, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestListFileSystem() {
|
||||
routeURL, err := s.router.Get(UpdateFileSystemPath).URL(RunnerIDKey, tests.DefaultMockID)
|
||||
s.Require().NoError(err)
|
||||
mockCall := s.runnerMock.On("ListFileSystem", mock.AnythingOfType("string"),
|
||||
mock.AnythingOfType("bool"), mock.Anything, mock.AnythingOfType("bool"), mock.Anything)
|
||||
|
||||
s.Run("default parameters", func() {
|
||||
mockCall.Run(func(args mock.Arguments) {
|
||||
path, ok := args.Get(0).(string)
|
||||
s.True(ok)
|
||||
s.Equal("./", path)
|
||||
recursive, ok := args.Get(1).(bool)
|
||||
s.True(ok)
|
||||
s.True(recursive)
|
||||
mockCall.ReturnArguments = mock.Arguments{nil}
|
||||
})
|
||||
request, err := http.NewRequest(http.MethodGet, routeURL.String(), strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusOK, s.recorder.Code)
|
||||
})
|
||||
|
||||
s.recorder = httptest.NewRecorder()
|
||||
s.Run("passed parameters", func() {
|
||||
expectedPath := "/flag"
|
||||
|
||||
mockCall.Run(func(args mock.Arguments) {
|
||||
path, ok := args.Get(0).(string)
|
||||
s.True(ok)
|
||||
s.Equal(expectedPath, path)
|
||||
recursive, ok := args.Get(1).(bool)
|
||||
s.True(ok)
|
||||
s.False(recursive)
|
||||
mockCall.ReturnArguments = mock.Arguments{nil}
|
||||
})
|
||||
|
||||
query := routeURL.Query()
|
||||
query.Set(PathKey, expectedPath)
|
||||
query.Set(RecursiveKey, strconv.FormatBool(false))
|
||||
routeURL.RawQuery = query.Encode()
|
||||
|
||||
request, err := http.NewRequest(http.MethodGet, routeURL.String(), strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusOK, s.recorder.Code)
|
||||
})
|
||||
|
||||
s.recorder = httptest.NewRecorder()
|
||||
s.Run("Internal Server Error on failure", func() {
|
||||
mockCall.Run(func(args mock.Arguments) {
|
||||
mockCall.ReturnArguments = mock.Arguments{runner.ErrRunnerNotFound}
|
||||
})
|
||||
|
||||
request, err := http.NewRequest(http.MethodGet, routeURL.String(), strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusInternalServerError, s.recorder.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestFileContent() {
|
||||
routeURL, err := s.router.Get(FileContentRawPath).URL(RunnerIDKey, tests.DefaultMockID)
|
||||
s.Require().NoError(err)
|
||||
mockCall := s.runnerMock.On("GetFileContent",
|
||||
mock.AnythingOfType("string"), mock.Anything, mock.AnythingOfType("bool"), mock.Anything)
|
||||
|
||||
s.Run("Not Found", func() {
|
||||
mockCall.Return(runner.ErrFileNotFound)
|
||||
request, err := http.NewRequest(http.MethodGet, routeURL.String(), strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusFailedDependency, s.recorder.Code)
|
||||
})
|
||||
|
||||
s.recorder = httptest.NewRecorder()
|
||||
s.Run("Unknown Error", func() {
|
||||
mockCall.Return(nomad.ErrorExecutorCommunicationFailed)
|
||||
request, err := http.NewRequest(http.MethodGet, routeURL.String(), strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusInternalServerError, s.recorder.Code)
|
||||
})
|
||||
|
||||
s.recorder = httptest.NewRecorder()
|
||||
s.Run("No Error", func() {
|
||||
mockCall.Return(nil)
|
||||
request, err := http.NewRequest(http.MethodGet, routeURL.String(), strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusOK, s.recorder.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteRunnerRouteTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(DeleteRunnerRouteTestSuite))
|
||||
}
|
||||
|
||||
type DeleteRunnerRouteTestSuite struct {
|
||||
RunnerRouteTestSuite
|
||||
path string
|
||||
}
|
||||
|
||||
func (s *DeleteRunnerRouteTestSuite) SetupTest() {
|
||||
s.RunnerRouteTestSuite.SetupTest()
|
||||
deleteURL, err := s.router.Get(DeleteRoute).URL(RunnerIDKey, s.runner.ID())
|
||||
s.Require().NoError(err)
|
||||
s.path = deleteURL.String()
|
||||
}
|
||||
|
||||
func (s *DeleteRunnerRouteTestSuite) TestValidRequestReturnsNoContent() {
|
||||
s.runnerManager.On("Return", s.runner).Return(nil)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request, err := http.NewRequest(http.MethodDelete, s.path, http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
|
||||
s.Equal(http.StatusNoContent, recorder.Code)
|
||||
|
||||
s.Run("runner was returned to runner manager", func() {
|
||||
s.runnerManager.AssertCalled(s.T(), "Return", s.runner)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DeleteRunnerRouteTestSuite) TestReturnInternalServerErrorWhenApiCallToNomadFailed() {
|
||||
s.runnerManager.On("Return", s.runner).Return(tests.ErrDefault)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request, err := http.NewRequest(http.MethodDelete, s.path, http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
|
||||
s.Equal(http.StatusInternalServerError, recorder.Code)
|
||||
}
|
||||
|
||||
func (s *DeleteRunnerRouteTestSuite) TestDeleteInvalidRunnerIdReturnsGone() {
|
||||
s.runnerManager.On("Get", mock.AnythingOfType("string")).Return(nil, tests.ErrDefault)
|
||||
deleteURL, err := s.router.Get(DeleteRoute).URL(RunnerIDKey, "1nv4l1dID")
|
||||
s.Require().NoError(err)
|
||||
deletePath := deleteURL.String()
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request, err := http.NewRequest(http.MethodDelete, deletePath, http.NoBody)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
|
||||
s.Equal(http.StatusGone, recorder.Code)
|
||||
}
|
@ -1,494 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/openHPI/poseidon/internal/environment"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/openHPI/poseidon/tests/helpers"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWebSocketTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(WebSocketTestSuite))
|
||||
}
|
||||
|
||||
type WebSocketTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
router *mux.Router
|
||||
executionID string
|
||||
runner runner.Runner
|
||||
apiMock *nomad.ExecutorAPIMock
|
||||
server *httptest.Server
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) SetupTest() {
|
||||
s.MemoryLeakTestSuite.SetupTest()
|
||||
runnerID := "runner-id"
|
||||
s.runner, s.apiMock = newNomadAllocationWithMockedAPIClient(runnerID)
|
||||
|
||||
// default execution
|
||||
s.executionID = tests.DefaultExecutionID
|
||||
s.runner.StoreExecution(s.executionID, &executionRequestLs)
|
||||
mockAPIExecuteLs(s.apiMock)
|
||||
|
||||
runnerManager := &runner.ManagerMock{}
|
||||
runnerManager.On("Get", s.runner.ID()).Return(s.runner, nil)
|
||||
s.router = NewRouter(runnerManager, nil)
|
||||
s.server = httptest.NewServer(s.router)
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TearDownTest() {
|
||||
defer s.MemoryLeakTestSuite.TearDownTest()
|
||||
s.server.Close()
|
||||
err := s.runner.Destroy(nil)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketConnectionCanBeEstablished() {
|
||||
wsURL, err := s.webSocketURL("ws", s.runner.ID(), s.executionID)
|
||||
s.Require().NoError(err)
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
<-time.After(tests.ShortTimeout)
|
||||
err = conn.Close()
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketReturns404IfExecutionDoesNotExist() {
|
||||
wsURL, err := s.webSocketURL("ws", s.runner.ID(), "invalid-execution-id")
|
||||
s.Require().NoError(err)
|
||||
_, response, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().ErrorIs(err, websocket.ErrBadHandshake)
|
||||
s.Equal(http.StatusNotFound, response.StatusCode)
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketReturns400IfRequestedViaHttp() {
|
||||
wsURL, err := s.webSocketURL("http", s.runner.ID(), s.executionID)
|
||||
s.Require().NoError(err)
|
||||
response, err := http.Get(wsURL.String())
|
||||
s.Require().NoError(err)
|
||||
// This functionality is implemented by the WebSocket library.
|
||||
s.Equal(http.StatusBadRequest, response.StatusCode)
|
||||
_, err = io.ReadAll(response.Body)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketConnection() {
|
||||
s.runner.StoreExecution(s.executionID, &executionRequestHead)
|
||||
mockAPIExecuteHead(s.apiMock)
|
||||
wsURL, err := s.webSocketURL("ws", s.runner.ID(), s.executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
err = connection.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("Receives start message", func() {
|
||||
message, err := helpers.ReceiveNextWebSocketMessage(connection)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
})
|
||||
|
||||
s.Run("Executes the request in the runner", func() {
|
||||
<-time.After(tests.ShortTimeout)
|
||||
s.apiMock.AssertCalled(s.T(), "ExecuteCommand",
|
||||
mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.AnythingOfType("bool"),
|
||||
mock.Anything, mock.Anything, mock.Anything)
|
||||
})
|
||||
|
||||
s.Run("Can send input", func() {
|
||||
err = connection.WriteMessage(websocket.TextMessage, []byte("Hello World\n"))
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
|
||||
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
|
||||
s.Run("Receives output message", func() {
|
||||
stdout, _, _ := helpers.WebSocketOutputMessages(messages)
|
||||
s.Equal("Hello World", stdout)
|
||||
})
|
||||
|
||||
s.Run("Receives exit message", func() {
|
||||
controlMessages := helpers.WebSocketControlMessages(messages)
|
||||
s.Require().Equal(1, len(controlMessages))
|
||||
s.Equal(dto.WebSocketExit, controlMessages[0].Type)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestCancelWebSocketConnection() {
|
||||
executionID := "sleeping-execution"
|
||||
s.runner.StoreExecution(executionID, &executionRequestSleep)
|
||||
canceled := mockAPIExecuteSleep(s.apiMock)
|
||||
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
message, err := helpers.ReceiveNextWebSocketMessage(connection)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
|
||||
select {
|
||||
case <-canceled:
|
||||
s.Fail("ExecuteInteractively canceled unexpected")
|
||||
default:
|
||||
}
|
||||
|
||||
err = connection.WriteControl(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second))
|
||||
s.Require().NoError(err)
|
||||
|
||||
select {
|
||||
case <-canceled:
|
||||
case <-time.After(time.Second):
|
||||
s.Fail("ExecuteInteractively not canceled")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebSocketConnectionTimeout() {
|
||||
executionID := "time-out-execution"
|
||||
limitExecution := executionRequestSleep
|
||||
limitExecution.TimeLimit = 2
|
||||
s.runner.StoreExecution(executionID, &limitExecution)
|
||||
canceled := mockAPIExecuteSleep(s.apiMock)
|
||||
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
message, err := helpers.ReceiveNextWebSocketMessage(connection)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
|
||||
select {
|
||||
case <-canceled:
|
||||
s.Fail("ExecuteInteractively canceled unexpected")
|
||||
case <-time.After(time.Duration(limitExecution.TimeLimit-1) * time.Second):
|
||||
<-time.After(time.Second)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-canceled:
|
||||
case <-time.After(time.Second):
|
||||
s.Fail("ExecuteInteractively not canceled")
|
||||
}
|
||||
|
||||
message, err = helpers.ReceiveNextWebSocketMessage(connection)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaTimeout, message.Type)
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketStdoutAndStderr() {
|
||||
executionID := "ls-execution"
|
||||
s.runner.StoreExecution(executionID, &executionRequestLs)
|
||||
mockAPIExecuteLs(s.apiMock)
|
||||
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
stdout, stderr, _ := helpers.WebSocketOutputMessages(messages)
|
||||
|
||||
s.Contains(stdout, "existing-file")
|
||||
|
||||
s.Contains(stderr, "non-existing-file")
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketError() {
|
||||
executionID := "error-execution"
|
||||
s.runner.StoreExecution(executionID, &executionRequestError)
|
||||
mockAPIExecuteError(s.apiMock)
|
||||
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
|
||||
_, _, errMessages := helpers.WebSocketOutputMessages(messages)
|
||||
s.Require().Equal(1, len(errMessages))
|
||||
s.Equal("Error executing the request", errMessages[0])
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) TestWebsocketNonZeroExit() {
|
||||
executionID := "exit-execution"
|
||||
s.runner.StoreExecution(executionID, &executionRequestExitNonZero)
|
||||
mockAPIExecuteExitNonZero(s.apiMock)
|
||||
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
|
||||
controlMessages := helpers.WebSocketControlMessages(messages)
|
||||
s.Equal(2, len(controlMessages))
|
||||
s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit, ExitCode: 42}, controlMessages[1])
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestWebsocketTLS() {
|
||||
runnerID := "runner-id"
|
||||
r, apiMock := newNomadAllocationWithMockedAPIClient(runnerID)
|
||||
|
||||
executionID := tests.DefaultExecutionID
|
||||
r.StoreExecution(executionID, &executionRequestLs)
|
||||
mockAPIExecuteLs(apiMock)
|
||||
|
||||
runnerManager := &runner.ManagerMock{}
|
||||
runnerManager.On("Get", r.ID()).Return(r, nil)
|
||||
router := NewRouter(runnerManager, nil)
|
||||
|
||||
server, err := helpers.StartTLSServer(s.T(), router)
|
||||
s.Require().NoError(err)
|
||||
defer server.Close()
|
||||
|
||||
wsURL, err := webSocketURL("wss", server, router, runnerID, executionID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
config := &tls.Config{RootCAs: nil, InsecureSkipVerify: true} //nolint:gosec // test needs self-signed cert
|
||||
d := websocket.Dialer{TLSClientConfig: config}
|
||||
connection, _, err := d.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
message, err := helpers.ReceiveNextWebSocketMessage(connection)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
_, err = helpers.ReceiveAllWebSocketMessages(connection)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
s.NoError(r.Destroy(nil))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestWebSocketProxyStopsReadingTheWebSocketAfterClosingIt() {
|
||||
apiMock := &nomad.ExecutorAPIMock{}
|
||||
executionID := tests.DefaultExecutionID
|
||||
r, wsURL, cleanup := newRunnerWithNotMockedRunnerManager(s, apiMock, executionID)
|
||||
defer cleanup()
|
||||
|
||||
logger, hook := test.NewNullLogger()
|
||||
log = logger.WithField("pkg", "api")
|
||||
|
||||
r.StoreExecution(executionID, &executionRequestHead)
|
||||
mockAPIExecute(apiMock, &executionRequestHead,
|
||||
func(_ string, ctx context.Context, _ string, _ bool, _ io.Reader, _, _ io.Writer) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, err = helpers.ReceiveAllWebSocketMessages(connection)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
for _, logMsg := range hook.Entries {
|
||||
if logMsg.Level < logrus.InfoLevel {
|
||||
s.Fail(logMsg.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Test suite specific test helpers ---
|
||||
|
||||
func newNomadAllocationWithMockedAPIClient(runnerID string) (runner.Runner, *nomad.ExecutorAPIMock) {
|
||||
executorAPIMock := &nomad.ExecutorAPIMock{}
|
||||
executorAPIMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
manager := &runner.ManagerMock{}
|
||||
manager.On("Return", mock.Anything).Return(nil)
|
||||
r := runner.NewNomadJob(runnerID, nil, executorAPIMock, nil)
|
||||
return r, executorAPIMock
|
||||
}
|
||||
|
||||
func newRunnerWithNotMockedRunnerManager(s *MainTestSuite, apiMock *nomad.ExecutorAPIMock, executionID string) (
|
||||
r runner.Runner, wsURL *url.URL, cleanup func()) {
|
||||
s.T().Helper()
|
||||
apiMock.On("MarkRunnerAsUsed", mock.AnythingOfType("string"), mock.AnythingOfType("int")).Return(nil)
|
||||
apiMock.On("LoadRunnerIDs", mock.AnythingOfType("string")).Return([]string{}, nil)
|
||||
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||
apiMock.On("RegisterRunnerJob", mock.AnythingOfType("*api.Job")).Return(nil)
|
||||
call := apiMock.On("WatchEventStream", mock.Anything, mock.Anything, mock.Anything)
|
||||
call.Run(func(args mock.Arguments) {
|
||||
<-s.TestCtx.Done()
|
||||
call.ReturnArguments = mock.Arguments{nil}
|
||||
})
|
||||
|
||||
runnerManager := runner.NewNomadRunnerManager(apiMock, s.TestCtx)
|
||||
router := NewRouter(runnerManager, nil)
|
||||
s.ExpectedGoroutineIncrease++ // The server is not closing properly. Therefore, we don't even try.
|
||||
server := httptest.NewServer(router)
|
||||
|
||||
runnerID := tests.DefaultRunnerID
|
||||
runnerJob := runner.NewNomadJob(runnerID, nil, apiMock, nil)
|
||||
e, err := environment.NewNomadEnvironment(0, apiMock, "job \"template-0\" {}")
|
||||
s.Require().NoError(err)
|
||||
eID, err := nomad.EnvironmentIDFromRunnerID(runnerID)
|
||||
s.Require().NoError(err)
|
||||
e.SetID(eID)
|
||||
e.SetPrewarmingPoolSize(0)
|
||||
runnerManager.StoreEnvironment(e)
|
||||
e.AddRunner(runnerJob)
|
||||
|
||||
r, err = runnerManager.Claim(e.ID(), int(tests.DefaultTestTimeout.Seconds()))
|
||||
s.Require().NoError(err)
|
||||
wsURL, err = webSocketURL("ws", server, router, r.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
return r, wsURL, func() {
|
||||
err = r.Destroy(tests.ErrCleanupDestroyReason)
|
||||
s.NoError(err)
|
||||
err = e.Delete(tests.ErrCleanupDestroyReason)
|
||||
s.NoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func webSocketURL(scheme string, server *httptest.Server, router *mux.Router,
|
||||
runnerID string, executionID string,
|
||||
) (*url.URL, error) {
|
||||
websocketURL, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err := router.Get(WebsocketPath).URL(RunnerIDKey, runnerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
websocketURL.Scheme = scheme
|
||||
websocketURL.Path = path.Path
|
||||
websocketURL.RawQuery = fmt.Sprintf("executionID=%s", executionID)
|
||||
return websocketURL, nil
|
||||
}
|
||||
|
||||
func (s *WebSocketTestSuite) webSocketURL(scheme, runnerID, executionID string) (*url.URL, error) {
|
||||
return webSocketURL(scheme, s.server, s.router, runnerID, executionID)
|
||||
}
|
||||
|
||||
var executionRequestLs = dto.ExecutionRequest{Command: "ls"}
|
||||
|
||||
// mockAPIExecuteLs mocks the ExecuteCommand of an ExecutorApi to act as if
|
||||
// 'ls existing-file non-existing-file' was executed.
|
||||
func mockAPIExecuteLs(api *nomad.ExecutorAPIMock) {
|
||||
mockAPIExecute(api, &executionRequestLs,
|
||||
func(_ string, _ context.Context, _ string, _ bool, _ io.Reader, stdout, stderr io.Writer) (int, error) {
|
||||
_, _ = stdout.Write([]byte("existing-file\n"))
|
||||
_, _ = stderr.Write([]byte("ls: cannot access 'non-existing-file': No such file or directory\n"))
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
|
||||
var executionRequestHead = dto.ExecutionRequest{Command: "head -n 1"}
|
||||
|
||||
// mockAPIExecuteHead mocks the ExecuteCommand of an ExecutorApi to act as if 'head -n 1' was executed.
|
||||
func mockAPIExecuteHead(api *nomad.ExecutorAPIMock) {
|
||||
mockAPIExecute(api, &executionRequestHead,
|
||||
func(_ string, _ context.Context, _ string, _ bool,
|
||||
stdin io.Reader, stdout io.Writer, stderr io.Writer,
|
||||
) (int, error) {
|
||||
scanner := bufio.NewScanner(stdin)
|
||||
for !scanner.Scan() {
|
||||
scanner = bufio.NewScanner(stdin)
|
||||
}
|
||||
_, _ = stdout.Write(scanner.Bytes())
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
|
||||
var executionRequestSleep = dto.ExecutionRequest{Command: "sleep infinity"}
|
||||
|
||||
// mockAPIExecuteSleep mocks the ExecuteCommand method of an ExecutorAPI to sleep
|
||||
// until the execution receives a SIGQUIT.
|
||||
func mockAPIExecuteSleep(api *nomad.ExecutorAPIMock) <-chan bool {
|
||||
canceled := make(chan bool, 1)
|
||||
|
||||
mockAPIExecute(api, &executionRequestSleep,
|
||||
func(_ string, ctx context.Context, _ string, _ bool,
|
||||
stdin io.Reader, stdout io.Writer, stderr io.Writer,
|
||||
) (int, error) {
|
||||
var err error
|
||||
buffer := make([]byte, 1) //nolint:makezero // if the length is zero, the Read call never reads anything
|
||||
for n := 0; !(n == 1 && buffer[0] == runner.SIGQUIT); n, err = stdin.Read(buffer) {
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error while reading stdin: %w", err)
|
||||
}
|
||||
}
|
||||
close(canceled)
|
||||
return 0, ctx.Err()
|
||||
})
|
||||
return canceled
|
||||
}
|
||||
|
||||
var executionRequestError = dto.ExecutionRequest{Command: "error"}
|
||||
|
||||
// mockAPIExecuteError mocks the ExecuteCommand method of an ExecutorApi to return an error.
|
||||
func mockAPIExecuteError(api *nomad.ExecutorAPIMock) {
|
||||
mockAPIExecute(api, &executionRequestError,
|
||||
func(_ string, _ context.Context, _ string, _ bool, _ io.Reader, _, _ io.Writer) (int, error) {
|
||||
return 0, tests.ErrDefault
|
||||
})
|
||||
}
|
||||
|
||||
var executionRequestExitNonZero = dto.ExecutionRequest{Command: "exit 42"}
|
||||
|
||||
// mockAPIExecuteExitNonZero mocks the ExecuteCommand method of an ExecutorApi to exit with exit status 42.
|
||||
func mockAPIExecuteExitNonZero(api *nomad.ExecutorAPIMock) {
|
||||
mockAPIExecute(api, &executionRequestExitNonZero,
|
||||
func(_ string, _ context.Context, _ string, _ bool, _ io.Reader, _, _ io.Writer) (int, error) {
|
||||
return 42, nil
|
||||
})
|
||||
}
|
||||
|
||||
// mockAPIExecute mocks the ExecuteCommand method of an ExecutorApi to call the given method run when the command
|
||||
// corresponding to the given ExecutionRequest is called.
|
||||
func mockAPIExecute(api *nomad.ExecutorAPIMock, request *dto.ExecutionRequest,
|
||||
run func(runnerId string, ctx context.Context, command string, tty bool,
|
||||
stdin io.Reader, stdout, stderr io.Writer) (int, error)) {
|
||||
tests.RemoveMethodFromMock(&api.Mock, "ExecuteCommand")
|
||||
call := api.On("ExecuteCommand",
|
||||
mock.AnythingOfType("string"),
|
||||
mock.Anything,
|
||||
request.FullCommand(),
|
||||
mock.AnythingOfType("bool"),
|
||||
mock.AnythingOfType("bool"),
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything)
|
||||
call.Run(func(args mock.Arguments) {
|
||||
exit, err := run(args.Get(0).(string),
|
||||
args.Get(1).(context.Context),
|
||||
args.Get(2).(string),
|
||||
args.Get(3).(bool),
|
||||
args.Get(5).(io.Reader),
|
||||
args.Get(6).(io.Writer),
|
||||
args.Get(7).(io.Writer))
|
||||
call.ReturnArguments = mock.Arguments{exit, err}
|
||||
})
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type MainTestSuite struct {
|
||||
tests.MemoryLeakTestSuite
|
||||
}
|
||||
|
||||
func TestMainTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MainTestSuite))
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestCodeOceanToRawReaderReturnsOnlyAfterOneByteWasRead() {
|
||||
readingCtx, cancel := context.WithCancel(context.Background())
|
||||
forwardingCtx := readingCtx
|
||||
defer cancel()
|
||||
reader := NewCodeOceanToRawReader(nil, readingCtx, forwardingCtx)
|
||||
|
||||
read := make(chan bool)
|
||||
go func() {
|
||||
//nolint:makezero // we can't make zero initial length here as the reader otherwise doesn't block
|
||||
p := make([]byte, 10)
|
||||
_, err := reader.Read(p)
|
||||
s.Require().NoError(err)
|
||||
read <- true
|
||||
}()
|
||||
|
||||
s.Run("Does not return immediately when there is no data", func() {
|
||||
s.False(tests.ChannelReceivesSomething(read, tests.ShortTimeout))
|
||||
})
|
||||
|
||||
s.Run("Returns when there is data available", func() {
|
||||
reader.buffer <- byte(42)
|
||||
s.True(tests.ChannelReceivesSomething(read, tests.ShortTimeout))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestCodeOceanToRawReaderReturnsOnlyAfterOneByteWasReadFromConnection() {
|
||||
messages := make(chan io.Reader)
|
||||
defer close(messages)
|
||||
|
||||
connection := &ConnectionMock{}
|
||||
connection.On("WriteMessage", mock.AnythingOfType("int"), mock.AnythingOfType("[]uint8")).Return(nil)
|
||||
connection.On("CloseHandler").Return(nil)
|
||||
connection.On("SetCloseHandler", mock.Anything).Return()
|
||||
call := connection.On("NextReader")
|
||||
call.Run(func(_ mock.Arguments) {
|
||||
call.Return(websocket.TextMessage, <-messages, nil)
|
||||
})
|
||||
|
||||
readingCtx, cancel := context.WithCancel(context.Background())
|
||||
forwardingCtx := readingCtx
|
||||
defer cancel()
|
||||
reader := NewCodeOceanToRawReader(connection, readingCtx, forwardingCtx)
|
||||
reader.Start()
|
||||
|
||||
read := make(chan bool)
|
||||
//nolint:makezero // this is required here to make the Read call blocking
|
||||
message := make([]byte, 10)
|
||||
go func() {
|
||||
_, err := reader.Read(message)
|
||||
s.Require().NoError(err)
|
||||
read <- true
|
||||
}()
|
||||
|
||||
s.Run("Does not return immediately when there is no data", func() {
|
||||
s.False(tests.ChannelReceivesSomething(read, tests.ShortTimeout))
|
||||
})
|
||||
|
||||
s.Run("Returns when there is data available", func() {
|
||||
messages <- strings.NewReader("Hello")
|
||||
s.True(tests.ChannelReceivesSomething(read, tests.ShortTimeout))
|
||||
})
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func (s *MainTestSuite) TestRawToCodeOceanWriter() {
|
||||
connectionMock, messages := buildConnectionMock(&s.MemoryLeakTestSuite)
|
||||
proxyCtx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
output := NewCodeOceanOutputWriter(connectionMock, proxyCtx, cancel)
|
||||
defer output.Close(nil)
|
||||
<-messages // start messages
|
||||
|
||||
s.Run("StdOut", func() {
|
||||
testMessage := "testStdOut"
|
||||
_, err := output.StdOut().Write([]byte(testMessage))
|
||||
s.Require().NoError(err)
|
||||
|
||||
expected, err := json.Marshal(struct {
|
||||
Type string `json:"type"`
|
||||
Data string `json:"data"`
|
||||
}{string(dto.WebSocketOutputStdout), testMessage})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Equal(expected, <-messages)
|
||||
})
|
||||
|
||||
s.Run("StdErr", func() {
|
||||
testMessage := "testStdErr"
|
||||
_, err := output.StdErr().Write([]byte(testMessage))
|
||||
s.Require().NoError(err)
|
||||
|
||||
expected, err := json.Marshal(struct {
|
||||
Type string `json:"type"`
|
||||
Data string `json:"data"`
|
||||
}{string(dto.WebSocketOutputStderr), testMessage})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Equal(expected, <-messages)
|
||||
})
|
||||
}
|
||||
|
||||
type sendExitInfoTestCase struct {
|
||||
name string
|
||||
info *runner.ExitInfo
|
||||
message dto.WebSocketMessage
|
||||
}
|
||||
|
||||
func (s *MainTestSuite) TestCodeOceanOutputWriter_SendExitInfo() {
|
||||
testCases := []sendExitInfoTestCase{
|
||||
{"Timeout", &runner.ExitInfo{Err: runner.ErrorRunnerInactivityTimeout},
|
||||
dto.WebSocketMessage{Type: dto.WebSocketMetaTimeout}},
|
||||
{"Error", &runner.ExitInfo{Err: websocket.ErrCloseSent},
|
||||
dto.WebSocketMessage{Type: dto.WebSocketOutputError, Data: "Error executing the request"}},
|
||||
// CodeOcean expects this exact string in case of a OOM Killed runner.
|
||||
{"Specific data for OOM Killed runner", &runner.ExitInfo{Err: runner.ErrOOMKilled},
|
||||
dto.WebSocketMessage{Type: dto.WebSocketOutputError, Data: "the allocation was OOM Killed"}},
|
||||
{"Exit", &runner.ExitInfo{Code: 21},
|
||||
dto.WebSocketMessage{Type: dto.WebSocketExit, ExitCode: 21}},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
s.Run(test.name, func() {
|
||||
connectionMock, messages := buildConnectionMock(&s.MemoryLeakTestSuite)
|
||||
proxyCtx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
output := NewCodeOceanOutputWriter(connectionMock, proxyCtx, cancel)
|
||||
<-messages // start messages
|
||||
|
||||
output.Close(test.info)
|
||||
expected, err := json.Marshal(test.message)
|
||||
s.Require().NoError(err)
|
||||
|
||||
msg := <-messages
|
||||
s.Equal(expected, msg)
|
||||
|
||||
<-messages // close message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func buildConnectionMock(s *tests.MemoryLeakTestSuite) (conn *ConnectionMock, messages <-chan []byte) {
|
||||
s.T().Helper()
|
||||
message := make(chan []byte)
|
||||
connectionMock := &ConnectionMock{}
|
||||
connectionMock.On("WriteMessage", mock.AnythingOfType("int"), mock.AnythingOfType("[]uint8")).
|
||||
Run(func(args mock.Arguments) {
|
||||
m, ok := args.Get(1).([]byte)
|
||||
s.Require().True(ok)
|
||||
select {
|
||||
case <-s.TestCtx.Done():
|
||||
case message <- m:
|
||||
}
|
||||
}).
|
||||
Return(nil)
|
||||
connectionMock.On("CloseHandler").Return(nil)
|
||||
connectionMock.On("SetCloseHandler", mock.Anything).Return()
|
||||
connectionMock.On("Close").Return(nil)
|
||||
return connectionMock, message
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
// Code generated by mockery v2.13.1. DO NOT EDIT.
|
||||
|
||||
package ws
|
||||
|
||||
import (
|
||||
io "io"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// ConnectionMock is an autogenerated mock type for the Connection type
|
||||
type ConnectionMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Close provides a mock function with given fields:
|
||||
func (_m *ConnectionMock) Close() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// CloseHandler provides a mock function with given fields:
|
||||
func (_m *ConnectionMock) CloseHandler() func(int, string) error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 func(int, string) error
|
||||
if rf, ok := ret.Get(0).(func() func(int, string) error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(func(int, string) error)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NextReader provides a mock function with given fields:
|
||||
func (_m *ConnectionMock) NextReader() (int, io.Reader, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func() int); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
var r1 io.Reader
|
||||
if rf, ok := ret.Get(1).(func() io.Reader); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(io.Reader)
|
||||
}
|
||||
}
|
||||
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(2).(func() error); ok {
|
||||
r2 = rf()
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// SetCloseHandler provides a mock function with given fields: handler
|
||||
func (_m *ConnectionMock) SetCloseHandler(handler func(int, string) error) {
|
||||
_m.Called(handler)
|
||||
}
|
||||
|
||||
// WriteMessage provides a mock function with given fields: messageType, data
|
||||
func (_m *ConnectionMock) WriteMessage(messageType int, data []byte) error {
|
||||
ret := _m.Called(messageType, data)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(int, []byte) error); ok {
|
||||
r0 = rf(messageType, data)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewConnectionMock interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewConnectionMock creates a new instance of ConnectionMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewConnectionMock(t mockConstructorTestingTNewConnectionMock) *ConnectionMock {
|
||||
mock := &ConnectionMock{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
Reference in New Issue
Block a user