
as we identified two issues where the goroutine count from before differs from after the test. 1) It seemed like a Go runtime specific Goroutine appeared in rare cases before the test. To avoid this, we introduced a short timeout before looking up the Goroutines. Another solution might be to do the lookup twice and check if the count matches. 2) A Goroutine that periodically monitors some storage unexpectedly got closed in rare cases. As we could not identify the cause for this, we removed the leaking Goroutines by properly cleaning up.
309 lines
10 KiB
Go
309 lines
10 KiB
Go
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)
|
|
}
|