github.com/kubeshop/testkube@v1.17.23/internal/app/api/v1/executions_test.go (about)

     1  package v1
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"net/http/httptest"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/kubeshop/testkube/pkg/featureflags"
    11  	"github.com/kubeshop/testkube/pkg/repository/result"
    12  
    13  	"github.com/gofiber/fiber/v2"
    14  	"github.com/golang/mock/gomock"
    15  	"github.com/stretchr/testify/assert"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"k8s.io/apimachinery/pkg/runtime"
    18  	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
    19  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    20  
    21  	executorv1 "github.com/kubeshop/testkube-operator/api/executor/v1"
    22  	executorsclientv1 "github.com/kubeshop/testkube-operator/pkg/client/executors/v1"
    23  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    24  	executorclient "github.com/kubeshop/testkube/pkg/executor/client"
    25  	"github.com/kubeshop/testkube/pkg/executor/output"
    26  	"github.com/kubeshop/testkube/pkg/log"
    27  	logclient "github.com/kubeshop/testkube/pkg/logs/client"
    28  	"github.com/kubeshop/testkube/pkg/logs/events"
    29  	"github.com/kubeshop/testkube/pkg/server"
    30  )
    31  
    32  func TestTestkubeAPI_ExecutionLogsHandler(t *testing.T) {
    33  	app := fiber.New()
    34  	resultRepo := MockExecutionResultsRepository{}
    35  	executor := &MockExecutor{}
    36  	s := &TestkubeAPI{
    37  		HTTPServer: server.HTTPServer{
    38  			Mux: app,
    39  			Log: log.DefaultLogger,
    40  		},
    41  		ExecutionResults: &resultRepo,
    42  		ExecutorsClient:  getMockExecutorClient(),
    43  		Executor:         executor,
    44  	}
    45  	app.Get("/executions/:executionID/logs", s.ExecutionLogsHandler())
    46  
    47  	tests := []struct {
    48  		name         string
    49  		route        string
    50  		expectedCode int
    51  		execution    testkube.Execution
    52  		jobLogs      testkube.ExecutorOutput
    53  		wantLogs     string
    54  	}{
    55  		{
    56  			name:         "Test getting execution from result output",
    57  			route:        "/executions/finished-1234/logs",
    58  			expectedCode: 200,
    59  			execution: testkube.Execution{
    60  				Id: "finished-1234",
    61  				ExecutionResult: &testkube.ExecutionResult{
    62  					Status: testkube.StatusPtr(testkube.PASSED_ExecutionStatus),
    63  					Output: "storage logs",
    64  				},
    65  			},
    66  			wantLogs: "storage logs",
    67  		},
    68  		{
    69  			name:         "Test getting execution from job",
    70  			route:        "/executions/running-1234/logs",
    71  			expectedCode: 200,
    72  			execution: testkube.Execution{
    73  				Id:       "running-1234",
    74  				TestType: "curl/test",
    75  				ExecutionResult: &testkube.ExecutionResult{
    76  					Status: testkube.StatusPtr(testkube.RUNNING_ExecutionStatus),
    77  				},
    78  			},
    79  			jobLogs: testkube.ExecutorOutput{
    80  				Type_:   output.TypeLogLine,
    81  				Content: "job logs",
    82  			},
    83  			wantLogs: "job logs",
    84  		},
    85  	}
    86  	responsePrefix := "data: "
    87  	for _, tt := range tests {
    88  		t.Run(tt.name, func(t *testing.T) {
    89  			resultRepo.GetFn = func(ctx context.Context, id string) (testkube.Execution, error) {
    90  				assert.Equal(t, tt.execution.Id, id)
    91  
    92  				return tt.execution, nil
    93  			}
    94  			executor.LogsFn = func(id string) (out chan output.Output, err error) {
    95  				assert.Equal(t, tt.execution.Id, id)
    96  
    97  				out = make(chan output.Output)
    98  				go func() {
    99  					defer func() {
   100  						close(out)
   101  					}()
   102  
   103  					out <- output.Output(tt.jobLogs)
   104  				}()
   105  				return
   106  			}
   107  
   108  			req := httptest.NewRequest("GET", tt.route, nil)
   109  			resp, err := app.Test(req, -1)
   110  			assert.NoError(t, err)
   111  			defer resp.Body.Close()
   112  
   113  			assert.Equal(t, tt.expectedCode, resp.StatusCode, tt.name)
   114  
   115  			b := make([]byte, len(responsePrefix))
   116  			resp.Body.Read(b)
   117  			assert.Equal(t, responsePrefix, string(b))
   118  
   119  			var res output.Output
   120  			err = json.NewDecoder(resp.Body).Decode(&res)
   121  			assert.NoError(t, err)
   122  			assert.Equal(t, tt.wantLogs, res.Content)
   123  		})
   124  	}
   125  }
   126  
   127  func TestTestkubeAPI_ExecutionLogsHandlerV2(t *testing.T) {
   128  	app := fiber.New()
   129  
   130  	mockCtrl := gomock.NewController(t)
   131  	defer mockCtrl.Finish()
   132  
   133  	grpcClient := logclient.NewMockStreamGetter(mockCtrl)
   134  
   135  	eventLog := events.Log{
   136  		Content: "storage logs",
   137  		Source:  events.SourceJobPod,
   138  		Version: string(events.LogVersionV2),
   139  	}
   140  
   141  	out := make(chan events.LogResponse)
   142  	go func() {
   143  		defer func() {
   144  			close(out)
   145  		}()
   146  
   147  		out <- events.LogResponse{Log: eventLog}
   148  	}()
   149  
   150  	grpcClient.EXPECT().Get(gomock.Any(), "test-execution-1").Return(out, nil)
   151  	s := &TestkubeAPI{
   152  		HTTPServer: server.HTTPServer{
   153  			Mux: app,
   154  			Log: log.DefaultLogger,
   155  		},
   156  		featureFlags:  featureflags.FeatureFlags{LogsV2: true},
   157  		logGrpcClient: grpcClient,
   158  	}
   159  	app.Get("/executions/:executionID/logs/v2", s.ExecutionLogsHandlerV2())
   160  
   161  	tests := []struct {
   162  		name         string
   163  		route        string
   164  		expectedCode int
   165  		eventLog     events.Log
   166  	}{
   167  		{
   168  			name:         "Test getting logs from grpc client",
   169  			route:        "/executions/test-execution-1/logs/v2",
   170  			expectedCode: 200,
   171  			eventLog:     eventLog,
   172  		},
   173  	}
   174  
   175  	responsePrefix := "data: "
   176  	for _, tt := range tests {
   177  		t.Run(tt.name, func(t *testing.T) {
   178  			req := httptest.NewRequest("GET", tt.route, nil)
   179  			resp, err := app.Test(req, -1)
   180  			assert.NoError(t, err)
   181  			defer resp.Body.Close()
   182  
   183  			assert.Equal(t, tt.expectedCode, resp.StatusCode, tt.name)
   184  
   185  			b := make([]byte, len(responsePrefix))
   186  			resp.Body.Read(b)
   187  			assert.Equal(t, responsePrefix, string(b))
   188  
   189  			var res events.Log
   190  			err = json.NewDecoder(resp.Body).Decode(&res)
   191  			assert.NoError(t, err)
   192  			assert.Equal(t, tt.eventLog, res)
   193  		})
   194  	}
   195  }
   196  
   197  type MockExecutionResultsRepository struct {
   198  	GetFn func(ctx context.Context, id string) (testkube.Execution, error)
   199  }
   200  
   201  func (r MockExecutionResultsRepository) Get(ctx context.Context, id string) (testkube.Execution, error) {
   202  	if r.GetFn == nil {
   203  		panic("not implemented")
   204  	}
   205  	return r.GetFn(ctx, id)
   206  }
   207  
   208  func (r MockExecutionResultsRepository) GetExecution(ctx context.Context, id string) (testkube.Execution, error) {
   209  	if r.GetFn == nil {
   210  		panic("not implemented")
   211  	}
   212  	return r.GetFn(ctx, id)
   213  }
   214  
   215  func (r MockExecutionResultsRepository) GetByNameAndTest(ctx context.Context, name, testName string) (testkube.Execution, error) {
   216  	panic("not implemented")
   217  }
   218  
   219  func (r MockExecutionResultsRepository) GetLatestByTest(ctx context.Context, testName string) (*testkube.Execution, error) {
   220  	panic("not implemented")
   221  }
   222  
   223  func (r MockExecutionResultsRepository) GetLatestByTests(ctx context.Context, testNames []string) (executions []testkube.Execution, err error) {
   224  	panic("not implemented")
   225  }
   226  
   227  func (r MockExecutionResultsRepository) GetExecutions(ctx context.Context, filter result.Filter) ([]testkube.Execution, error) {
   228  	panic("not implemented")
   229  }
   230  
   231  func (r MockExecutionResultsRepository) GetExecutionTotals(ctx context.Context, paging bool, filter ...result.Filter) (result testkube.ExecutionsTotals, err error) {
   232  	panic("not implemented")
   233  }
   234  
   235  func (r MockExecutionResultsRepository) GetNextExecutionNumber(ctx context.Context, testName string) (int32, error) {
   236  	panic("not implemented")
   237  }
   238  
   239  func (r MockExecutionResultsRepository) Insert(ctx context.Context, result testkube.Execution) error {
   240  	panic("not implemented")
   241  }
   242  
   243  func (r MockExecutionResultsRepository) Update(ctx context.Context, result testkube.Execution) error {
   244  	panic("not implemented")
   245  }
   246  
   247  func (r MockExecutionResultsRepository) UpdateResult(ctx context.Context, id string, execution testkube.Execution) error {
   248  	panic("not implemented")
   249  }
   250  
   251  func (r MockExecutionResultsRepository) StartExecution(ctx context.Context, id string, startTime time.Time) error {
   252  	panic("not implemented")
   253  }
   254  
   255  func (r MockExecutionResultsRepository) EndExecution(ctx context.Context, execution testkube.Execution) error {
   256  	panic("not implemented")
   257  }
   258  
   259  func (r MockExecutionResultsRepository) GetLabels(ctx context.Context) (labels map[string][]string, err error) {
   260  	panic("not implemented")
   261  }
   262  
   263  func (r MockExecutionResultsRepository) DeleteByTest(ctx context.Context, testName string) error {
   264  	panic("not implemented")
   265  }
   266  
   267  func (r MockExecutionResultsRepository) DeleteByTestSuite(ctx context.Context, testSuiteName string) error {
   268  	panic("not implemented")
   269  }
   270  
   271  func (r MockExecutionResultsRepository) DeleteAll(ctx context.Context) error {
   272  	panic("not implemented")
   273  }
   274  
   275  func (r MockExecutionResultsRepository) DeleteByTests(ctx context.Context, testNames []string) error {
   276  	panic("not implemented")
   277  }
   278  
   279  func (r MockExecutionResultsRepository) DeleteByTestSuites(ctx context.Context, testSuiteNames []string) error {
   280  	panic("not implemented")
   281  }
   282  
   283  func (r MockExecutionResultsRepository) DeleteForAllTestSuites(ctx context.Context) error {
   284  	panic("not implemented")
   285  }
   286  
   287  func (r MockExecutionResultsRepository) GetTestMetrics(ctx context.Context, name string, limit, last int) (testkube.ExecutionsMetrics, error) {
   288  	panic("not implemented")
   289  }
   290  
   291  func (r MockExecutionResultsRepository) Count(ctx context.Context, filter result.Filter) (int64, error) {
   292  	panic("not implemented")
   293  }
   294  
   295  type MockExecutor struct {
   296  	LogsFn func(id string) (chan output.Output, error)
   297  }
   298  
   299  func (e MockExecutor) Execute(ctx context.Context, execution *testkube.Execution, options executorclient.ExecuteOptions) (*testkube.ExecutionResult, error) {
   300  	panic("not implemented")
   301  }
   302  
   303  func (e MockExecutor) Abort(ctx context.Context, execution *testkube.Execution) (*testkube.ExecutionResult, error) {
   304  	panic("not implemented")
   305  }
   306  
   307  func (e MockExecutor) Logs(ctx context.Context, id, namespace string) (chan output.Output, error) {
   308  	if e.LogsFn == nil {
   309  		panic("not implemented")
   310  	}
   311  	return e.LogsFn(id)
   312  }
   313  
   314  func getMockExecutorClient() *executorsclientv1.ExecutorsClient {
   315  	scheme := runtime.NewScheme()
   316  	executorv1.AddToScheme(scheme)
   317  
   318  	initObjects := []k8sclient.Object{
   319  		&executorv1.Executor{
   320  			TypeMeta: metav1.TypeMeta{
   321  				Kind:       "Executor",
   322  				APIVersion: "executor.testkube.io/v1",
   323  			},
   324  			ObjectMeta: metav1.ObjectMeta{
   325  				Name:      "sample",
   326  				Namespace: "default",
   327  			},
   328  			Spec: executorv1.ExecutorSpec{
   329  				Types:                []string{"curl/test"},
   330  				ExecutorType:         "",
   331  				JobTemplate:          "",
   332  				JobTemplateReference: "",
   333  			},
   334  			Status: executorv1.ExecutorStatus{},
   335  		},
   336  	}
   337  
   338  	fakeClient := fake.NewClientBuilder().
   339  		WithScheme(scheme).
   340  		WithObjects(initObjects...).
   341  		Build()
   342  	return executorsclientv1.NewClient(fakeClient, "")
   343  }