diff --git a/cli/server/server.go b/cli/server/server.go index 2600ab1..11728c9 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -16,6 +16,7 @@ import ( "github.com/goto/dex/internal/server" "github.com/goto/dex/internal/server/gcs" + "github.com/goto/dex/internal/server/v1/optimus" "github.com/goto/dex/pkg/logger" "github.com/goto/dex/pkg/telemetry" ) @@ -101,6 +102,7 @@ func runServer(baseCtx context.Context, nrApp *newrelic.Application, zapLog *zap } return server.Serve(ctx, cfg.Service.Addr(), nrApp, zapLog, shieldv1beta1.NewShieldServiceClient(shieldConn), + &optimus.ClientBuilder{}, entropyv1beta1.NewResourceServiceClient(entropyConn), sirenv1beta1.NewSirenServiceClient(sirenConn), compassv1beta1grpc.NewCompassServiceClient(compassConn), diff --git a/internal/server/server.go b/internal/server/server.go index 59d3322..bb8328d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -30,6 +30,7 @@ import ( func Serve(ctx context.Context, addr string, nrApp *newrelic.Application, logger *zap.Logger, shieldClient shieldv1beta1.ShieldServiceClient, + optimusClient optimusv1.OptimusClientBuilder, entropyClient entropyv1beta1.ResourceServiceClient, sirenClient sirenv1beta1.SirenServiceClient, compassClient compassv1beta1grpc.CompassServiceClient, @@ -59,7 +60,7 @@ func Serve(ctx context.Context, addr string, r.Get("/alertTemplates", alertSvc.HandleListTemplates()) r.Route("/subscriptions", alertsv1.SubscriptionRoutes(sirenClient, shieldClient)) r.Route("/alerts", alertsv1.AlertRoutes(sirenClient, shieldClient)) - r.Route("/optimus", optimusv1.Routes(shieldClient)) + r.Route("/optimus", optimusv1.Routes(shieldClient, optimusClient)) r.Route("/projects", projectsv1.Routes(shieldClient)) r.Route("/dlq", dlqv1.Routes(entropyClient, gcsClient)) r.Route("/firehoses", firehosev1.Routes(entropyClient, shieldClient, alertSvc, compassClient, odinAddr, stencilAddr)) diff --git a/internal/server/v1/optimus/optimus_client.go b/internal/server/v1/optimus/optimus_client.go index e00810e..153e128 100644 --- a/internal/server/v1/optimus/optimus_client.go +++ b/internal/server/v1/optimus/optimus_client.go @@ -6,13 +6,13 @@ import ( "google.golang.org/grpc/credentials/insecure" ) +type ClientBuilder struct{} + type OptimusClientBuilder interface { BuildOptimusClient(hostname string) (optimusv1beta1grpc.JobSpecificationServiceClient, error) } -type clientBuilder struct{} - -func (*clientBuilder) BuildOptimusClient(hostname string) (optimusv1beta1grpc.JobSpecificationServiceClient, error) { +func (*ClientBuilder) BuildOptimusClient(hostname string) (optimusv1beta1grpc.JobSpecificationServiceClient, error) { optimusConn, err := grpc.Dial(hostname, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return nil, err diff --git a/internal/server/v1/optimus/optimus_client_mock.go b/internal/server/v1/optimus/optimus_client_mock.go new file mode 100644 index 0000000..b737f59 --- /dev/null +++ b/internal/server/v1/optimus/optimus_client_mock.go @@ -0,0 +1,22 @@ +package optimus + +import ( + optimusv1beta1grpc "buf.build/gen/go/gotocompany/proton/grpc/go/gotocompany/optimus/core/v1beta1/corev1beta1grpc" + "github.com/stretchr/testify/mock" +) + +type OptimusClientMock struct { + mock.Mock +} + +type OptimusClientBuilderMock interface { + BuildOptimusClient(hostname string) (optimusv1beta1grpc.JobSpecificationServiceClient, error) +} + +func (mock *OptimusClientMock) BuildOptimusClient(hostname string) (optimusv1beta1grpc.JobSpecificationServiceClient, error) { + args := mock.Called(hostname) + if args[0] == nil { + return nil, args[1].(error) + } + return args.Get(0).(optimusv1beta1grpc.JobSpecificationServiceClient), args.Error(1) +} diff --git a/internal/server/v1/optimus/routes.go b/internal/server/v1/optimus/routes.go index 4508a45..0e270d6 100644 --- a/internal/server/v1/optimus/routes.go +++ b/internal/server/v1/optimus/routes.go @@ -5,8 +5,8 @@ import ( "github.com/go-chi/chi/v5" ) -func Routes(shieldClient shieldv1beta1rpc.ShieldServiceClient) func(r chi.Router) { - service := NewService(shieldClient, &clientBuilder{}) +func Routes(shieldClient shieldv1beta1rpc.ShieldServiceClient, optimusClient OptimusClientBuilder) func(r chi.Router) { + service := NewService(shieldClient, optimusClient) handler := NewHandler(service) return func(r chi.Router) { diff --git a/internal/server/v1/optimus/routes_test.go b/internal/server/v1/optimus/routes_test.go new file mode 100644 index 0000000..a8cd83a --- /dev/null +++ b/internal/server/v1/optimus/routes_test.go @@ -0,0 +1,269 @@ +package optimus_test + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + optimusv1beta1 "buf.build/gen/go/gotocompany/proton/protocolbuffers/go/gotocompany/optimus/core/v1beta1" + shieldv1beta1 "buf.build/gen/go/gotocompany/proton/protocolbuffers/go/gotocompany/shield/v1beta1" + "github.com/go-chi/chi/v5" + "github.com/goto/dex/internal/server/v1/optimus" + "github.com/goto/dex/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestRoutesFindJobSpec(t *testing.T) { + jobName := "sample-optimus-job-name" + hostname := "optimus.staging.golabs.io:80" + projectSlug := "sample-project" + method := http.MethodGet + path := fmt.Sprintf("/projects/%s/jobs/%s", projectSlug, jobName) + + t.Run("should return 200 with job spec", func(t *testing.T) { + jobSpecRes := &optimusv1beta1.JobSpecificationResponse{ + ProjectName: projectSlug, + NamespaceName: "test-namespcace", + Job: &optimusv1beta1.JobSpecification{ + Version: 1, + Name: "sample-job", + Owner: "goto", + TaskName: "sample-task-name", + }, + } + + projectRes := &shieldv1beta1.GetProjectResponse{ + Project: &shieldv1beta1.Project{ + Slug: "test-project", + Metadata: newStruct(t, map[string]interface{}{ + "optimus_host": hostname, + }), + }, + } + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", mock.Anything, &shieldv1beta1.GetProjectRequest{ + Id: projectSlug, + }).Return(projectRes, nil) + + optimusClient := new(optimus.OptimusClientMock) + defer shieldClient.AssertExpectations(t) + + client := mocks.NewJobSpecificationServiceClient(t) + optimusClient.On("BuildOptimusClient", hostname).Return(client, nil) + defer optimusClient.AssertExpectations(t) + + client.On("GetJobSpecifications", mock.Anything, &optimusv1beta1.GetJobSpecificationsRequest{ + ProjectName: projectSlug, + JobName: jobName, + }).Return(&optimusv1beta1.GetJobSpecificationsResponse{ + JobSpecificationResponses: []*optimusv1beta1.JobSpecificationResponse{ + jobSpecRes, + }, + }, nil) + + response := httptest.NewRecorder() + request := httptest.NewRequest(method, path, nil) + router := chi.NewRouter() + optimus.Routes(shieldClient, optimusClient)(router) + router.ServeHTTP(response, request) + // assert + assert.Equal(t, http.StatusOK, response.Code) + resultJSON := response.Body.Bytes() + expectedJSON, err := json.Marshal(jobSpecRes) + require.NoError(t, err) + assert.JSONEq(t, string(expectedJSON), string(resultJSON)) + }) + + t.Run("should return 404 if job could not be found", func(t *testing.T) { + projectRes := &shieldv1beta1.GetProjectResponse{ + Project: &shieldv1beta1.Project{ + Slug: "test-project", + Metadata: newStruct(t, map[string]interface{}{ + "optimus_host": hostname, + }), + }, + } + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", mock.Anything, &shieldv1beta1.GetProjectRequest{ + Id: projectSlug, + }).Return(projectRes, nil) + + optimusClient := new(optimus.OptimusClientMock) + defer shieldClient.AssertExpectations(t) + + client := mocks.NewJobSpecificationServiceClient(t) + optimusClient.On("BuildOptimusClient", hostname).Return(client, nil) + defer optimusClient.AssertExpectations(t) + + client.On("GetJobSpecifications", mock.Anything, &optimusv1beta1.GetJobSpecificationsRequest{ + ProjectName: projectSlug, + JobName: jobName, + }).Return(&optimusv1beta1.GetJobSpecificationsResponse{}, nil) + defer client.AssertExpectations(t) + response := httptest.NewRecorder() + request := httptest.NewRequest(method, path, nil) + router := chi.NewRouter() + optimus.Routes(shieldClient, optimusClient)(router) + router.ServeHTTP(response, request) + assert.Equal(t, http.StatusNotFound, response.Code) + }) + + t.Run("should return 500 for internal error", func(t *testing.T) { + clientError := status.Error(codes.Internal, "Internal") + + projectRes := &shieldv1beta1.GetProjectResponse{ + Project: &shieldv1beta1.Project{ + Slug: "test-project", + Metadata: newStruct(t, map[string]interface{}{ + "optimus_host": hostname, + }), + }, + } + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", mock.Anything, &shieldv1beta1.GetProjectRequest{ + Id: projectSlug, + }).Return(projectRes, nil) + + optimusClient := new(optimus.OptimusClientMock) + defer shieldClient.AssertExpectations(t) + + client := mocks.NewJobSpecificationServiceClient(t) + optimusClient.On("BuildOptimusClient", hostname).Return(client, nil) + defer optimusClient.AssertExpectations(t) + client.On("GetJobSpecifications", mock.Anything, &optimusv1beta1.GetJobSpecificationsRequest{ + ProjectName: projectSlug, + JobName: jobName, + }).Return(nil, clientError) + defer client.AssertExpectations(t) + response := httptest.NewRecorder() + request := httptest.NewRequest(method, path, nil) + router := chi.NewRouter() + optimus.Routes(shieldClient, optimusClient)(router) + router.ServeHTTP(response, request) + assert.Equal(t, http.StatusInternalServerError, response.Code) + }) +} + +func TestRoutesListJobs(t *testing.T) { + projectSlug := "sample-project" + hostname := "optimus.staging.golabs.io:80" + method := http.MethodGet + path := fmt.Sprintf("/projects/%s/jobs", projectSlug) + + t.Run("should return 200 with jobs list", func(t *testing.T) { + jobSpec1 := &optimusv1beta1.JobSpecificationResponse{ + ProjectName: "test-project", + NamespaceName: "test-namespcace", + Job: &optimusv1beta1.JobSpecification{ + Version: 1, + Name: "sample-job1", + Owner: "goto", + TaskName: "sample-task-name", + }, + } + + jobSpec2 := &optimusv1beta1.JobSpecificationResponse{ + ProjectName: "test-project", + Job: &optimusv1beta1.JobSpecification{ + Version: 1, + Name: "sample-job2", + Owner: "goto", + TaskName: "sample-task-name", + }, + } + + expectedJobSpecRes := []*optimusv1beta1.JobSpecificationResponse{ + jobSpec1, + jobSpec2, + } + + projectRes := &shieldv1beta1.GetProjectResponse{ + Project: &shieldv1beta1.Project{ + Slug: "test-project", + Metadata: newStruct(t, map[string]interface{}{ + "optimus_host": hostname, + }), + }, + } + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", mock.Anything, &shieldv1beta1.GetProjectRequest{ + Id: projectSlug, + }).Return(projectRes, nil) + + optimusClient := new(optimus.OptimusClientMock) + defer shieldClient.AssertExpectations(t) + + client := mocks.NewJobSpecificationServiceClient(t) + optimusClient.On("BuildOptimusClient", hostname).Return(client, nil) + defer optimusClient.AssertExpectations(t) + + client.On("GetJobSpecifications", mock.Anything, &optimusv1beta1.GetJobSpecificationsRequest{ + ProjectName: projectSlug, + }).Return(&optimusv1beta1.GetJobSpecificationsResponse{ + JobSpecificationResponses: expectedJobSpecRes, + }, nil) + defer client.AssertExpectations(t) + + response := httptest.NewRecorder() + request := httptest.NewRequest(method, path, nil) + + router := chi.NewRouter() + optimus.Routes(shieldClient, optimusClient)(router) + router.ServeHTTP(response, request) + + // assert + assert.Equal(t, http.StatusOK, response.Code) + resultJSON := response.Body.Bytes() + expectedJSON, err := json.Marshal(expectedJobSpecRes) + require.NoError(t, err) + assert.JSONEq(t, string(expectedJSON), string(resultJSON)) + }) + + t.Run("should return 500 for internal error", func(t *testing.T) { + clientError := status.Error(codes.Internal, "Internal") + + projectRes := &shieldv1beta1.GetProjectResponse{ + Project: &shieldv1beta1.Project{ + Slug: "test-project", + Metadata: newStruct(t, map[string]interface{}{ + "optimus_host": hostname, + }), + }, + } + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", mock.Anything, &shieldv1beta1.GetProjectRequest{ + Id: projectSlug, + }).Return(projectRes, nil) + + optimusClient := new(optimus.OptimusClientMock) + defer shieldClient.AssertExpectations(t) + + client := mocks.NewJobSpecificationServiceClient(t) + optimusClient.On("BuildOptimusClient", hostname).Return(client, nil) + defer optimusClient.AssertExpectations(t) + + client.On("GetJobSpecifications", mock.Anything, &optimusv1beta1.GetJobSpecificationsRequest{ + ProjectName: projectSlug, + }).Return(nil, clientError) + defer client.AssertExpectations(t) + + response := httptest.NewRecorder() + request := httptest.NewRequest(method, path, nil) + router := chi.NewRouter() + optimus.Routes(shieldClient, optimusClient)(router) + router.ServeHTTP(response, request) + + assert.Equal(t, http.StatusInternalServerError, response.Code) + }) +} diff --git a/internal/server/v1/optimus/service.go b/internal/server/v1/optimus/service.go index 5429a12..cab5bac 100644 --- a/internal/server/v1/optimus/service.go +++ b/internal/server/v1/optimus/service.go @@ -67,10 +67,10 @@ func (svc *Service) ListJobs(ctx context.Context, projectSlug string) ([]*optimu func (svc *Service) getOptimusClient(ctx context.Context, projectSlug string) (optimusv1beta1grpc.JobSpecificationServiceClient, error) { // retrieve hostname from cache - if cl, exists := svc.data[projectSlug]; exists { return cl, nil } + // retrieve hostname from shield prj, err := svc.shieldClient.GetProject(ctx, &shieldv1beta1.GetProjectRequest{ Id: projectSlug, diff --git a/internal/server/v1/optimus/service_test.go b/internal/server/v1/optimus/service_test.go new file mode 100644 index 0000000..8765fad --- /dev/null +++ b/internal/server/v1/optimus/service_test.go @@ -0,0 +1,305 @@ +package optimus_test + +import ( + "context" + "testing" + + optimusv1beta1 "buf.build/gen/go/gotocompany/proton/protocolbuffers/go/gotocompany/optimus/core/v1beta1" + shieldv1beta1 "buf.build/gen/go/gotocompany/proton/protocolbuffers/go/gotocompany/shield/v1beta1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/goto/dex/internal/server/v1/optimus" + "github.com/goto/dex/mocks" + "github.com/goto/dex/pkg/errors" +) + +func TestServiceFindJobSpec(t *testing.T) { + jobName := "sample-optimus-job-name" + projectSlug := "g-pilotdata-gl" + hostname := "optimus.staging.golabs.io:80" + + t.Run("should return job spec using job name and project name from argument", func(t *testing.T) { + + projectRes := &shieldv1beta1.GetProjectResponse{ + Project: &shieldv1beta1.Project{ + Slug: "test-project", + Metadata: newStruct(t, map[string]interface{}{ + "optimus_host": hostname, + }), + }, + } + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", context.TODO(), &shieldv1beta1.GetProjectRequest{ + Id: projectSlug, + }).Return(projectRes, nil) + + jobSpecRes := &optimusv1beta1.JobSpecificationResponse{ + ProjectName: "test-project", + NamespaceName: "test-namespcace", + Job: &optimusv1beta1.JobSpecification{ + Version: 1, + Name: jobName, + Owner: "goto", + TaskName: "sample-task-name", + }, + } + + optimusClient := new(optimus.OptimusClientMock) + service := optimus.NewService(shieldClient, optimusClient) + defer shieldClient.AssertExpectations(t) + + client := mocks.NewJobSpecificationServiceClient(t) + optimusClient.On("BuildOptimusClient", hostname).Return(client, nil) + defer optimusClient.AssertExpectations(t) + + client.On("GetJobSpecifications", context.TODO(), &optimusv1beta1.GetJobSpecificationsRequest{ + ProjectName: projectSlug, + JobName: jobName, + }).Return(&optimusv1beta1.GetJobSpecificationsResponse{ + JobSpecificationResponses: []*optimusv1beta1.JobSpecificationResponse{ + jobSpecRes, + }, + }, nil) + defer client.AssertExpectations(t) + job, err := service.FindJobSpec(context.TODO(), jobName, projectSlug) + assert.NoError(t, err) + assert.Equal(t, jobSpecRes, job) + }) + + t.Run("should return error, if project not found", func(t *testing.T) { + expectedErr := status.Error(codes.NotFound, "Not found") + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", context.TODO(), &shieldv1beta1.GetProjectRequest{ + Id: projectSlug, + }).Return(nil, expectedErr) + + optimusClient := new(optimus.OptimusClientMock) + service := optimus.NewService(shieldClient, optimusClient) + defer shieldClient.AssertExpectations(t) + defer optimusClient.AssertExpectations(t) + + _, err := service.FindJobSpec(context.TODO(), jobName, projectSlug) + assert.ErrorIs(t, err, expectedErr) + }) + + t.Run("should return error if metadata doesn't contain optimus hostname", func(t *testing.T) { + projectRes := &shieldv1beta1.GetProjectResponse{ + Project: &shieldv1beta1.Project{ + Slug: "test-project", + Metadata: newStruct(t, map[string]interface{}{}), + }, + } + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", context.TODO(), &shieldv1beta1.GetProjectRequest{ + Id: projectSlug, + }).Return(projectRes, nil) + + optimusClient := new(optimus.OptimusClientMock) + service := optimus.NewService(shieldClient, optimusClient) + defer shieldClient.AssertExpectations(t) + defer optimusClient.AssertExpectations(t) + + _, err := service.FindJobSpec(context.TODO(), jobName, projectSlug) + assert.ErrorIs(t, err, optimus.ErrOptimusHostNotFound) + }) + + t.Run("should return error, if creation of optimus client fails to create", func(t *testing.T) { + expectedErr := status.Error(codes.Internal, "Internal") + + projectRes := &shieldv1beta1.GetProjectResponse{ + Project: &shieldv1beta1.Project{ + Slug: "test-project", + Metadata: newStruct(t, map[string]interface{}{ + "optimus_host": hostname, + }), + }, + } + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", context.TODO(), &shieldv1beta1.GetProjectRequest{ + Id: projectSlug, + }).Return(projectRes, nil) + + optimusClient := new(optimus.OptimusClientMock) + service := optimus.NewService(shieldClient, optimusClient) + defer shieldClient.AssertExpectations(t) + + _ = mocks.NewJobSpecificationServiceClient(t) + optimusClient.On("BuildOptimusClient", hostname).Return(nil, expectedErr) + defer optimusClient.AssertExpectations(t) + + _, err := service.FindJobSpec(context.TODO(), jobName, projectSlug) + assert.ErrorIs(t, err, expectedErr) + }) + + t.Run("should return not found, if job could not be found", func(t *testing.T) { + projectRes := &shieldv1beta1.GetProjectResponse{ + Project: &shieldv1beta1.Project{ + Slug: "test-project", + Metadata: newStruct(t, map[string]interface{}{ + "optimus_host": hostname, + }), + }, + } + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", context.TODO(), &shieldv1beta1.GetProjectRequest{ + Id: projectSlug, + }).Return(projectRes, nil) + + optimusClient := new(optimus.OptimusClientMock) + service := optimus.NewService(shieldClient, optimusClient) + defer shieldClient.AssertExpectations(t) + + client := mocks.NewJobSpecificationServiceClient(t) + optimusClient.On("BuildOptimusClient", hostname).Return(client, nil) + defer optimusClient.AssertExpectations(t) + + client.On("GetJobSpecifications", context.TODO(), &optimusv1beta1.GetJobSpecificationsRequest{ + ProjectName: projectSlug, + JobName: jobName, + }).Return(&optimusv1beta1.GetJobSpecificationsResponse{}, nil) + defer client.AssertExpectations(t) + + _, err := service.FindJobSpec(context.TODO(), jobName, projectSlug) + assert.ErrorIs(t, err, errors.ErrNotFound) + }) +} + +func TestServiceListJobs(t *testing.T) { + projectName := "test-project" + hostname := "optimus.staging.golabs.io:80" + + t.Run("should return list of jobs using project in argument", func(t *testing.T) { + jobSpecRes := &optimusv1beta1.JobSpecificationResponse{ + ProjectName: projectName, + NamespaceName: "test-namespcace", + Job: &optimusv1beta1.JobSpecification{ + Version: 1, + Name: "sample-job", + Owner: "goto", + TaskName: "sample-task-name", + }, + } + + expectedResp := []*optimusv1beta1.JobSpecificationResponse{jobSpecRes} + + projectRes := &shieldv1beta1.GetProjectResponse{ + Project: &shieldv1beta1.Project{ + Slug: "test-project", + Metadata: newStruct(t, map[string]interface{}{ + "optimus_host": hostname, + }), + }, + } + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", context.TODO(), &shieldv1beta1.GetProjectRequest{ + Id: projectName, + }).Return(projectRes, nil) + + optimusClient := new(optimus.OptimusClientMock) + service := optimus.NewService(shieldClient, optimusClient) + defer shieldClient.AssertExpectations(t) + + client := mocks.NewJobSpecificationServiceClient(t) + optimusClient.On("BuildOptimusClient", hostname).Return(client, nil) + defer optimusClient.AssertExpectations(t) + + client.On("GetJobSpecifications", context.TODO(), &optimusv1beta1.GetJobSpecificationsRequest{ + ProjectName: projectName, + }).Return(&optimusv1beta1.GetJobSpecificationsResponse{ + JobSpecificationResponses: []*optimusv1beta1.JobSpecificationResponse{ + jobSpecRes, + }, + }, nil) + defer client.AssertExpectations(t) + resp, err := service.ListJobs(context.TODO(), projectName) + assert.Equal(t, expectedResp, resp) + assert.NoError(t, err) + }) + + t.Run("should return error, if project not found", func(t *testing.T) { + expectedErr := status.Error(codes.NotFound, "Not found") + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", context.TODO(), &shieldv1beta1.GetProjectRequest{ + Id: projectName, + }).Return(nil, expectedErr) + + optimusClient := new(optimus.OptimusClientMock) + service := optimus.NewService(shieldClient, optimusClient) + defer shieldClient.AssertExpectations(t) + defer optimusClient.AssertExpectations(t) + + _, err := service.ListJobs(context.TODO(), projectName) + assert.ErrorIs(t, err, expectedErr) + }) + + t.Run("should return error if metadata doesn't contain optimus hostname", func(t *testing.T) { + projectRes := &shieldv1beta1.GetProjectResponse{ + Project: &shieldv1beta1.Project{ + Slug: "test-project", + Metadata: newStruct(t, map[string]interface{}{}), + }, + } + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", context.TODO(), &shieldv1beta1.GetProjectRequest{ + Id: projectName, + }).Return(projectRes, nil) + + optimusClient := new(optimus.OptimusClientMock) + service := optimus.NewService(shieldClient, optimusClient) + defer shieldClient.AssertExpectations(t) + defer optimusClient.AssertExpectations(t) + + _, err := service.ListJobs(context.TODO(), projectName) + assert.ErrorIs(t, err, optimus.ErrOptimusHostNotFound) + }) + + t.Run("should return error, if creation of optimus client fails to create", func(t *testing.T) { + expectedErr := status.Error(codes.Internal, "Internal") + + projectRes := &shieldv1beta1.GetProjectResponse{ + Project: &shieldv1beta1.Project{ + Slug: "test-project", + Metadata: newStruct(t, map[string]interface{}{ + "optimus_host": hostname, + }), + }, + } + + shieldClient := new(mocks.ShieldServiceClient) + shieldClient.On("GetProject", context.TODO(), &shieldv1beta1.GetProjectRequest{ + Id: projectName, + }).Return(projectRes, nil) + + optimusClient := new(optimus.OptimusClientMock) + service := optimus.NewService(shieldClient, optimusClient) + defer shieldClient.AssertExpectations(t) + + _ = mocks.NewJobSpecificationServiceClient(t) + optimusClient.On("BuildOptimusClient", hostname).Return(nil, expectedErr) + defer optimusClient.AssertExpectations(t) + + _, err := service.ListJobs(context.TODO(), projectName) + assert.ErrorIs(t, err, expectedErr) + }) + +} + +func newStruct(t *testing.T, d map[string]interface{}) *structpb.Struct { + t.Helper() + + strct, err := structpb.NewStruct(d) + require.NoError(t, err) + return strct +}