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

     1  package v1
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"math"
     9  	"net/http"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/gofiber/fiber/v2"
    16  	"go.mongodb.org/mongo-driver/mongo"
    17  	"k8s.io/apimachinery/pkg/api/errors"
    18  	"k8s.io/apimachinery/pkg/util/yaml"
    19  
    20  	testsuitesv3 "github.com/kubeshop/testkube-operator/api/testsuite/v3"
    21  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    22  	"github.com/kubeshop/testkube/pkg/crd"
    23  	"github.com/kubeshop/testkube/pkg/datefilter"
    24  	"github.com/kubeshop/testkube/pkg/event/bus"
    25  	testsmapper "github.com/kubeshop/testkube/pkg/mapper/tests"
    26  	testsuiteexecutionsmapper "github.com/kubeshop/testkube/pkg/mapper/testsuiteexecutions"
    27  	testsuitesmapper "github.com/kubeshop/testkube/pkg/mapper/testsuites"
    28  	"github.com/kubeshop/testkube/pkg/repository/testresult"
    29  	"github.com/kubeshop/testkube/pkg/scheduler"
    30  	"github.com/kubeshop/testkube/pkg/types"
    31  	"github.com/kubeshop/testkube/pkg/utils"
    32  	"github.com/kubeshop/testkube/pkg/workerpool"
    33  )
    34  
    35  // CreateTestSuiteHandler for getting test object
    36  func (s TestkubeAPI) CreateTestSuiteHandler() fiber.Handler {
    37  	return func(c *fiber.Ctx) error {
    38  		errPrefix := "failed to create test suite"
    39  		var testSuite testsuitesv3.TestSuite
    40  		if string(c.Request().Header.ContentType()) == mediaTypeYAML {
    41  			testSuiteSpec := string(c.Body())
    42  			decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(testSuiteSpec), len(testSuiteSpec))
    43  			if err := decoder.Decode(&testSuite); err != nil {
    44  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse yaml request: %w", errPrefix, err))
    45  			}
    46  			errPrefix = errPrefix + " " + testSuite.Name
    47  		} else {
    48  			var request testkube.TestSuiteUpsertRequest
    49  			data := c.Body()
    50  			if string(c.Request().Header.ContentType()) != mediaTypeJSON {
    51  				return s.Error(c, http.StatusBadRequest, fiber.ErrUnprocessableEntity)
    52  			}
    53  
    54  			err := json.Unmarshal(data, &request)
    55  			if err != nil {
    56  				s.Log.Warnw("could not parse json request", "error", err)
    57  			}
    58  			errPrefix = errPrefix + " " + request.Name
    59  
    60  			emptyBatch := true
    61  			for _, step := range request.Steps {
    62  				if len(step.Execute) != 0 {
    63  					emptyBatch = false
    64  					break
    65  				}
    66  			}
    67  
    68  			if emptyBatch {
    69  				var requestV2 testkube.TestSuiteUpsertRequestV2
    70  				if err := json.Unmarshal(data, &requestV2); err != nil {
    71  					return s.Error(c, http.StatusBadRequest, err)
    72  				}
    73  
    74  				request = *requestV2.ToTestSuiteUpsertRequest()
    75  			}
    76  
    77  			if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML {
    78  				request.QuoteTestSuiteTextFields()
    79  				data, err := crd.GenerateYAML(crd.TemplateTestSuite, []testkube.TestSuiteUpsertRequest{request})
    80  				return s.getCRDs(c, data, err)
    81  			}
    82  
    83  			testSuite, err = testsuitesmapper.MapTestSuiteUpsertRequestToTestCRD(request)
    84  			if err != nil {
    85  				return s.Error(c, http.StatusBadRequest, err)
    86  			}
    87  
    88  			testSuite.Namespace = s.Namespace
    89  		}
    90  
    91  		s.Log.Infow("creating test suite", "testSuite", testSuite)
    92  
    93  		created, err := s.TestsSuitesClient.Create(&testSuite, s.disableSecretCreation)
    94  
    95  		s.Metrics.IncCreateTestSuite(err)
    96  
    97  		if err != nil {
    98  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not create test suite: %w", errPrefix, err))
    99  		}
   100  
   101  		c.Status(http.StatusCreated)
   102  		return c.JSON(created)
   103  	}
   104  }
   105  
   106  // UpdateTestSuiteHandler updates an existing TestSuite CR based on TestSuite content
   107  func (s TestkubeAPI) UpdateTestSuiteHandler() fiber.Handler {
   108  	return func(c *fiber.Ctx) error {
   109  		errPrefix := "failed to update test suite"
   110  		var request testkube.TestSuiteUpdateRequest
   111  		if string(c.Request().Header.ContentType()) == mediaTypeYAML {
   112  			var testSuite testsuitesv3.TestSuite
   113  			testSuiteSpec := string(c.Body())
   114  			decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(testSuiteSpec), len(testSuiteSpec))
   115  			if err := decoder.Decode(&testSuite); err != nil {
   116  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse yaml request: %w", errPrefix, err))
   117  			}
   118  			request = testsuitesmapper.MapTestSuiteTestCRDToUpdateRequest(&testSuite)
   119  		} else {
   120  			data := c.Body()
   121  			if string(c.Request().Header.ContentType()) != mediaTypeJSON {
   122  				return s.Error(c, http.StatusBadRequest, fiber.ErrUnprocessableEntity)
   123  			}
   124  
   125  			err := json.Unmarshal(data, &request)
   126  			if err != nil {
   127  				s.Log.Warnw("could not parse json request", "error", err)
   128  			}
   129  
   130  			if request.Steps != nil {
   131  				emptyBatch := true
   132  				for _, step := range *request.Steps {
   133  					if len(step.Execute) != 0 {
   134  						emptyBatch = false
   135  						break
   136  					}
   137  				}
   138  
   139  				if emptyBatch {
   140  					var requestV2 testkube.TestSuiteUpdateRequestV2
   141  					if err := json.Unmarshal(data, &requestV2); err != nil {
   142  						return s.Error(c, http.StatusBadRequest, err)
   143  					}
   144  
   145  					request = *requestV2.ToTestSuiteUpdateRequest()
   146  				}
   147  			}
   148  		}
   149  
   150  		var name string
   151  		if request.Name != nil {
   152  			name = *request.Name
   153  		}
   154  		errPrefix = errPrefix + " " + name
   155  
   156  		// we need to get resource first and load its metadata.ResourceVersion
   157  		testSuite, err := s.TestsSuitesClient.Get(name)
   158  		if err != nil {
   159  			if errors.IsNotFound(err) {
   160  				return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: test suite not found: %w", errPrefix, err))
   161  			}
   162  
   163  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get test suite: %w", errPrefix, err))
   164  		}
   165  
   166  		// map TestSuite but load spec only to not override metadata.ResourceVersion
   167  		testSuiteSpec, err := testsuitesmapper.MapTestSuiteUpdateRequestToTestCRD(request, testSuite)
   168  		if err != nil {
   169  			return s.Error(c, http.StatusBadRequest, err)
   170  		}
   171  
   172  		updatedTestSuite, err := s.TestsSuitesClient.Update(testSuiteSpec, s.disableSecretCreation)
   173  
   174  		s.Metrics.IncUpdateTestSuite(err)
   175  
   176  		if err != nil {
   177  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not update test suite: %w", errPrefix, err))
   178  		}
   179  
   180  		return c.JSON(updatedTestSuite)
   181  	}
   182  }
   183  
   184  // GetTestSuiteHandler for getting TestSuite object
   185  func (s TestkubeAPI) GetTestSuiteHandler() fiber.Handler {
   186  	return func(c *fiber.Ctx) error {
   187  		name := c.Params("id")
   188  		errPrefix := "failed to get test suite " + name
   189  
   190  		crTestSuite, err := s.TestsSuitesClient.Get(name)
   191  		if err != nil {
   192  			if errors.IsNotFound(err) {
   193  				return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite not found: %w", errPrefix, err))
   194  			}
   195  
   196  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get test suite: %w", errPrefix, err))
   197  		}
   198  
   199  		testSuite := testsuitesmapper.MapCRToAPI(*crTestSuite)
   200  		if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML {
   201  			testSuite.QuoteTestSuiteTextFields()
   202  			data, err := crd.GenerateYAML(crd.TemplateTestSuite, []testkube.TestSuite{testSuite})
   203  			return s.getCRDs(c, data, err)
   204  		}
   205  
   206  		return c.JSON(testSuite)
   207  	}
   208  }
   209  
   210  // GetTestSuiteWithExecutionHandler for getting TestSuite object with execution
   211  func (s TestkubeAPI) GetTestSuiteWithExecutionHandler() fiber.Handler {
   212  	return func(c *fiber.Ctx) error {
   213  		name := c.Params("id")
   214  		errPrefix := fmt.Sprintf("failed to get test suite %s with execution", name)
   215  		crTestSuite, err := s.TestsSuitesClient.Get(name)
   216  		if err != nil {
   217  			if errors.IsNotFound(err) {
   218  				return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite not found: %w", errPrefix, err))
   219  			}
   220  
   221  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get test suite: %w", errPrefix, err))
   222  		}
   223  
   224  		testSuite := testsuitesmapper.MapCRToAPI(*crTestSuite)
   225  		if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML {
   226  			testSuite.QuoteTestSuiteTextFields()
   227  			data, err := crd.GenerateYAML(crd.TemplateTestSuite, []testkube.TestSuite{testSuite})
   228  			return s.getCRDs(c, data, err)
   229  		}
   230  
   231  		ctx := c.Context()
   232  		execution, err := s.TestExecutionResults.GetLatestByTestSuite(ctx, name)
   233  		if err != nil && err != mongo.ErrNoDocuments {
   234  			return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not get execution: %w", errPrefix, err))
   235  		}
   236  
   237  		return c.JSON(testkube.TestSuiteWithExecution{
   238  			TestSuite:       &testSuite,
   239  			LatestExecution: execution,
   240  		})
   241  	}
   242  }
   243  
   244  // DeleteTestSuiteHandler for deleting a TestSuite with id
   245  func (s TestkubeAPI) DeleteTestSuiteHandler() fiber.Handler {
   246  	return func(c *fiber.Ctx) error {
   247  		name := c.Params("id")
   248  		errPrefix := fmt.Sprintf("failed to delete test suite %s", name)
   249  
   250  		err := s.TestsSuitesClient.Delete(name)
   251  		if err != nil {
   252  			if errors.IsNotFound(err) {
   253  				return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite not found: %w", errPrefix, err))
   254  			}
   255  
   256  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete test suite: %w", errPrefix, err))
   257  		}
   258  
   259  		// delete executions for test
   260  		if err = s.ExecutionResults.DeleteByTestSuite(c.Context(), name); err != nil {
   261  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete test suite test executions: %w", errPrefix, err))
   262  		}
   263  
   264  		// delete executions for test suite
   265  		if err = s.TestExecutionResults.DeleteByTestSuite(c.Context(), name); err != nil {
   266  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete test suite executions: %w", errPrefix, err))
   267  		}
   268  
   269  		return c.SendStatus(http.StatusNoContent)
   270  	}
   271  }
   272  
   273  // DeleteTestSuitesHandler for deleting all TestSuites
   274  func (s TestkubeAPI) DeleteTestSuitesHandler() fiber.Handler {
   275  	return func(c *fiber.Ctx) error {
   276  		errPrefix := "failed to delete test suites"
   277  
   278  		var err error
   279  		var testSuiteNames []string
   280  		selector := c.Query("selector")
   281  		if selector == "" {
   282  			err = s.TestsSuitesClient.DeleteAll()
   283  		} else {
   284  			var testSuiteList *testsuitesv3.TestSuiteList
   285  			testSuiteList, err = s.TestsSuitesClient.List(selector)
   286  			if err != nil {
   287  				if !errors.IsNotFound(err) {
   288  					return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list test suites: %w", errPrefix, err))
   289  				}
   290  			} else {
   291  				for _, item := range testSuiteList.Items {
   292  					testSuiteNames = append(testSuiteNames, item.Name)
   293  				}
   294  			}
   295  
   296  			err = s.TestsSuitesClient.DeleteByLabels(selector)
   297  		}
   298  
   299  		if err != nil {
   300  			if errors.IsNotFound(err) {
   301  				return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite not found: %w", errPrefix, err))
   302  			}
   303  
   304  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete test suites: %w", errPrefix, err))
   305  		}
   306  
   307  		// delete all executions for tests
   308  		if selector == "" {
   309  			err = s.ExecutionResults.DeleteForAllTestSuites(c.Context())
   310  		} else {
   311  			err = s.ExecutionResults.DeleteByTestSuites(c.Context(), testSuiteNames)
   312  		}
   313  
   314  		if err != nil {
   315  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list test suite test executions: %w", errPrefix, err))
   316  		}
   317  
   318  		// delete all executions for test suites
   319  		if selector == "" {
   320  			err = s.TestExecutionResults.DeleteAll(c.Context())
   321  		} else {
   322  			err = s.TestExecutionResults.DeleteByTestSuites(c.Context(), testSuiteNames)
   323  		}
   324  
   325  		if err != nil {
   326  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list test suite executions: %w", errPrefix, err))
   327  		}
   328  
   329  		return c.SendStatus(http.StatusNoContent)
   330  	}
   331  }
   332  
   333  func (s TestkubeAPI) getFilteredTestSuitesList(c *fiber.Ctx) (*testsuitesv3.TestSuiteList, error) {
   334  	crTestSuites, err := s.TestsSuitesClient.List(c.Query("selector"))
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  
   339  	search := c.Query("textSearch")
   340  	if search != "" {
   341  		// filter items array
   342  		for i := len(crTestSuites.Items) - 1; i >= 0; i-- {
   343  			if !strings.Contains(crTestSuites.Items[i].Name, search) {
   344  				crTestSuites.Items = append(crTestSuites.Items[:i], crTestSuites.Items[i+1:]...)
   345  			}
   346  		}
   347  	}
   348  
   349  	return crTestSuites, nil
   350  }
   351  
   352  // ListTestSuitesHandler for getting list of all available TestSuites
   353  func (s TestkubeAPI) ListTestSuitesHandler() fiber.Handler {
   354  	return func(c *fiber.Ctx) error {
   355  		errPrefix := "failed to list test suites"
   356  
   357  		crTestSuites, err := s.getFilteredTestSuitesList(c)
   358  		if err != nil {
   359  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list test suites: %w", errPrefix, err))
   360  		}
   361  
   362  		testSuites := testsuitesmapper.MapTestSuiteListKubeToAPI(*crTestSuites)
   363  		if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML {
   364  			for i := range testSuites {
   365  				testSuites[i].QuoteTestSuiteTextFields()
   366  			}
   367  
   368  			data, err := crd.GenerateYAML(crd.TemplateTestSuite, testSuites)
   369  			return s.getCRDs(c, data, err)
   370  		}
   371  
   372  		return c.JSON(testSuites)
   373  	}
   374  }
   375  
   376  // TestSuiteMetricsHandler returns basic metrics for given testsuite
   377  func (s TestkubeAPI) TestSuiteMetricsHandler() fiber.Handler {
   378  	return func(c *fiber.Ctx) error {
   379  		errPrefix := "failed to get test suite metrics"
   380  		const (
   381  			DefaultLastDays = 0
   382  			DefaultLimit    = 0
   383  		)
   384  
   385  		testSuiteName := c.Params("id")
   386  
   387  		limit, err := strconv.Atoi(c.Query("limit", strconv.Itoa(DefaultLimit)))
   388  		if err != nil {
   389  			limit = DefaultLimit
   390  		}
   391  
   392  		last, err := strconv.Atoi(c.Query("last", strconv.Itoa(DefaultLastDays)))
   393  		if err != nil {
   394  			last = DefaultLastDays
   395  		}
   396  
   397  		metrics, err := s.TestExecutionResults.GetTestSuiteMetrics(context.Background(), testSuiteName, limit, last)
   398  		if err != nil {
   399  			return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: failed to get metrics from client: %w", errPrefix, err))
   400  		}
   401  
   402  		return c.JSON(metrics)
   403  	}
   404  }
   405  
   406  // getLatestTestSuiteExecutions return latest test suite executions either by starttime or endtine for tests
   407  func (s TestkubeAPI) getLatestTestSuiteExecutions(ctx context.Context, testSuiteNames []string) (map[string]testkube.TestSuiteExecution, error) {
   408  	executions, err := s.TestExecutionResults.GetLatestByTestSuites(ctx, testSuiteNames)
   409  	if err != nil && err != mongo.ErrNoDocuments {
   410  		return nil, err
   411  	}
   412  
   413  	executionMap := make(map[string]testkube.TestSuiteExecution, len(executions))
   414  	for i := range executions {
   415  		if executions[i].TestSuite == nil {
   416  			continue
   417  		}
   418  		executionMap[executions[i].TestSuite.Name] = executions[i]
   419  	}
   420  	return executionMap, nil
   421  }
   422  
   423  // ListTestSuiteWithExecutionsHandler for getting list of all available TestSuite with latest executions
   424  func (s TestkubeAPI) ListTestSuiteWithExecutionsHandler() fiber.Handler {
   425  	return func(c *fiber.Ctx) error {
   426  		errPrefix := "failed to list test suites with executions"
   427  
   428  		crTestSuites, err := s.getFilteredTestSuitesList(c)
   429  		if err != nil {
   430  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list test suites: %w", errPrefix, err))
   431  		}
   432  
   433  		testSuites := testsuitesmapper.MapTestSuiteListKubeToAPI(*crTestSuites)
   434  		if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML {
   435  			for i := range testSuites {
   436  				testSuites[i].QuoteTestSuiteTextFields()
   437  			}
   438  
   439  			data, err := crd.GenerateYAML(crd.TemplateTestSuite, testSuites)
   440  			return s.getCRDs(c, data, err)
   441  		}
   442  
   443  		results := make([]testkube.TestSuiteWithExecutionSummary, 0, len(testSuites))
   444  		testSuiteNames := make([]string, len(testSuites))
   445  		for i := range testSuites {
   446  			testSuiteNames[i] = testSuites[i].Name
   447  		}
   448  
   449  		executionMap, err := s.getLatestTestSuiteExecutions(c.Context(), testSuiteNames)
   450  		if err != nil {
   451  			return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not list test suite executions from db: %w", errPrefix, err))
   452  		}
   453  
   454  		for i := range testSuites {
   455  			if execution, ok := executionMap[testSuites[i].Name]; ok {
   456  				results = append(results, testkube.TestSuiteWithExecutionSummary{
   457  					TestSuite:       &testSuites[i],
   458  					LatestExecution: testsuiteexecutionsmapper.MapToSummary(&execution),
   459  				})
   460  			} else {
   461  				results = append(results, testkube.TestSuiteWithExecutionSummary{
   462  					TestSuite: &testSuites[i],
   463  				})
   464  			}
   465  		}
   466  
   467  		sort.Slice(results, func(i, j int) bool {
   468  			iTime := results[i].TestSuite.Created
   469  			if results[i].LatestExecution != nil {
   470  				iTime = results[i].LatestExecution.EndTime
   471  				if results[i].LatestExecution.StartTime.After(results[i].LatestExecution.EndTime) {
   472  					iTime = results[i].LatestExecution.StartTime
   473  				}
   474  			}
   475  
   476  			jTime := results[j].TestSuite.Created
   477  			if results[j].LatestExecution != nil {
   478  				jTime = results[j].LatestExecution.EndTime
   479  				if results[j].LatestExecution.StartTime.After(results[j].LatestExecution.EndTime) {
   480  					jTime = results[j].LatestExecution.StartTime
   481  				}
   482  			}
   483  
   484  			return iTime.After(jTime)
   485  		})
   486  
   487  		status := c.Query("status")
   488  		if status != "" {
   489  			statusList, err := testkube.ParseTestSuiteExecutionStatusList(status, ",")
   490  			if err != nil {
   491  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: test suite execution status filter invalid: %w", errPrefix, err))
   492  			}
   493  
   494  			statusMap := statusList.ToMap()
   495  			// filter items array
   496  			for i := len(results) - 1; i >= 0; i-- {
   497  				if results[i].LatestExecution != nil && results[i].LatestExecution.Status != nil {
   498  					if _, ok := statusMap[*results[i].LatestExecution.Status]; ok {
   499  						continue
   500  					}
   501  				}
   502  
   503  				results = append(results[:i], results[i+1:]...)
   504  			}
   505  		}
   506  
   507  		var page, pageSize int
   508  		pageParam := c.Query("page", "")
   509  		if pageParam != "" {
   510  			pageSize = testresult.PageDefaultLimit
   511  			page, err = strconv.Atoi(pageParam)
   512  			if err != nil {
   513  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: test suite page filter invalid: %w", errPrefix, err))
   514  			}
   515  		}
   516  
   517  		pageSizeParam := c.Query("pageSize", "")
   518  		if pageSizeParam != "" {
   519  			pageSize, err = strconv.Atoi(pageSizeParam)
   520  			if err != nil {
   521  				s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: test suite page size filter invalid: %w", errPrefix, err))
   522  			}
   523  		}
   524  
   525  		if pageParam != "" || pageSizeParam != "" {
   526  			startPos := page * pageSize
   527  			endPos := (page + 1) * pageSize
   528  			if startPos < len(results) {
   529  				if endPos > len(results) {
   530  					endPos = len(results)
   531  				}
   532  
   533  				results = results[startPos:endPos]
   534  			}
   535  		}
   536  
   537  		return c.JSON(results)
   538  	}
   539  }
   540  
   541  func (s TestkubeAPI) ExecuteTestSuitesHandler() fiber.Handler {
   542  	return func(c *fiber.Ctx) error {
   543  		errPrefix := "failed to execute test suite"
   544  		var request testkube.TestSuiteExecutionRequest
   545  		err := c.BodyParser(&request)
   546  		if err != nil {
   547  			return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: test execution request body invalid: %w", errPrefix, err))
   548  		}
   549  
   550  		name := c.Params("id")
   551  		selector := c.Query("selector")
   552  		s.Log.Debugw("getting test suite", "name", name, "selector", selector)
   553  
   554  		var testSuites []testsuitesv3.TestSuite
   555  		if name != "" {
   556  			errPrefix = errPrefix + " " + name
   557  			testSuite, err := s.TestsSuitesClient.Get(name)
   558  			if err != nil {
   559  				if errors.IsNotFound(err) {
   560  					return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite not found: %w", errPrefix, err))
   561  				}
   562  
   563  				return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could get test suite: %w", errPrefix, err))
   564  			}
   565  			testSuites = append(testSuites, *testSuite)
   566  		} else {
   567  			testSuiteList, err := s.TestsSuitesClient.List(selector)
   568  			if err != nil {
   569  				return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: can't list test suites: %w", errPrefix, err))
   570  			}
   571  
   572  			testSuites = append(testSuites, testSuiteList.Items...)
   573  		}
   574  
   575  		var results []testkube.TestSuiteExecution
   576  		if len(testSuites) != 0 {
   577  			request.TestSuiteExecutionName = strings.Clone(c.Query("testSuiteExecutionName"))
   578  			concurrencyLevel, err := strconv.Atoi(c.Query("concurrency", strconv.Itoa(scheduler.DefaultConcurrencyLevel)))
   579  			if err != nil {
   580  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: can't detect concurrency level: %w", errPrefix, err))
   581  			}
   582  
   583  			workerpoolService := workerpool.New[testkube.TestSuite, testkube.TestSuiteExecutionRequest, testkube.TestSuiteExecution](concurrencyLevel)
   584  
   585  			go workerpoolService.SendRequests(s.scheduler.PrepareTestSuiteRequests(testSuites, request))
   586  			go workerpoolService.Run(c.Context())
   587  
   588  			for r := range workerpoolService.GetResponses() {
   589  				results = append(results, r.Result)
   590  			}
   591  		}
   592  
   593  		s.Log.Debugw("executing test", "name", name, "selector", selector)
   594  		if name != "" && len(results) != 0 {
   595  			if results[0].IsFailed() {
   596  				return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: Test suite failed %v", errPrefix, name))
   597  			}
   598  
   599  			c.Status(http.StatusCreated)
   600  			return c.JSON(results[0])
   601  		}
   602  
   603  		c.Status(http.StatusCreated)
   604  		return c.JSON(results)
   605  	}
   606  }
   607  
   608  func (s TestkubeAPI) ListTestSuiteExecutionsHandler() fiber.Handler {
   609  	return func(c *fiber.Ctx) error {
   610  
   611  		now := time.Now()
   612  		var l = s.Log.With("handler", "ListTestSuiteExecutionsHandler", "id", utils.RandAlphanum(10))
   613  
   614  		errPrefix := "failed to list test suite execution"
   615  		filter := getExecutionsFilterFromRequest(c)
   616  
   617  		ctx := c.Context()
   618  		executionsTotals, err := s.TestExecutionResults.GetExecutionsTotals(ctx, filter)
   619  		if err != nil {
   620  			return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: client could not get executions totals: %w", errPrefix, err))
   621  		}
   622  		l.Debugw("got executions totals", "totals", executionsTotals, "time", time.Since(now))
   623  		filterAllTotals := *filter.(*testresult.FilterImpl)
   624  		filterAllTotals.WithPage(0).WithPageSize(math.MaxInt64)
   625  		allExecutionsTotals, err := s.TestExecutionResults.GetExecutionsTotals(ctx, filterAllTotals)
   626  		if err != nil {
   627  			return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: client could not get all executions totals: %w", errPrefix, err))
   628  		}
   629  		l.Debugw("got all executions totals", "totals", executionsTotals, "time", time.Since(now))
   630  
   631  		executions, err := s.TestExecutionResults.GetExecutions(ctx, filter)
   632  		if err != nil {
   633  			return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: client could not get executions: %w", errPrefix, err))
   634  		}
   635  		l.Debugw("got executions", "time", time.Since(now))
   636  
   637  		return c.JSON(testkube.TestSuiteExecutionsResult{
   638  			Totals:   &allExecutionsTotals,
   639  			Filtered: &executionsTotals,
   640  			Results:  testsuitesmapper.MapToTestExecutionSummary(executions),
   641  		})
   642  	}
   643  }
   644  
   645  func (s TestkubeAPI) GetTestSuiteExecutionHandler() fiber.Handler {
   646  	return func(c *fiber.Ctx) error {
   647  		id := c.Params("executionID")
   648  		errPrefix := fmt.Sprintf("failed to get test suite execution %s", id)
   649  
   650  		execution, err := s.TestExecutionResults.Get(c.Context(), id)
   651  		if err == mongo.ErrNoDocuments {
   652  			return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: test suite with execution id/name %s not found", errPrefix, id))
   653  		}
   654  		if err != nil {
   655  			return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not get test suite executions from db: %w", errPrefix, err))
   656  		}
   657  
   658  		execution.Duration = types.FormatDuration(execution.Duration)
   659  
   660  		secretMap := make(map[string]string)
   661  		if execution.SecretUUID != "" && execution.TestSuite != nil {
   662  			secretMap, err = s.TestsSuitesClient.GetSecretTestSuiteVars(execution.TestSuite.Name, execution.SecretUUID)
   663  			if err != nil {
   664  				return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get test suite secrets: %w", errPrefix, err))
   665  			}
   666  		}
   667  
   668  		for key, value := range secretMap {
   669  			if variable, ok := execution.Variables[key]; ok && value != "" {
   670  				variable.Value = value
   671  				variable.SecretRef = nil
   672  				execution.Variables[key] = variable
   673  			}
   674  		}
   675  
   676  		return c.JSON(execution)
   677  	}
   678  }
   679  
   680  func (s TestkubeAPI) ListTestSuiteArtifactsHandler() fiber.Handler {
   681  	return func(c *fiber.Ctx) error {
   682  		s.Log.Infow("listing testsuite artifacts", "executionID", c.Params("executionID"))
   683  		id := c.Params("executionID")
   684  		errPrefix := fmt.Sprintf("failed to list test suite artifacts %s", id)
   685  		execution, err := s.TestExecutionResults.Get(c.Context(), id)
   686  		if err == mongo.ErrNoDocuments {
   687  			return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: test suite with execution id/name %s not found", errPrefix, id))
   688  		}
   689  		if err != nil {
   690  			return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not get test suite execution from db: %w", errPrefix, err))
   691  		}
   692  
   693  		var artifacts []testkube.Artifact
   694  		for _, stepResult := range execution.StepResults {
   695  			if stepResult.Execution == nil || stepResult.Execution.Id == "" {
   696  				continue
   697  			}
   698  
   699  			artifacts, err = s.getExecutionArtfacts(c.Context(), stepResult.Execution, artifacts)
   700  			if err != nil {
   701  				continue
   702  			}
   703  		}
   704  
   705  		for _, stepResults := range execution.ExecuteStepResults {
   706  			for _, stepResult := range stepResults.Execute {
   707  				if stepResult.Execution == nil || stepResult.Execution.Id == "" {
   708  					continue
   709  				}
   710  
   711  				artifacts, err = s.getExecutionArtfacts(c.Context(), stepResult.Execution, artifacts)
   712  				if err != nil {
   713  					continue
   714  				}
   715  			}
   716  		}
   717  
   718  		if err != nil {
   719  			return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not list artifacts: %w", errPrefix, err))
   720  		}
   721  
   722  		return c.JSON(artifacts)
   723  	}
   724  }
   725  
   726  func (s TestkubeAPI) getExecutionArtfacts(ctx context.Context, execution *testkube.Execution,
   727  	artifacts []testkube.Artifact) ([]testkube.Artifact, error) {
   728  	var stepArtifacts []testkube.Artifact
   729  	var bucket string
   730  
   731  	artifactsStorage := s.ArtifactsStorage
   732  	folder := execution.Id
   733  	if execution.ArtifactRequest != nil {
   734  		bucket = execution.ArtifactRequest.StorageBucket
   735  		if execution.ArtifactRequest.OmitFolderPerExecution {
   736  			folder = ""
   737  		}
   738  	}
   739  
   740  	var err error
   741  	if bucket != "" {
   742  		artifactsStorage, err = s.getArtifactStorage(bucket)
   743  		if err != nil {
   744  			s.Log.Warnw("can't get artifact storage", "executionID", execution.Id, "error", err)
   745  			return artifacts, err
   746  		}
   747  	}
   748  
   749  	stepArtifacts, err = artifactsStorage.ListFiles(ctx, folder, execution.TestName, execution.TestSuiteName, "")
   750  	if err != nil {
   751  		s.Log.Warnw("can't list artifacts", "executionID", execution.Id, "error", err)
   752  		return artifacts, err
   753  	}
   754  
   755  	s.Log.Debugw("listing artifacts for step", "executionID", execution.Id, "artifacts", stepArtifacts)
   756  	for i := range stepArtifacts {
   757  		stepArtifacts[i].ExecutionName = execution.Name
   758  		artifacts = append(artifacts, stepArtifacts[i])
   759  	}
   760  
   761  	return artifacts, nil
   762  }
   763  
   764  // AbortTestSuiteHandler for aborting a TestSuite with id
   765  func (s TestkubeAPI) AbortTestSuiteHandler() fiber.Handler {
   766  	return func(c *fiber.Ctx) error {
   767  		ctx := c.Context()
   768  		name := c.Params("id")
   769  		if name == "" {
   770  			return s.Error(c, http.StatusBadRequest, fmt.Errorf("failed to abort test suite: id cannot be empty"))
   771  		}
   772  		errPrefix := fmt.Sprintf("failed to abort test suite %s", name)
   773  		filter := testresult.NewExecutionsFilter().WithName(name).WithStatus(string(testkube.RUNNING_ExecutionStatus))
   774  		executions, err := s.TestExecutionResults.GetExecutions(ctx, filter)
   775  		if err != nil {
   776  			if err == mongo.ErrNoDocuments {
   777  				return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: executions with test syute name %s not found", errPrefix, name))
   778  			}
   779  			return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not get executions: %w", errPrefix, err))
   780  		}
   781  
   782  		for _, execution := range executions {
   783  			execution.Status = testkube.TestSuiteExecutionStatusAborting
   784  			s.Log.Infow("aborting test suite execution", "executionID", execution.Id)
   785  			err := s.eventsBus.PublishTopic(bus.InternalPublishTopic, testkube.NewEventEndTestSuiteAborted(&execution))
   786  
   787  			if err != nil {
   788  				return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not sent test suite abortion event: %w", errPrefix, err))
   789  			}
   790  
   791  			s.Log.Infow("test suite execution aborted, event sent", "executionID", c.Params("executionID"))
   792  		}
   793  
   794  		return c.Status(http.StatusNoContent).SendString("")
   795  	}
   796  }
   797  
   798  func (s TestkubeAPI) AbortTestSuiteExecutionHandler() fiber.Handler {
   799  	return func(c *fiber.Ctx) error {
   800  		s.Log.Infow("aborting test suite execution", "executionID", c.Params("executionID"))
   801  		id := c.Params("executionID")
   802  		errPrefix := fmt.Sprintf("failed to abort test suite execution %s", id)
   803  		execution, err := s.TestExecutionResults.Get(c.Context(), id)
   804  		if err == mongo.ErrNoDocuments {
   805  			return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: test suite with execution id/name %s not found", errPrefix, id))
   806  		}
   807  		if err != nil {
   808  			return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not abort test suite execution: %w", errPrefix, err))
   809  		}
   810  
   811  		execution.Status = testkube.TestSuiteExecutionStatusAborting
   812  
   813  		err = s.eventsBus.PublishTopic(bus.InternalPublishTopic, testkube.NewEventEndTestSuiteAborted(&execution))
   814  
   815  		if err != nil {
   816  			return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not sent test suite abortion event: %w", errPrefix, err))
   817  		}
   818  		s.Log.Infow("test suite execution aborted, event sent", "executionID", c.Params("executionID"))
   819  
   820  		return c.Status(http.StatusNoContent).SendString("")
   821  	}
   822  }
   823  
   824  // ListTestSuiteTestsHandler for getting list of all available Tests for TestSuites
   825  func (s TestkubeAPI) ListTestSuiteTestsHandler() fiber.Handler {
   826  	return func(c *fiber.Ctx) error {
   827  		name := c.Params("id")
   828  		errPrefix := fmt.Sprintf("failed to list tests for test suite %s", name)
   829  		crTestSuite, err := s.TestsSuitesClient.Get(name)
   830  		if err != nil {
   831  			if errors.IsNotFound(err) {
   832  				return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite with id/name %s not found", errPrefix, name))
   833  			}
   834  
   835  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could get test suite: %w", errPrefix, err))
   836  		}
   837  
   838  		testSuite := testsuitesmapper.MapCRToAPI(*crTestSuite)
   839  		crTests, err := s.TestsClient.ListByNames(testSuite.GetTestNames())
   840  		if err != nil {
   841  			if errors.IsNotFound(err) {
   842  				return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite tests with id/name %s not found", errPrefix, testSuite.GetTestNames()))
   843  			}
   844  
   845  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could get tests for test suite: %w", errPrefix, err))
   846  		}
   847  
   848  		return c.JSON(testsmapper.MapTestArrayKubeToAPI(crTests))
   849  	}
   850  }
   851  
   852  func getExecutionsFilterFromRequest(c *fiber.Ctx) testresult.Filter {
   853  
   854  	filter := testresult.NewExecutionsFilter()
   855  	name := c.Query("id", "")
   856  	if name != "" {
   857  		filter = filter.WithName(name)
   858  	}
   859  
   860  	textSearch := c.Query("textSearch", "")
   861  	if textSearch != "" {
   862  		filter = filter.WithTextSearch(textSearch)
   863  	}
   864  
   865  	page, err := strconv.Atoi(c.Query("page", ""))
   866  	if err == nil {
   867  		filter = filter.WithPage(page)
   868  	}
   869  
   870  	pageSize, err := strconv.Atoi(c.Query("pageSize", ""))
   871  	if err == nil && pageSize != 0 {
   872  		filter = filter.WithPageSize(pageSize)
   873  	}
   874  
   875  	status := c.Query("status", "")
   876  	if status != "" {
   877  		filter = filter.WithStatus(status)
   878  	}
   879  
   880  	last, err := strconv.Atoi(c.Query("last", "0"))
   881  	if err == nil && last != 0 {
   882  		filter = filter.WithLastNDays(last)
   883  	}
   884  
   885  	dFilter := datefilter.NewDateFilter(c.Query("startDate", ""), c.Query("endDate", ""))
   886  	if dFilter.IsStartValid {
   887  		filter = filter.WithStartDate(dFilter.Start)
   888  	}
   889  
   890  	if dFilter.IsEndValid {
   891  		filter = filter.WithEndDate(dFilter.End)
   892  	}
   893  
   894  	selector := c.Query("selector")
   895  	if selector != "" {
   896  		filter = filter.WithSelector(selector)
   897  	}
   898  
   899  	return filter
   900  }