diff --git a/cmd/poseidon/main_test.go b/cmd/poseidon/main_test.go new file mode 100644 index 0000000..d0d46b7 --- /dev/null +++ b/cmd/poseidon/main_test.go @@ -0,0 +1,26 @@ +package main + +import ( + "github.com/openHPI/poseidon/internal/environment" + "github.com/openHPI/poseidon/internal/runner" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestAWSDisabledUsesNomadManager(t *testing.T) { + runnerManager, environmentManager := createManagerHandler(createNomadManager, true, + runner.NewAbstractManager(), &environment.AbstractManager{}) + awsRunnerManager, awsEnvironmentManager := createManagerHandler(createAWSManager, false, + runnerManager, environmentManager) + assert.Equal(t, runnerManager, awsRunnerManager) + assert.Equal(t, environmentManager, awsEnvironmentManager) +} + +func TestAWSEnabledWrappesNomadManager(t *testing.T) { + runnerManager, environmentManager := createManagerHandler(createNomadManager, true, + runner.NewAbstractManager(), &environment.AbstractManager{}) + awsRunnerManager, awsEnvironmentManager := createManagerHandler(createAWSManager, + true, runnerManager, environmentManager) + assert.NotEqual(t, runnerManager, awsRunnerManager) + assert.NotEqual(t, environmentManager, awsEnvironmentManager) +} diff --git a/internal/environment/aws_manager_test.go b/internal/environment/aws_manager_test.go new file mode 100644 index 0000000..8453c51 --- /dev/null +++ b/internal/environment/aws_manager_test.go @@ -0,0 +1,112 @@ +package environment + +import ( + "github.com/openHPI/poseidon/internal/runner" + "github.com/openHPI/poseidon/pkg/dto" + "github.com/openHPI/poseidon/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "testing" +) + +func TestAWSEnvironmentManager_CreateOrUpdate(t *testing.T) { + runnerManager := runner.NewAWSRunnerManager() + m := NewAWSEnvironmentManager(runnerManager) + uniqueImage := "random image string" + + t.Run("can create default Java environment", func(t *testing.T) { + _, err := m.CreateOrUpdate(runner.AwsJavaEnvironmentID, dto.ExecutionEnvironmentRequest{Image: uniqueImage}) + assert.NoError(t, err) + }) + + t.Run("can retrieve added environment", func(t *testing.T) { + environment, err := m.Get(runner.AwsJavaEnvironmentID, false) + assert.NoError(t, err) + assert.Equal(t, environment.Image(), uniqueImage) + }) + + t.Run("non handleable requests are forwarded to the next manager", func(t *testing.T) { + nextHandler := &ManagerHandlerMock{} + nextHandler.On("CreateOrUpdate", mock.AnythingOfType("dto.EnvironmentID"), + mock.AnythingOfType("dto.ExecutionEnvironmentRequest")).Return(true, nil) + m.SetNextHandler(nextHandler) + + request := dto.ExecutionEnvironmentRequest{} + _, err := m.CreateOrUpdate(tests.DefaultEnvironmentIDAsInteger, request) + assert.NoError(t, err) + nextHandler.AssertCalled(t, "CreateOrUpdate", + dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), request) + }) +} + +func TestAWSEnvironmentManager_Get(t *testing.T) { + runnerManager := runner.NewAWSRunnerManager() + m := NewAWSEnvironmentManager(runnerManager) + + t.Run("Calls next handler when not found", func(t *testing.T) { + nextHandler := &ManagerHandlerMock{} + nextHandler.On("Get", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("bool")). + Return(nil, nil) + m.SetNextHandler(nextHandler) + + _, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false) + assert.NoError(t, err) + nextHandler.AssertCalled(t, "Get", dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), false) + }) + + t.Run("Returns error when not found", func(t *testing.T) { + nextHandler := &AbstractManager{nil} + m.SetNextHandler(nextHandler) + + _, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false) + assert.ErrorIs(t, err, runner.ErrRunnerNotFound) + }) + + t.Run("Returns environment when it was added before", func(t *testing.T) { + expectedEnvironment := NewAWSEnvironment() + expectedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger) + runnerManager.StoreEnvironment(expectedEnvironment) + + environment, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false) + assert.NoError(t, err) + assert.Equal(t, expectedEnvironment, environment) + }) +} + +func TestAWSEnvironmentManager_List(t *testing.T) { + runnerManager := runner.NewAWSRunnerManager() + m := NewAWSEnvironmentManager(runnerManager) + + t.Run("contains the \"Load\"-ed environments", func(t *testing.T) { + environments, err := m.List(false) + assert.NoError(t, err) + require.Len(t, environments, 1) + assert.Equal(t, environments[0].ID(), dto.EnvironmentID(runner.AwsJavaEnvironmentID)) + }) + + t.Run("returs also environments of the rest of the manager chain", func(t *testing.T) { + nextHandler := &ManagerHandlerMock{} + existingEnvironment := NewAWSEnvironment() + nextHandler.On("List", mock.AnythingOfType("bool")). + Return([]runner.ExecutionEnvironment{existingEnvironment}, nil) + m.SetNextHandler(nextHandler) + + environments, err := m.List(false) + assert.NoError(t, err) + require.Len(t, environments, 2) + assert.Contains(t, environments, existingEnvironment) + }) + m.SetNextHandler(nil) + + t.Run("Returns added environment", func(t *testing.T) { + localEnvironment := NewAWSEnvironment() + localEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger) + runnerManager.StoreEnvironment(localEnvironment) + + environments, err := m.List(false) + assert.NoError(t, err) + assert.Len(t, environments, 2) + assert.Contains(t, environments, localEnvironment) + }) +} diff --git a/internal/runner/aws_manager_test.go b/internal/runner/aws_manager_test.go new file mode 100644 index 0000000..f0eb1ad --- /dev/null +++ b/internal/runner/aws_manager_test.go @@ -0,0 +1,87 @@ +package runner + +import ( + "github.com/openHPI/poseidon/pkg/dto" + "github.com/openHPI/poseidon/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "testing" +) + +func TestAWSRunnerManager_EnvironmentAccessor(t *testing.T) { + m := NewAWSRunnerManager() + + environments := m.ListEnvironments() + assert.Empty(t, environments) + + environment := &ExecutionEnvironmentMock{} + environment.On("ID").Return(dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger)) + m.StoreEnvironment(environment) + + environments = m.ListEnvironments() + assert.Len(t, environments, 1) + assert.Equal(t, environments[0].ID(), dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger)) + + e, ok := m.GetEnvironment(tests.DefaultEnvironmentIDAsInteger) + assert.True(t, ok) + assert.Equal(t, environment, e) + + _, ok = m.GetEnvironment(tests.AnotherEnvironmentIDAsInteger) + assert.False(t, ok) +} + +func TestAWSRunnerManager_Claim(t *testing.T) { + m := NewAWSRunnerManager() + environment := &ExecutionEnvironmentMock{} + environment.On("ID").Return(dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger)) + r, err := NewAWSFunctionWorkload(environment, nil) + assert.NoError(t, err) + environment.On("Sample").Return(r, true) + m.StoreEnvironment(environment) + + t.Run("returns runner for AWS environment", func(t *testing.T) { + r, err := m.Claim(tests.DefaultEnvironmentIDAsInteger, 60) + assert.NoError(t, err) + assert.NotNil(t, r) + }) + + t.Run("forwards request for non AWS environments", func(t *testing.T) { + nextHandler := &ManagerMock{} + nextHandler.On("Claim", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("int")). + Return(nil, nil) + m.SetNextHandler(nextHandler) + + _, err := m.Claim(tests.AnotherEnvironmentIDAsInteger, 60) + assert.Nil(t, err) + nextHandler.AssertCalled(t, "Claim", dto.EnvironmentID(tests.AnotherEnvironmentIDAsInteger), 60) + }) +} + +func TestAWSRunnerManager_Return(t *testing.T) { + m := NewAWSRunnerManager() + environment := &ExecutionEnvironmentMock{} + environment.On("ID").Return(dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger)) + m.StoreEnvironment(environment) + r, err := NewAWSFunctionWorkload(environment, nil) + assert.NoError(t, err) + + t.Run("removes usedRunner", func(t *testing.T) { + m.usedRunners.Add(r) + assert.Contains(t, m.usedRunners.List(), r) + + err := m.Return(r) + assert.NoError(t, err) + assert.NotContains(t, m.usedRunners.List(), r) + }) + + t.Run("calls nextHandler for non AWS runner", func(t *testing.T) { + nextHandler := &ManagerMock{} + nextHandler.On("Return", mock.AnythingOfType("*runner.NomadJob")).Return(nil) + m.SetNextHandler(nextHandler) + + nonAWSRunner := NewNomadJob(tests.DefaultRunnerID, nil, nil, nil) + err := m.Return(nonAWSRunner) + assert.NoError(t, err) + nextHandler.AssertCalled(t, "Return", nonAWSRunner) + }) +} diff --git a/internal/runner/aws_runner_test.go b/internal/runner/aws_runner_test.go new file mode 100644 index 0000000..46c4469 --- /dev/null +++ b/internal/runner/aws_runner_test.go @@ -0,0 +1,143 @@ +package runner + +import ( + "context" + "encoding/base64" + "github.com/gorilla/websocket" + "github.com/openHPI/poseidon/internal/config" + "github.com/openHPI/poseidon/pkg/dto" + "github.com/openHPI/poseidon/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestAWSExecutionRequestIsStored(t *testing.T) { + r, err := NewAWSFunctionWorkload(nil, nil) + assert.NoError(t, err) + executionRequest := &dto.ExecutionRequest{ + Command: "command", + TimeLimit: 10, + Environment: nil, + } + r.StoreExecution(defaultExecutionID, executionRequest) + assert.True(t, r.ExecutionExists(defaultExecutionID)) + storedExecutionRunner, ok := r.executions.Pop(defaultExecutionID) + assert.True(t, ok, "Getting an execution should not return ok false") + assert.Equal(t, executionRequest, storedExecutionRunner) +} + +type awsEndpointMock struct { + hasConnected bool + ctx context.Context + receivedData string +} + +func (a *awsEndpointMock) handler(w http.ResponseWriter, r *http.Request) { + upgrader := websocket.Upgrader{} + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + defer c.Close() + + a.hasConnected = true + for a.ctx.Err() == nil { + _, message, err := c.ReadMessage() + if err != nil { + break + } + a.receivedData = string(message) + } +} + +func TestAWSFunctionWorkload_ExecuteInteractively(t *testing.T) { + environment := &ExecutionEnvironmentMock{} + environment.On("Image").Return("testImage or AWS endpoint") + r, err := NewAWSFunctionWorkload(environment, nil) + require.NoError(t, err) + + var cancel context.CancelFunc + awsMock := &awsEndpointMock{} + s := httptest.NewServer(http.HandlerFunc(awsMock.handler)) + + t.Run("establishes WebSocket connection to AWS endpoint", func(t *testing.T) { + // Convert http://127.0.0.1 to ws://127.0.0. + config.Config.AWS.Endpoint = "ws" + strings.TrimPrefix(s.URL, "http") + awsMock.ctx, cancel = context.WithCancel(context.Background()) + cancel() + + r.StoreExecution(defaultExecutionID, &dto.ExecutionRequest{}) + exit, _, err := r.ExecuteInteractively(defaultExecutionID, nil, io.Discard, io.Discard) + require.NoError(t, err) + <-exit + assert.True(t, awsMock.hasConnected) + }) + + t.Run("sends execution request", func(t *testing.T) { + awsMock.ctx, cancel = context.WithTimeout(context.Background(), tests.ShortTimeout) + defer cancel() + command := "sl" + request := &dto.ExecutionRequest{Command: command} + r.StoreExecution(defaultExecutionID, request) + + _, cancel, err := r.ExecuteInteractively(defaultExecutionID, nil, io.Discard, io.Discard) + require.NoError(t, err) + <-time.After(tests.ShortTimeout) + cancel() + + expectedRequestData := "{\"action\":\"" + environment.Image() + + "\",\"cmd\":[\"env\",\"sh\",\"-c\",\"" + command + "\"],\"files\":{}}" + assert.Equal(t, expectedRequestData, awsMock.receivedData) + }) +} + +func TestAWSFunctionWorkload_UpdateFileSystem(t *testing.T) { + environment := &ExecutionEnvironmentMock{} + environment.On("Image").Return("testImage or AWS endpoint") + r, err := NewAWSFunctionWorkload(environment, nil) + require.NoError(t, err) + + var cancel context.CancelFunc + awsMock := &awsEndpointMock{} + s := httptest.NewServer(http.HandlerFunc(awsMock.handler)) + + // Convert http://127.0.0.1 to ws://127.0.0. + config.Config.AWS.Endpoint = "ws" + strings.TrimPrefix(s.URL, "http") + awsMock.ctx, cancel = context.WithTimeout(context.Background(), tests.ShortTimeout) + defer cancel() + command := "sl" + request := &dto.ExecutionRequest{Command: command} + r.StoreExecution(defaultExecutionID, request) + myFile := dto.File{Path: "myPath", Content: []byte("myContent")} + + err = r.UpdateFileSystem(&dto.UpdateFileSystemRequest{Copy: []dto.File{myFile}}) + assert.NoError(t, err) + _, execCancel, err := r.ExecuteInteractively(defaultExecutionID, nil, io.Discard, io.Discard) + require.NoError(t, err) + <-time.After(tests.ShortTimeout) + execCancel() + + expectedRequestData := "{\"action\":\"" + environment.Image() + + "\",\"cmd\":[\"env\",\"sh\",\"-c\",\"" + command + "\"]," + + "\"files\":{\"" + string(myFile.Path) + "\":\"" + base64.StdEncoding.EncodeToString(myFile.Content) + "\"}}" + assert.Equal(t, expectedRequestData, awsMock.receivedData) +} + +func TestAWSFunctionWorkload_Destroy(t *testing.T) { + hasDestroyBeenCalled := false + r, err := NewAWSFunctionWorkload(nil, func(_ Runner) error { + hasDestroyBeenCalled = true + return nil + }) + require.NoError(t, err) + + err = r.Destroy() + assert.NoError(t, err) + assert.True(t, hasDestroyBeenCalled) +}