diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index f7d22ad65d..f17a4a4301 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -4,7 +4,6 @@ on: push: branches: [main] pull_request: - branches: [main] env: VERSION: ${{ github.sha }} @@ -23,6 +22,7 @@ jobs: backend-arch-graph: name: Generate backend architecture graph runs-on: ubuntu-latest + continue-on-error: true steps: - uses: actions/checkout@v3 with: @@ -88,7 +88,7 @@ jobs: cache: true cache-dependency-path: go.work - name: Run unit tests - run: cd server; make test + run: cd server; make test -B unit-test-web: name: WebUI unit tests @@ -304,7 +304,7 @@ jobs: - name: Start services run: | ./run.sh down up - ./run.sh logstt > /tmp/docker-log & + ./run.sh tracetest-logs > /tmp/docker-log & - name: Run tests run: | chmod +x ./dist/tracetest ./dist/tracetest-server @@ -400,7 +400,7 @@ jobs: - name: Run integration tests run: | ./run.sh down up - ./run.sh logstt > /tmp/docker-log & + ./run.sh tracetest-logs > /tmp/docker-log & ./scripts/wait-for-port.sh 11633 if [ "${{env.CYPRESS_RECORD_KEY}}" = "" ]; then diff --git a/api/openapi.yaml b/api/openapi.yaml index baaaf8c243..f82c82ffb9 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -31,32 +31,6 @@ paths: application/json: schema: $ref: "./definition.yaml#/components/schemas/ExecuteDefinitionResponse" - - put: - tags: - - api - summary: "Upsert a definition" - description: "Upsert a definition" - operationId: upsertDefinition - requestBody: - content: - text/json: - schema: - $ref: "./definition.yaml#/components/schemas/TextDefinition" - responses: - 201: - description: Definition created - content: - application/json: - schema: - $ref: "./definition.yaml#/components/schemas/UpsertDefinitionResponse" - 200: - description: Definition updated - content: - application/json: - schema: - $ref: "./definition.yaml#/components/schemas/UpsertDefinitionResponse" - # Transactions /transactions: get: @@ -305,7 +279,7 @@ paths: /tests: get: tags: - - api + - resource-api summary: "Get tests" description: "get tests" operationId: getTests @@ -318,22 +292,20 @@ paths: responses: 200: description: successful operation - headers: - X-Total-Count: - schema: - type: integer - description: Total records count content: application/json: schema: - type: array - items: - $ref: "./tests.yaml#/components/schemas/Test" + $ref: "./tests.yaml#/components/schemas/TestResourceList" + text/yaml: + schema: + $ref: "./tests.yaml#/components/schemas/TestResourceList" + 400: + description: "invalid query for test, some data was sent in incorrect format." 500: description: "problem with getting tests" post: tags: - - api + - resource-api summary: "Create new test" description: "Create new test action" operationId: createTest @@ -354,24 +326,23 @@ paths: /tests/{testId}: get: tags: - - api + - resource-api parameters: - $ref: "./parameters.yaml#/components/parameters/testId" summary: "get test" description: "get test" - operationId: getTest responses: 200: description: successful operation content: application/json: schema: - $ref: "./tests.yaml#/components/schemas/Test" + $ref: "./tests.yaml#/components/schemas/TestResource" 500: description: "problem with getting a test" put: tags: - - api + - resource-api parameters: - $ref: "./parameters.yaml#/components/parameters/testId" summary: "update test" @@ -389,7 +360,7 @@ paths: description: "problem with updating test" delete: tags: - - api + - resource-api parameters: - $ref: "./parameters.yaml#/components/parameters/testId" summary: "delete a test" @@ -625,23 +596,6 @@ paths: $ref: "./tests.yaml#/components/schemas/Test" 500: description: "problem with getting a test" - /tests/{testId}/version/{version}/definition.yaml: - get: - tags: - - api - parameters: - - $ref: "./parameters.yaml#/components/parameters/testId" - - $ref: "./parameters.yaml#/components/parameters/version" - summary: Get the test definition as an YAML file - description: Get the test definition as an YAML file - operationId: getTestVersionDefinitionFile - responses: - 200: - description: OK - content: - application/yaml: - schema: - type: string /tests/{testId}/run/{runId}/stop: post: tags: diff --git a/api/tests.yaml b/api/tests.yaml index d419399976..0687e98ff7 100644 --- a/api/tests.yaml +++ b/api/tests.yaml @@ -1,6 +1,29 @@ openapi: 3.0.0 components: schemas: + + TestResourceList: + type: object + properties: + count: + type: integer + items: + type: array + items: + $ref: "#/components/schemas/TestResource" + + TestResource: + type: object + description: "Represents a test structured into the Resources format." + properties: + type: + type: string + description: "Represents the type of this resource. It should always be set as 'Test'." + enum: + - Test + spec: + $ref: "#/components/schemas/Test" + Test: type: object properties: @@ -17,7 +40,7 @@ components: createdAt: type: string format: date-time - serviceUnderTest: + trigger: $ref: "./triggers.yaml#/components/schemas/Trigger" specs: type: array diff --git a/api/triggers.yaml b/api/triggers.yaml index e8e0a1aa12..cbb7e8065b 100644 --- a/api/triggers.yaml +++ b/api/triggers.yaml @@ -4,10 +4,10 @@ components: Trigger: type: object properties: - triggerType: + type: type: string enum: ["http", "grpc", "traceid"] - http: + httpRequest: $ref: "./http.yaml#/components/schemas/HTTPRequest" grpc: $ref: "./grpc.yaml#/components/schemas/GRPCRequest" @@ -17,7 +17,7 @@ components: TriggerResult: type: object properties: - triggerType: + type: type: string enum: ["http", "grpc", "traceid"] triggerResult: diff --git a/cli/actions/export_test_action.go b/cli/actions/export_test_action.go deleted file mode 100644 index 268e23cd0d..0000000000 --- a/cli/actions/export_test_action.go +++ /dev/null @@ -1,89 +0,0 @@ -package actions - -import ( - "context" - "fmt" - - "github.com/kubeshop/tracetest/cli/config" - "github.com/kubeshop/tracetest/cli/file" - "github.com/kubeshop/tracetest/cli/openapi" - "go.uber.org/zap" -) - -type ExportTestConfig struct { - TestId string - OutputFile string - Version int32 -} - -type exportTestAction struct { - config config.Config - logger *zap.Logger - client *openapi.APIClient -} - -func NewExportTestAction(config config.Config, logger *zap.Logger, client *openapi.APIClient) exportTestAction { - return exportTestAction{ - config: config, - logger: logger, - client: client, - } -} - -func (a exportTestAction) Run(ctx context.Context, args ExportTestConfig) error { - if args.OutputFile == "" { - return fmt.Errorf("output file must be provided") - } - - if args.TestId == "" { - return fmt.Errorf("test id must be provided") - } - - a.logger.Debug("exporting test", zap.String("testID", args.TestId), zap.String("outputFile", args.OutputFile)) - definition, err := a.getDefinitionFromServer(ctx, args) - if err != nil { - return fmt.Errorf("could not get definition from server: %w", err) - } - - f, err := file.New(args.OutputFile, []byte(definition)) - if err != nil { - return fmt.Errorf("could not process definition from server: %w", err) - } - - _, err = f.Write() - if err != nil { - return fmt.Errorf("could not save exported definition into file: %w", err) - } - - return nil -} - -func (a exportTestAction) getDefinitionFromServer(ctx context.Context, args ExportTestConfig) (string, error) { - expectedVersion := args.Version - if expectedVersion < 0 { - test, err := a.getTestFromServer(ctx, args.TestId) - if err != nil { - return "", fmt.Errorf("could not get test: %w", err) - } - - expectedVersion = *test.Version - } - - request := a.client.ApiApi.GetTestVersionDefinitionFile(ctx, args.TestId, expectedVersion) - definitionString, _, err := a.client.ApiApi.GetTestVersionDefinitionFileExecute(request) - if err != nil { - return "", fmt.Errorf("could not get test definition: %w", err) - } - - return definitionString, nil -} - -func (a exportTestAction) getTestFromServer(ctx context.Context, testID string) (openapi.Test, error) { - req := a.client.ApiApi.GetTest(ctx, testID) - openapiTest, _, err := a.client.ApiApi.GetTestExecute(req) - if err != nil { - return openapi.Test{}, fmt.Errorf("could not execute getTest request: %w", err) - } - - return *openapiTest, nil -} diff --git a/cli/actions/list_tests_action.go b/cli/actions/list_tests_action.go deleted file mode 100644 index 0afaa222df..0000000000 --- a/cli/actions/list_tests_action.go +++ /dev/null @@ -1,54 +0,0 @@ -package actions - -import ( - "context" - "fmt" - - "github.com/kubeshop/tracetest/cli/config" - "github.com/kubeshop/tracetest/cli/formatters" - "github.com/kubeshop/tracetest/cli/openapi" - "go.uber.org/zap" -) - -type ListTestConfig struct{} - -type listTestsAction struct { - config config.Config - logger *zap.Logger - client *openapi.APIClient -} - -func NewListTestsAction(config config.Config, logger *zap.Logger, client *openapi.APIClient) listTestsAction { - return listTestsAction{config, logger, client} -} - -func (a listTestsAction) Run(ctx context.Context, args ListTestConfig) error { - tests, err := a.executeRequest(ctx) - if err != nil { - return err - } - - formatter := formatters.TestsList(a.config) - formattedOutput := formatter.Format(tests) - fmt.Println(formattedOutput) - - return nil -} - -func (a listTestsAction) executeRequest(ctx context.Context) ([]openapi.Test, error) { - request := a.client.ApiApi.GetTests(ctx) - tests, response, err := a.client.ApiApi.GetTestsExecute(request) - if err != nil { - return []openapi.Test{}, fmt.Errorf("could not get tests: %w", err) - } - - if response.StatusCode != 200 { - return []openapi.Test{}, fmt.Errorf("get tests request failed. Expected 200, got %d", response.StatusCode) - } - - if tests == nil { - return []openapi.Test{}, nil - } - - return tests, nil -} diff --git a/cli/actions/run_test_action.go b/cli/actions/run_test_action.go index 9c0d2b7ae6..1ae90eb24c 100644 --- a/cli/actions/run_test_action.go +++ b/cli/actions/run_test_action.go @@ -4,25 +4,23 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "os" - "path/filepath" "strconv" "sync" "time" - "github.com/Jeffail/gabs/v2" cienvironment "github.com/cucumber/ci-environment/go" + "github.com/goccy/go-yaml" "github.com/kubeshop/tracetest/cli/config" - "github.com/kubeshop/tracetest/cli/file" "github.com/kubeshop/tracetest/cli/formatters" "github.com/kubeshop/tracetest/cli/openapi" "github.com/kubeshop/tracetest/cli/pkg/fileutil" "github.com/kubeshop/tracetest/cli/pkg/resourcemanager" "github.com/kubeshop/tracetest/cli/ui" "github.com/kubeshop/tracetest/cli/utils" - "github.com/kubeshop/tracetest/server/model/yaml" + "github.com/kubeshop/tracetest/cli/variable" "go.uber.org/zap" ) @@ -33,34 +31,66 @@ type RunResourceArgs struct { JUnit string } -type runTestAction struct { - config config.Config - logger *zap.Logger - client *openapi.APIClient - environments resourcemanager.Client - cliExit func(int) -} +func (args RunResourceArgs) Validate() error { + if args.DefinitionFile == "" { + return fmt.Errorf("you must specify a definition file to run a test") + } + + if args.JUnit != "" && !args.WaitForResult { + return fmt.Errorf("--junit option requires --wait-for-result") + } -type runDefParams struct { - DefinitionFile string - EnvID string - WaitForResult bool - JunitFile string - Metadata map[string]string - EnvironmentVariables map[string]string + return nil } -func NewRunTestAction(config config.Config, logger *zap.Logger, client *openapi.APIClient, envs resourcemanager.Client, cliExit func(int)) runTestAction { - return runTestAction{config, logger, client, envs, cliExit} +type runTestAction struct { + config config.Config + logger *zap.Logger + openapiClient *openapi.APIClient + environments resourcemanager.Client + tests resourcemanager.Client + transactions resourcemanager.Client + yamlFormat resourcemanager.Format + jsonFormat resourcemanager.Format + cliExit func(int) } -func (a runTestAction) Run(ctx context.Context, args RunResourceArgs) error { - if args.DefinitionFile == "" { - return fmt.Errorf("you must specify a definition file to run a test") +func NewRunTestAction( + config config.Config, + logger *zap.Logger, + openapiClient *openapi.APIClient, + tests resourcemanager.Client, + transactions resourcemanager.Client, + environments resourcemanager.Client, + cliExit func(int), +) runTestAction { + + yamlFormat, err := resourcemanager.Formats.Get(resourcemanager.FormatYAML) + if err != nil { + panic(fmt.Errorf("could not get yaml format: %w", err)) } - if args.JUnit != "" && !args.WaitForResult { - return fmt.Errorf("--junit option requires --wait-for-result") + jsonFormat, err := resourcemanager.Formats.Get(resourcemanager.FormatJSON) + if err != nil { + panic(fmt.Errorf("could not get json format: %w", err)) + } + + return runTestAction{ + config, + logger, + openapiClient, + environments, + tests, + transactions, + yamlFormat, + jsonFormat, + cliExit, + } +} + +func (a runTestAction) Run(ctx context.Context, args RunResourceArgs) error { + if err := args.Validate(); err != nil { + return err } a.logger.Debug( @@ -71,435 +101,635 @@ func (a runTestAction) Run(ctx context.Context, args RunResourceArgs) error { zap.String("junit", args.JUnit), ) - envID := args.EnvID + f, err := fileutil.Read(args.DefinitionFile) + if err != nil { + return fmt.Errorf("cannot read definition file %s: %w", args.DefinitionFile, err) + } + df := defFile{f} + a.logger.Debug("Definition file read", zap.String("absolutePath", df.AbsPath())) + + envID, err := a.resolveEnvID(ctx, args.EnvID) + if err != nil { + return fmt.Errorf("cannot resolve environment id: %w", err) + } + a.logger.Debug("env resolved", zap.String("ID", envID)) - if fileutil.IsFilePath(args.EnvID) { - a.logger.Debug("resolve envID from file reference", zap.String("path", envID)) - jsonFormat, err := resourcemanager.Formats.Get(resourcemanager.FormatJSON) - if err != nil { - return fmt.Errorf("could not get json format: %w", err) - } + df, err = a.apply(ctx, df) + if err != nil { + return fmt.Errorf("cannot apply definition file: %w", err) + } - envJson, err := a.environments.Apply(ctx, args.EnvID, jsonFormat) - if err != nil { - return fmt.Errorf("could not read environment file: %w", err) + a.logger.Debug("Definition file applied", zap.String("updated", string(df.Contents()))) + + var run runResult + var envVars []envVar + + // iterate until we have all env vars, + // or the server returns an actual error + for { + run, err = a.run(ctx, df, envID, envVars) + if err == nil { + break + } + missingEnvVarsErr, ok := err.(missingEnvVarsError) + if !ok { + // actual error, return + return fmt.Errorf("cannot run test: %w", err) } - parsed, err := gabs.ParseJSON([]byte(envJson)) + // missing vars error + envVars = a.askForMissingVars([]envVar(missingEnvVarsErr)) + } + + if !args.WaitForResult { + fmt.Println(a.formatResult(run, false)) + a.cliExit(0) + } + + result, err := a.waitForResult(ctx, run) + if err != nil { + return fmt.Errorf("cannot wait for test result: %w", err) + } + + fmt.Println(a.formatResult(result, true)) + a.cliExit(a.exitCode(result)) + + if args.JUnit != "" { + err := a.writeJUnitReport(ctx, result, args.JUnit) if err != nil { - return fmt.Errorf("could not parse environment json: %w", err) + return fmt.Errorf("cannot write junit report: %w", err) } + } - ok := true - envID, ok = parsed.Path("spec.id").Data().(string) - if !ok { - return fmt.Errorf("could not parse environment id from json") + return nil +} + +func (a runTestAction) exitCode(res runResult) int { + switch res.ResourceType { + case "Test": + if !res.Run.(openapi.TestRun).Result.GetAllPassed() { + return 1 + } + case "Transaction": + for _, step := range res.Run.(openapi.TransactionRun).Steps { + if !step.Result.GetAllPassed() { + return 1 + } } } + return 0 +} - a.logger.Debug("resolved env", zap.String("envID", envID)) +func (a runTestAction) resolveEnvID(ctx context.Context, envID string) (string, error) { + if !fileutil.IsFilePath(envID) { + a.logger.Debug("envID is not a file path", zap.String("envID", envID)) + return envID, nil + } - params := runDefParams{ - DefinitionFile: args.DefinitionFile, - EnvID: envID, - WaitForResult: args.WaitForResult, - JunitFile: args.JUnit, - Metadata: a.getMetadata(), + f, err := fileutil.Read(envID) + if err != nil { + return "", fmt.Errorf("cannot read environment file %s: %w", envID, err) } - err := a.runDefinition(ctx, params) + a.logger.Debug("envID is a file path", zap.String("filePath", envID), zap.Any("file", f)) + updatedEnv, err := a.environments.Apply(ctx, f, a.yamlFormat) if err != nil { - return fmt.Errorf("could not run definition: %w", err) + return "", fmt.Errorf("could not read environment file: %w", err) } - return nil + var env openapi.EnvironmentResource + err = yaml.Unmarshal([]byte(updatedEnv), &env) + if err != nil { + a.logger.Error("error parsing json", zap.String("content", updatedEnv), zap.Error(err)) + return "", fmt.Errorf("could not unmarshal environment json: %w", err) + } + + return env.Spec.GetId(), nil } -func (a runTestAction) testFileToID(ctx context.Context, originalPath, filePath string) (string, error) { - path := filepath.Join(originalPath, filePath) - f, err := file.Read(path) +func (a runTestAction) injectLocalEnvVars(ctx context.Context, df defFile) (defFile, error) { + variableInjector := variable.NewInjector(variable.WithVariableProvider( + variable.EnvironmentVariableProvider{}, + )) + + injected, err := variableInjector.ReplaceInString(string(df.Contents())) if err != nil { - return "", err + return df, fmt.Errorf("cannot inject local environment variables: %w", err) } - if t, err := f.Definition().Test(); err == nil && t.Trigger.Type == "grpc" { - newFile, err := getUpdatedTestWithGrpcTrigger(f, t) - if err != nil { - return "", err - } + df = defFile{fileutil.New(df.AbsPath(), []byte(injected))} - if newFile != nil { // has new file, update it - f = *newFile - } + return df, nil +} + +func (a runTestAction) apply(ctx context.Context, df defFile) (defFile, error) { + defType, err := getTypeFromFile(df) + if err != nil { + return df, fmt.Errorf("cannot get type from definition file: %w", err) } + a.logger.Debug("definition file type", zap.String("type", defType)) - body, _, err := a.client.ApiApi. - UpsertDefinition(ctx). - TextDefinition(openapi.TextDefinition{ - Content: openapi.PtrString(f.Contents()), - }). - Execute() + switch defType { + case "Test": + return a.applyTest(ctx, df) + case "Transaction": + return a.applyTransaction(ctx, df) + default: + return df, fmt.Errorf("unknown type %s", defType) + } +} +func (a runTestAction) applyTest(ctx context.Context, df defFile) (defFile, error) { + df, err := a.injectLocalEnvVars(ctx, df) if err != nil { - return "", fmt.Errorf("could not upsert definition: %w", err) + return df, fmt.Errorf("cannot inject local env vars: %w", err) } - return body.GetId(), nil -} + var test openapi.TestResource + err = yaml.Unmarshal(df.Contents(), &test) + if err != nil { + a.logger.Error("error parsing test", zap.String("content", string(df.Contents())), zap.Error(err)) + return df, fmt.Errorf("could not unmarshal test yaml: %w", err) + } -func (a runTestAction) runDefinition(ctx context.Context, params runDefParams) error { - f, err := file.Read(params.DefinitionFile) + test, err = a.consolidateGRPCFile(df, test) if err != nil { - return err + return df, fmt.Errorf("could not consolidate grpc file: %w", err) } - defFile := f.Definition() - if err = defFile.Validate(); err != nil { - return fmt.Errorf("invalid definition file: %w", err) + marshalled, err := yaml.Marshal(test) + if err != nil { + return df, fmt.Errorf("could not marshal test yaml: %w", err) } + df = defFile{fileutil.New(df.AbsPath(), marshalled)} - return a.runDefinitionFile(ctx, f, params) -} + a.logger.Debug("applying test", + zap.String("absolutePath", df.AbsPath()), + zap.String("id", test.Spec.GetId()), + zap.String("marshalled", string(marshalled)), + ) -func (a runTestAction) runDefinitionFile(ctx context.Context, f file.File, params runDefParams) error { - f, err := f.ResolveVariables() + updated, err := a.tests.Apply(ctx, df.File, a.yamlFormat) if err != nil { - return err + return df, fmt.Errorf("could not read test file: %w", err) } - if t, err := f.Definition().Transaction(); err == nil { - for i, step := range t.Steps { - // since step could be a relative path in relation of the definition file, - // to check it properly we need to convert it to an absolute path - stepPath := filepath.Join(f.AbsDir(), step) + df = defFile{fileutil.New(df.AbsPath(), []byte(updated))} - if !fileutil.IsFilePath(stepPath) { - // not referencing a file, keep the value - continue - } + err = yaml.Unmarshal(df.Contents(), &test) + if err != nil { + a.logger.Error("error parsing updated test", zap.String("content", string(df.Contents())), zap.Error(err)) + return df, fmt.Errorf("could not unmarshal test yaml: %w", err) + } - // references a file, resolve to its ID - id, err := a.testFileToID(ctx, f.AbsDir(), step) - if err != nil { - return fmt.Errorf(`cannot translate path "%s" to an ID: %w`, step, err) - } + a.logger.Debug("test applied", + zap.String("absolutePath", df.AbsPath()), + zap.String("id", test.Spec.GetId()), + ) - t.Steps[i] = id - } + return df, nil +} - def := yaml.File{ - Type: yaml.FileTypeTransaction, - Spec: t, - } +func (a runTestAction) consolidateGRPCFile(df defFile, test openapi.TestResource) (openapi.TestResource, error) { + if test.Spec.Trigger.GetType() != "grpc" { + a.logger.Debug("test does not use grpc", zap.String("triggerType", test.Spec.Trigger.GetType())) + return test, nil + } - updated, err := def.Encode() - if err != nil { - return fmt.Errorf(`cannot encode updated transaction: %w`, err) - } + definedPBFile := test.Spec.Trigger.Grpc.GetProtobufFile() + if !fileutil.LooksLikeFilePath(definedPBFile) { + a.logger.Debug("protobuf file is not a file path", zap.String("protobufFile", definedPBFile)) + return test, nil + } - f, err = file.New(f.Path(), updated) - if err != nil { - return fmt.Errorf(`cannot recreate updated file: %w`, err) - } + pbFilePath := df.RelativeFile(definedPBFile) + a.logger.Debug("protobuf file", zap.String("path", pbFilePath)) + + pbFile, err := fileutil.Read(pbFilePath) + if err != nil { + return test, fmt.Errorf(`cannot read protobuf file: %w`, err) } + a.logger.Debug("protobuf file contents", zap.String("contents", string(pbFile.Contents()))) - if t, err := f.Definition().Test(); err == nil && t.Trigger.Type == "grpc" { - newFile, err := getUpdatedTestWithGrpcTrigger(f, t) - if err != nil { - return err - } + test.Spec.Trigger.Grpc.SetProtobufFile(string(pbFile.Contents())) - if newFile != nil { // has new file, update it - f = *newFile - } + return test, nil +} + +func (a runTestAction) applyTransaction(ctx context.Context, df defFile) (defFile, error) { + df, err := a.injectLocalEnvVars(ctx, df) + if err != nil { + return df, fmt.Errorf("cannot inject local env vars: %w", err) } - variables := make([]openapi.EnvironmentValue, 0) - for name, value := range params.EnvironmentVariables { - variables = append(variables, openapi.EnvironmentValue{Key: openapi.PtrString(name), Value: openapi.PtrString(value)}) + var tran openapi.TransactionResource + err = yaml.Unmarshal(df.Contents(), &tran) + if err != nil { + a.logger.Error("error parsing transaction", zap.String("content", string(df.Contents())), zap.Error(err)) + return df, fmt.Errorf("could not unmarshal transaction yaml: %w", err) } - body, resp, err := a.client.ApiApi. - ExecuteDefinition(ctx). - TextDefinition(openapi.TextDefinition{ - Content: openapi.PtrString(f.Contents()), - RunInformation: &openapi.RunInformation{ - Metadata: params.Metadata, - EnvironmentId: ¶ms.EnvID, - Variables: variables, - }, - }). - Execute() + tran, err = a.mapTransactionSteps(ctx, df, tran) + if err != nil { + return df, fmt.Errorf("could not map transaction steps: %w", err) + } - if resp != nil && resp.StatusCode == http.StatusUnprocessableEntity { - filledVariables, err := a.askForMissingVariables(resp) - if err != nil { - return err - } + marshalled, err := yaml.Marshal(tran) + if err != nil { + return df, fmt.Errorf("could not marshal test yaml: %w", err) + } + df = defFile{fileutil.New(df.AbsPath(), marshalled)} - params.EnvironmentVariables = filledVariables + a.logger.Debug("applying transaction", + zap.String("absolutePath", df.AbsPath()), + zap.String("id", tran.Spec.GetId()), + zap.String("marshalled", string(marshalled)), + ) - return a.runDefinitionFile(ctx, f, params) + updated, err := a.transactions.Apply(ctx, df.File, a.yamlFormat) + if err != nil { + return df, fmt.Errorf("could not read transaction file: %w", err) } + df = defFile{fileutil.New(df.AbsPath(), []byte(updated))} + + err = yaml.Unmarshal(df.Contents(), &tran) if err != nil { - return fmt.Errorf("could not execute definition: %w", err) + a.logger.Error("error parsing updated transaction", zap.String("content", updated), zap.Error(err)) + return df, fmt.Errorf("could not unmarshal transaction yaml: %w", err) } - if resp.StatusCode == http.StatusCreated && !f.HasID() { - f, err = f.SetID(body.GetId()) - if err != nil { - return fmt.Errorf("could not update definition file: %w", err) + a.logger.Debug("transaction applied", + zap.String("absolutePath", df.AbsPath()), + zap.String("updated id", tran.Spec.GetId()), + ) + + return df, nil +} + +func (a runTestAction) mapTransactionSteps(ctx context.Context, df defFile, tran openapi.TransactionResource) (openapi.TransactionResource, error) { + for i, step := range tran.Spec.GetSteps() { + a.logger.Debug("mapping transaction step", + zap.Int("index", i), + zap.String("step", step), + ) + if !fileutil.LooksLikeFilePath(step) { + a.logger.Debug("does not look like a file path", + zap.Int("index", i), + zap.String("step", step), + ) + continue } - _, err = f.Write() + f, err := fileutil.Read(df.RelativeFile(step)) if err != nil { - return fmt.Errorf("could not update definition file: %w", err) + return openapi.TransactionResource{}, fmt.Errorf("cannot read test file: %w", err) } - } - runID := getTestRunIDFromString(body.GetRunId()) - a.logger.Debug( - "executed", - zap.Int32("runID", runID), - zap.String("runType", body.GetType()), - ) - - switch body.GetType() { - case "Test": - test, err := a.getTest(ctx, body.GetId()) + testDF, err := a.applyTest(ctx, defFile{f}) if err != nil { - return fmt.Errorf("could not get test info: %w", err) + return openapi.TransactionResource{}, fmt.Errorf("cannot apply test '%s': %w", step, err) } - return a.testRun(ctx, test, runID, params) - case "Transaction": - test, err := a.getTransaction(ctx, body.GetId()) + + var test openapi.TestResource + err = yaml.Unmarshal(testDF.Contents(), &test) if err != nil { - return fmt.Errorf("could not get test info: %w", err) + return openapi.TransactionResource{}, fmt.Errorf("cannot unmarshal updated test '%s': %w", step, err) } - return a.transactionRun(ctx, test, runID, params) + + a.logger.Debug("mapped transaction step", + zap.Int("index", i), + zap.String("step", step), + zap.String("mapped step", test.Spec.GetId()), + ) + + tran.Spec.Steps[i] = test.Spec.GetId() } - return fmt.Errorf(`unsuported run type "%s"`, body.GetType()) + return tran, nil } -func (a runTestAction) askForMissingVariables(resp *http.Response) (map[string]string, error) { - body, err := ioutil.ReadAll(resp.Body) +func getTypeFromFile(df defFile) (string, error) { + var raw map[string]any + err := yaml.Unmarshal(df.Contents(), &raw) if err != nil { - return map[string]string{}, fmt.Errorf("could not read response body: %w", err) + return "", fmt.Errorf("cannot unmarshal definition file: %w", err) } - var missingVariablesError openapi.MissingVariablesError - err = json.Unmarshal(body, &missingVariablesError) - if err != nil { - return map[string]string{}, fmt.Errorf("could not unmarshal response: %w", err) + if raw["type"] == nil { + return "", fmt.Errorf("missing type in definition file") } - uniqueMissingVariables := map[string]string{} - for _, missingVariables := range missingVariablesError.MissingVariables { - for _, variable := range missingVariables.Variables { - defaultValue := "" - if variable.DefaultValue != nil { - defaultValue = *variable.DefaultValue - } - uniqueMissingVariables[*variable.Key] = defaultValue - } + defType, ok := raw["type"].(string) + if !ok { + return "", fmt.Errorf("type is not a string") } - if len(uniqueMissingVariables) > 0 { - ui.DefaultUI.Warning("Some variables are required by one or more tests") - ui.DefaultUI.Info("Fill the values for each variable:") - } + return defType, nil + +} - filledVariables := map[string]string{} +type envVar struct { + Name string + DefaultValue string + UserValue string +} - for variableName, variableDefaultValue := range uniqueMissingVariables { - value := ui.DefaultUI.TextInput(variableName, variableDefaultValue) - filledVariables[variableName] = value +func (ev envVar) value() string { + if ev.UserValue != "" { + return ev.UserValue } - return filledVariables, nil + return ev.DefaultValue } -func (a runTestAction) getTransaction(ctx context.Context, id string) (openapi.Transaction, error) { - a.client.GetConfig().AddDefaultHeader("X-Tracetest-Augmented", "true") - transaction, _, err := a.client.ResourceApiApi. - GetTransaction(ctx, id). - Execute() - - // reset augmented header - delete(a.client.GetConfig().DefaultHeader, "X-Tracetest-Augmented") +type envVars []envVar - if err != nil { - return openapi.Transaction{}, fmt.Errorf("could not execute request: %w", err) +func (evs envVars) toOpenapi() []openapi.EnvironmentValue { + vars := make([]openapi.EnvironmentValue, len(evs)) + for i, ev := range evs { + vars[i] = openapi.EnvironmentValue{ + Key: openapi.PtrString(ev.Name), + Value: openapi.PtrString(ev.value()), + } } - return transaction.GetSpec(), nil + return vars } -func (a runTestAction) getTest(ctx context.Context, id string) (openapi.Test, error) { - test, _, err := a.client.ApiApi. - GetTest(ctx, id). - Execute() - if err != nil { - return openapi.Test{}, fmt.Errorf("could not execute request: %w", err) +func (evs envVars) unique() envVars { + seen := make(map[string]bool) + vars := make(envVars, 0, len(evs)) + for _, ev := range evs { + if seen[ev.Name] { + continue + } + + seen[ev.Name] = true + vars = append(vars, ev) } - return *test, nil + return vars +} + +type missingEnvVarsError envVars + +func (e missingEnvVarsError) Error() string { + return fmt.Sprintf("missing env vars: %v", []envVar(e)) } -func (a runTestAction) testRun(ctx context.Context, test openapi.Test, runID int32, params runDefParams) error { - a.logger.Debug("run test", zap.Bool("wait-for-results", params.WaitForResult)) - testID := test.GetId() - testRun, err := a.getTestRun(ctx, testID, runID) +type runResult struct { + ResourceType string + Resource any + Run any +} + +func (a runTestAction) run(ctx context.Context, df defFile, envID string, ev envVars) (runResult, error) { + res := runResult{} + + defType, err := getTypeFromFile(df) if err != nil { - return fmt.Errorf("could not run test: %w", err) + return res, fmt.Errorf("cannot get type from file: %w", err) + } + res.ResourceType = defType + + a.logger.Debug("running definition", + zap.String("type", defType), + zap.String("envID", envID), + zap.Any("envVars", ev), + ) + + runInfo := openapi.RunInformation{ + EnvironmentId: openapi.PtrString(envID), + Variables: ev.toOpenapi(), + Metadata: getMetadata(), } - if params.WaitForResult { - updatedTestRun, err := a.waitForTestResult(ctx, testID, getTestRunID(testRun)) + switch defType { + case "Test": + var test openapi.TestResource + err = yaml.Unmarshal(df.Contents(), &test) if err != nil { - return fmt.Errorf("could not wait for result: %w", err) + a.logger.Error("error parsing test", zap.String("content", string(df.Contents())), zap.Error(err)) + return res, fmt.Errorf("could not unmarshal test yaml: %w", err) } - testRun = updatedTestRun + req := a.openapiClient.ApiApi. + RunTest(ctx, test.Spec.GetId()). + RunInformation(runInfo) + + a.logger.Debug("running test", zap.String("id", test.Spec.GetId())) - if err := a.saveJUnitFile(ctx, testID, getTestRunID(testRun), params.JunitFile); err != nil { - return fmt.Errorf("could not save junit file: %w", err) + run, resp, err := a.openapiClient.ApiApi.RunTestExecute(req) + err = a.handleRunError(resp, err) + if err != nil { + return res, err } - } - tro := formatters.TestRunOutput{ - HasResults: params.WaitForResult, - Test: test, - Run: testRun, - } + full, err := a.tests.Get(ctx, test.Spec.GetId(), a.jsonFormat) + if err != nil { + return res, fmt.Errorf("cannot get full test '%s': %w", test.Spec.GetId(), err) + } + err = json.Unmarshal([]byte(full), &test) + if err != nil { + return res, fmt.Errorf("cannot get full test '%s': %w", test.Spec.GetId(), err) + } - formatter := formatters.TestRun(a.config, true) - formattedOutput := formatter.Format(tro) - fmt.Print(formattedOutput) + res.Resource = test + res.Run = *run - allPassed := tro.Run.Result.GetAllPassed() - if params.WaitForResult && !allPassed { - // It failed, so we have to return an error status - a.cliExit(1) - } + case "Transaction": + var tran openapi.TransactionResource + err = yaml.Unmarshal(df.Contents(), &tran) + if err != nil { + a.logger.Error("error parsing transaction", zap.String("content", string(df.Contents())), zap.Error(err)) + return res, fmt.Errorf("could not unmarshal transaction yaml: %w", err) + } - return nil -} + req := a.openapiClient.ApiApi. + RunTransaction(ctx, tran.Spec.GetId()). + RunInformation(runInfo) -func (a runTestAction) transactionRun(ctx context.Context, transaction openapi.Transaction, rid int32, params runDefParams) error { - a.logger.Debug("run transaction", zap.Bool("wait-for-results", params.WaitForResult)) - transactionID := transaction.GetId() - transactionRun, err := a.getTransactionRun(ctx, transactionID, int32(rid)) - if err != nil { - return fmt.Errorf("could not run transaction: %w", err) - } + a.logger.Debug("running transaction", zap.String("id", tran.Spec.GetId())) - if params.WaitForResult { - updatedTestRun, err := a.waitForTransactionResult(ctx, transactionID, transactionRun.GetId()) + run, resp, err := a.openapiClient.ApiApi.RunTransactionExecute(req) + err = a.handleRunError(resp, err) if err != nil { - return fmt.Errorf("could not wait for result: %w", err) + return res, err } - transactionRun = updatedTestRun + full, err := a.transactions.Get(ctx, tran.Spec.GetId(), a.jsonFormat) + if err != nil { + return res, fmt.Errorf("cannot get full transaction '%s': %w", tran.Spec.GetId(), err) + } + err = json.Unmarshal([]byte(full), &tran) + if err != nil { + return res, fmt.Errorf("cannot get full transaction '%s': %w", tran.Spec.GetId(), err) + } + + res.Resource = tran + res.Run = *run + default: + return res, fmt.Errorf("unknown type: %s", defType) } - tro := formatters.TransactionRunOutput{ - HasResults: params.WaitForResult, - Transaction: transaction, - Run: transactionRun, + a.logger.Debug("definition run", + zap.String("type", defType), + zap.String("envID", envID), + zap.Any("envVars", ev), + zap.Any("response", res), + ) + + return res, nil +} + +func (a runTestAction) handleRunError(resp *http.Response, reqErr error) error { + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("could not read response body: %w", err) } + resp.Body.Close() - formatter := formatters.TransactionRun(a.config, true) - formattedOutput := formatter.Format(tro) - fmt.Print(formattedOutput) + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("resource not found in server") + } - if params.WaitForResult { - if utils.RunStateIsFailed(tro.Run.GetState()) { - // It failed, so we have to return an error status - a.cliExit(1) - } + if resp.StatusCode == http.StatusUnprocessableEntity { + return buildMissingEnvVarsError(body) + } - for _, step := range tro.Run.Steps { - if !step.Result.GetAllPassed() { - // if any test doesn't pass, fail the transaction execution - a.cliExit(1) - } - } + if reqErr != nil { + a.logger.Error("error running transaction", zap.Error(err), zap.String("body", string(body))) + return fmt.Errorf("could not run transaction: %w", err) } return nil } -func (a runTestAction) saveJUnitFile(ctx context.Context, testId string, testRunId int32, outputFile string) error { - if outputFile == "" { - return nil +func buildMissingEnvVarsError(body []byte) error { + var missingVarsErrResp openapi.MissingVariablesError + err := json.Unmarshal(body, &missingVarsErrResp) + if err != nil { + return fmt.Errorf("could not unmarshal response body: %w", err) } - req := a.client.ApiApi.GetRunResultJUnit(ctx, testId, testRunId) - junit, _, err := a.client.ApiApi.GetRunResultJUnitExecute(req) - if err != nil { - return fmt.Errorf("could not execute request: %w", err) + missingVars := envVars{} + + for _, missingVarErr := range missingVarsErrResp.MissingVariables { + for _, missingVar := range missingVarErr.Variables { + missingVars = append(missingVars, envVar{ + Name: missingVar.GetKey(), + DefaultValue: missingVar.GetDefaultValue(), + }) + } } - f, err := os.Create(outputFile) - if err != nil { - return fmt.Errorf("could not create junit output file: %w", err) + return missingEnvVarsError(missingVars.unique()) +} + +func (a runTestAction) askForMissingVars(missingVars []envVar) []envVar { + ui.DefaultUI.Warning("Some variables are required by one or more tests") + ui.DefaultUI.Info("Fill the values for each variable:") + + filledVariables := make([]envVar, 0, len(missingVars)) + + for _, missingVar := range missingVars { + answer := missingVar + answer.UserValue = ui.DefaultUI.TextInput(missingVar.Name, missingVar.DefaultValue) + filledVariables = append(filledVariables, answer) } - _, err = f.WriteString(junit) + a.logger.Debug("filled variables", zap.Any("variables", filledVariables)) - return err + return filledVariables +} +func (a runTestAction) formatResult(result runResult, hasResults bool) string { + switch result.ResourceType { + case "Test": + return a.formatTestResult(result, hasResults) + case "Transaction": + return a.formatTransactionResult(result, hasResults) + } + return "" } -func (a runTestAction) getTestRun(ctx context.Context, testID string, runID int32) (openapi.TestRun, error) { - run, _, err := a.client.ApiApi. - GetTestRun(ctx, testID, runID). - Execute() - if err != nil { - return openapi.TestRun{}, fmt.Errorf("could not execute request: %w", err) +func (a runTestAction) formatTestResult(result runResult, hasResults bool) string { + test := result.Resource.(openapi.TestResource) + run := result.Run.(openapi.TestRun) + + tro := formatters.TestRunOutput{ + HasResults: hasResults, + Test: test.GetSpec(), + Run: run, } - return *run, nil + formatter := formatters.TestRun(a.config, true) + return formatter.Format(tro) } +func (a runTestAction) formatTransactionResult(result runResult, hasResults bool) string { + tran := result.Resource.(openapi.TransactionResource) + run := result.Run.(openapi.TransactionRun) -func (a runTestAction) getTransactionRun(ctx context.Context, transactionID string, runID int32) (openapi.TransactionRun, error) { - run, _, err := a.client.ApiApi. - GetTransactionRun(ctx, transactionID, runID). - Execute() - if err != nil { - return openapi.TransactionRun{}, fmt.Errorf("could not execute request: %w", err) + tro := formatters.TransactionRunOutput{ + HasResults: hasResults, + Transaction: tran.GetSpec(), + Run: run, } - return *run, nil + return formatters. + TransactionRun(a.config, true). + Format(tro) +} + +func (a runTestAction) waitForResult(ctx context.Context, run runResult) (runResult, error) { + switch run.ResourceType { + case "Test": + tr, err := a.waitForTestResult(ctx, run) + if err != nil { + return run, err + } + + run.Run = tr + return run, nil + + case "Transaction": + tr, err := a.waitForTransactionResult(ctx, run) + if err != nil { + return run, err + } + + run.Run = tr + return run, nil + } + return run, fmt.Errorf("unknown resource type: %s", run.ResourceType) } -func (a runTestAction) waitForTestResult(ctx context.Context, testID string, testRunID int32) (openapi.TestRun, error) { +func (a runTestAction) waitForTestResult(ctx context.Context, result runResult) (openapi.TestRun, error) { var ( testRun openapi.TestRun lastError error wg sync.WaitGroup ) + + test := result.Resource.(openapi.TestResource) + run := result.Run.(openapi.TestRun) + wg.Add(1) ticker := time.NewTicker(1 * time.Second) // TODO: change to websockets go func() { - for { - select { - case <-ticker.C: - readyTestRun, err := a.isTestReady(ctx, testID, testRunID) - if err != nil { - lastError = err - wg.Done() - return - } - - if readyTestRun != nil { - testRun = *readyTestRun - wg.Done() - return - } + for range ticker.C { + readyTestRun, err := a.isTestReady(ctx, test.Spec.GetId(), run.GetId()) + if err != nil { + lastError = err + wg.Done() + return + } + + if readyTestRun != nil { + testRun = *readyTestRun + wg.Done() + return } } }() @@ -512,30 +742,50 @@ func (a runTestAction) waitForTestResult(ctx context.Context, testID string, tes return testRun, nil } -func (a runTestAction) waitForTransactionResult(ctx context.Context, transactionID, transactionRunID string) (openapi.TransactionRun, error) { +func (a runTestAction) isTestReady(ctx context.Context, testID string, testRunID string) (*openapi.TestRun, error) { + runID, err := strconv.Atoi(testRunID) + if err != nil { + return nil, fmt.Errorf("invalid transaction run id format: %w", err) + } + + req := a.openapiClient.ApiApi.GetTestRun(ctx, testID, int32(runID)) + run, _, err := a.openapiClient.ApiApi.GetTestRunExecute(req) + if err != nil { + return &openapi.TestRun{}, fmt.Errorf("could not execute GetTestRun request: %w", err) + } + + if utils.RunStateIsFinished(run.GetState()) { + return run, nil + } + + return nil, nil +} + +func (a runTestAction) waitForTransactionResult(ctx context.Context, result runResult) (openapi.TransactionRun, error) { var ( transactionRun openapi.TransactionRun lastError error wg sync.WaitGroup ) + + tran := result.Resource.(openapi.TransactionResource) + run := result.Run.(openapi.TransactionRun) + wg.Add(1) ticker := time.NewTicker(1 * time.Second) // TODO: change to websockets go func() { - for { - select { - case <-ticker.C: - readyTransactionRun, err := a.isTransactionReady(ctx, transactionID, transactionRunID) - if err != nil { - lastError = err - wg.Done() - return - } - - if readyTransactionRun != nil { - transactionRun = *readyTransactionRun - wg.Done() - return - } + for range ticker.C { + readyTransactionRun, err := a.isTransactionReady(ctx, tran.Spec.GetId(), run.GetId()) + if err != nil { + lastError = err + wg.Done() + return + } + + if readyTransactionRun != nil { + transactionRun = *readyTransactionRun + wg.Done() + return } } }() @@ -548,11 +798,16 @@ func (a runTestAction) waitForTransactionResult(ctx context.Context, transaction return transactionRun, nil } -func (a runTestAction) isTestReady(ctx context.Context, testID string, testRunId int32) (*openapi.TestRun, error) { - req := a.client.ApiApi.GetTestRun(ctx, testID, testRunId) - run, _, err := a.client.ApiApi.GetTestRunExecute(req) +func (a runTestAction) isTransactionReady(ctx context.Context, transactionID, transactionRunID string) (*openapi.TransactionRun, error) { + runID, err := strconv.Atoi(transactionRunID) if err != nil { - return &openapi.TestRun{}, fmt.Errorf("could not execute GetTestRun request: %w", err) + return nil, fmt.Errorf("invalid transaction run id format: %w", err) + } + + req := a.openapiClient.ApiApi.GetTransactionRun(ctx, transactionID, int32(runID)) + run, _, err := a.openapiClient.ApiApi.GetTransactionRunExecute(req) + if err != nil { + return nil, fmt.Errorf("could not execute GetTestRun request: %w", err) } if utils.RunStateIsFinished(run.GetState()) { @@ -562,26 +817,32 @@ func (a runTestAction) isTestReady(ctx context.Context, testID string, testRunId return nil, nil } -func (a runTestAction) isTransactionReady(ctx context.Context, transactionID, transactionRunId string) (*openapi.TransactionRun, error) { - runId, err := strconv.Atoi(transactionRunId) +func (a runTestAction) writeJUnitReport(ctx context.Context, result runResult, outputFile string) error { + test := result.Resource.(openapi.TestResource) + run := result.Run.(openapi.TestRun) + runID, err := strconv.Atoi(run.GetId()) if err != nil { - return nil, fmt.Errorf("invalid transaction run id format: %w", err) + return fmt.Errorf("invalid run id format: %w", err) } - req := a.client.ApiApi.GetTransactionRun(ctx, transactionID, int32(runId)) - run, _, err := a.client.ApiApi.GetTransactionRunExecute(req) + req := a.openapiClient.ApiApi.GetRunResultJUnit(ctx, test.Spec.GetId(), int32(runID)) + junit, _, err := a.openapiClient.ApiApi.GetRunResultJUnitExecute(req) if err != nil { - return nil, fmt.Errorf("could not execute GetTestRun request: %w", err) + return fmt.Errorf("could not execute request: %w", err) } - if utils.RunStateIsFinished(run.GetState()) { - return run, nil + f, err := os.Create(junit) + if err != nil { + return fmt.Errorf("could not create junit output file: %w", err) } - return nil, nil + _, err = f.WriteString(outputFile) + + return err + } -func (a runTestAction) getMetadata() map[string]string { +func getMetadata() map[string]string { ci := cienvironment.DetectCIEnvironment() if ci == nil { return map[string]string{} @@ -602,44 +863,6 @@ func (a runTestAction) getMetadata() map[string]string { return metadata } -func getTestRunIDFromString(testRunIDAsString string) int32 { - testRunID, _ := strconv.Atoi(testRunIDAsString) - return int32(testRunID) -} - -func getTestRunID(testRun openapi.TestRun) int32 { - return getTestRunIDFromString(testRun.GetId()) -} - -func getUpdatedTestWithGrpcTrigger(f file.File, t yaml.Test) (*file.File, error) { - protobufFile := filepath.Join(f.AbsDir(), t.Trigger.GRPC.ProtobufFile) - - if !fileutil.IsFilePath(protobufFile) { - return nil, nil - } - - // referencing a file, keep the value - fileContent, err := os.ReadFile(protobufFile) - if err != nil { - return nil, fmt.Errorf(`cannot read protobuf file: %w`, err) - } - - t.Trigger.GRPC.ProtobufFile = string(fileContent) - - def := yaml.File{ - Type: yaml.FileTypeTest, - Spec: t, - } - - updated, err := def.Encode() - if err != nil { - return nil, fmt.Errorf(`cannot encode updated test: %w`, err) - } - - f, err = file.New(f.Path(), updated) - if err != nil { - return nil, fmt.Errorf(`cannot recreate updated file: %w`, err) - } - - return &f, nil +type defFile struct { + fileutil.File } diff --git a/cli/cmd/config.go b/cli/cmd/config.go index be3d177935..ea769f7002 100644 --- a/cli/cmd/config.go +++ b/cli/cmd/config.go @@ -3,17 +3,12 @@ package cmd import ( "context" "fmt" - "net/http" "os" - "strings" - "time" - "github.com/Jeffail/gabs/v2" "github.com/kubeshop/tracetest/cli/actions" "github.com/kubeshop/tracetest/cli/analytics" "github.com/kubeshop/tracetest/cli/config" "github.com/kubeshop/tracetest/cli/formatters" - "github.com/kubeshop/tracetest/cli/pkg/resourcemanager" "github.com/kubeshop/tracetest/cli/utils" "github.com/spf13/cobra" "go.uber.org/zap" @@ -25,7 +20,6 @@ var ( cliLogger *zap.Logger versionText string isVersionMatch bool - resourceParams = &resourceParameters{} ) type setupConfig struct { @@ -47,140 +41,6 @@ func SkipVersionMismatchCheck() setupOption { } } -var httpClient = &resourcemanager.HTTPClient{} -var resources = resourcemanager.NewRegistry(). - Register( - resourcemanager.NewClient( - httpClient, - "config", "configs", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "ANALYTICS ENABLED", Path: "spec.analyticsEnabled"}, - }, - }), - ), - ). - Register( - resourcemanager.NewClient( - httpClient, - "analyzer", "analyzers", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "ENABLED", Path: "spec.enabled"}, - {Header: "MINIMUM SCORE", Path: "spec.minimumScore"}, - }, - }), - ), - ). - Register( - resourcemanager.NewClient( - httpClient, - "pollingprofile", "pollingprofiles", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "STRATEGY", Path: "spec.strategy"}, - }, - }), - ), - ). - Register( - resourcemanager.NewClient( - httpClient, - "demo", "demos", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "TYPE", Path: "spec.type"}, - {Header: "ENABLED", Path: "spec.enabled"}, - }, - }), - ), - ). - Register( - resourcemanager.NewClient( - httpClient, - "datastore", "datastores", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "DEFAULT", Path: "spec.default"}, - }, - ItemModifier: func(item *gabs.Container) error { - isDefault := item.Path("spec.default").Data().(bool) - if !isDefault { - item.SetP("", "spec.default") - } else { - item.SetP("*", "spec.default") - } - return nil - }, - }), - resourcemanager.WithDeleteEnabled("DataStore removed. Defaulting back to no-tracing mode"), - ), - ). - Register( - resourcemanager.NewClient( - httpClient, - "environment", "environments", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "DESCRIPTION", Path: "spec.description"}, - }, - }), - ), - ). - Register( - resourcemanager.NewClient( - httpClient, - "transaction", "transactions", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "VERSION", Path: "spec.version"}, - {Header: "STEPS", Path: "spec.summary.steps"}, - {Header: "RUNS", Path: "spec.summary.runs"}, - {Header: "LAST RUN TIME", Path: "spec.summary.lastRun.time"}, - {Header: "LAST RUN SUCCESSES", Path: "spec.summary.lastRun.passes"}, - {Header: "LAST RUN FAILURES", Path: "spec.summary.lastRun.fails"}, - }, - ItemModifier: func(item *gabs.Container) error { - // set spec.summary.steps to the number of steps in the transaction - item.SetP(len(item.Path("spec.steps").Children()), "spec.summary.steps") - - // if lastRun.time is not empty, show it in a nicer format - lastRunTime := item.Path("spec.summary.lastRun.time").Data().(string) - if lastRunTime != "" { - date, err := time.Parse(time.RFC3339, lastRunTime) - if err != nil { - return fmt.Errorf("failed to parse last run time: %s", err) - } - if date.IsZero() { - item.SetP("", "spec.summary.lastRun.time") - } else { - item.SetP(date.Format(time.DateTime), "spec.summary.lastRun.time") - } - } - return nil - }, - }), - ), - ) - -func resourceList() string { - return strings.Join(resources.List(), "|") -} - func setupCommand(options ...setupOption) func(cmd *cobra.Command, args []string) { config := setupConfig{ shouldValidateConfig: true, @@ -196,19 +56,7 @@ func setupCommand(options ...setupOption) func(cmd *cobra.Command, args []string loadConfig(cmd, args) overrideConfig() setupVersion() - - extraHeaders := http.Header{} - extraHeaders.Set("x-client-id", analytics.ClientID()) - extraHeaders.Set("x-source", "cli") - - // To avoid a ciruclar reference initialization when setting up the registry and its resources, - // we create the resources with a pointer to an unconfigured HTTPClient. - // When each command is run, this function is run in the PreRun stage, before any of the actual `Run` code is executed - // We take this chance to configure the HTTPClient with the correct URL and headers. - // To make this configuration propagate to all the resources, we need to replace the pointer to the HTTPClient. - // For more details, see https://github.com/kubeshop/tracetest/pull/2832#discussion_r1245616804 - hc := resourcemanager.NewHTTPClient(cliConfig.URL(), extraHeaders) - *httpClient = *hc + setupResources() if config.shouldValidateConfig { validateConfig(cmd, args) diff --git a/cli/cmd/configure_cmd.go b/cli/cmd/configure_cmd.go index 8a673a4e50..040792d4a2 100644 --- a/cli/cmd/configure_cmd.go +++ b/cli/cmd/configure_cmd.go @@ -54,14 +54,14 @@ func (p configureParameters) Validate(cmd *cobra.Command, args []string) []error if cmd.Flags().Lookup("endpoint").Changed { if p.Endpoint == "" { - errors = append(errors, ParamError{ + errors = append(errors, paramError{ Parameter: "endpoint", Message: "endpoint cannot be empty", }) } else { _, err := url.Parse(p.Endpoint) if err != nil { - errors = append(errors, ParamError{ + errors = append(errors, paramError{ Parameter: "endpoint", Message: "endpoint is not a valid URL", }) diff --git a/cli/cmd/docgen_cmd.go b/cli/cmd/docgen_cmd.go index 5bc948a512..ca2dc4d094 100644 --- a/cli/cmd/docgen_cmd.go +++ b/cli/cmd/docgen_cmd.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -64,7 +63,7 @@ module.exports = pages; fileContent := fmt.Sprintf(fileContentTemplate, sidebarItemsContent) outputFile := filepath.Join(outputDir, "cli-sidebar.js") - err = ioutil.WriteFile(outputFile, []byte(fileContent), 0644) + err = os.WriteFile(outputFile, []byte(fileContent), 0644) if err != nil { return fmt.Errorf("could not write sidebar output file: %w", err) } @@ -73,7 +72,7 @@ module.exports = pages; } func generateContentItems(inputDir string, docusaurusRootFolder string) (string, error) { - files, err := ioutil.ReadDir(inputDir) + files, err := os.ReadDir(inputDir) if err != nil { return "", fmt.Errorf("could not read dir: %w", err) } diff --git a/cli/cmd/datastore_legacy_cmd.go b/cli/cmd/legacy_datastore_cmd.go similarity index 96% rename from cli/cmd/datastore_legacy_cmd.go rename to cli/cmd/legacy_datastore_cmd.go index 14b1779762..8de78d5ca3 100644 --- a/cli/cmd/datastore_legacy_cmd.go +++ b/cli/cmd/legacy_datastore_cmd.go @@ -17,7 +17,7 @@ var dataStoreCmd = &cobra.Command{ Use: "datastore", Short: "Manage your tracetest data stores", Long: "Manage your tracetest data stores", - Deprecated: "Please use `tracetest (apply|delete|export|get) datastore` commands instead.", + Deprecated: "Please use `tracetest (apply|delete|export|get|list) datastore` commands instead.", PreRun: setupCommand(), Run: func(cmd *cobra.Command, args []string) { cmd.Help() diff --git a/cli/cmd/environment_legacy_cmd.go b/cli/cmd/legacy_environment_cmd.go similarity index 94% rename from cli/cmd/environment_legacy_cmd.go rename to cli/cmd/legacy_environment_cmd.go index e57a53830f..f45812d7bb 100644 --- a/cli/cmd/environment_legacy_cmd.go +++ b/cli/cmd/legacy_environment_cmd.go @@ -11,7 +11,7 @@ var environmentCmd = &cobra.Command{ Use: "environment", Short: "Manage your tracetest environments", Long: "Manage your tracetest environments", - Deprecated: "Please use `tracetest (apply|delete|list|get|export) environment` commands instead.", + Deprecated: "Please use `tracetest (apply|delete|export|get|list) environment` commands instead.", PreRun: setupCommand(), Run: func(cmd *cobra.Command, args []string) { cmd.Help() diff --git a/cli/cmd/legacy_test_cmd.go b/cli/cmd/legacy_test_cmd.go new file mode 100644 index 0000000000..eb26de5b4a --- /dev/null +++ b/cli/cmd/legacy_test_cmd.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var testCmd = &cobra.Command{ + GroupID: cmdGroupTests.ID, + Use: "test", + Short: "Manage your tracetest tests", + Long: "Manage your tracetest tests", + PreRun: setupCommand(), + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + PostRun: teardownCommand, +} + +var testListCmd = &cobra.Command{ + Use: "list", + Short: "List all tests", + Long: "List all tests", + Deprecated: "Please use `tracetest list test` command instead.", + PreRun: setupCommand(), + Run: func(_ *cobra.Command, _ []string) { + listCmd.Run(listCmd, []string{"test"}) + }, + PostRun: teardownCommand, +} + +var testExportCmd = &cobra.Command{ + Use: "export", + Short: "Exports a test into a file", + Long: "Exports a test into a file", + Deprecated: "Please use `tracetest export test` command instead.", + PreRun: setupCommand(), + Run: func(_ *cobra.Command, _ []string) { + exportCmd.Run(exportCmd, []string{"test"}) + }, + PostRun: teardownCommand, +} + +func init() { + rootCmd.AddCommand(testCmd) + + // list + testCmd.AddCommand(testListCmd) + + // export + testExportCmd.PersistentFlags().StringVarP(&exportParams.ResourceID, "id", "", "", "id of the test") + testExportCmd.PersistentFlags().StringVarP(&exportParams.OutputFile, "output", "o", "", "file to be created with definition") + testCmd.AddCommand(testExportCmd) +} diff --git a/cli/cmd/middleware.go b/cli/cmd/middleware.go index c9bda90680..6fc16a218e 100644 --- a/cli/cmd/middleware.go +++ b/cli/cmd/middleware.go @@ -75,7 +75,7 @@ type resourceParameters struct { func (p *resourceParameters) Validate(cmd *cobra.Command, args []string) []error { if len(args) == 0 || args[0] == "" { return []error{ - ParamError{ + paramError{ Parameter: "resource", Message: "resource name must be provided", }, @@ -87,7 +87,7 @@ func (p *resourceParameters) Validate(cmd *cobra.Command, args []string) []error _, err := resources.Get(p.ResourceName) if errors.Is(err, resourcemanager.ErrResourceNotFound) { return []error{ - ParamError{ + paramError{ Parameter: "resource", Message: fmt.Sprintf("resource must be %s", resourceList()), }, @@ -96,3 +96,12 @@ func (p *resourceParameters) Validate(cmd *cobra.Command, args []string) []error return nil } + +type paramError struct { + Parameter string + Message string +} + +func (pe paramError) Error() string { + return fmt.Sprintf(`[%s] %s`, pe.Parameter, pe.Message) +} diff --git a/cli/cmd/param_error.go b/cli/cmd/param_error.go deleted file mode 100644 index a0db51374c..0000000000 --- a/cli/cmd/param_error.go +++ /dev/null @@ -1,12 +0,0 @@ -package cmd - -import "fmt" - -type ParamError struct { - Parameter string - Message string -} - -func (pe ParamError) Error() string { - return fmt.Sprintf(`[%s] %s`, pe.Parameter, pe.Message) -} diff --git a/cli/cmd/apply_cmd.go b/cli/cmd/resource_apply_cmd.go similarity index 80% rename from cli/cmd/apply_cmd.go rename to cli/cmd/resource_apply_cmd.go index cee504e989..fa91fb884f 100644 --- a/cli/cmd/apply_cmd.go +++ b/cli/cmd/resource_apply_cmd.go @@ -2,7 +2,9 @@ package cmd import ( "context" + "fmt" + "github.com/kubeshop/tracetest/cli/pkg/fileutil" "github.com/kubeshop/tracetest/cli/pkg/resourcemanager" "github.com/spf13/cobra" ) @@ -33,7 +35,12 @@ func init() { return "", err } - result, err := resourceClient.Apply(ctx, applyParams.DefinitionFile, resultFormat) + inputFile, err := fileutil.Read(applyParams.DefinitionFile) + if err != nil { + return "", fmt.Errorf("cannot read file %s: %w", applyParams.DefinitionFile, err) + } + + result, err := resourceClient.Apply(ctx, inputFile, resultFormat) if err != nil { return "", err } @@ -55,7 +62,7 @@ func (p applyParameters) Validate(cmd *cobra.Command, args []string) []error { errors := make([]error, 0) if p.DefinitionFile == "" { - errors = append(errors, ParamError{ + errors = append(errors, paramError{ Parameter: "file", Message: "Definition file must be provided", }) diff --git a/cli/cmd/delete_cmd.go b/cli/cmd/resource_delete_cmd.go similarity index 100% rename from cli/cmd/delete_cmd.go rename to cli/cmd/resource_delete_cmd.go diff --git a/cli/cmd/export_cmd.go b/cli/cmd/resource_export_cmd.go similarity index 98% rename from cli/cmd/export_cmd.go rename to cli/cmd/resource_export_cmd.go index 6a2532416a..7f04165811 100644 --- a/cli/cmd/export_cmd.go +++ b/cli/cmd/resource_export_cmd.go @@ -65,7 +65,7 @@ func (p exportParameters) Validate(cmd *cobra.Command, args []string) []error { errors := p.resourceIDParameters.Validate(cmd, args) if p.OutputFile == "" { - errors = append(errors, ParamError{ + errors = append(errors, paramError{ Parameter: "file", Message: "output file must be provided", }) diff --git a/cli/cmd/get_cmd.go b/cli/cmd/resource_get_cmd.go similarity index 97% rename from cli/cmd/get_cmd.go rename to cli/cmd/resource_get_cmd.go index 3cfc410fb5..130a1bc551 100644 --- a/cli/cmd/get_cmd.go +++ b/cli/cmd/resource_get_cmd.go @@ -55,7 +55,7 @@ func (p resourceIDParameters) Validate(cmd *cobra.Command, args []string) []erro errors := make([]error, 0) if p.ResourceID == "" { - errors = append(errors, ParamError{ + errors = append(errors, paramError{ Parameter: "id", Message: "resource id must be provided", }) diff --git a/cli/cmd/list_cmd.go b/cli/cmd/resource_list_cmd.go similarity index 95% rename from cli/cmd/list_cmd.go rename to cli/cmd/resource_list_cmd.go index 0ebe6dffdb..59f6bcb7d1 100644 --- a/cli/cmd/list_cmd.go +++ b/cli/cmd/resource_list_cmd.go @@ -72,21 +72,21 @@ func (p listParameters) Validate(cmd *cobra.Command, args []string) []error { errors := make([]error, 0) if p.Take < 0 { - errors = append(errors, ParamError{ + errors = append(errors, paramError{ Parameter: "take", Message: "take parameter must be greater than 0", }) } if p.Skip < 0 { - errors = append(errors, ParamError{ + errors = append(errors, paramError{ Parameter: "skip", Message: "skip parameter must be greater than 0", }) } if p.SortDirection != "" && p.SortDirection != "asc" && p.SortDirection != "desc" { - errors = append(errors, ParamError{ + errors = append(errors, paramError{ Parameter: "sortDirection", Message: "sortDirection parameter must be either asc or desc", }) diff --git a/cli/cmd/resources.go b/cli/cmd/resources.go new file mode 100644 index 0000000000..5540e1f8a5 --- /dev/null +++ b/cli/cmd/resources.go @@ -0,0 +1,215 @@ +package cmd + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/Jeffail/gabs/v2" + "github.com/kubeshop/tracetest/cli/analytics" + "github.com/kubeshop/tracetest/cli/pkg/resourcemanager" +) + +var resourceParams = &resourceParameters{} +var httpClient = &resourcemanager.HTTPClient{} + +var resources = resourcemanager.NewRegistry(). + Register( + resourcemanager.NewClient( + httpClient, + "config", "configs", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "ANALYTICS ENABLED", Path: "spec.analyticsEnabled"}, + }, + }), + ), + ). + Register( + resourcemanager.NewClient( + httpClient, + "analyzer", "analyzers", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "ENABLED", Path: "spec.enabled"}, + {Header: "MINIMUM SCORE", Path: "spec.minimumScore"}, + }, + }), + ), + ). + Register( + resourcemanager.NewClient( + httpClient, + "pollingprofile", "pollingprofiles", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "STRATEGY", Path: "spec.strategy"}, + }, + }), + ), + ). + Register( + resourcemanager.NewClient( + httpClient, + "demo", "demos", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "TYPE", Path: "spec.type"}, + {Header: "ENABLED", Path: "spec.enabled"}, + }, + }), + ), + ). + Register( + resourcemanager.NewClient( + httpClient, + "datastore", "datastores", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "DEFAULT", Path: "spec.default"}, + }, + ItemModifier: func(item *gabs.Container) error { + isDefault := item.Path("spec.default").Data().(bool) + if !isDefault { + item.SetP("", "spec.default") + } else { + item.SetP("*", "spec.default") + } + return nil + }, + }), + resourcemanager.WithDeleteEnabled("DataStore removed. Defaulting back to no-tracing mode"), + ), + ). + Register( + resourcemanager.NewClient( + httpClient, + "environment", "environments", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "DESCRIPTION", Path: "spec.description"}, + }, + }), + ), + ). + Register( + resourcemanager.NewClient( + httpClient, + "transaction", "transactions", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "VERSION", Path: "spec.version"}, + {Header: "STEPS", Path: "spec.summary.steps"}, + {Header: "RUNS", Path: "spec.summary.runs"}, + {Header: "LAST RUN TIME", Path: "spec.summary.lastRun.time"}, + {Header: "LAST RUN SUCCESSES", Path: "spec.summary.lastRun.passes"}, + {Header: "LAST RUN FAILURES", Path: "spec.summary.lastRun.fails"}, + }, + ItemModifier: func(item *gabs.Container) error { + // set spec.summary.steps to the number of steps in the transaction + item.SetP(len(item.Path("spec.steps").Children()), "spec.summary.steps") + + if err := formatItemDate(item, "spec.summary.lastRun.time"); err != nil { + return err + } + + return nil + }, + }), + ), + ). + Register( + resourcemanager.NewClient( + httpClient, + "test", "tests", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "VERSION", Path: "spec.version"}, + {Header: "TRIGGER TYPE", Path: "spec.trigger.type"}, + {Header: "RUNS", Path: "spec.summary.runs"}, + {Header: "LAST RUN TIME", Path: "spec.summary.lastRun.time"}, + {Header: "LAST RUN SUCCESSES", Path: "spec.summary.lastRun.passes"}, + {Header: "LAST RUN FAILURES", Path: "spec.summary.lastRun.fails"}, + {Header: "URL", Path: "spec.url"}, + }, + ItemModifier: func(item *gabs.Container) error { + // set spec.summary.steps to the number of steps in the transaction + id, ok := item.Path("spec.id").Data().(string) + if !ok { + return fmt.Errorf("test id '%s' is not a string", id) + } + + url := cliConfig.URL() + "/test/" + id + item.SetP(url, "spec.url") + + if err := formatItemDate(item, "spec.summary.lastRun.time"); err != nil { + return err + } + + return nil + }, + }), + ), + ) + +func resourceList() string { + return strings.Join(resources.List(), "|") +} + +func setupResources() { + extraHeaders := http.Header{} + extraHeaders.Set("x-client-id", analytics.ClientID()) + extraHeaders.Set("x-source", "cli") + + // To avoid a ciruclar reference initialization when setting up the registry and its resources, + // we create the resources with a pointer to an unconfigured HTTPClient. + // When each command is run, this function is run in the PreRun stage, before any of the actual `Run` code is executed + // We take this chance to configure the HTTPClient with the correct URL and headers. + // To make this configuration propagate to all the resources, we need to replace the pointer to the HTTPClient. + // For more details, see https://github.com/kubeshop/tracetest/pull/2832#discussion_r1245616804 + hc := resourcemanager.NewHTTPClient(cliConfig.URL(), extraHeaders) + *httpClient = *hc +} + +func formatItemDate(item *gabs.Container, path string) error { + rawDate := item.Path(path).Data() + if rawDate == nil { + return nil + } + dateStr := rawDate.(string) + // if field is empty, do nothing + if dateStr == "" { + return nil + } + + date, err := time.Parse(time.RFC3339, dateStr) + if err != nil { + return fmt.Errorf("failed to parse datetime field '%s' (value '%s'): %s", path, dateStr, err) + } + + if date.IsZero() { + // sometime the date comes like 0000-00-00T00:00:00Z... show nothing in that case + item.SetP("", path) + return nil + } + + item.SetP(date.Format(time.DateTime), path) + return nil +} diff --git a/cli/cmd/server_install_cmd.go b/cli/cmd/server_install_cmd.go index 5dbaab7b11..3aaaf82868 100644 --- a/cli/cmd/server_install_cmd.go +++ b/cli/cmd/server_install_cmd.go @@ -60,25 +60,25 @@ type installerParameters struct { KubernetesContext string } -func (p installerParameters) Validate(cmd *cobra.Command, args []string) []ParamError { - errors := make([]ParamError, 0) +func (p installerParameters) Validate(cmd *cobra.Command, args []string) []paramError { + errors := make([]paramError, 0) if cmd.Flags().Lookup("run-environment").Changed && slices.Contains(AllowedRunEnvironments, p.RunEnvironment) { - errors = append(errors, ParamError{ + errors = append(errors, paramError{ Parameter: "run-environment", Message: "run-environment must be one of 'none', 'docker' or 'kubernetes'", }) } if cmd.Flags().Lookup("mode").Changed && slices.Contains(AllowedInstallationMode, p.InstallationMode) { - errors = append(errors, ParamError{ + errors = append(errors, paramError{ Parameter: "mode", Message: "mode must be one of 'not-chosen', 'with-demo' or 'just-tracetest'", }) } if cmd.Flags().Lookup("kubernetes-context").Changed && p.KubernetesContext == "" { - errors = append(errors, ParamError{ + errors = append(errors, paramError{ Parameter: "kubernetes-context", Message: "kubernetes-context cannot be empty", }) diff --git a/cli/cmd/test_cmd.go b/cli/cmd/test_cmd.go deleted file mode 100644 index 427a9b72b0..0000000000 --- a/cli/cmd/test_cmd.go +++ /dev/null @@ -1,21 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" -) - -var testCmd = &cobra.Command{ - GroupID: cmdGroupTests.ID, - Use: "test", - Short: "Manage your tracetest tests", - Long: "Manage your tracetest tests", - PreRun: setupCommand(), - Run: func(cmd *cobra.Command, args []string) { - cmd.Help() - }, - PostRun: teardownCommand, -} - -func init() { - rootCmd.AddCommand(testCmd) -} diff --git a/cli/cmd/test_export_cmd.go b/cli/cmd/test_export_cmd.go deleted file mode 100644 index 9a25ad769b..0000000000 --- a/cli/cmd/test_export_cmd.go +++ /dev/null @@ -1,47 +0,0 @@ -package cmd - -import ( - "context" - - "github.com/kubeshop/tracetest/cli/actions" - "github.com/kubeshop/tracetest/cli/utils" - "github.com/spf13/cobra" - "go.uber.org/zap" -) - -var ( - exportTestId string - exportTestOutputFile string - version int32 -) - -var testExportCmd = &cobra.Command{ - Use: "export", - Short: "Exports a test into a file", - Long: "Exports a test into a file", - PreRun: setupCommand(), - Run: WithResultHandler(func(cmd *cobra.Command, args []string) (string, error) { - ctx := context.Background() - cliLogger.Debug("Exporting test", zap.String("testID", exportTestId)) - client := utils.GetAPIClient(cliConfig) - exportTestAction := actions.NewExportTestAction(cliConfig, cliLogger, client) - - actionArgs := actions.ExportTestConfig{ - TestId: exportTestId, - OutputFile: exportTestOutputFile, - Version: version, - } - - err := exportTestAction.Run(ctx, actionArgs) - return "", err - }), - PostRun: teardownCommand, -} - -func init() { - testExportCmd.PersistentFlags().StringVarP(&exportTestId, "id", "", "", "id of the test") - testExportCmd.PersistentFlags().StringVarP(&exportTestOutputFile, "output", "o", "", "file to be created with definition") - testExportCmd.PersistentFlags().Int32VarP(&version, "version", "", -1, "version of the test. Default is latest") - - testCmd.AddCommand(testExportCmd) -} diff --git a/cli/cmd/test_list_cmd.go b/cli/cmd/test_list_cmd.go deleted file mode 100644 index c98a041a88..0000000000 --- a/cli/cmd/test_list_cmd.go +++ /dev/null @@ -1,32 +0,0 @@ -package cmd - -import ( - "context" - - "github.com/kubeshop/tracetest/cli/actions" - "github.com/kubeshop/tracetest/cli/utils" - "github.com/spf13/cobra" - "go.uber.org/zap" -) - -var testListCmd = &cobra.Command{ - Use: "list", - Short: "List all tests", - Long: "List all tests", - PreRun: setupCommand(), - Run: WithResultHandler(func(_ *cobra.Command, _ []string) (string, error) { - ctx := context.Background() - cliLogger.Debug("Retrieving list of tests", zap.String("endpoint", cliConfig.Endpoint)) - client := utils.GetAPIClient(cliConfig) - listTestsAction := actions.NewListTestsAction(cliConfig, cliLogger, client) - - actionArgs := actions.ListTestConfig{} - err := listTestsAction.Run(ctx, actionArgs) - return "", err - }), - PostRun: teardownCommand, -} - -func init() { - testCmd.AddCommand(testListCmd) -} diff --git a/cli/cmd/test_run_cmd.go b/cli/cmd/test_run_cmd.go index fc3588b98b..8b34971cbc 100644 --- a/cli/cmd/test_run_cmd.go +++ b/cli/cmd/test_run_cmd.go @@ -30,7 +30,25 @@ var testRunCmd = &cobra.Command{ return "", fmt.Errorf("failed to get environment client: %w", err) } - runTestAction := actions.NewRunTestAction(cliConfig, cliLogger, client, envClient, ExitCLI) + testClient, err := resources.Get("test") + if err != nil { + return "", fmt.Errorf("failed to get test client: %w", err) + } + + transactionsClient, err := resources.Get("transaction") + if err != nil { + return "", fmt.Errorf("failed to get transaction client: %w", err) + } + + runTestAction := actions.NewRunTestAction( + cliConfig, + cliLogger, + client, + testClient, + transactionsClient, + envClient, + ExitCLI, + ) actionArgs := actions.RunResourceArgs{ DefinitionFile: runTestFileDefinition, EnvID: runTestEnvID, diff --git a/cli/file/definition.go b/cli/file/definition.go deleted file mode 100644 index 7670325ac7..0000000000 --- a/cli/file/definition.go +++ /dev/null @@ -1,180 +0,0 @@ -package file - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/kubeshop/tracetest/cli/variable" - tracetestYaml "github.com/kubeshop/tracetest/server/model/yaml" - "gopkg.in/yaml.v3" -) - -type SpecWithID struct { - ID string `yaml:"id"` -} - -type File struct { - path string - contents []byte - file tracetestYaml.File -} - -func Read(path string) (File, error) { - b, err := os.ReadFile(path) - if err != nil { - return File{}, fmt.Errorf("could not read definition file %s: %w", path, err) - } - - return New(path, b) -} - -func ReadRaw(path string) (File, error) { - b, err := os.ReadFile(path) - if err != nil { - return File{}, fmt.Errorf("could not read definition file %s: %w", path, err) - } - - return NewFromRaw(path, b) -} - -func New(path string, b []byte) (File, error) { - yf, err := tracetestYaml.Decode(b) - if err != nil { - return File{}, fmt.Errorf("could not parse definition file: %w", err) - } - - file := File{ - contents: b, - file: yf, - path: path, - } - - return file, nil -} - -func NewFromRaw(path string, b []byte) (File, error) { - var f tracetestYaml.File - err := yaml.Unmarshal(b, &f) - if err != nil { - return File{}, fmt.Errorf("could not parse definition file: %w", err) - } - - file := File{ - contents: b, - path: path, - file: f, - } - - return file, nil -} - -func (f File) Path() string { - return f.path -} - -func (f File) AbsDir() string { - abs, err := filepath.Abs(f.path) - if err != nil { - panic(fmt.Errorf(`cannot get absolute path from "%s": %w`, f.path, err)) - } - - return filepath.Dir(abs) -} - -func (f File) ResolveVariables() (File, error) { - variableInjector := variable.NewInjector(variable.WithVariableProvider( - variable.EnvironmentVariableProvider{}, - )) - - err := variableInjector.Inject(&f.file) - if err != nil { - return File{}, err - } - - bytes, err := tracetestYaml.Encode(f.file) - if err != nil { - return File{}, err - } - - f.contents = bytes - - return f, nil -} - -func (f File) Definition() tracetestYaml.File { - return f.file -} - -func (f File) Contents() string { - return string(f.contents) -} - -func (f File) ContentType() string { - return "text/yaml" -} - -var ( - hasIDRegex = regexp.MustCompile(`(?m:^\s+id:\s*[0-9a-zA-Z\-_]+$)`) - indentSizeRegex = regexp.MustCompile(`(?m:^(\s+)\w+)`) -) - -var ErrFileHasID = errors.New("file already has ID") - -func (f File) HasID() bool { - fileID := hasIDRegex.Find(f.contents) - return fileID != nil -} - -func (f File) SetID(id string) (File, error) { - if f.HasID() { - return f, ErrFileHasID - } - - indent := indentSizeRegex.FindSubmatchIndex(f.contents) - if len(indent) < 4 { - return f, fmt.Errorf("cannot detect indentation size") - } - - indentSize := indent[3] - indent[2] - // indent[2] is the index of the first indentation. - // we can assume that's the first line within the `specs` block - // so we can use it as the place to inejct the ID - - var newContents []byte - newContents = append(newContents, f.contents[0:indent[2]]...) - - newContents = append(newContents, []byte(strings.Repeat(" ", indentSize))...) - newContents = append(newContents, []byte("id: "+id+"\n")...) - - newContents = append(newContents, f.contents[indent[2]:]...) - - return New(f.path, newContents) -} - -func (f File) Write() (File, error) { - err := os.WriteFile(f.path, f.contents, 0644) - if err != nil { - return f, fmt.Errorf("could not write file: %w", err) - } - - return Read(f.path) -} - -func (f File) WriteRaw() (File, error) { - err := os.WriteFile(f.path, f.contents, 0644) - if err != nil { - return f, fmt.Errorf("could not write file: %w", err) - } - - return ReadRaw(f.path) -} - -func (f File) SaveChanges(changes string) File { - f.contents = []byte(changes) - - return f -} diff --git a/cli/file/definition_test.go b/cli/file/definition_test.go deleted file mode 100644 index 10434d354f..0000000000 --- a/cli/file/definition_test.go +++ /dev/null @@ -1,227 +0,0 @@ -package file_test - -import ( - "os" - "testing" - - "github.com/kubeshop/tracetest/cli/file" - "github.com/kubeshop/tracetest/server/model/yaml" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestLoadDefinition(t *testing.T) { - testCases := []struct { - name string - file string - expected yaml.File - expectSuccess bool - envVariables map[string]string - }{ - { - name: "Basic", - file: "../testdata/definitions/valid_http_test_definition.yml", - expectSuccess: true, - expected: yaml.File{ - Type: yaml.FileTypeTest, - Spec: yaml.Test{ - Name: "POST import pokemon", - Description: "Import a pokemon using its ID", - Trigger: yaml.TestTrigger{ - Type: "http", - HTTPRequest: yaml.HTTPRequest{ - URL: "http://pokemon-demo.tracetest.io/pokemon/import", - Method: "POST", - Headers: []yaml.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: `{ "id": 52 }`, - }, - }, - Specs: []yaml.TestSpec{ - { - Selector: "span[name = \"POST /pokemon/import\"]", - Assertions: []string{ - "tracetest.span.duration <= 100ms", - "http.status_code = 200", - }, - }, - { - Selector: "span[name = \"send message to queue\"]", - Assertions: []string{ - "messaging.message.payload contains 52", - }, - }, - { - Selector: "span[name = \"consume message from queue\"]:last", - Assertions: []string{ - "messaging.message.payload contains 52", - }, - }, - { - Selector: "span[name = \"consume message from queue\"]:last span[name = \"import pokemon from pokeapi\"]", - Assertions: []string{ - "http.status_code = 200", - }, - }, - { - Selector: "span[name = \"consume message from queue\"]:last span[name = \"save pokemon on database\"]", - Assertions: []string{ - "db.repository.operation = \"create\"", - "tracetest.span.duration <= 100ms", - `tracetest.response.body contains "\"id\": 52"`, - }, - }, - }, - }, - }, - }, - { - name: "HandleID", - file: "../testdata/definitions/valid_http_test_definition_with_id.yml", - expectSuccess: true, - expected: yaml.File{ - Type: yaml.FileTypeTest, - Spec: yaml.Test{ - ID: "3fd66887-4ee7-44d5-bad8-9934ab9c1a9a", - Name: "POST import pokemon", - Description: "Import a pokemon using its ID", - Trigger: yaml.TestTrigger{ - Type: "http", - HTTPRequest: yaml.HTTPRequest{ - URL: "http://pokemon-demo.tracetest.io/pokemon/import", - Method: "POST", - Headers: []yaml.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: `{ "id": 52 }`, - }, - }, - Specs: []yaml.TestSpec{ - { - Selector: "span[name = \"POST /pokemon/import\"]", - Assertions: []string{ - "tracetest.span.duration <= 100ms", - "http.status_code = 200", - }, - }, - { - Selector: "span[name = \"send message to queue\"]", - Assertions: []string{ - "messaging.message.payload contains 52", - }, - }, - { - Selector: "span[name = \"consume message from queue\"]:last", - Assertions: []string{ - "messaging.message.payload contains 52", - }, - }, - { - Selector: "span[name = \"consume message from queue\"]:last span[name = \"import pokemon from pokeapi\"]", - Assertions: []string{ - "http.status_code = 200", - }, - }, - { - Selector: "span[name = \"consume message from queue\"]:last span[name = \"save pokemon on database\"]", - Assertions: []string{ - "db.repository.operation = \"create\"", - "tracetest.span.duration <= 100ms", - }, - }, - }, - }, - }, - }, - { - name: "EnvVars", - file: "../testdata/definitions/valid_http_test_definition_with_env_variables.yml", - expectSuccess: true, - envVariables: map[string]string{ - "POKEMON_APP_API_KEY": "1234", - }, - expected: yaml.File{ - Type: yaml.FileTypeTest, - Spec: yaml.Test{ - Name: "POST import pokemon", - Description: "Import a pokemon using its ID", - Trigger: yaml.TestTrigger{ - Type: "http", - HTTPRequest: yaml.HTTPRequest{ - URL: "http://pokemon-demo.tracetest.io/pokemon/import", - Method: "POST", - Headers: []yaml.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: `{ "id": 52 }`, - Authentication: &yaml.HTTPAuthentication{ - Type: "apiKey", - APIKey: &yaml.HTTPAPIKeyAuth{ - Key: "X-Key", - Value: "1234", - In: "header", - }, - }, - }, - }, - }, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - - for envName, envValue := range testCase.envVariables { - os.Setenv(envName, envValue) - } - - defer func() { - for envName := range testCase.envVariables { - os.Unsetenv(envName) - } - }() - - actual, err := file.Read(testCase.file) - if testCase.expectSuccess { - require.NoError(t, err) - - err = actual.Definition().Validate() - assert.NoError(t, err) - - resolvedFile, err := actual.ResolveVariables() - assert.NoError(t, err) - - assert.Equal(t, testCase.expected, resolvedFile.Definition()) - } else { - require.Error(t, err, "LoadDefinition should fail") - } - }) - } -} - -func TestSetID(t *testing.T) { - t.Run("NoID", func(t *testing.T) { - f, err := file.Read("../testdata/definitions/valid_http_test_definition.yml") - require.NoError(t, err) - - test, _ := f.Definition().Test() - assert.Equal(t, "", test.ID) - - f, err = f.SetID("new-id") - require.NoError(t, err) - - test, _ = f.Definition().Test() - assert.Equal(t, "new-id", test.ID) - }) - - t.Run("WithID", func(t *testing.T) { - f, err := file.Read("../testdata/definitions/valid_http_test_definition_with_id.yml") - require.NoError(t, err) - - _, err = f.SetID("new-id") - require.ErrorIs(t, err, file.ErrFileHasID) - }) - -} diff --git a/cli/formatters/formatter.go b/cli/formatters/formatter.go deleted file mode 100644 index 79006188e0..0000000000 --- a/cli/formatters/formatter.go +++ /dev/null @@ -1,76 +0,0 @@ -package formatters - -import ( - "fmt" - - "github.com/alexeyco/simpletable" - "github.com/kubeshop/tracetest/cli/file" -) - -type ToStruct func(*file.File) (interface{}, error) -type ToListStruct func(*file.File) ([]interface{}, error) - -type ToTable func(*file.File) (*simpletable.Header, *simpletable.Body, error) -type ToListTable ToTable - -type ResourceFormatter interface { - ToTable(*file.File) (*simpletable.Header, *simpletable.Body, error) - ToListTable(*file.File) (*simpletable.Header, *simpletable.Body, error) - ToStruct(*file.File) (interface{}, error) - ToListStruct(*file.File) ([]interface{}, error) -} - -type FormatterInterface interface { - Format(*file.File) (string, error) - FormatList(*file.File) (string, error) - Type() string -} - -type Formatter struct { - formatType string - registry map[string]FormatterInterface -} - -func NewFormatter(formatType string, formatters ...FormatterInterface) Formatter { - registry := make(map[string]FormatterInterface, len(formatters)) - - for _, option := range formatters { - registry[option.Type()] = option - } - - return Formatter{formatType, registry} -} - -func (f Formatter) Format(file *file.File) (string, error) { - formatter, ok := f.registry[f.formatType] - if !ok { - return "", fmt.Errorf("formatter %s not found", f.formatType) - } - - return formatter.Format(file) -} - -func (f Formatter) FormatList(file *file.File) (string, error) { - formatter, ok := f.registry[f.formatType] - if !ok { - return "", fmt.Errorf("formatter %s not found", f.formatType) - } - - return formatter.FormatList(file) -} - -func BuildFormatter(formatType string, defaultType Output, resourceFormatter ResourceFormatter) Formatter { - jsonFormatter := NewJson(resourceFormatter) - yamlFormatter := NewYaml(resourceFormatter) - tableFormatter := NewTable(resourceFormatter) - - if defaultType == "" { - defaultType = YAML - } - - if formatType == "" { - formatType = string(defaultType) - } - - return NewFormatter(formatType, jsonFormatter, yamlFormatter, tableFormatter) -} diff --git a/cli/formatters/json.go b/cli/formatters/json.go deleted file mode 100644 index f04debb8fa..0000000000 --- a/cli/formatters/json.go +++ /dev/null @@ -1,54 +0,0 @@ -package formatters - -import ( - "encoding/json" - "fmt" - - "github.com/kubeshop/tracetest/cli/file" -) - -type Json struct { - toStructFn ToStruct - toListStructFn ToListStruct -} - -var _ FormatterInterface = Json{} - -func NewJson(resourceFormatter ResourceFormatter) Json { - return Json{ - toStructFn: resourceFormatter.ToStruct, - toListStructFn: resourceFormatter.ToListStruct, - } -} - -func (j Json) Type() string { - return "json" -} - -func (j Json) Format(file *file.File) (string, error) { - data, err := j.toStructFn(file) - if err != nil { - return "", fmt.Errorf("could not convert file to struct: %w", err) - } - - bytes, err := json.MarshalIndent(data, "", " ") - if err != nil { - return "", fmt.Errorf("could not marshal output json: %w", err) - } - - return string(bytes), nil -} - -func (j Json) FormatList(file *file.File) (string, error) { - data, err := j.toListStructFn(file) - if err != nil { - return "", fmt.Errorf("could not convert file to struct: %w", err) - } - - bytes, err := json.MarshalIndent(data, "", " ") - if err != nil { - return "", fmt.Errorf("could not marshal output json: %w", err) - } - - return string(bytes), nil -} diff --git a/cli/formatters/table.go b/cli/formatters/table.go deleted file mode 100644 index 896341d70d..0000000000 --- a/cli/formatters/table.go +++ /dev/null @@ -1,54 +0,0 @@ -package formatters - -import ( - "github.com/alexeyco/simpletable" - "github.com/kubeshop/tracetest/cli/file" -) - -type Table struct { - toTableFn ToTable - toListTableFn ToListTable -} - -var _ FormatterInterface = Table{} - -func NewTable(resourceFormatter ResourceFormatter) Table { - return Table{ - toTableFn: resourceFormatter.ToTable, - toListTableFn: resourceFormatter.ToListTable, - } -} - -func (t Table) Type() string { - return "pretty" -} - -func (t Table) Format(file *file.File) (string, error) { - table := simpletable.New() - - header, body, err := t.toTableFn(file) - if err != nil { - return "", err - } - - table.Header = header - table.Body = body - - table.SetStyle(simpletable.StyleCompactLite) - return table.String(), nil -} - -func (t Table) FormatList(file *file.File) (string, error) { - table := simpletable.New() - - header, body, err := t.toListTableFn(file) - if err != nil { - return "", err - } - - table.Header = header - table.Body = body - - table.SetStyle(simpletable.StyleCompactLite) - return table.String(), nil -} diff --git a/cli/formatters/test_list_test.go b/cli/formatters/test_list_test.go deleted file mode 100644 index 55033ee7ec..0000000000 --- a/cli/formatters/test_list_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package formatters_test - -import ( - "strings" - "testing" - - "github.com/kubeshop/tracetest/cli/config" - "github.com/kubeshop/tracetest/cli/formatters" - "github.com/kubeshop/tracetest/cli/openapi" - "github.com/stretchr/testify/assert" -) - -func TestListOutput(t *testing.T) { - cases := []struct { - name string - tests []openapi.Test - expected string - }{ - { - name: "NoTests", - tests: []openapi.Test{}, - expected: `No tests`, - }, - { - name: "HaveTests", - tests: []openapi.Test{ - { - Id: openapi.PtrString("123456"), - Name: openapi.PtrString("Test One"), - }, - { - Id: openapi.PtrString("456789"), - Name: openapi.PtrString("Test Two"), - }, - }, - expected: "" + // vs code trims the last whitespace on save. this awful method avoids that\ - " ID NAME URL \n" + - "-------- ---------- ------------------------------------\n" + - " 123456 Test One http://localhost:11633/test/123456 \n" + - " 456789 Test Two http://localhost:11633/test/456789 \n", - }, - } - - formatter := formatters.TestsList(config.Config{ - Scheme: "http", - Endpoint: "localhost:11633", - }) - for _, c := range cases { - output := formatter.Format(c.tests) - assert.Equal(t, strings.Trim(c.expected, "\n"), output) - } -} diff --git a/cli/formatters/tests_list.go b/cli/formatters/tests_list.go deleted file mode 100644 index d43db48da1..0000000000 --- a/cli/formatters/tests_list.go +++ /dev/null @@ -1,72 +0,0 @@ -package formatters - -import ( - "encoding/json" - "fmt" - - "github.com/alexeyco/simpletable" - "github.com/kubeshop/tracetest/cli/config" - "github.com/kubeshop/tracetest/cli/openapi" -) - -type testsList struct { - config config.Config -} - -func TestsList(config config.Config) testsList { - return testsList{ - config: config, - } -} - -func (f testsList) Format(tests []openapi.Test) string { - switch CurrentOutput { - case Pretty: - return f.pretty(tests) - case JSON: - return f.json(tests) - } - - return "" -} - -func (f testsList) json(tests []openapi.Test) string { - bytes, err := json.Marshal(tests) - if err != nil { - panic(fmt.Errorf("could not marshal output json: %w", err)) - } - - return string(bytes) -} - -func (f testsList) pretty(tests []openapi.Test) string { - if len(tests) == 0 { - return "No tests" - } - - table := simpletable.New() - - table.Header = &simpletable.Header{ - Cells: []*simpletable.Cell{ - {Text: "ID"}, - {Text: "NAME"}, - {Text: "URL"}, - }, - } - - for _, t := range tests { - table.Body.Cells = append(table.Body.Cells, []*simpletable.Cell{ - {Text: *t.Id}, - {Text: *t.Name}, - {Text: f.getTestLink(t)}, - }) - } - - table.SetStyle(simpletable.StyleCompactLite) - - return table.String() -} - -func (f testsList) getTestLink(test openapi.Test) string { - return fmt.Sprintf("%s://%s/test/%s", f.config.Scheme, f.config.Endpoint, *test.Id) -} diff --git a/cli/formatters/yaml.go b/cli/formatters/yaml.go deleted file mode 100644 index 8fa778563a..0000000000 --- a/cli/formatters/yaml.go +++ /dev/null @@ -1,60 +0,0 @@ -package formatters - -import ( - "fmt" - - "github.com/goccy/go-yaml" - - "github.com/kubeshop/tracetest/cli/file" -) - -type Yaml struct { - toStructFn ToStruct - toListStructFn ToListStruct -} - -var _ FormatterInterface = Yaml{} - -func NewYaml(resourceFormatter ResourceFormatter) Yaml { - return Yaml{ - toStructFn: resourceFormatter.ToStruct, - toListStructFn: resourceFormatter.ToListStruct, - } -} - -func (Yaml) Type() string { - return "yaml" -} - -func (y Yaml) FormatList(file *file.File) (string, error) { - data, err := y.toListStructFn(file) - if err != nil { - return "", fmt.Errorf("could not convert file to struct: %w", err) - } - - result := "" - for _, value := range data { - bytes, err := yaml.Marshal(value) - if err != nil { - return "", fmt.Errorf("could not marshal output json: %w", err) - } - - result += "---\n" + string(bytes) + "\n" - } - - return result, nil -} - -func (y Yaml) Format(file *file.File) (string, error) { - data, err := y.toStructFn(file) - if err != nil { - return "", fmt.Errorf("could not convert file to struct: %w", err) - } - - bytes, err := yaml.Marshal(data) - if err != nil { - return "", fmt.Errorf("could not marshal output json: %w", err) - } - - return "---\n" + string(bytes), nil -} diff --git a/cli/go.mod b/cli/go.mod index 01415de841..6193ab6f80 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -6,7 +6,7 @@ require ( github.com/Jeffail/gabs/v2 v2.7.0 github.com/alexeyco/simpletable v1.0.0 github.com/compose-spec/compose-go v1.5.1 - github.com/cucumber/ci-environment/go v0.0.0-20220915001957-711b1c82415f + github.com/cucumber/ci-environment/go v0.0.0-20230703185945-ddbd134c44fd github.com/denisbrodbeck/machineid v1.0.1 github.com/goccy/go-yaml v1.11.0 github.com/kubeshop/tracetest/server v0.0.0-20230512142545-cb5e526e06f9 @@ -14,7 +14,7 @@ require ( github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.15.0 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.23.0 golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 gopkg.in/yaml.v3 v3.0.1 diff --git a/cli/go.sum b/cli/go.sum index 6f66f25ac4..bce49c5f95 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -80,8 +80,8 @@ github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkX github.com/containerd/containerd v1.6.18 h1:qZbsLvmyu+Vlty0/Ex5xc0z2YtKpIsb5n45mAMI+2Ns= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cucumber/ci-environment/go v0.0.0-20220915001957-711b1c82415f h1:JoQPsirXW3nGs5shRwidDODWeicHf4CDFaaMzSl9aeE= -github.com/cucumber/ci-environment/go v0.0.0-20220915001957-711b1c82415f/go.mod h1:ND1mBObqeFyxrBaIuDkdtM+2gqNsCrPCMyZ9sfJ97ls= +github.com/cucumber/ci-environment/go v0.0.0-20230703185945-ddbd134c44fd h1:YnPJcbnNHcsFFyckLIynp4B6nvT75GEpH+yOc8aYhhE= +github.com/cucumber/ci-environment/go v0.0.0-20230703185945-ddbd134c44fd/go.mod h1:W5gTLX4+0/cT8vgja6OSH3OsnuNK8HfTOK3PrXyGQ/Q= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -323,8 +323,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 h1:xzABM9let0HLLqFypcxvLmlvEciCHL7+Lv+4vwZqecI= diff --git a/cli/openapi/api_api.go b/cli/openapi/api_api.go index 73bd693e19..9e609ec422 100644 --- a/cli/openapi/api_api.go +++ b/cli/openapi/api_api.go @@ -22,206 +22,6 @@ import ( // ApiApiService ApiApi service type ApiApiService service -type ApiCreateTestRequest struct { - ctx context.Context - ApiService *ApiApiService - test *Test -} - -func (r ApiCreateTestRequest) Test(test Test) ApiCreateTestRequest { - r.test = &test - return r -} - -func (r ApiCreateTestRequest) Execute() (*Test, *http.Response, error) { - return r.ApiService.CreateTestExecute(r) -} - -/* -CreateTest Create new test - -Create new test action - - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @return ApiCreateTestRequest -*/ -func (a *ApiApiService) CreateTest(ctx context.Context) ApiCreateTestRequest { - return ApiCreateTestRequest{ - ApiService: a, - ctx: ctx, - } -} - -// Execute executes the request -// -// @return Test -func (a *ApiApiService) CreateTestExecute(r ApiCreateTestRequest) (*Test, *http.Response, error) { - var ( - localVarHTTPMethod = http.MethodPost - localVarPostBody interface{} - formFiles []formFile - localVarReturnValue *Test - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ApiApiService.CreateTest") - if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/tests" - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"application/json"} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - // body params - localVarPostBody = r.test - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) - if err != nil { - return localVarReturnValue, nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - return localVarReturnValue, localVarHTTPResponse, nil -} - -type ApiDeleteTestRequest struct { - ctx context.Context - ApiService *ApiApiService - testId string -} - -func (r ApiDeleteTestRequest) Execute() (*http.Response, error) { - return r.ApiService.DeleteTestExecute(r) -} - -/* -DeleteTest delete a test - -delete a test - - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param testId id of the test - @return ApiDeleteTestRequest -*/ -func (a *ApiApiService) DeleteTest(ctx context.Context, testId string) ApiDeleteTestRequest { - return ApiDeleteTestRequest{ - ApiService: a, - ctx: ctx, - testId: testId, - } -} - -// Execute executes the request -func (a *ApiApiService) DeleteTestExecute(r ApiDeleteTestRequest) (*http.Response, error) { - var ( - localVarHTTPMethod = http.MethodDelete - localVarPostBody interface{} - formFiles []formFile - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ApiApiService.DeleteTest") - if err != nil { - return nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/tests/{testId}" - localVarPath = strings.Replace(localVarPath, "{"+"testId"+"}", url.PathEscape(parameterValueToString(r.testId, "testId")), -1) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) - if err != nil { - return nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarHTTPResponse, err - } - - localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - return localVarHTTPResponse, newErr - } - - return localVarHTTPResponse, nil -} - type ApiDeleteTestRunRequest struct { ctx context.Context ApiService *ApiApiService @@ -1112,110 +912,6 @@ func (a *ApiApiService) GetRunResultJUnitExecute(r ApiGetRunResultJUnitRequest) return localVarReturnValue, localVarHTTPResponse, nil } -type ApiGetTestRequest struct { - ctx context.Context - ApiService *ApiApiService - testId string -} - -func (r ApiGetTestRequest) Execute() (*Test, *http.Response, error) { - return r.ApiService.GetTestExecute(r) -} - -/* -GetTest get test - -get test - - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param testId id of the test - @return ApiGetTestRequest -*/ -func (a *ApiApiService) GetTest(ctx context.Context, testId string) ApiGetTestRequest { - return ApiGetTestRequest{ - ApiService: a, - ctx: ctx, - testId: testId, - } -} - -// Execute executes the request -// -// @return Test -func (a *ApiApiService) GetTestExecute(r ApiGetTestRequest) (*Test, *http.Response, error) { - var ( - localVarHTTPMethod = http.MethodGet - localVarPostBody interface{} - formFiles []formFile - localVarReturnValue *Test - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ApiApiService.GetTest") - if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/tests/{testId}" - localVarPath = strings.Replace(localVarPath, "{"+"testId"+"}", url.PathEscape(parameterValueToString(r.testId, "testId")), -1) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) - if err != nil { - return localVarReturnValue, nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - return localVarReturnValue, localVarHTTPResponse, nil -} - type ApiGetTestResultSelectedSpansRequest struct { ctx context.Context ApiService *ApiApiService @@ -1709,232 +1405,16 @@ func (a *ApiApiService) GetTestSpecsExecute(r ApiGetTestSpecsRequest) ([]TestSpe localVarHTTPMethod = http.MethodGet localVarPostBody interface{} formFiles []formFile - localVarReturnValue []TestSpecs - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ApiApiService.GetTestSpecs") - if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/tests/{testId}/definition" - localVarPath = strings.Replace(localVarPath, "{"+"testId"+"}", url.PathEscape(parameterValueToString(r.testId, "testId")), -1) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) - if err != nil { - return localVarReturnValue, nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - return localVarReturnValue, localVarHTTPResponse, nil -} - -type ApiGetTestVersionRequest struct { - ctx context.Context - ApiService *ApiApiService - testId string - version int32 -} - -func (r ApiGetTestVersionRequest) Execute() (*Test, *http.Response, error) { - return r.ApiService.GetTestVersionExecute(r) -} - -/* -GetTestVersion get a test specific version - -get a test specific version - - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param testId id of the test - @param version version of the test - @return ApiGetTestVersionRequest -*/ -func (a *ApiApiService) GetTestVersion(ctx context.Context, testId string, version int32) ApiGetTestVersionRequest { - return ApiGetTestVersionRequest{ - ApiService: a, - ctx: ctx, - testId: testId, - version: version, - } -} - -// Execute executes the request -// -// @return Test -func (a *ApiApiService) GetTestVersionExecute(r ApiGetTestVersionRequest) (*Test, *http.Response, error) { - var ( - localVarHTTPMethod = http.MethodGet - localVarPostBody interface{} - formFiles []formFile - localVarReturnValue *Test - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ApiApiService.GetTestVersion") - if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/tests/{testId}/version/{version}" - localVarPath = strings.Replace(localVarPath, "{"+"testId"+"}", url.PathEscape(parameterValueToString(r.testId, "testId")), -1) - localVarPath = strings.Replace(localVarPath, "{"+"version"+"}", url.PathEscape(parameterValueToString(r.version, "version")), -1) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) - if err != nil { - return localVarReturnValue, nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - return localVarReturnValue, localVarHTTPResponse, nil -} - -type ApiGetTestVersionDefinitionFileRequest struct { - ctx context.Context - ApiService *ApiApiService - testId string - version int32 -} - -func (r ApiGetTestVersionDefinitionFileRequest) Execute() (string, *http.Response, error) { - return r.ApiService.GetTestVersionDefinitionFileExecute(r) -} - -/* -GetTestVersionDefinitionFile Get the test definition as an YAML file - -Get the test definition as an YAML file - - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param testId id of the test - @param version version of the test - @return ApiGetTestVersionDefinitionFileRequest -*/ -func (a *ApiApiService) GetTestVersionDefinitionFile(ctx context.Context, testId string, version int32) ApiGetTestVersionDefinitionFileRequest { - return ApiGetTestVersionDefinitionFileRequest{ - ApiService: a, - ctx: ctx, - testId: testId, - version: version, - } -} - -// Execute executes the request -// -// @return string -func (a *ApiApiService) GetTestVersionDefinitionFileExecute(r ApiGetTestVersionDefinitionFileRequest) (string, *http.Response, error) { - var ( - localVarHTTPMethod = http.MethodGet - localVarPostBody interface{} - formFiles []formFile - localVarReturnValue string + localVarReturnValue []TestSpecs ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ApiApiService.GetTestVersionDefinitionFile") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ApiApiService.GetTestSpecs") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } - localVarPath := localBasePath + "/tests/{testId}/version/{version}/definition.yaml" + localVarPath := localBasePath + "/tests/{testId}/definition" localVarPath = strings.Replace(localVarPath, "{"+"testId"+"}", url.PathEscape(parameterValueToString(r.testId, "testId")), -1) - localVarPath = strings.Replace(localVarPath, "{"+"version"+"}", url.PathEscape(parameterValueToString(r.version, "version")), -1) localVarHeaderParams := make(map[string]string) localVarQueryParams := url.Values{} @@ -1950,7 +1430,7 @@ func (a *ApiApiService) GetTestVersionDefinitionFileExecute(r ApiGetTestVersionD } // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/yaml"} + localVarHTTPHeaderAccepts := []string{"application/json"} // set Accept header localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) @@ -1994,102 +1474,60 @@ func (a *ApiApiService) GetTestVersionDefinitionFileExecute(r ApiGetTestVersionD return localVarReturnValue, localVarHTTPResponse, nil } -type ApiGetTestsRequest struct { - ctx context.Context - ApiService *ApiApiService - take *int32 - skip *int32 - query *string - sortBy *string - sortDirection *string -} - -// indicates how many resources can be returned by each page -func (r ApiGetTestsRequest) Take(take int32) ApiGetTestsRequest { - r.take = &take - return r -} - -// indicates how many resources will be skipped when paginating -func (r ApiGetTestsRequest) Skip(skip int32) ApiGetTestsRequest { - r.skip = &skip - return r -} - -// query to search resources -func (r ApiGetTestsRequest) Query(query string) ApiGetTestsRequest { - r.query = &query - return r -} - -// indicates the sort field for the resources -func (r ApiGetTestsRequest) SortBy(sortBy string) ApiGetTestsRequest { - r.sortBy = &sortBy - return r -} - -// indicates the sort direction for the resources -func (r ApiGetTestsRequest) SortDirection(sortDirection string) ApiGetTestsRequest { - r.sortDirection = &sortDirection - return r +type ApiGetTestVersionRequest struct { + ctx context.Context + ApiService *ApiApiService + testId string + version int32 } -func (r ApiGetTestsRequest) Execute() ([]Test, *http.Response, error) { - return r.ApiService.GetTestsExecute(r) +func (r ApiGetTestVersionRequest) Execute() (*Test, *http.Response, error) { + return r.ApiService.GetTestVersionExecute(r) } /* -GetTests Get tests +GetTestVersion get a test specific version -get tests +get a test specific version @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @return ApiGetTestsRequest + @param testId id of the test + @param version version of the test + @return ApiGetTestVersionRequest */ -func (a *ApiApiService) GetTests(ctx context.Context) ApiGetTestsRequest { - return ApiGetTestsRequest{ +func (a *ApiApiService) GetTestVersion(ctx context.Context, testId string, version int32) ApiGetTestVersionRequest { + return ApiGetTestVersionRequest{ ApiService: a, ctx: ctx, + testId: testId, + version: version, } } // Execute executes the request // -// @return []Test -func (a *ApiApiService) GetTestsExecute(r ApiGetTestsRequest) ([]Test, *http.Response, error) { +// @return Test +func (a *ApiApiService) GetTestVersionExecute(r ApiGetTestVersionRequest) (*Test, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} formFiles []formFile - localVarReturnValue []Test + localVarReturnValue *Test ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ApiApiService.GetTests") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ApiApiService.GetTestVersion") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } - localVarPath := localBasePath + "/tests" + localVarPath := localBasePath + "/tests/{testId}/version/{version}" + localVarPath = strings.Replace(localVarPath, "{"+"testId"+"}", url.PathEscape(parameterValueToString(r.testId, "testId")), -1) + localVarPath = strings.Replace(localVarPath, "{"+"version"+"}", url.PathEscape(parameterValueToString(r.version, "version")), -1) localVarHeaderParams := make(map[string]string) localVarQueryParams := url.Values{} localVarFormParams := url.Values{} - if r.take != nil { - parameterAddToQuery(localVarQueryParams, "take", r.take, "") - } - if r.skip != nil { - parameterAddToQuery(localVarQueryParams, "skip", r.skip, "") - } - if r.query != nil { - parameterAddToQuery(localVarQueryParams, "query", r.query, "") - } - if r.sortBy != nil { - parameterAddToQuery(localVarQueryParams, "sortBy", r.sortBy, "") - } - if r.sortDirection != nil { - parameterAddToQuery(localVarQueryParams, "sortDirection", r.sortDirection, "") - } // to determine the Content-Type header localVarHTTPContentTypes := []string{} @@ -3345,211 +2783,3 @@ func (a *ApiApiService) TestConnectionExecute(r ApiTestConnectionRequest) (*Test return localVarReturnValue, localVarHTTPResponse, nil } - -type ApiUpdateTestRequest struct { - ctx context.Context - ApiService *ApiApiService - testId string - test *Test -} - -func (r ApiUpdateTestRequest) Test(test Test) ApiUpdateTestRequest { - r.test = &test - return r -} - -func (r ApiUpdateTestRequest) Execute() (*http.Response, error) { - return r.ApiService.UpdateTestExecute(r) -} - -/* -UpdateTest update test - -update test action - - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param testId id of the test - @return ApiUpdateTestRequest -*/ -func (a *ApiApiService) UpdateTest(ctx context.Context, testId string) ApiUpdateTestRequest { - return ApiUpdateTestRequest{ - ApiService: a, - ctx: ctx, - testId: testId, - } -} - -// Execute executes the request -func (a *ApiApiService) UpdateTestExecute(r ApiUpdateTestRequest) (*http.Response, error) { - var ( - localVarHTTPMethod = http.MethodPut - localVarPostBody interface{} - formFiles []formFile - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ApiApiService.UpdateTest") - if err != nil { - return nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/tests/{testId}" - localVarPath = strings.Replace(localVarPath, "{"+"testId"+"}", url.PathEscape(parameterValueToString(r.testId, "testId")), -1) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"application/json"} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - // body params - localVarPostBody = r.test - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) - if err != nil { - return nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarHTTPResponse, err - } - - localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - return localVarHTTPResponse, newErr - } - - return localVarHTTPResponse, nil -} - -type ApiUpsertDefinitionRequest struct { - ctx context.Context - ApiService *ApiApiService - textDefinition *TextDefinition -} - -func (r ApiUpsertDefinitionRequest) TextDefinition(textDefinition TextDefinition) ApiUpsertDefinitionRequest { - r.textDefinition = &textDefinition - return r -} - -func (r ApiUpsertDefinitionRequest) Execute() (*UpsertDefinitionResponse, *http.Response, error) { - return r.ApiService.UpsertDefinitionExecute(r) -} - -/* -UpsertDefinition Upsert a definition - -Upsert a definition - - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @return ApiUpsertDefinitionRequest -*/ -func (a *ApiApiService) UpsertDefinition(ctx context.Context) ApiUpsertDefinitionRequest { - return ApiUpsertDefinitionRequest{ - ApiService: a, - ctx: ctx, - } -} - -// Execute executes the request -// -// @return UpsertDefinitionResponse -func (a *ApiApiService) UpsertDefinitionExecute(r ApiUpsertDefinitionRequest) (*UpsertDefinitionResponse, *http.Response, error) { - var ( - localVarHTTPMethod = http.MethodPut - localVarPostBody interface{} - formFiles []formFile - localVarReturnValue *UpsertDefinitionResponse - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ApiApiService.UpsertDefinition") - if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/definition.yaml" - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"text/json"} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - // body params - localVarPostBody = r.textDefinition - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) - if err != nil { - return localVarReturnValue, nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - return localVarReturnValue, localVarHTTPResponse, nil -} diff --git a/cli/openapi/api_resource_api.go b/cli/openapi/api_resource_api.go index 0e0fee22eb..41c0fce596 100644 --- a/cli/openapi/api_resource_api.go +++ b/cli/openapi/api_resource_api.go @@ -346,6 +346,114 @@ func (a *ResourceApiApiService) CreateLinterExecute(r ApiCreateLinterRequest) (* return localVarReturnValue, localVarHTTPResponse, nil } +type ApiCreateTestRequest struct { + ctx context.Context + ApiService *ResourceApiApiService + test *Test +} + +func (r ApiCreateTestRequest) Test(test Test) ApiCreateTestRequest { + r.test = &test + return r +} + +func (r ApiCreateTestRequest) Execute() (*Test, *http.Response, error) { + return r.ApiService.CreateTestExecute(r) +} + +/* +CreateTest Create new test + +Create new test action + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiCreateTestRequest +*/ +func (a *ResourceApiApiService) CreateTest(ctx context.Context) ApiCreateTestRequest { + return ApiCreateTestRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return Test +func (a *ResourceApiApiService) CreateTestExecute(r ApiCreateTestRequest) (*Test, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *Test + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ResourceApiApiService.CreateTest") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/tests" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.test + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiCreateTransactionRequest struct { ctx context.Context ApiService *ResourceApiApiService @@ -822,6 +930,98 @@ func (a *ResourceApiApiService) DeleteLinterExecute(r ApiDeleteLinterRequest) (* return localVarHTTPResponse, nil } +type ApiDeleteTestRequest struct { + ctx context.Context + ApiService *ResourceApiApiService + testId string +} + +func (r ApiDeleteTestRequest) Execute() (*http.Response, error) { + return r.ApiService.DeleteTestExecute(r) +} + +/* +DeleteTest delete a test + +delete a test + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param testId id of the test + @return ApiDeleteTestRequest +*/ +func (a *ResourceApiApiService) DeleteTest(ctx context.Context, testId string) ApiDeleteTestRequest { + return ApiDeleteTestRequest{ + ApiService: a, + ctx: ctx, + testId: testId, + } +} + +// Execute executes the request +func (a *ResourceApiApiService) DeleteTestExecute(r ApiDeleteTestRequest) (*http.Response, error) { + var ( + localVarHTTPMethod = http.MethodDelete + localVarPostBody interface{} + formFiles []formFile + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ResourceApiApiService.DeleteTest") + if err != nil { + return nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/tests/{testId}" + localVarPath = strings.Replace(localVarPath, "{"+"testId"+"}", url.PathEscape(parameterValueToString(r.testId, "testId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarHTTPResponse, newErr + } + + return localVarHTTPResponse, nil +} + type ApiDeleteTransactionRequest struct { ctx context.Context ApiService *ResourceApiApiService @@ -1538,6 +1738,156 @@ func (a *ResourceApiApiService) GetPollingProfileExecute(r ApiGetPollingProfileR return localVarReturnValue, localVarHTTPResponse, nil } +type ApiGetTestsRequest struct { + ctx context.Context + ApiService *ResourceApiApiService + take *int32 + skip *int32 + query *string + sortBy *string + sortDirection *string +} + +// indicates how many resources can be returned by each page +func (r ApiGetTestsRequest) Take(take int32) ApiGetTestsRequest { + r.take = &take + return r +} + +// indicates how many resources will be skipped when paginating +func (r ApiGetTestsRequest) Skip(skip int32) ApiGetTestsRequest { + r.skip = &skip + return r +} + +// query to search resources +func (r ApiGetTestsRequest) Query(query string) ApiGetTestsRequest { + r.query = &query + return r +} + +// indicates the sort field for the resources +func (r ApiGetTestsRequest) SortBy(sortBy string) ApiGetTestsRequest { + r.sortBy = &sortBy + return r +} + +// indicates the sort direction for the resources +func (r ApiGetTestsRequest) SortDirection(sortDirection string) ApiGetTestsRequest { + r.sortDirection = &sortDirection + return r +} + +func (r ApiGetTestsRequest) Execute() (*TestResourceList, *http.Response, error) { + return r.ApiService.GetTestsExecute(r) +} + +/* +GetTests Get tests + +get tests + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiGetTestsRequest +*/ +func (a *ResourceApiApiService) GetTests(ctx context.Context) ApiGetTestsRequest { + return ApiGetTestsRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return TestResourceList +func (a *ResourceApiApiService) GetTestsExecute(r ApiGetTestsRequest) (*TestResourceList, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *TestResourceList + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ResourceApiApiService.GetTests") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/tests" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + if r.take != nil { + parameterAddToQuery(localVarQueryParams, "take", r.take, "") + } + if r.skip != nil { + parameterAddToQuery(localVarQueryParams, "skip", r.skip, "") + } + if r.query != nil { + parameterAddToQuery(localVarQueryParams, "query", r.query, "") + } + if r.sortBy != nil { + parameterAddToQuery(localVarQueryParams, "sortBy", r.sortBy, "") + } + if r.sortDirection != nil { + parameterAddToQuery(localVarQueryParams, "sortDirection", r.sortDirection, "") + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json", "text/yaml"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiGetTransactionRequest struct { ctx context.Context ApiService *ResourceApiApiService @@ -2632,6 +2982,110 @@ func (a *ResourceApiApiService) ListPollingProfileExecute(r ApiListPollingProfil return localVarReturnValue, localVarHTTPResponse, nil } +type ApiTestsTestIdGetRequest struct { + ctx context.Context + ApiService *ResourceApiApiService + testId string +} + +func (r ApiTestsTestIdGetRequest) Execute() (*TestResource, *http.Response, error) { + return r.ApiService.TestsTestIdGetExecute(r) +} + +/* +TestsTestIdGet get test + +get test + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param testId id of the test + @return ApiTestsTestIdGetRequest +*/ +func (a *ResourceApiApiService) TestsTestIdGet(ctx context.Context, testId string) ApiTestsTestIdGetRequest { + return ApiTestsTestIdGetRequest{ + ApiService: a, + ctx: ctx, + testId: testId, + } +} + +// Execute executes the request +// +// @return TestResource +func (a *ResourceApiApiService) TestsTestIdGetExecute(r ApiTestsTestIdGetRequest) (*TestResource, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *TestResource + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ResourceApiApiService.TestsTestIdGet") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/tests/{testId}" + localVarPath = strings.Replace(localVarPath, "{"+"testId"+"}", url.PathEscape(parameterValueToString(r.testId, "testId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiUpdateConfigurationRequest struct { ctx context.Context ApiService *ResourceApiApiService @@ -3292,6 +3746,106 @@ func (a *ResourceApiApiService) UpdatePollingProfileExecute(r ApiUpdatePollingPr return localVarReturnValue, localVarHTTPResponse, nil } +type ApiUpdateTestRequest struct { + ctx context.Context + ApiService *ResourceApiApiService + testId string + test *Test +} + +func (r ApiUpdateTestRequest) Test(test Test) ApiUpdateTestRequest { + r.test = &test + return r +} + +func (r ApiUpdateTestRequest) Execute() (*http.Response, error) { + return r.ApiService.UpdateTestExecute(r) +} + +/* +UpdateTest update test + +update test action + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param testId id of the test + @return ApiUpdateTestRequest +*/ +func (a *ResourceApiApiService) UpdateTest(ctx context.Context, testId string) ApiUpdateTestRequest { + return ApiUpdateTestRequest{ + ApiService: a, + ctx: ctx, + testId: testId, + } +} + +// Execute executes the request +func (a *ResourceApiApiService) UpdateTestExecute(r ApiUpdateTestRequest) (*http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPut + localVarPostBody interface{} + formFiles []formFile + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ResourceApiApiService.UpdateTest") + if err != nil { + return nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/tests/{testId}" + localVarPath = strings.Replace(localVarPath, "{"+"testId"+"}", url.PathEscape(parameterValueToString(r.testId, "testId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.test + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarHTTPResponse, newErr + } + + return localVarHTTPResponse, nil +} + type ApiUpdateTransactionRequest struct { ctx context.Context ApiService *ResourceApiApiService diff --git a/cli/openapi/model_test_.go b/cli/openapi/model_test_.go index e6122349b5..794182c84e 100644 --- a/cli/openapi/model_test_.go +++ b/cli/openapi/model_test_.go @@ -24,9 +24,9 @@ type Test struct { Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` // version number of the test - Version *int32 `json:"version,omitempty"` - CreatedAt *time.Time `json:"createdAt,omitempty"` - ServiceUnderTest *Trigger `json:"serviceUnderTest,omitempty"` + Version *int32 `json:"version,omitempty"` + CreatedAt *time.Time `json:"createdAt,omitempty"` + Trigger *Trigger `json:"trigger,omitempty"` // specification of assertions that are going to be made Specs []TestSpec `json:"specs,omitempty"` // define test outputs, in a key/value format. The value is processed as an expression @@ -211,36 +211,36 @@ func (o *Test) SetCreatedAt(v time.Time) { o.CreatedAt = &v } -// GetServiceUnderTest returns the ServiceUnderTest field value if set, zero value otherwise. -func (o *Test) GetServiceUnderTest() Trigger { - if o == nil || isNil(o.ServiceUnderTest) { +// GetTrigger returns the Trigger field value if set, zero value otherwise. +func (o *Test) GetTrigger() Trigger { + if o == nil || isNil(o.Trigger) { var ret Trigger return ret } - return *o.ServiceUnderTest + return *o.Trigger } -// GetServiceUnderTestOk returns a tuple with the ServiceUnderTest field value if set, nil otherwise +// GetTriggerOk returns a tuple with the Trigger field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *Test) GetServiceUnderTestOk() (*Trigger, bool) { - if o == nil || isNil(o.ServiceUnderTest) { +func (o *Test) GetTriggerOk() (*Trigger, bool) { + if o == nil || isNil(o.Trigger) { return nil, false } - return o.ServiceUnderTest, true + return o.Trigger, true } -// HasServiceUnderTest returns a boolean if a field has been set. -func (o *Test) HasServiceUnderTest() bool { - if o != nil && !isNil(o.ServiceUnderTest) { +// HasTrigger returns a boolean if a field has been set. +func (o *Test) HasTrigger() bool { + if o != nil && !isNil(o.Trigger) { return true } return false } -// SetServiceUnderTest gets a reference to the given Trigger and assigns it to the ServiceUnderTest field. -func (o *Test) SetServiceUnderTest(v Trigger) { - o.ServiceUnderTest = &v +// SetTrigger gets a reference to the given Trigger and assigns it to the Trigger field. +func (o *Test) SetTrigger(v Trigger) { + o.Trigger = &v } // GetSpecs returns the Specs field value if set, zero value otherwise. @@ -362,8 +362,8 @@ func (o Test) ToMap() (map[string]interface{}, error) { if !isNil(o.CreatedAt) { toSerialize["createdAt"] = o.CreatedAt } - if !isNil(o.ServiceUnderTest) { - toSerialize["serviceUnderTest"] = o.ServiceUnderTest + if !isNil(o.Trigger) { + toSerialize["trigger"] = o.Trigger } if !isNil(o.Specs) { toSerialize["specs"] = o.Specs diff --git a/cli/openapi/model_test_resource.go b/cli/openapi/model_test_resource.go new file mode 100644 index 0000000000..e6ffd84001 --- /dev/null +++ b/cli/openapi/model_test_resource.go @@ -0,0 +1,161 @@ +/* +TraceTest + +OpenAPI definition for TraceTest endpoint and resources + +API version: 0.2.1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the TestResource type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &TestResource{} + +// TestResource Represents a test structured into the Resources format. +type TestResource struct { + // Represents the type of this resource. It should always be set as 'Test'. + Type *string `json:"type,omitempty"` + Spec *Test `json:"spec,omitempty"` +} + +// NewTestResource instantiates a new TestResource object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewTestResource() *TestResource { + this := TestResource{} + return &this +} + +// NewTestResourceWithDefaults instantiates a new TestResource object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewTestResourceWithDefaults() *TestResource { + this := TestResource{} + return &this +} + +// GetType returns the Type field value if set, zero value otherwise. +func (o *TestResource) GetType() string { + if o == nil || isNil(o.Type) { + var ret string + return ret + } + return *o.Type +} + +// GetTypeOk returns a tuple with the Type field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *TestResource) GetTypeOk() (*string, bool) { + if o == nil || isNil(o.Type) { + return nil, false + } + return o.Type, true +} + +// HasType returns a boolean if a field has been set. +func (o *TestResource) HasType() bool { + if o != nil && !isNil(o.Type) { + return true + } + + return false +} + +// SetType gets a reference to the given string and assigns it to the Type field. +func (o *TestResource) SetType(v string) { + o.Type = &v +} + +// GetSpec returns the Spec field value if set, zero value otherwise. +func (o *TestResource) GetSpec() Test { + if o == nil || isNil(o.Spec) { + var ret Test + return ret + } + return *o.Spec +} + +// GetSpecOk returns a tuple with the Spec field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *TestResource) GetSpecOk() (*Test, bool) { + if o == nil || isNil(o.Spec) { + return nil, false + } + return o.Spec, true +} + +// HasSpec returns a boolean if a field has been set. +func (o *TestResource) HasSpec() bool { + if o != nil && !isNil(o.Spec) { + return true + } + + return false +} + +// SetSpec gets a reference to the given Test and assigns it to the Spec field. +func (o *TestResource) SetSpec(v Test) { + o.Spec = &v +} + +func (o TestResource) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o TestResource) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !isNil(o.Type) { + toSerialize["type"] = o.Type + } + if !isNil(o.Spec) { + toSerialize["spec"] = o.Spec + } + return toSerialize, nil +} + +type NullableTestResource struct { + value *TestResource + isSet bool +} + +func (v NullableTestResource) Get() *TestResource { + return v.value +} + +func (v *NullableTestResource) Set(val *TestResource) { + v.value = val + v.isSet = true +} + +func (v NullableTestResource) IsSet() bool { + return v.isSet +} + +func (v *NullableTestResource) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableTestResource(val *TestResource) *NullableTestResource { + return &NullableTestResource{value: val, isSet: true} +} + +func (v NullableTestResource) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableTestResource) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/cli/openapi/model_test_resource_list.go b/cli/openapi/model_test_resource_list.go new file mode 100644 index 0000000000..3c59bc38fc --- /dev/null +++ b/cli/openapi/model_test_resource_list.go @@ -0,0 +1,160 @@ +/* +TraceTest + +OpenAPI definition for TraceTest endpoint and resources + +API version: 0.2.1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the TestResourceList type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &TestResourceList{} + +// TestResourceList struct for TestResourceList +type TestResourceList struct { + Count *int32 `json:"count,omitempty"` + Items []TestResource `json:"items,omitempty"` +} + +// NewTestResourceList instantiates a new TestResourceList object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewTestResourceList() *TestResourceList { + this := TestResourceList{} + return &this +} + +// NewTestResourceListWithDefaults instantiates a new TestResourceList object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewTestResourceListWithDefaults() *TestResourceList { + this := TestResourceList{} + return &this +} + +// GetCount returns the Count field value if set, zero value otherwise. +func (o *TestResourceList) GetCount() int32 { + if o == nil || isNil(o.Count) { + var ret int32 + return ret + } + return *o.Count +} + +// GetCountOk returns a tuple with the Count field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *TestResourceList) GetCountOk() (*int32, bool) { + if o == nil || isNil(o.Count) { + return nil, false + } + return o.Count, true +} + +// HasCount returns a boolean if a field has been set. +func (o *TestResourceList) HasCount() bool { + if o != nil && !isNil(o.Count) { + return true + } + + return false +} + +// SetCount gets a reference to the given int32 and assigns it to the Count field. +func (o *TestResourceList) SetCount(v int32) { + o.Count = &v +} + +// GetItems returns the Items field value if set, zero value otherwise. +func (o *TestResourceList) GetItems() []TestResource { + if o == nil || isNil(o.Items) { + var ret []TestResource + return ret + } + return o.Items +} + +// GetItemsOk returns a tuple with the Items field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *TestResourceList) GetItemsOk() ([]TestResource, bool) { + if o == nil || isNil(o.Items) { + return nil, false + } + return o.Items, true +} + +// HasItems returns a boolean if a field has been set. +func (o *TestResourceList) HasItems() bool { + if o != nil && !isNil(o.Items) { + return true + } + + return false +} + +// SetItems gets a reference to the given []TestResource and assigns it to the Items field. +func (o *TestResourceList) SetItems(v []TestResource) { + o.Items = v +} + +func (o TestResourceList) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o TestResourceList) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !isNil(o.Count) { + toSerialize["count"] = o.Count + } + if !isNil(o.Items) { + toSerialize["items"] = o.Items + } + return toSerialize, nil +} + +type NullableTestResourceList struct { + value *TestResourceList + isSet bool +} + +func (v NullableTestResourceList) Get() *TestResourceList { + return v.value +} + +func (v *NullableTestResourceList) Set(val *TestResourceList) { + v.value = val + v.isSet = true +} + +func (v NullableTestResourceList) IsSet() bool { + return v.isSet +} + +func (v *NullableTestResourceList) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableTestResourceList(val *TestResourceList) *NullableTestResourceList { + return &NullableTestResourceList{value: val, isSet: true} +} + +func (v NullableTestResourceList) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableTestResourceList) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/cli/openapi/model_trigger.go b/cli/openapi/model_trigger.go index 4bf95ac50d..3e75321241 100644 --- a/cli/openapi/model_trigger.go +++ b/cli/openapi/model_trigger.go @@ -19,8 +19,8 @@ var _ MappedNullable = &Trigger{} // Trigger struct for Trigger type Trigger struct { - TriggerType *string `json:"triggerType,omitempty"` - Http *HTTPRequest `json:"http,omitempty"` + Type *string `json:"type,omitempty"` + HttpRequest *HTTPRequest `json:"httpRequest,omitempty"` Grpc *GRPCRequest `json:"grpc,omitempty"` Traceid *TRACEIDRequest `json:"traceid,omitempty"` } @@ -42,68 +42,68 @@ func NewTriggerWithDefaults() *Trigger { return &this } -// GetTriggerType returns the TriggerType field value if set, zero value otherwise. -func (o *Trigger) GetTriggerType() string { - if o == nil || isNil(o.TriggerType) { +// GetType returns the Type field value if set, zero value otherwise. +func (o *Trigger) GetType() string { + if o == nil || isNil(o.Type) { var ret string return ret } - return *o.TriggerType + return *o.Type } -// GetTriggerTypeOk returns a tuple with the TriggerType field value if set, nil otherwise +// GetTypeOk returns a tuple with the Type field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *Trigger) GetTriggerTypeOk() (*string, bool) { - if o == nil || isNil(o.TriggerType) { +func (o *Trigger) GetTypeOk() (*string, bool) { + if o == nil || isNil(o.Type) { return nil, false } - return o.TriggerType, true + return o.Type, true } -// HasTriggerType returns a boolean if a field has been set. -func (o *Trigger) HasTriggerType() bool { - if o != nil && !isNil(o.TriggerType) { +// HasType returns a boolean if a field has been set. +func (o *Trigger) HasType() bool { + if o != nil && !isNil(o.Type) { return true } return false } -// SetTriggerType gets a reference to the given string and assigns it to the TriggerType field. -func (o *Trigger) SetTriggerType(v string) { - o.TriggerType = &v +// SetType gets a reference to the given string and assigns it to the Type field. +func (o *Trigger) SetType(v string) { + o.Type = &v } -// GetHttp returns the Http field value if set, zero value otherwise. -func (o *Trigger) GetHttp() HTTPRequest { - if o == nil || isNil(o.Http) { +// GetHttpRequest returns the HttpRequest field value if set, zero value otherwise. +func (o *Trigger) GetHttpRequest() HTTPRequest { + if o == nil || isNil(o.HttpRequest) { var ret HTTPRequest return ret } - return *o.Http + return *o.HttpRequest } -// GetHttpOk returns a tuple with the Http field value if set, nil otherwise +// GetHttpRequestOk returns a tuple with the HttpRequest field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *Trigger) GetHttpOk() (*HTTPRequest, bool) { - if o == nil || isNil(o.Http) { +func (o *Trigger) GetHttpRequestOk() (*HTTPRequest, bool) { + if o == nil || isNil(o.HttpRequest) { return nil, false } - return o.Http, true + return o.HttpRequest, true } -// HasHttp returns a boolean if a field has been set. -func (o *Trigger) HasHttp() bool { - if o != nil && !isNil(o.Http) { +// HasHttpRequest returns a boolean if a field has been set. +func (o *Trigger) HasHttpRequest() bool { + if o != nil && !isNil(o.HttpRequest) { return true } return false } -// SetHttp gets a reference to the given HTTPRequest and assigns it to the Http field. -func (o *Trigger) SetHttp(v HTTPRequest) { - o.Http = &v +// SetHttpRequest gets a reference to the given HTTPRequest and assigns it to the HttpRequest field. +func (o *Trigger) SetHttpRequest(v HTTPRequest) { + o.HttpRequest = &v } // GetGrpc returns the Grpc field value if set, zero value otherwise. @@ -180,11 +180,11 @@ func (o Trigger) MarshalJSON() ([]byte, error) { func (o Trigger) ToMap() (map[string]interface{}, error) { toSerialize := map[string]interface{}{} - if !isNil(o.TriggerType) { - toSerialize["triggerType"] = o.TriggerType + if !isNil(o.Type) { + toSerialize["type"] = o.Type } - if !isNil(o.Http) { - toSerialize["http"] = o.Http + if !isNil(o.HttpRequest) { + toSerialize["httpRequest"] = o.HttpRequest } if !isNil(o.Grpc) { toSerialize["grpc"] = o.Grpc diff --git a/cli/openapi/model_trigger_result.go b/cli/openapi/model_trigger_result.go index b4210a3de2..740fb67e3b 100644 --- a/cli/openapi/model_trigger_result.go +++ b/cli/openapi/model_trigger_result.go @@ -19,7 +19,7 @@ var _ MappedNullable = &TriggerResult{} // TriggerResult struct for TriggerResult type TriggerResult struct { - TriggerType *string `json:"triggerType,omitempty"` + Type *string `json:"type,omitempty"` TriggerResult *TriggerResultTriggerResult `json:"triggerResult,omitempty"` } @@ -40,36 +40,36 @@ func NewTriggerResultWithDefaults() *TriggerResult { return &this } -// GetTriggerType returns the TriggerType field value if set, zero value otherwise. -func (o *TriggerResult) GetTriggerType() string { - if o == nil || isNil(o.TriggerType) { +// GetType returns the Type field value if set, zero value otherwise. +func (o *TriggerResult) GetType() string { + if o == nil || isNil(o.Type) { var ret string return ret } - return *o.TriggerType + return *o.Type } -// GetTriggerTypeOk returns a tuple with the TriggerType field value if set, nil otherwise +// GetTypeOk returns a tuple with the Type field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *TriggerResult) GetTriggerTypeOk() (*string, bool) { - if o == nil || isNil(o.TriggerType) { +func (o *TriggerResult) GetTypeOk() (*string, bool) { + if o == nil || isNil(o.Type) { return nil, false } - return o.TriggerType, true + return o.Type, true } -// HasTriggerType returns a boolean if a field has been set. -func (o *TriggerResult) HasTriggerType() bool { - if o != nil && !isNil(o.TriggerType) { +// HasType returns a boolean if a field has been set. +func (o *TriggerResult) HasType() bool { + if o != nil && !isNil(o.Type) { return true } return false } -// SetTriggerType gets a reference to the given string and assigns it to the TriggerType field. -func (o *TriggerResult) SetTriggerType(v string) { - o.TriggerType = &v +// SetType gets a reference to the given string and assigns it to the Type field. +func (o *TriggerResult) SetType(v string) { + o.Type = &v } // GetTriggerResult returns the TriggerResult field value if set, zero value otherwise. @@ -114,8 +114,8 @@ func (o TriggerResult) MarshalJSON() ([]byte, error) { func (o TriggerResult) ToMap() (map[string]interface{}, error) { toSerialize := map[string]interface{}{} - if !isNil(o.TriggerType) { - toSerialize["triggerType"] = o.TriggerType + if !isNil(o.Type) { + toSerialize["type"] = o.Type } if !isNil(o.TriggerResult) { toSerialize["triggerResult"] = o.TriggerResult diff --git a/cli/openapi/model_upsert_definition_response.go b/cli/openapi/model_upsert_definition_response.go deleted file mode 100644 index 7dc08c3257..0000000000 --- a/cli/openapi/model_upsert_definition_response.go +++ /dev/null @@ -1,162 +0,0 @@ -/* -TraceTest - -OpenAPI definition for TraceTest endpoint and resources - -API version: 0.2.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package openapi - -import ( - "encoding/json" -) - -// checks if the UpsertDefinitionResponse type satisfies the MappedNullable interface at compile time -var _ MappedNullable = &UpsertDefinitionResponse{} - -// UpsertDefinitionResponse struct for UpsertDefinitionResponse -type UpsertDefinitionResponse struct { - // resource ID - Id *string `json:"id,omitempty"` - // resource type - Type *string `json:"type,omitempty"` -} - -// NewUpsertDefinitionResponse instantiates a new UpsertDefinitionResponse object -// This constructor will assign default values to properties that have it defined, -// and makes sure properties required by API are set, but the set of arguments -// will change when the set of required properties is changed -func NewUpsertDefinitionResponse() *UpsertDefinitionResponse { - this := UpsertDefinitionResponse{} - return &this -} - -// NewUpsertDefinitionResponseWithDefaults instantiates a new UpsertDefinitionResponse object -// This constructor will only assign default values to properties that have it defined, -// but it doesn't guarantee that properties required by API are set -func NewUpsertDefinitionResponseWithDefaults() *UpsertDefinitionResponse { - this := UpsertDefinitionResponse{} - return &this -} - -// GetId returns the Id field value if set, zero value otherwise. -func (o *UpsertDefinitionResponse) GetId() string { - if o == nil || isNil(o.Id) { - var ret string - return ret - } - return *o.Id -} - -// GetIdOk returns a tuple with the Id field value if set, nil otherwise -// and a boolean to check if the value has been set. -func (o *UpsertDefinitionResponse) GetIdOk() (*string, bool) { - if o == nil || isNil(o.Id) { - return nil, false - } - return o.Id, true -} - -// HasId returns a boolean if a field has been set. -func (o *UpsertDefinitionResponse) HasId() bool { - if o != nil && !isNil(o.Id) { - return true - } - - return false -} - -// SetId gets a reference to the given string and assigns it to the Id field. -func (o *UpsertDefinitionResponse) SetId(v string) { - o.Id = &v -} - -// GetType returns the Type field value if set, zero value otherwise. -func (o *UpsertDefinitionResponse) GetType() string { - if o == nil || isNil(o.Type) { - var ret string - return ret - } - return *o.Type -} - -// GetTypeOk returns a tuple with the Type field value if set, nil otherwise -// and a boolean to check if the value has been set. -func (o *UpsertDefinitionResponse) GetTypeOk() (*string, bool) { - if o == nil || isNil(o.Type) { - return nil, false - } - return o.Type, true -} - -// HasType returns a boolean if a field has been set. -func (o *UpsertDefinitionResponse) HasType() bool { - if o != nil && !isNil(o.Type) { - return true - } - - return false -} - -// SetType gets a reference to the given string and assigns it to the Type field. -func (o *UpsertDefinitionResponse) SetType(v string) { - o.Type = &v -} - -func (o UpsertDefinitionResponse) MarshalJSON() ([]byte, error) { - toSerialize, err := o.ToMap() - if err != nil { - return []byte{}, err - } - return json.Marshal(toSerialize) -} - -func (o UpsertDefinitionResponse) ToMap() (map[string]interface{}, error) { - toSerialize := map[string]interface{}{} - if !isNil(o.Id) { - toSerialize["id"] = o.Id - } - if !isNil(o.Type) { - toSerialize["type"] = o.Type - } - return toSerialize, nil -} - -type NullableUpsertDefinitionResponse struct { - value *UpsertDefinitionResponse - isSet bool -} - -func (v NullableUpsertDefinitionResponse) Get() *UpsertDefinitionResponse { - return v.value -} - -func (v *NullableUpsertDefinitionResponse) Set(val *UpsertDefinitionResponse) { - v.value = val - v.isSet = true -} - -func (v NullableUpsertDefinitionResponse) IsSet() bool { - return v.isSet -} - -func (v *NullableUpsertDefinitionResponse) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableUpsertDefinitionResponse(val *UpsertDefinitionResponse) *NullableUpsertDefinitionResponse { - return &NullableUpsertDefinitionResponse{value: val, isSet: true} -} - -func (v NullableUpsertDefinitionResponse) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableUpsertDefinitionResponse) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} diff --git a/cli/pkg/fileutil/file.go b/cli/pkg/fileutil/file.go index 2dbd625000..8c28a1a715 100644 --- a/cli/pkg/fileutil/file.go +++ b/cli/pkg/fileutil/file.go @@ -11,22 +11,22 @@ import ( "strings" ) -type file struct { +type File struct { path string contents []byte } -func Read(filePath string) (file, error) { +func Read(filePath string) (File, error) { b, err := os.ReadFile(filePath) if err != nil { - return file{}, fmt.Errorf("could not read definition file %s: %w", filePath, err) + return File{}, fmt.Errorf("could not read definition file %s: %w", filePath, err) } return New(filePath, b), nil } -func New(path string, b []byte) file { - file := file{ +func New(path string, b []byte) File { + file := File{ contents: b, path: path, } @@ -34,7 +34,7 @@ func New(path string, b []byte) file { return file } -func (f file) Reader() io.Reader { +func (f File) Reader() io.Reader { return bytes.NewReader(f.contents) } @@ -45,12 +45,12 @@ var ( var ErrFileHasID = errors.New("file already has ID") -func (f file) HasID() bool { +func (f File) HasID() bool { fileID := hasIDRegex.Find(f.contents) return fileID != nil } -func (f file) SetID(id string) (file, error) { +func (f File) SetID(id string) (File, error) { if f.HasID() { return f, ErrFileHasID } @@ -76,16 +76,28 @@ func (f file) SetID(id string) (file, error) { return New(f.path, newContents), nil } -func (f file) AbsDir() string { +func (f File) AbsPath() string { abs, err := filepath.Abs(f.path) if err != nil { panic(fmt.Errorf(`cannot get absolute path from "%s": %w`, f.path, err)) } - return filepath.Dir(abs) + return abs } -func (f file) Write() (file, error) { +func (f File) AbsDir() string { + return filepath.Dir(f.AbsPath()) +} + +func (f File) RelativeFile(path string) string { + if filepath.IsAbs(path) { + return path + } + + return filepath.Join(f.AbsDir(), path) +} + +func (f File) Write() (File, error) { err := os.WriteFile(f.path, f.contents, 0644) if err != nil { return f, fmt.Errorf("could not write file %s: %w", f.path, err) @@ -94,6 +106,6 @@ func (f file) Write() (file, error) { return Read(f.path) } -func (f file) ReadAll() (string, error) { - return string(f.contents), nil +func (f File) Contents() []byte { + return f.contents } diff --git a/cli/pkg/fileutil/is_file_path.go b/cli/pkg/fileutil/path.go similarity index 52% rename from cli/pkg/fileutil/is_file_path.go rename to cli/pkg/fileutil/path.go index 60edee45ac..2af87cbf7f 100644 --- a/cli/pkg/fileutil/is_file_path.go +++ b/cli/pkg/fileutil/path.go @@ -3,8 +3,33 @@ package fileutil import ( "os" "path/filepath" + "strings" ) +func ToAbsDir(filePath string) (string, error) { + absPath, err := filepath.Abs(filePath) + if err != nil { + return "", err + } + + return filepath.Dir(absPath), nil +} + +func RelativeTo(path, relativeTo string) string { + if filepath.IsAbs(path) { + return path + } + + return filepath.Join(relativeTo, path) +} + +func LooksLikeFilePath(path string) bool { + return strings.HasPrefix(path, "./") || + strings.HasPrefix(path, "../") || + strings.HasPrefix(path, "/") + +} + func IsFilePath(path string) bool { // for the current working dir, check if the file exists // by finding its absolute path and executing a stat command diff --git a/cli/pkg/resourcemanager/apply.go b/cli/pkg/resourcemanager/apply.go index 2e5f37c03f..e81147580e 100644 --- a/cli/pkg/resourcemanager/apply.go +++ b/cli/pkg/resourcemanager/apply.go @@ -12,12 +12,7 @@ import ( const VerbApply Verb = "apply" -func (c Client) Apply(ctx context.Context, filePath string, requestedFormat Format) (string, error) { - inputFile, err := fileutil.Read(filePath) - if err != nil { - return "", fmt.Errorf("cannot read file %s: %w", filePath, err) - } - +func (c Client) Apply(ctx context.Context, inputFile fileutil.File, requestedFormat Format) (string, error) { url := c.client.url(c.resourceNamePlural) req, err := http.NewRequestWithContext(ctx, http.MethodPut, url.String(), inputFile.Reader()) if err != nil { diff --git a/cli/variable/injector.go b/cli/variable/injector.go index 05f045a4e8..7ab6b7acbd 100644 --- a/cli/variable/injector.go +++ b/cli/variable/injector.go @@ -110,7 +110,7 @@ func (i Injector) injectValueIntoField(field reflect.Value) error { // We only support variables replacements in strings right now strValue := field.String() - newValue, err := i.replaceVariable(strValue) + newValue, err := i.ReplaceInString(strValue) if err != nil { return err } @@ -119,7 +119,7 @@ func (i Injector) injectValueIntoField(field reflect.Value) error { return nil } -func (i Injector) replaceVariable(value string) (string, error) { +func (i Injector) ReplaceInString(value string) (string, error) { envVarRegex, err := regexp.Compile(`\$\{\w+\}`) if err != nil { return "", fmt.Errorf("could not compile env variable regex: %w", err) diff --git a/docker-compose.yaml b/docker-compose.yaml index 4a13cfded7..4afe747c47 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,6 +5,8 @@ services: image: kubeshop/tracetest:${TAG:-latest} extra_hosts: - "host.docker.internal:host-gateway" + build: + context: . volumes: - type: bind source: ./local-config/tracetest.config.yaml diff --git a/docs/docs/cli/configuring-your-cli.md b/docs/docs/cli/configuring-your-cli.md index c91c04ad7c..07b06cbe83 100644 --- a/docs/docs/cli/configuring-your-cli.md +++ b/docs/docs/cli/configuring-your-cli.md @@ -29,7 +29,7 @@ Allows you to list all tests. **How to Use**: ```sh -tracetest test list +tracetest list test ``` ### Run a Test diff --git a/go.work.sum b/go.work.sum index 4c5725dc68..ea521ccd1f 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,4 +1,3 @@ -<<<<<<< Updated upstream atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512 h1:SRsZGA7aFnCZETmov57jwPrWuTmaZK6+4R4v5FUe1/c= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= @@ -186,6 +185,7 @@ github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY github.com/containernetworking/cni v1.1.1/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw= github.com/containernetworking/plugins v1.1.1/go.mod h1:Sr5TH/eBsGLXK/h71HeLfX19sZPp3ry5uHSkI4LPxV8= github.com/containers/ocicrypt v1.1.3/go.mod h1:xpdkbVAuaH3WzbEabUd5yDsl9SwJA5pABH85425Es2g= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/deepmap/oapi-codegen v1.10.1/go.mod h1:TvVmDQlUkFli9gFij/gtW1o+tFBr4qCHyv2zG+R0YZY= github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= @@ -205,6 +205,7 @@ github.com/gocql/gocql v0.0.0-20210817081954-bc256bbb90de/go.mod h1:3gM2c4D3AnkI github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= @@ -232,10 +233,12 @@ github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= github.com/influxdata/influxdb-client-go/v2 v2.8.2/go.mod h1:x7Jo5UHHl+w8wu8UnGiNobDDHygojXwJX4mx7rXGKMk= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5 h1:2U0HzY8BJ8hVwDKIzp7y4voR9CX/nvcfymLmg2UiOio= github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/knadh/koanf v1.4.3/go.mod h1:5FAkuykKXZvLqhAbP4peWgM5CTcZmn7L1d27k/a+kfg= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= diff --git a/server/app/app.go b/server/app/app.go index b14e4ec29d..a680650914 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -2,7 +2,6 @@ package app import ( "context" - "database/sql" "errors" "fmt" "log" @@ -31,6 +30,7 @@ import ( "github.com/kubeshop/tracetest/server/provisioning" "github.com/kubeshop/tracetest/server/resourcemanager" "github.com/kubeshop/tracetest/server/subscription" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/testdb" "github.com/kubeshop/tracetest/server/tracedb" "github.com/kubeshop/tracetest/server/traces" @@ -150,9 +150,6 @@ func (app *App) Start(opts ...appOption) error { log.Fatal(err) } - transactionsRepository := transaction.NewRepository(db, testDB) - transactionRunRepository := transaction.NewRunRepository(db, testDB) - subscriptionManager := subscription.NewManager() app.subscribeToConfigChanges(subscriptionManager) @@ -194,19 +191,27 @@ func (app *App) Start(opts ...appOption) error { triggerRegistry := getTriggerRegistry(tracer, applicationTracer) + demoRepo := demo.NewRepository(db) pollingProfileRepo := pollingprofile.NewRepository(db) dataStoreRepo := datastore.NewRepository(db) environmentRepo := environment.NewRepository(db) linterRepo := analyzer.NewRepository(db) + testRepo := test.NewRepository(db) + runRepo := test.NewRunRepository(db) + + transactionsRepository := transaction.NewRepository(db, testRepo) + transactionRunRepository := transaction.NewRunRepository(db, runRepo) eventEmitter := executor.NewEventEmitter(testDB, subscriptionManager) - registerOtlpServer(app, testDB, eventEmitter, dataStoreRepo) + registerOtlpServer(app, runRepo, eventEmitter, dataStoreRepo) rf := newRunnerFacades( pollingProfileRepo, dataStoreRepo, linterRepo, testDB, + testRepo, + runRepo, transactionRunRepository, applicationTracer, tracer, @@ -246,7 +251,17 @@ func (app *App) Start(opts ...appOption) error { provisioner := provisioning.New() - router, mappers := controller(app.cfg, testDB, transactionsRepository, transactionRunRepository, tracer, environmentRepo, rf, triggerRegistry) + router, mappers := controller(app.cfg, + testDB, + transactionsRepository, + transactionRunRepository, + testRepo, + runRepo, + tracer, + environmentRepo, + rf, + triggerRegistry, + ) registerWSHandler(router, mappers, subscriptionManager) // use the analytics middleware on complete router @@ -258,17 +273,13 @@ func (app *App) Start(opts ...appOption) error { Subrouter() registerTransactionResource(transactionsRepository, apiRouter, provisioner, tracer) - - registerConfigResource(configRepo, apiRouter, db, provisioner, tracer) - - registerPollingProfilesResource(pollingProfileRepo, apiRouter, db, provisioner, tracer) - registerEnvironmentResource(environmentRepo, apiRouter, db, provisioner, tracer) - - demoRepo := demo.NewRepository(db) - registerDemosResource(demoRepo, apiRouter, db, provisioner, tracer) - - registerDataStoreResource(dataStoreRepo, apiRouter, db, provisioner, tracer) - registerAnalyzerResource(linterRepo, apiRouter, db, provisioner, tracer) + registerConfigResource(configRepo, apiRouter, provisioner, tracer) + registerPollingProfilesResource(pollingProfileRepo, apiRouter, provisioner, tracer) + registerEnvironmentResource(environmentRepo, apiRouter, provisioner, tracer) + registerDemosResource(demoRepo, apiRouter, provisioner, tracer) + registerDataStoreResource(dataStoreRepo, apiRouter, provisioner, tracer) + registerAnalyzer(linterRepo, apiRouter, provisioner, tracer) + registerTestResource(testRepo, apiRouter, provisioner, tracer) isTracetestDev := os.Getenv("TRACETEST_DEV") != "" registerSPAHandler(router, app.cfg, configFromDB.IsAnalyticsEnabled(), serverID, isTracetestDev) @@ -340,8 +351,8 @@ func registerSPAHandler(router *mux.Router, cfg httpServerConfig, analyticsEnabl ) } -func registerOtlpServer(app *App, testDB model.Repository, eventEmitter executor.EventEmitter, dsRepo *datastore.Repository) { - ingester := otlp.NewIngester(testDB, eventEmitter, dsRepo) +func registerOtlpServer(app *App, runRepository test.RunRepository, eventEmitter executor.EventEmitter, dsRepo *datastore.Repository) { + ingester := otlp.NewIngester(runRepository, eventEmitter, dsRepo) grpcOtlpServer := otlp.NewGrpcServer(":4317", ingester) httpOtlpServer := otlp.NewHttpServer(":4318", ingester) go grpcOtlpServer.Start() @@ -356,7 +367,7 @@ func registerOtlpServer(app *App, testDB model.Repository, eventEmitter executor }) } -func registerAnalyzerResource(linterRepo *analyzer.Repository, router *mux.Router, db *sql.DB, provisioner *provisioning.Provisioner, tracer trace.Tracer) { +func registerAnalyzer(linterRepo *analyzer.Repository, router *mux.Router, provisioner *provisioning.Provisioner, tracer trace.Tracer) { manager := resourcemanager.New[analyzer.Linter]( analyzer.ResourceName, analyzer.ResourceNamePlural, @@ -380,7 +391,7 @@ func registerTransactionResource(repo *transaction.Repository, router *mux.Route provisioner.AddResourceProvisioner(manager) } -func registerConfigResource(configRepo *config.Repository, router *mux.Router, db *sql.DB, provisioner *provisioning.Provisioner, tracer trace.Tracer) { +func registerConfigResource(configRepo *config.Repository, router *mux.Router, provisioner *provisioning.Provisioner, tracer trace.Tracer) { manager := resourcemanager.New[config.Config]( config.ResourceName, config.ResourceNamePlural, @@ -392,7 +403,7 @@ func registerConfigResource(configRepo *config.Repository, router *mux.Router, d provisioner.AddResourceProvisioner(manager) } -func registerPollingProfilesResource(repository *pollingprofile.Repository, router *mux.Router, db *sql.DB, provisioner *provisioning.Provisioner, tracer trace.Tracer) { +func registerPollingProfilesResource(repository *pollingprofile.Repository, router *mux.Router, provisioner *provisioning.Provisioner, tracer trace.Tracer) { manager := resourcemanager.New[pollingprofile.PollingProfile]( pollingprofile.ResourceName, pollingprofile.ResourceNamePlural, @@ -404,7 +415,7 @@ func registerPollingProfilesResource(repository *pollingprofile.Repository, rout provisioner.AddResourceProvisioner(manager) } -func registerEnvironmentResource(repository *environment.Repository, router *mux.Router, db *sql.DB, provisioner *provisioning.Provisioner, tracer trace.Tracer) { +func registerEnvironmentResource(repository *environment.Repository, router *mux.Router, provisioner *provisioning.Provisioner, tracer trace.Tracer) { manager := resourcemanager.New[environment.Environment]( environment.ResourceName, environment.ResourceNamePlural, @@ -415,7 +426,7 @@ func registerEnvironmentResource(repository *environment.Repository, router *mux provisioner.AddResourceProvisioner(manager) } -func registerDemosResource(repository *demo.Repository, router *mux.Router, db *sql.DB, provisioner *provisioning.Provisioner, tracer trace.Tracer) { +func registerDemosResource(repository *demo.Repository, router *mux.Router, provisioner *provisioning.Provisioner, tracer trace.Tracer) { manager := resourcemanager.New[demo.Demo]( demo.ResourceName, demo.ResourceNamePlural, @@ -426,7 +437,7 @@ func registerDemosResource(repository *demo.Repository, router *mux.Router, db * provisioner.AddResourceProvisioner(manager) } -func registerDataStoreResource(repository *datastore.Repository, router *mux.Router, db *sql.DB, provisioner *provisioning.Provisioner, tracer trace.Tracer) { +func registerDataStoreResource(repository *datastore.Repository, router *mux.Router, provisioner *provisioning.Provisioner, tracer trace.Tracer) { manager := resourcemanager.New[datastore.DataStore]( datastore.ResourceName, datastore.ResourceNamePlural, @@ -437,6 +448,18 @@ func registerDataStoreResource(repository *datastore.Repository, router *mux.Rou provisioner.AddResourceProvisioner(manager) } +func registerTestResource(repository test.Repository, router *mux.Router, provisioner *provisioning.Provisioner, tracer trace.Tracer) { + manager := resourcemanager.New[test.Test]( + test.ResourceName, + test.ResourceNamePlural, + repository, + resourcemanager.WithTracer(tracer), + resourcemanager.CanBeAugmented(), + ) + manager.RegisterRoutes(router) + provisioner.AddResourceProvisioner(manager) +} + func getTriggerRegistry(tracer, appTracer trace.Tracer) *trigger.Registry { triggerReg := trigger.NewRegsitry(tracer, appTracer) triggerReg.Add(trigger.HTTP()) @@ -467,14 +490,28 @@ func controller( testDB model.Repository, transactionRepository *transaction.Repository, transactionRunRepository *transaction.RunRepository, + testRepository test.Repository, + runRepository test.RunRepository, tracer trace.Tracer, environmentRepo *environment.Repository, rf *runnerFacade, triggerRegistry *trigger.Registry, ) (*mux.Router, mappings.Mappings) { - mappers := mappings.New(tracesConversionConfig(), comparator.DefaultRegistry(), testDB) + mappers := mappings.New(tracesConversionConfig(), comparator.DefaultRegistry()) - router := openapi.NewRouter(httpRouter(cfg, testDB, transactionRepository, transactionRunRepository, tracer, environmentRepo, rf, mappers, triggerRegistry)) + router := openapi.NewRouter(httpRouter( + cfg, + testDB, + transactionRepository, + transactionRunRepository, + testRepository, + runRepository, + tracer, + environmentRepo, + rf, + mappers, + triggerRegistry, + )) return router, mappers } @@ -484,13 +521,28 @@ func httpRouter( testDB model.Repository, transactionRepo *transaction.Repository, transactionRunRepo *transaction.RunRepository, + testRepo test.Repository, + testRunRepo test.RunRepository, tracer trace.Tracer, environmentRepo *environment.Repository, rf *runnerFacade, mappers mappings.Mappings, triggerRegistry *trigger.Registry, ) openapi.Router { - controller := httpServer.NewController(testDB, transactionRepo, transactionRunRepo, tracedb.Factory(testDB), rf, mappers, environmentRepo, triggerRegistry, tracer, Version) + controller := httpServer.NewController( + testDB, + transactionRepo, + transactionRunRepo, + testRepo, + testRunRepo, + tracedb.Factory(testRunRepo), + rf, + mappers, + environmentRepo, + triggerRegistry, + tracer, + Version, + ) apiApiController := openapi.NewApiApiController(controller) customController := httpServer.NewCustomController(controller, apiApiController, openapi.DefaultErrorHandler, tracer) httpRouter := customController diff --git a/server/app/facade.go b/server/app/facade.go index 89c29ee7be..6131372eb8 100644 --- a/server/app/facade.go +++ b/server/app/facade.go @@ -12,6 +12,7 @@ import ( "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/subscription" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/tracedb" "github.com/kubeshop/tracetest/server/transaction" "go.opentelemetry.io/otel/trace" @@ -38,11 +39,11 @@ func (rf runnerFacade) StopTest(testID id.ID, runID int) { }) } -func (rf runnerFacade) RunTest(ctx context.Context, test model.Test, rm model.RunMetadata, env environment.Environment) model.Run { +func (rf runnerFacade) RunTest(ctx context.Context, test test.Test, rm test.RunMetadata, env environment.Environment) test.Run { return rf.runner.Run(ctx, test, rm, env) } -func (rf runnerFacade) RunTransaction(ctx context.Context, tr transaction.Transaction, rm model.RunMetadata, env environment.Environment) transaction.TransactionRun { +func (rf runnerFacade) RunTransaction(ctx context.Context, tr transaction.Transaction, rm test.RunMetadata, env environment.Environment) transaction.TransactionRun { return rf.transactionRunner.Run(ctx, tr, rm, env) } @@ -54,17 +55,19 @@ func newRunnerFacades( ppRepo *pollingprofile.Repository, dsRepo *datastore.Repository, lintRepo *analyzer.Repository, - testDB model.Repository, + db model.Repository, + testRepo test.Repository, + runRepo test.RunRepository, transactionRunRepository *transaction.RunRepository, appTracer trace.Tracer, tracer trace.Tracer, subscriptionManager *subscription.Manager, triggerRegistry *trigger.Registry, ) *runnerFacade { - eventEmitter := executor.NewEventEmitter(testDB, subscriptionManager) + eventEmitter := executor.NewEventEmitter(db, subscriptionManager) execTestUpdater := (executor.CompositeUpdater{}). - Add(executor.NewDBUpdater(testDB)). + Add(executor.NewDBUpdater(runRepo)). Add(executor.NewSubscriptionUpdater(subscriptionManager)) assertionRunner := executor.NewAssertionRunner( @@ -87,7 +90,7 @@ func newRunnerFacades( ppRepo, tracer, execTestUpdater, - tracedb.Factory(testDB), + tracedb.Factory(runRepo), dsRepo, eventEmitter, ) @@ -105,12 +108,12 @@ func newRunnerFacades( runner := executor.NewPersistentRunner( triggerRegistry, - testDB, + runRepo, execTestUpdater, tracePoller, tracer, subscriptionManager, - tracedb.Factory(testDB), + tracedb.Factory(runRepo), dsRepo, eventEmitter, ppRepo, @@ -118,7 +121,7 @@ func newRunnerFacades( transactionRunner := executor.NewTransactionRunner( runner, - testDB, + testRepo, transactionRunRepository, subscriptionManager, ) diff --git a/server/assertions/selectors/selector.go b/server/assertions/selectors/selector.go index a4ac22036f..f4a0843036 100644 --- a/server/assertions/selectors/selector.go +++ b/server/assertions/selectors/selector.go @@ -5,9 +5,10 @@ import ( "strconv" "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/test" ) -func FromSpanQuery(sq model.SpanQuery) Selector { +func FromSpanQuery(sq test.SpanQuery) Selector { sel, _ := New(string(sq)) return sel } diff --git a/server/executor/assertion_executor.go b/server/executor/assertion_executor.go index 033fbe8ce9..9614e430be 100644 --- a/server/executor/assertion_executor.go +++ b/server/executor/assertion_executor.go @@ -7,44 +7,44 @@ import ( "github.com/kubeshop/tracetest/server/expression" "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/test" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) type AssertionExecutor interface { - Assert(context.Context, maps.Ordered[model.SpanQuery, model.NamedAssertions], model.Trace, []expression.DataStore) (maps.Ordered[model.SpanQuery, []model.AssertionResult], bool) + Assert(context.Context, test.Specs, model.Trace, []expression.DataStore) (maps.Ordered[test.SpanQuery, []test.AssertionResult], bool) } type defaultAssertionExecutor struct{} -func (e defaultAssertionExecutor) Assert(_ context.Context, defs maps.Ordered[model.SpanQuery, model.NamedAssertions], trace model.Trace, ds []expression.DataStore) (maps.Ordered[model.SpanQuery, []model.AssertionResult], bool) { - testResult := maps.Ordered[model.SpanQuery, []model.AssertionResult]{} +func (e defaultAssertionExecutor) Assert(_ context.Context, specs test.Specs, trace model.Trace, ds []expression.DataStore) (maps.Ordered[test.SpanQuery, []test.AssertionResult], bool) { + testResult := maps.Ordered[test.SpanQuery, []test.AssertionResult]{} allPassed := true - defs.ForEach(func(spanQuery model.SpanQuery, asserts model.NamedAssertions) error { - spans := selector(spanQuery).Filter(trace) - assertionResults := make([]model.AssertionResult, 0) - for _, assertion := range asserts.Assertions { + for _, spec := range specs { + spans := selector(spec.Selector).Filter(trace) + assertionResults := make([]test.AssertionResult, 0) + for _, assertion := range spec.Assertions { res := e.assert(assertion, spans, ds) if !res.AllPassed { allPassed = false } assertionResults = append(assertionResults, res) } - testResult, _ = testResult.Add(spanQuery, assertionResults) - return nil - }) + testResult, _ = testResult.Add(spec.Selector, assertionResults) + } return testResult, allPassed } -func (e defaultAssertionExecutor) assert(assertion model.Assertion, spans model.Spans, ds []expression.DataStore) model.AssertionResult { +func (e defaultAssertionExecutor) assert(assertion test.Assertion, spans model.Spans, ds []expression.DataStore) test.AssertionResult { ds = append([]expression.DataStore{ expression.MetaAttributesDataStore{SelectedSpans: spans}, expression.VariableDataStore{}, }, ds...) allPassed := true - spanResults := make([]model.SpanAssertionResult, 0, len(spans)) + spanResults := make([]test.SpanAssertionResult, 0, len(spans)) spans. ForEach(func(_ int, span model.Span) bool { res := e.assertSpan(span, ds, string(assertion)) @@ -62,20 +62,20 @@ func (e defaultAssertionExecutor) assert(assertion model.Assertion, spans model. allPassed = res.CompareErr == nil }) - return model.AssertionResult{ + return test.AssertionResult{ Assertion: assertion, AllPassed: allPassed, Results: spanResults, } } -func (e defaultAssertionExecutor) assertSpan(span model.Span, ds []expression.DataStore, assertion string) model.SpanAssertionResult { +func (e defaultAssertionExecutor) assertSpan(span model.Span, ds []expression.DataStore, assertion string) test.SpanAssertionResult { ds = append([]expression.DataStore{expression.AttributeDataStore{Span: span}}, ds...) expressionExecutor := expression.NewExecutor(ds...) actualValue, _, err := expressionExecutor.Statement(assertion) - sar := model.SpanAssertionResult{ + sar := test.SpanAssertionResult{ ObservedValue: actualValue, CompareErr: err, } @@ -92,7 +92,7 @@ type instrumentedAssertionExecutor struct { tracer trace.Tracer } -func (e instrumentedAssertionExecutor) Assert(ctx context.Context, defs maps.Ordered[model.SpanQuery, model.NamedAssertions], trace model.Trace, ds []expression.DataStore) (maps.Ordered[model.SpanQuery, []model.AssertionResult], bool) { +func (e instrumentedAssertionExecutor) Assert(ctx context.Context, defs test.Specs, trace model.Trace, ds []expression.DataStore) (maps.Ordered[test.SpanQuery, []test.AssertionResult], bool) { ctx, span := e.tracer.Start(ctx, "Execute assertions") defer span.End() @@ -111,7 +111,7 @@ func NewAssertionExecutor(tracer trace.Tracer) AssertionExecutor { } } -func selector(sq model.SpanQuery) selectors.Selector { +func selector(sq test.SpanQuery) selectors.Selector { sel, _ := selectors.New(string(sq)) return sel } diff --git a/server/executor/assertion_runner.go b/server/executor/assertion_runner.go index ff3eb76c23..5c9689f9ee 100644 --- a/server/executor/assertion_runner.go +++ b/server/executor/assertion_runner.go @@ -10,18 +10,18 @@ import ( "github.com/kubeshop/tracetest/server/analytics" "github.com/kubeshop/tracetest/server/environment" "github.com/kubeshop/tracetest/server/expression" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/model/events" "github.com/kubeshop/tracetest/server/pkg/maps" "github.com/kubeshop/tracetest/server/subscription" + "github.com/kubeshop/tracetest/server/test" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" ) type AssertionRequest struct { carrier propagation.MapCarrier - Test model.Test - Run model.Run + Test test.Test + Run test.Run } func (r AssertionRequest) Context() context.Context { @@ -108,7 +108,7 @@ func (e *defaultAssertionRunner) startWorker() { } } -func (e *defaultAssertionRunner) runAssertionsAndUpdateResult(ctx context.Context, request AssertionRequest) (model.Run, error) { +func (e *defaultAssertionRunner) runAssertionsAndUpdateResult(ctx context.Context, request AssertionRequest) (test.Run, error) { log.Printf("[AssertionRunner] Test %s Run %d: Starting\n", request.Test.ID, request.Run.ID) err := e.eventEmitter.Emit(ctx, events.TestSpecsRunStart(request.Test.ID, request.Run.ID)) @@ -130,7 +130,7 @@ func (e *defaultAssertionRunner) runAssertionsAndUpdateResult(ctx context.Contex "finalState": string(run.State), }) - return model.Run{}, e.updater.Update(ctx, run) + return test.Run{}, e.updater.Update(ctx, run) } log.Printf("[AssertionRunner] Test %s Run %d: Success. pass: %d, fail: %d\n", request.Test.ID, request.Run.ID, run.Pass, run.Fail) @@ -143,7 +143,7 @@ func (e *defaultAssertionRunner) runAssertionsAndUpdateResult(ctx context.Contex log.Printf("[AssertionRunner] Test %s Run %d: fail to emit TestSpecsRunPersistenceError event: %s\n", request.Test.ID, request.Run.ID, anotherErr.Error()) } - return model.Run{}, fmt.Errorf("could not save result on database: %w", err) + return test.Run{}, fmt.Errorf("could not save result on database: %w", err) } err = e.eventEmitter.Emit(ctx, events.TestSpecsRunSuccess(request.Test.ID, request.Run.ID)) @@ -154,10 +154,10 @@ func (e *defaultAssertionRunner) runAssertionsAndUpdateResult(ctx context.Contex return run, nil } -func (e *defaultAssertionRunner) executeAssertions(ctx context.Context, req AssertionRequest) (model.Run, error) { +func (e *defaultAssertionRunner) executeAssertions(ctx context.Context, req AssertionRequest) (test.Run, error) { run := req.Run if run.Trace == nil { - return model.Run{}, fmt.Errorf("trace not available") + return test.Run{}, fmt.Errorf("trace not available") } ds := []expression.DataStore{expression.EnvironmentDataStore{ @@ -166,7 +166,7 @@ func (e *defaultAssertionRunner) executeAssertions(ctx context.Context, req Asse outputs, err := e.outputsProcessor(ctx, req.Test.Outputs, *run.Trace, ds) if err != nil { - return model.Run{}, fmt.Errorf("cannot process outputs: %w", err) + return test.Run{}, fmt.Errorf("cannot process outputs: %w", err) } e.validateOutputResolution(ctx, req, outputs) @@ -192,7 +192,7 @@ func (e *defaultAssertionRunner) executeAssertions(ctx context.Context, req Asse return run, nil } -func (e *defaultAssertionRunner) emitFailedAssertions(ctx context.Context, req AssertionRequest, result maps.Ordered[model.SpanQuery, []model.AssertionResult]) { +func (e *defaultAssertionRunner) emitFailedAssertions(ctx context.Context, req AssertionRequest, result maps.Ordered[test.SpanQuery, []test.AssertionResult]) { for _, assertionResults := range result.Unordered() { for _, assertionResult := range assertionResults { for _, spanAssertionResult := range assertionResult.Results { @@ -203,7 +203,7 @@ func (e *defaultAssertionRunner) emitFailedAssertions(ctx context.Context, req A req.Run.TestID, req.Run.ID, unwrappedError, - spanAssertionResult.SafeSpanIDString(), + spanAssertionResult.SpanIDString(), string(assertionResult.Assertion), )) } @@ -213,7 +213,7 @@ func (e *defaultAssertionRunner) emitFailedAssertions(ctx context.Context, req A req.Run.TestID, req.Run.ID, spanAssertionResult.CompareErr, - spanAssertionResult.SafeSpanIDString(), + spanAssertionResult.SpanIDString(), string(assertionResult.Assertion), )) } @@ -223,9 +223,9 @@ func (e *defaultAssertionRunner) emitFailedAssertions(ctx context.Context, req A } } -func createEnvironment(env environment.Environment, outputs maps.Ordered[string, model.RunOutput]) environment.Environment { +func createEnvironment(env environment.Environment, outputs maps.Ordered[string, test.RunOutput]) environment.Environment { outputVariables := make([]environment.EnvironmentValue, 0) - outputs.ForEach(func(key string, val model.RunOutput) error { + outputs.ForEach(func(key string, val test.RunOutput) error { outputVariables = append(outputVariables, environment.EnvironmentValue{ Key: val.Name, Value: val.Value, @@ -248,8 +248,8 @@ func (e *defaultAssertionRunner) RunAssertions(ctx context.Context, request Asse e.inputChannel <- request } -func (e *defaultAssertionRunner) validateOutputResolution(ctx context.Context, request AssertionRequest, outputs maps.Ordered[string, model.RunOutput]) { - err := outputs.ForEach(func(outputName string, outputModel model.RunOutput) error { +func (e *defaultAssertionRunner) validateOutputResolution(ctx context.Context, request AssertionRequest, outputs maps.Ordered[string, test.RunOutput]) { + err := outputs.ForEach(func(outputName string, outputModel test.RunOutput) error { if outputModel.Resolved { return nil } diff --git a/server/executor/assetion_executor_test.go b/server/executor/assetion_executor_test.go index 9b0945f2ff..271a94314c 100644 --- a/server/executor/assetion_executor_test.go +++ b/server/executor/assetion_executor_test.go @@ -10,6 +10,7 @@ import ( "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace" @@ -20,18 +21,21 @@ func TestAssertion(t *testing.T) { spanID := id.NewRandGenerator().SpanID() cases := []struct { name string - testDef maps.Ordered[model.SpanQuery, model.NamedAssertions] + testDef test.Specs trace model.Trace - expectedResult maps.Ordered[model.SpanQuery, []model.AssertionResult] + expectedResult maps.Ordered[test.SpanQuery, []test.AssertionResult] expectedAllPassed bool }{ { name: "CanAssert", - testDef: (maps.Ordered[model.SpanQuery, model.NamedAssertions]{}).MustAdd(`span[service.name="Pokeshop"]`, model.NamedAssertions{ - Assertions: []model.Assertion{ - `attr:tracetest.span.duration = 2000ns`, + testDef: test.Specs{ + { + Selector: test.SpanQuery(`span[service.name="Pokeshop"]`), + Assertions: []test.Assertion{ + `attr:tracetest.span.duration = 2000ns`, + }, }, - }), + }, trace: model.Trace{ RootSpan: model.Span{ ID: spanID, @@ -42,10 +46,10 @@ func TestAssertion(t *testing.T) { }, }, expectedAllPassed: true, - expectedResult: (maps.Ordered[model.SpanQuery, []model.AssertionResult]{}).MustAdd(`span[service.name="Pokeshop"]`, []model.AssertionResult{ + expectedResult: (maps.Ordered[test.SpanQuery, []test.AssertionResult]{}).MustAdd(`span[service.name="Pokeshop"]`, []test.AssertionResult{ { Assertion: `attr:tracetest.span.duration = 2000ns`, - Results: []model.SpanAssertionResult{ + Results: []test.SpanAssertionResult{ { SpanID: &spanID, ObservedValue: "2us", @@ -57,14 +61,20 @@ func TestAssertion(t *testing.T) { }, { name: "CanAssertOnSpanMatchCount", - testDef: (maps.Ordered[model.SpanQuery, model.NamedAssertions]{}).MustAdd(`span[service.name="Pokeshop"]`, model.NamedAssertions{ - Assertions: []model.Assertion{ - `attr:tracetest.selected_spans.count = 1`, - }, - }).MustAdd(`span[service.name="NotExists"]`, model.NamedAssertions{ - Assertions: []model.Assertion{ - `attr:tracetest.selected_spans.count = 0`, - }}), + testDef: test.Specs{ + { + Selector: test.SpanQuery(`span[service.name="Pokeshop"]`), + Assertions: []test.Assertion{ + `attr:tracetest.selected_spans.count = 1`, + }, + }, + { + Selector: test.SpanQuery(`span[service.name="NotExists"]`), + Assertions: []test.Assertion{ + `attr:tracetest.selected_spans.count = 0`, + }, + }, + }, trace: model.Trace{ RootSpan: model.Span{ ID: spanID, @@ -75,10 +85,10 @@ func TestAssertion(t *testing.T) { }, }, expectedAllPassed: true, - expectedResult: (maps.Ordered[model.SpanQuery, []model.AssertionResult]{}).MustAdd(`span[service.name="Pokeshop"]`, []model.AssertionResult{ + expectedResult: (maps.Ordered[test.SpanQuery, []test.AssertionResult]{}).MustAdd(`span[service.name="Pokeshop"]`, []test.AssertionResult{ { Assertion: `attr:tracetest.selected_spans.count = 1`, - Results: []model.SpanAssertionResult{ + Results: []test.SpanAssertionResult{ { SpanID: &spanID, ObservedValue: "1", @@ -86,10 +96,10 @@ func TestAssertion(t *testing.T) { }, }, }, - }).MustAdd(`span[service.name="NotExists"]`, []model.AssertionResult{ + }).MustAdd(`span[service.name="NotExists"]`, []test.AssertionResult{ { Assertion: `attr:tracetest.selected_spans.count = 0`, - Results: []model.SpanAssertionResult{ + Results: []test.SpanAssertionResult{ { SpanID: nil, ObservedValue: "0", @@ -102,11 +112,15 @@ func TestAssertion(t *testing.T) { // https://github.com/kubeshop/tracetest/issues/617 { name: "ContainsWithJSON", - testDef: (maps.Ordered[model.SpanQuery, model.NamedAssertions]{}).MustAdd(`span[service.name="Pokeshop"]`, model.NamedAssertions{ - Assertions: []model.Assertion{ - `attr:http.response.body contains 52`, - `attr:tracetest.span.duration <= 21ms`, - }}), + testDef: test.Specs{ + { + Selector: test.SpanQuery(`span[service.name="Pokeshop"]`), + Assertions: []test.Assertion{ + `attr:http.response.body contains 52`, + `attr:tracetest.span.duration <= 21ms`, + }, + }, + }, trace: model.Trace{ RootSpan: model.Span{ ID: spanID, @@ -118,10 +132,10 @@ func TestAssertion(t *testing.T) { }, }, expectedAllPassed: true, - expectedResult: (maps.Ordered[model.SpanQuery, []model.AssertionResult]{}).MustAdd(`span[service.name="Pokeshop"]`, []model.AssertionResult{ + expectedResult: (maps.Ordered[test.SpanQuery, []test.AssertionResult]{}).MustAdd(`span[service.name="Pokeshop"]`, []test.AssertionResult{ { Assertion: `attr:http.response.body contains 52`, - Results: []model.SpanAssertionResult{ + Results: []test.SpanAssertionResult{ { SpanID: &spanID, ObservedValue: `{"id":52}`, @@ -131,7 +145,7 @@ func TestAssertion(t *testing.T) { }, { Assertion: `attr:tracetest.span.duration <= 21ms`, - Results: []model.SpanAssertionResult{ + Results: []test.SpanAssertionResult{ { SpanID: &spanID, ObservedValue: "21ms", @@ -144,10 +158,14 @@ func TestAssertion(t *testing.T) { // https://github.com/kubeshop/tracetest/issues/1203 { name: "DurationComparison", - testDef: (maps.Ordered[model.SpanQuery, model.NamedAssertions]{}).MustAdd(`span[service.name="Pokeshop"]`, model.NamedAssertions{ - Assertions: []model.Assertion{ - `attr:tracetest.span.duration <= 25ms`, - }}), + testDef: test.Specs{ + { + Selector: test.SpanQuery(`span[service.name="Pokeshop"]`), + Assertions: []test.Assertion{ + `attr:tracetest.span.duration <= 25ms`, + }, + }, + }, trace: model.Trace{ RootSpan: model.Span{ ID: spanID, @@ -159,10 +177,10 @@ func TestAssertion(t *testing.T) { }, }, expectedAllPassed: true, - expectedResult: (maps.Ordered[model.SpanQuery, []model.AssertionResult]{}).MustAdd(`span[service.name="Pokeshop"]`, []model.AssertionResult{ + expectedResult: (maps.Ordered[test.SpanQuery, []test.AssertionResult]{}).MustAdd(`span[service.name="Pokeshop"]`, []test.AssertionResult{ { Assertion: `attr:tracetest.span.duration <= 25ms`, - Results: []model.SpanAssertionResult{ + Results: []test.SpanAssertionResult{ { SpanID: &spanID, ObservedValue: "25ms", @@ -175,10 +193,14 @@ func TestAssertion(t *testing.T) { // https://github.com/kubeshop/tracetest/issues/1421 { name: "FailedAssertionsConvertDurationFieldsIntoDurationFormat", - testDef: (maps.Ordered[model.SpanQuery, model.NamedAssertions]{}).MustAdd(`span[service.name="Pokeshop"]`, model.NamedAssertions{ - Assertions: []model.Assertion{ - `attr:tracetest.span.duration <= 25ms`, - }}), + testDef: test.Specs{ + { + Selector: test.SpanQuery(`span[service.name="Pokeshop"]`), + Assertions: []test.Assertion{ + `attr:tracetest.span.duration <= 25ms`, + }, + }, + }, trace: model.Trace{ RootSpan: model.Span{ ID: spanID, @@ -190,10 +212,10 @@ func TestAssertion(t *testing.T) { }, }, expectedAllPassed: false, - expectedResult: (maps.Ordered[model.SpanQuery, []model.AssertionResult]{}).MustAdd(`span[service.name="Pokeshop"]`, []model.AssertionResult{ + expectedResult: (maps.Ordered[test.SpanQuery, []test.AssertionResult]{}).MustAdd(`span[service.name="Pokeshop"]`, []test.AssertionResult{ { Assertion: `attr:tracetest.span.duration <= 25ms`, - Results: []model.SpanAssertionResult{ + Results: []test.SpanAssertionResult{ { SpanID: &spanID, ObservedValue: "35ms", @@ -214,7 +236,7 @@ func TestAssertion(t *testing.T) { assert.Equal(t, cl.expectedAllPassed, allPassed) - cl.expectedResult.ForEach(func(expectedSel model.SpanQuery, expectedAssertionResults []model.AssertionResult) error { + cl.expectedResult.ForEach(func(expectedSel test.SpanQuery, expectedAssertionResults []test.AssertionResult) error { actualAssertionResults := actual.Get(expectedSel) assert.NotEmpty(t, actualAssertionResults, `expected selector "%s" not found`, expectedSel) for i := 0; i < len(expectedAssertionResults); i++ { diff --git a/server/executor/default_poller_executor.go b/server/executor/default_poller_executor.go index 8a18d2818f..1db6860764 100644 --- a/server/executor/default_poller_executor.go +++ b/server/executor/default_poller_executor.go @@ -9,6 +9,7 @@ import ( "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/model/events" "github.com/kubeshop/tracetest/server/resourcemanager" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/tracedb" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -29,7 +30,7 @@ type InstrumentedPollerExecutor struct { pollerExecutor PollerExecutor } -func (pe InstrumentedPollerExecutor) ExecuteRequest(request *PollingRequest) (bool, string, model.Run, error) { +func (pe InstrumentedPollerExecutor) ExecuteRequest(request *PollingRequest) (bool, string, test.Run, error) { _, span := pe.tracer.Start(request.Context(), "Fetch trace") defer span.End() @@ -98,7 +99,7 @@ func (pe DefaultPollerExecutor) traceDB(ctx context.Context) (tracedb.TraceDB, e return tdb, nil } -func (pe DefaultPollerExecutor) ExecuteRequest(request *PollingRequest) (bool, string, model.Run, error) { +func (pe DefaultPollerExecutor) ExecuteRequest(request *PollingRequest) (bool, string, test.Run, error) { log.Printf("[PollerExecutor] Test %s Run %d: ExecuteRequest\n", request.test.ID, request.run.ID) run := request.run ctx := request.Context() @@ -106,7 +107,7 @@ func (pe DefaultPollerExecutor) ExecuteRequest(request *PollingRequest) (bool, s traceDB, err := pe.traceDB(ctx) if err != nil { log.Printf("[PollerExecutor] Test %s Run %d: GetDataStore error: %s\n", request.test.ID, request.run.ID, err.Error()) - return false, "", model.Run{}, err + return false, "", test.Run{}, err } if request.IsFirstRequest() { @@ -122,7 +123,7 @@ func (pe DefaultPollerExecutor) ExecuteRequest(request *PollingRequest) (bool, s endpoints := traceDB.GetEndpoints() ds, err := pe.dsRepo.Current(ctx) if err != nil { - return false, "", model.Run{}, fmt.Errorf("could not get current datastore: %w", err) + return false, "", test.Run{}, fmt.Errorf("could not get current datastore: %w", err) } err = pe.eventEmitter.Emit(ctx, events.TracePollingStart(request.test.ID, request.run.ID, string(ds.Type), endpoints)) @@ -140,7 +141,7 @@ func (pe DefaultPollerExecutor) ExecuteRequest(request *PollingRequest) (bool, s } log.Printf("[PollerExecutor] Test %s Run %d: GetTraceByID (traceID %s) error: %s\n", request.test.ID, request.run.ID, traceID, err.Error()) - return false, "", model.Run{}, err + return false, "", test.Run{}, err } trace.ID = run.TraceID @@ -166,7 +167,7 @@ func (pe DefaultPollerExecutor) ExecuteRequest(request *PollingRequest) (bool, s request.run = run if !trace.HasRootSpan() { - newRoot := model.NewTracetestRootSpan(run) + newRoot := test.NewTracetestRootSpan(run) run.Trace = run.Trace.InsertRootSpan(newRoot) } else { run.Trace.RootSpan = model.AugmentRootSpan(run.Trace.RootSpan, run.TriggerResult) @@ -179,7 +180,7 @@ func (pe DefaultPollerExecutor) ExecuteRequest(request *PollingRequest) (bool, s err = pe.updater.Update(ctx, run) if err != nil { log.Printf("[PollerExecutor] Test %s Run %d: Update error: %s\n", request.test.ID, request.run.ID, err.Error()) - return false, "", model.Run{}, err + return false, "", test.Run{}, err } return true, reason, run, nil diff --git a/server/executor/eventemitter_test.go b/server/executor/eventemitter_test.go index 3c8a58a296..e4f9410dda 100644 --- a/server/executor/eventemitter_test.go +++ b/server/executor/eventemitter_test.go @@ -9,6 +9,8 @@ import ( "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/subscription" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "github.com/kubeshop/tracetest/server/testdb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,17 +19,17 @@ import ( func TestEventEmitter_SuccessfulScenario(t *testing.T) { // Given I have a test run event - run := model.NewRun() + run := test.NewRun() - test := model.Test{ + testObj := test.Test{ ID: id.ID("some-test"), - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, + Trigger: trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, }, } testRunEvent := model.TestRunEvent{ - TestID: test.ID, + TestID: testObj.ID, RunID: run.ID, Type: "EVENT_1", Stage: model.StageTrigger, @@ -60,17 +62,17 @@ func TestEventEmitter_SuccessfulScenario(t *testing.T) { func TestEventEmitter_FailedScenario(t *testing.T) { // Given I have a test run event - run := model.NewRun() + run := test.NewRun() - test := model.Test{ + testObj := test.Test{ ID: id.ID("some-test"), - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, + Trigger: trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, }, } testRunEvent := model.TestRunEvent{ - TestID: test.ID, + TestID: testObj.ID, RunID: run.ID, Type: "EVENT_1", Stage: model.StageTrigger, diff --git a/server/executor/linter_runner.go b/server/executor/linter_runner.go index d7481e6506..6a1042d50e 100644 --- a/server/executor/linter_runner.go +++ b/server/executor/linter_runner.go @@ -9,17 +9,17 @@ import ( "github.com/kubeshop/tracetest/server/analytics" "github.com/kubeshop/tracetest/server/linter" "github.com/kubeshop/tracetest/server/linter/analyzer" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/model/events" "github.com/kubeshop/tracetest/server/subscription" + "github.com/kubeshop/tracetest/server/test" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" ) type LinterRequest struct { carrier propagation.MapCarrier - Test model.Test - Run model.Run + Test test.Test + Run test.Run } func (r LinterRequest) Context() context.Context { @@ -145,7 +145,7 @@ func (e *defaultlinterRunner) RunLinter(ctx context.Context, request LinterReque e.inputChannel <- request } -func (e *defaultlinterRunner) onFinish(ctx context.Context, request LinterRequest, run model.Run) { +func (e *defaultlinterRunner) onFinish(ctx context.Context, request LinterRequest, run test.Run) { assertionRequest := AssertionRequest{ Test: request.Test, Run: run, @@ -153,7 +153,7 @@ func (e *defaultlinterRunner) onFinish(ctx context.Context, request LinterReques e.assertionRunner.RunAssertions(ctx, assertionRequest) } -func (e *defaultlinterRunner) onRun(ctx context.Context, request LinterRequest, linter linter.Linter, analyzer analyzer.Linter) (model.Run, error) { +func (e *defaultlinterRunner) onRun(ctx context.Context, request LinterRequest, linter linter.Linter, analyzer analyzer.Linter) (test.Run, error) { run := request.Run log.Printf("[linterRunner] Test %s Run %d: Starting\n", request.Test.ID, request.Run.ID) @@ -172,7 +172,7 @@ func (e *defaultlinterRunner) onRun(ctx context.Context, request LinterRequest, err = e.updater.Update(ctx, run) if err != nil { log.Printf("[linterRunner] Test %s Run %d: error updating run: %s\n", request.Test.ID, request.Run.ID, err.Error()) - return model.Run{}, fmt.Errorf("could not save result on database: %w", err) + return test.Run{}, fmt.Errorf("could not save result on database: %w", err) } err = e.eventEmitter.Emit(ctx, events.TraceLinterSuccess(request.Test.ID, request.Run.ID)) @@ -183,7 +183,7 @@ func (e *defaultlinterRunner) onRun(ctx context.Context, request LinterRequest, return run, nil } -func (e *defaultlinterRunner) onError(ctx context.Context, request LinterRequest, run model.Run, err error) (model.Run, error) { +func (e *defaultlinterRunner) onError(ctx context.Context, request LinterRequest, run test.Run, err error) (test.Run, error) { log.Printf("[linterRunner] Test %s Run %d: Linter failed. Reason: %s\n", request.Test.ID, request.Run.ID, err.Error()) anotherErr := e.eventEmitter.Emit(ctx, events.TraceLinterError(request.Test.ID, request.Run.ID, err)) if anotherErr != nil { diff --git a/server/executor/outputs_processor.go b/server/executor/outputs_processor.go index 833e720b42..4bba7f9223 100644 --- a/server/executor/outputs_processor.go +++ b/server/executor/outputs_processor.go @@ -9,12 +9,13 @@ import ( "github.com/kubeshop/tracetest/server/expression" "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/test" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) -type OutputsProcessorFn func(context.Context, maps.Ordered[string, model.Output], model.Trace, []expression.DataStore) (maps.Ordered[string, model.RunOutput], error) +type OutputsProcessorFn func(context.Context, test.Outputs, model.Trace, []expression.DataStore) (maps.Ordered[string, test.RunOutput], error) func InstrumentedOutputProcessor(tracer trace.Tracer) OutputsProcessorFn { op := instrumentedOutputProcessor{tracer} @@ -25,7 +26,7 @@ type instrumentedOutputProcessor struct { tracer trace.Tracer } -func (op instrumentedOutputProcessor) process(ctx context.Context, outputs maps.Ordered[string, model.Output], t model.Trace, ds []expression.DataStore) (maps.Ordered[string, model.RunOutput], error) { +func (op instrumentedOutputProcessor) process(ctx context.Context, outputs test.Outputs, t model.Trace, ds []expression.DataStore) (maps.Ordered[string, test.RunOutput], error) { ctx, span := op.tracer.Start(ctx, "Process outputs") defer span.End() @@ -50,8 +51,8 @@ func (op instrumentedOutputProcessor) process(ctx context.Context, outputs maps. return result, err } -func outputProcessor(ctx context.Context, outputs maps.Ordered[string, model.Output], tr model.Trace, ds []expression.DataStore) (maps.Ordered[string, model.RunOutput], error) { - res := maps.Ordered[string, model.RunOutput]{} +func outputProcessor(ctx context.Context, outputs test.Outputs, tr model.Trace, ds []expression.DataStore) (maps.Ordered[string, test.RunOutput], error) { + res := maps.Ordered[string, test.RunOutput]{} parsed, err := parseOutputs(outputs) if err != nil { @@ -60,7 +61,7 @@ func outputProcessor(ctx context.Context, outputs maps.Ordered[string, model.Out err = parsed.ForEach(func(key string, out parsedOutput) error { if out.err != nil { - res, err = res.Add(key, model.RunOutput{ + res, err = res.Add(key, test.RunOutput{ Value: "", SpanID: "", Name: key, @@ -96,7 +97,7 @@ func outputProcessor(ctx context.Context, outputs maps.Ordered[string, model.Out outputError = fmt.Errorf(`cannot find matching spans for output "%s"`, key) }) - res, err = res.Add(key, model.RunOutput{ + res, err = res.Add(key, test.RunOutput{ Value: value, SpanID: spanId, Name: key, @@ -111,7 +112,7 @@ func outputProcessor(ctx context.Context, outputs maps.Ordered[string, model.Out }) if err != nil { - return maps.Ordered[string, model.RunOutput]{}, err + return maps.Ordered[string, test.RunOutput]{}, err } return res, nil @@ -133,13 +134,15 @@ type parsedOutput struct { err error } -func parseOutputs(outputs maps.Ordered[string, model.Output]) (maps.Ordered[string, parsedOutput], error) { +func parseOutputs(outputs test.Outputs) (maps.Ordered[string, parsedOutput], error) { var parsed maps.Ordered[string, parsedOutput] - parseErr := outputs.ForEach(func(key string, out model.Output) error { + for _, output := range outputs { + key := output.Name + out := output var selector selectors.Selector var expr expression.Expr - var outputErr error = nil + var outputErr error expr, err := expression.Parse(out.Value) if err != nil { @@ -156,11 +159,6 @@ func parseOutputs(outputs maps.Ordered[string, model.Output]) (maps.Ordered[stri expr: expr, err: outputErr, }) - return nil - }) - - if parseErr != nil { - return maps.Ordered[string, parsedOutput]{}, parseErr } return parsed, nil diff --git a/server/executor/poller_executor_test.go b/server/executor/poller_executor_test.go index e99f6e69e0..66a24f85f6 100644 --- a/server/executor/poller_executor_test.go +++ b/server/executor/poller_executor_test.go @@ -12,6 +12,8 @@ import ( "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/subscription" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "github.com/kubeshop/tracetest/server/testdb" "github.com/kubeshop/tracetest/server/tracedb" "github.com/kubeshop/tracetest/server/tracedb/connection" @@ -446,12 +448,12 @@ type iterationExpectedValues struct { func executeAndValidatePollingRequests(t *testing.T, pollerExecutor executor.PollerExecutor, expectedValues []iterationExpectedValues) { ctx := context.Background() - run := model.NewRun() + run := test.NewRun() - test := model.Test{ + test := test.Test{ ID: id.ID("some-test"), - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, + Trigger: trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, }, } @@ -528,7 +530,7 @@ func getPollerExecutorWithMocks(t *testing.T, retryDelay, maxWaitTimeForTrace ti // RunUpdater type runUpdaterMock struct{} -func (m runUpdaterMock) Update(context.Context, model.Run) error { return nil } +func (m runUpdaterMock) Update(context.Context, test.Run) error { return nil } func getRunUpdaterMock(t *testing.T) executor.RunUpdater { return runUpdaterMock{} diff --git a/server/executor/run_stop.go b/server/executor/run_stop.go index 752566b43c..7f3569ec67 100644 --- a/server/executor/run_stop.go +++ b/server/executor/run_stop.go @@ -4,10 +4,10 @@ import ( "context" "log" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/model/events" "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/subscription" + "github.com/kubeshop/tracetest/server/test" ) type StopRequest struct { @@ -16,11 +16,11 @@ type StopRequest struct { } func (sr StopRequest) ResourceID() string { - runID := (model.Run{ID: sr.RunID, TestID: sr.TestID}).ResourceID() + runID := (test.Run{ID: sr.RunID, TestID: sr.TestID}).ResourceID() return runID + "/stop" } -func (r persistentRunner) listenForStopRequests(ctx context.Context, cancelCtx context.CancelFunc, run model.Run) { +func (r persistentRunner) listenForStopRequests(ctx context.Context, cancelCtx context.CancelFunc, run test.Run) { sfn := subscription.NewSubscriberFunction(func(m subscription.Message) error { stopRequest, ok := m.Content.(StopRequest) if !ok { diff --git a/server/executor/run_updater.go b/server/executor/run_updater.go index 023c593e5d..be547f61bc 100644 --- a/server/executor/run_updater.go +++ b/server/executor/run_updater.go @@ -4,12 +4,12 @@ import ( "context" "fmt" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/subscription" + "github.com/kubeshop/tracetest/server/test" ) type RunUpdater interface { - Update(context.Context, model.Run) error + Update(context.Context, test.Run) error } type CompositeUpdater struct { @@ -23,7 +23,7 @@ func (u CompositeUpdater) Add(l RunUpdater) CompositeUpdater { var _ RunUpdater = CompositeUpdater{} -func (u CompositeUpdater) Update(ctx context.Context, run model.Run) error { +func (u CompositeUpdater) Update(ctx context.Context, run test.Run) error { for _, l := range u.listeners { if err := l.Update(ctx, run); err != nil { return fmt.Errorf("composite updating error: %w", err) @@ -34,14 +34,14 @@ func (u CompositeUpdater) Update(ctx context.Context, run model.Run) error { } type dbUpdater struct { - repo model.RunRepository + repo test.RunRepository } -func NewDBUpdater(repo model.RunRepository) RunUpdater { +func NewDBUpdater(repo test.RunRepository) RunUpdater { return dbUpdater{repo} } -func (u dbUpdater) Update(ctx context.Context, run model.Run) error { +func (u dbUpdater) Update(ctx context.Context, run test.Run) error { return u.repo.UpdateRun(ctx, run) } @@ -53,7 +53,7 @@ func NewSubscriptionUpdater(manager *subscription.Manager) RunUpdater { return subscriptionUpdater{manager} } -func (u subscriptionUpdater) Update(ctx context.Context, run model.Run) error { +func (u subscriptionUpdater) Update(ctx context.Context, run test.Run) error { u.manager.PublishUpdate(subscription.Message{ ResourceID: run.ResourceID(), Type: "result_update", diff --git a/server/executor/runner.go b/server/executor/runner.go index c8780abc44..9074fb66c2 100644 --- a/server/executor/runner.go +++ b/server/executor/runner.go @@ -11,12 +11,14 @@ import ( "github.com/kubeshop/tracetest/server/analytics" "github.com/kubeshop/tracetest/server/datastore" "github.com/kubeshop/tracetest/server/environment" - "github.com/kubeshop/tracetest/server/executor/trigger" + triggerer "github.com/kubeshop/tracetest/server/executor/trigger" "github.com/kubeshop/tracetest/server/expression" "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/model/events" "github.com/kubeshop/tracetest/server/resourcemanager" "github.com/kubeshop/tracetest/server/subscription" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "github.com/kubeshop/tracetest/server/tracedb" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" @@ -24,12 +26,12 @@ import ( ) type RunResult struct { - Run model.Run + Run test.Run Err error } type Runner interface { - Run(context.Context, model.Test, model.RunMetadata, environment.Environment) model.Run + Run(context.Context, test.Test, test.RunMetadata, environment.Environment) test.Run } type PersistentRunner interface { @@ -38,8 +40,8 @@ type PersistentRunner interface { } func NewPersistentRunner( - triggers *trigger.Registry, - runs model.RunRepository, + triggers *triggerer.Registry, + runs test.RunRepository, updater RunUpdater, tp TracePoller, tracer trace.Tracer, @@ -66,9 +68,9 @@ func NewPersistentRunner( } type persistentRunner struct { - triggers *trigger.Registry + triggers *triggerer.Registry tp TracePoller - runs model.RunRepository + runs test.RunRepository updater RunUpdater tracer trace.Tracer subscriptionManager *subscription.Manager @@ -83,19 +85,19 @@ type persistentRunner struct { type execReq struct { ctx context.Context - test model.Test - run model.Run + test test.Test + run test.Run Headers propagation.MapCarrier executor expression.Executor } -func (r persistentRunner) handleDBError(run model.Run, err error) { +func (r persistentRunner) handleDBError(run test.Run, err error) { if err != nil { fmt.Printf("test %s run #%d trigger DB error: %s\n", run.TestID, run.ID, err.Error()) } } -func (r persistentRunner) handleError(run model.Run, err error) { +func (r persistentRunner) handleError(run test.Run, err error) { if err != nil { fmt.Printf("test %s run #%d trigger DB error: %s\n", run.TestID, run.ID, err.Error()) } @@ -137,15 +139,15 @@ func getNewCtx(ctx context.Context) context.Context { return otel.GetTextMapPropagator().Extract(context.Background(), carrier) } -func (r persistentRunner) Run(ctx context.Context, test model.Test, metadata model.RunMetadata, environment environment.Environment) model.Run { +func (r persistentRunner) Run(ctx context.Context, testObj test.Test, metadata test.RunMetadata, environment environment.Environment) test.Run { ctx, cancelCtx := context.WithCancel( getNewCtx(ctx), ) - run := model.NewRun() + run := test.NewRun() run.Metadata = metadata run.Environment = environment - run, err := r.runs.CreateRun(ctx, test, run) + run, err := r.runs.CreateRun(ctx, testObj, run) r.handleDBError(run, err) r.listenForStopRequests(ctx, cancelCtx, run) @@ -158,7 +160,7 @@ func (r persistentRunner) Run(ctx context.Context, test model.Test, metadata mod r.executeQueue <- execReq{ ctx: ctx, - test: test, + test: testObj, run: run, executor: executor, } @@ -189,7 +191,7 @@ func (r persistentRunner) processExecQueue(job execReq) { r.handleError(job.run, err) } - triggerer, err := r.triggers.Get(job.test.ServiceUnderTest.Type) + triggererObj, err := r.triggers.Get(job.test.Trigger.Type) if err != nil { r.handleError(job.run, err) } @@ -203,7 +205,7 @@ func (r persistentRunner) processExecQueue(job execReq) { run.TraceID = traceID r.handleDBError(run, r.updater.Update(job.ctx, run)) - triggerOptions := &trigger.TriggerOptions{ + triggerOptions := &triggerer.TriggerOptions{ TraceID: traceID, Executor: job.executor, } @@ -213,7 +215,7 @@ func (r persistentRunner) processExecQueue(job execReq) { r.handleError(job.run, err) } - resolvedTest, err := triggerer.Resolve(job.ctx, job.test, triggerOptions) + resolvedTest, err := triggererObj.Resolve(job.ctx, job.test, triggerOptions) if err != nil { emitErr := r.eventEmitter.Emit(job.ctx, events.TriggerResolveError(job.run.TestID, job.run.ID, err)) if emitErr != nil { @@ -228,8 +230,8 @@ func (r persistentRunner) processExecQueue(job execReq) { r.handleError(job.run, err) } - if job.test.ServiceUnderTest.Type == model.TriggerTypeTRACEID { - traceIDFromParam, err := trace.TraceIDFromHex(job.test.ServiceUnderTest.TraceID.ID) + if job.test.Trigger.Type == trigger.TriggerTypeTraceID { + traceIDFromParam, err := trace.TraceIDFromHex(job.test.Trigger.TraceID.ID) if err == nil { run.TraceID = traceIDFromParam } @@ -240,7 +242,7 @@ func (r persistentRunner) processExecQueue(job execReq) { r.handleError(job.run, err) } - response, err := triggerer.Trigger(job.ctx, resolvedTest, triggerOptions) + response, err := triggererObj.Trigger(job.ctx, resolvedTest, triggerOptions) run = r.handleExecutionResult(run, response, err) if err != nil { if isConnectionError(err) { @@ -272,14 +274,14 @@ func (r persistentRunner) processExecQueue(job execReq) { run.SpanID = response.SpanID r.handleDBError(run, r.updater.Update(job.ctx, run)) - if run.State == model.RunStateAwaitingTrace { + if run.State == test.RunStateAwaitingTrace { ctx, pollingSpan := r.tracer.Start(job.ctx, "Start Polling trace") defer pollingSpan.End() r.tp.Poll(ctx, job.test, run, r.ppGetter.GetDefault(ctx)) } } -func (r persistentRunner) handleExecutionResult(run model.Run, response trigger.Response, err error) model.Run { +func (r persistentRunner) handleExecutionResult(run test.Run, response triggerer.Response, err error) test.Run { run = run.TriggerCompleted(response.Result) if err != nil { run = run.TriggerFailed(err) @@ -296,10 +298,10 @@ func (r persistentRunner) handleExecutionResult(run model.Run, response trigger. func (r persistentRunner) emitUnreachableEndpointEvent(job execReq, err error) { var event model.TestRunEvent - switch job.test.ServiceUnderTest.Type { - case model.TriggerTypeHTTP: + switch job.test.Trigger.Type { + case trigger.TriggerTypeHTTP: event = events.TriggerHTTPUnreachableHostError(job.run.TestID, job.run.ID, err) - case model.TriggerTypeGRPC: + case trigger.TriggerTypeGRPC: event = events.TriggergRPCUnreachableHostError(job.run.TestID, job.run.ID, err) } @@ -336,11 +338,11 @@ func isConnectionError(err error) bool { func isTargetLocalhost(job execReq) bool { var endpoint string - switch job.test.ServiceUnderTest.Type { - case model.TriggerTypeHTTP: - endpoint = job.test.ServiceUnderTest.HTTP.URL - case model.TriggerTypeGRPC: - endpoint = job.test.ServiceUnderTest.GRPC.Address + switch job.test.Trigger.Type { + case trigger.TriggerTypeHTTP: + endpoint = job.test.Trigger.HTTP.URL + case trigger.TriggerTypeGRPC: + endpoint = job.test.Trigger.GRPC.Address } url, err := url.Parse(endpoint) diff --git a/server/executor/runner_test.go b/server/executor/runner_test.go index f258db08f7..f3c87f789a 100644 --- a/server/executor/runner_test.go +++ b/server/executor/runner_test.go @@ -10,10 +10,11 @@ import ( "github.com/kubeshop/tracetest/server/environment" "github.com/kubeshop/tracetest/server/executor" "github.com/kubeshop/tracetest/server/executor/pollingprofile" - "github.com/kubeshop/tracetest/server/executor/trigger" - "github.com/kubeshop/tracetest/server/model" + triggerer "github.com/kubeshop/tracetest/server/executor/trigger" "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/subscription" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "github.com/kubeshop/tracetest/server/testdb" "github.com/kubeshop/tracetest/server/tracedb" "github.com/kubeshop/tracetest/server/tracing" @@ -26,17 +27,17 @@ func TestPersistentRunner(t *testing.T) { t.Run("TestIsTriggerd", func(t *testing.T) { t.Parallel() - test := model.Test{ - ID: id.ID("test1"), - ServiceUnderTest: sampleTrigger, + testObj := test.Test{ + ID: id.ID("test1"), + Trigger: sampleTrigger, } f := runnerSetup(t) - f.expectSuccessExec(test) + f.expectSuccessExec(testObj) - f.run([]model.Test{test}, 10*time.Millisecond) + f.run([]test.Test{testObj}, 10*time.Millisecond) - result := f.mockDB.runs[test.ID] + result := f.mockDB.runs[testObj.ID] require.NotNil(t, result) assert.Greater(t, result.ServiceTriggerCompletedAt.UnixNano(), result.CreatedAt.UnixNano()) @@ -46,14 +47,14 @@ func TestPersistentRunner(t *testing.T) { t.Run("TestsCanBeTriggerdConcurrently", func(t *testing.T) { t.Parallel() - test1 := model.Test{ID: id.ID("test1"), ServiceUnderTest: sampleTrigger} - test2 := model.Test{ID: id.ID("test2"), ServiceUnderTest: sampleTrigger} + test1 := test.Test{ID: id.ID("test1"), Trigger: sampleTrigger} + test2 := test.Test{ID: id.ID("test2"), Trigger: sampleTrigger} f := runnerSetup(t) f.expectSuccessExecLong(test1) f.expectSuccessExec(test2) - f.run([]model.Test{test1, test2}, 100*time.Millisecond) + f.run([]test.Test{test1, test2}, 100*time.Millisecond) run1 := f.mockDB.runs[test1.ID] run2 := f.mockDB.runs[test2.ID] @@ -66,24 +67,24 @@ func TestPersistentRunner(t *testing.T) { var ( noError error = nil - sampleResponse = trigger.Response{ + sampleResponse = triggerer.Response{ SpanAttributes: map[string]string{ "tracetest.run.trigger.http.response_code": "200", }, - Result: model.TriggerResult{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPResponse{ + Result: trigger.TriggerResult{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPResponse{ StatusCode: 200, Body: "this is the body", - Headers: []model.HTTPHeader{ + Headers: []trigger.HTTPHeader{ {Key: "Content-Type", Value: "text/plain"}, }, }, }, } - sampleTrigger = model.Trigger{ - Type: model.TriggerTypeHTTP, + sampleTrigger = trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, } ) @@ -94,27 +95,27 @@ type runnerFixture struct { mockTracePoller *mockTracePoller } -func (f runnerFixture) run(tests []model.Test, ttl time.Duration) { +func (f runnerFixture) run(tests []test.Test, ttl time.Duration) { f.runner.Start(2) time.Sleep(10 * time.Millisecond) - for _, test := range tests { - f.runner.Run(context.TODO(), test, model.RunMetadata{}, environment.Environment{}) + for _, testObj := range tests { + f.runner.Run(context.TODO(), testObj, test.RunMetadata{}, environment.Environment{}) } time.Sleep(ttl) f.runner.Stop() } -func (f runnerFixture) expectSuccessExecLong(test model.Test) { +func (f runnerFixture) expectSuccessExecLong(test test.Test) { f.mockExecutor.expectTriggerTestLong(test) f.expectSuccessResultPersist(test) } -func (f runnerFixture) expectSuccessExec(test model.Test) { +func (f runnerFixture) expectSuccessExec(test test.Test) { f.mockExecutor.expectTriggerTest(test) f.expectSuccessResultPersist(test) } -func (f runnerFixture) expectSuccessResultPersist(test model.Test) { +func (f runnerFixture) expectSuccessResultPersist(test test.Test) { expectCreateRun(f.mockDB, test) f.mockDB.On("UpdateRun", mock.Anything).Return(noError) f.mockDB.On("UpdateRun", mock.Anything).Return(noError) @@ -128,7 +129,7 @@ func (f runnerFixture) assert(t *testing.T) { func runnerSetup(t *testing.T) runnerFixture { tr, _ := tracing.NewTracer(context.TODO(), config.Must(config.New())) - reg := trigger.NewRegsitry(tr, tr) + reg := triggerer.NewRegsitry(tr, tr) me := new(mockTriggerer) me.t = t @@ -156,7 +157,7 @@ func runnerSetup(t *testing.T) runnerFixture { mtp, tracer, subscription.NewManager(), - tracedb.Factory(&testDB), + tracedb.Factory(db), getDataStoreRepositoryMock(t), eventEmitter, defaultProfileGetter{5 * time.Second, 30 * time.Second}, @@ -174,22 +175,24 @@ func runnerSetup(t *testing.T) runnerFixture { type mockDB struct { testdb.MockRepository - runs map[id.ID]model.Run + runs map[id.ID]test.Run } -func (m *mockDB) CreateRun(_ context.Context, test model.Test, run model.Run) (model.Run, error) { - args := m.Called(test.ID) +var _ test.RunRepository = &mockDB{} + +func (m *mockDB) CreateRun(_ context.Context, testObj test.Test, run test.Run) (test.Run, error) { + args := m.Called(testObj.ID) if m.runs == nil { - m.runs = map[id.ID]model.Run{} + m.runs = map[id.ID]test.Run{} } run.ID = rand.Intn(100) - m.runs[test.ID] = run + m.runs[testObj.ID] = run return run, args.Error(0) } -func (m *mockDB) UpdateRun(_ context.Context, run model.Run) error { +func (m *mockDB) UpdateRun(_ context.Context, run test.Run) error { args := m.Called(run.ID) for k, v := range m.runs { if v.ID == run.ID { @@ -200,26 +203,35 @@ func (m *mockDB) UpdateRun(_ context.Context, run model.Run) error { return args.Error(0) } +func (m *mockDB) GetTransactionRunSteps(ctx context.Context, id id.ID, runID int) ([]test.Run, error) { + args := m.Called(ctx, id, runID) + return args.Get(0).([]test.Run), args.Error(1) +} + +type mockRunRepository struct { + mock.Mock +} + type mockTriggerer struct { mock.Mock t *testing.T } -func (m *mockTriggerer) Type() model.TriggerType { - return model.TriggerTypeHTTP +func (m *mockTriggerer) Type() trigger.TriggerType { + return trigger.TriggerTypeHTTP } -func (m *mockTriggerer) Trigger(_ context.Context, test model.Test, opts *trigger.TriggerOptions) (trigger.Response, error) { +func (m *mockTriggerer) Trigger(_ context.Context, test test.Test, opts *triggerer.TriggerOptions) (triggerer.Response, error) { args := m.Called(test.ID) - return args.Get(0).(trigger.Response), args.Error(1) + return args.Get(0).(triggerer.Response), args.Error(1) } -func (m *mockTriggerer) Resolve(_ context.Context, test model.Test, opts *trigger.TriggerOptions) (model.Test, error) { - args := m.Called(test.ID) - return args.Get(0).(model.Test), args.Error(1) +func (m *mockTriggerer) Resolve(_ context.Context, testObj test.Test, opts *triggerer.TriggerOptions) (test.Test, error) { + args := m.Called(testObj.ID) + return args.Get(0).(test.Test), args.Error(1) } -func (m *mockTriggerer) expectTriggerTest(test model.Test) *mock.Call { +func (m *mockTriggerer) expectTriggerTest(test test.Test) *mock.Call { return m. On("Resolve", test.ID). Return(test, noError). @@ -227,7 +239,7 @@ func (m *mockTriggerer) expectTriggerTest(test model.Test) *mock.Call { Return(sampleResponse, noError) } -func (m *mockTriggerer) expectTriggerTestLong(test model.Test) *mock.Call { +func (m *mockTriggerer) expectTriggerTestLong(test test.Test) *mock.Call { return m. On("Trigger", test.ID). After(50*time.Millisecond). @@ -236,7 +248,7 @@ func (m *mockTriggerer) expectTriggerTestLong(test model.Test) *mock.Call { Return(test, noError) } -func expectCreateRun(m *mockDB, test model.Test) *mock.Call { +func expectCreateRun(m *mockDB, test test.Test) *mock.Call { return m. On("CreateRun", test.ID). Return(noError) @@ -247,11 +259,11 @@ type mockTracePoller struct { t *testing.T } -func (m *mockTracePoller) Poll(_ context.Context, test model.Test, run model.Run, pollingProfile pollingprofile.PollingProfile) { +func (m *mockTracePoller) Poll(_ context.Context, test test.Test, run test.Run, pollingProfile pollingprofile.PollingProfile) { m.Called(test.ID) } -func (m *mockTracePoller) expectPoll(test model.Test) *mock.Call { +func (m *mockTracePoller) expectPoll(test test.Test) *mock.Call { return m. On("Poll", test.ID) } diff --git a/server/executor/selector_based_poller_executor.go b/server/executor/selector_based_poller_executor.go index 7b1a141ef9..5d1b8f60a6 100644 --- a/server/executor/selector_based_poller_executor.go +++ b/server/executor/selector_based_poller_executor.go @@ -4,8 +4,8 @@ import ( "fmt" "strconv" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/model/events" + "github.com/kubeshop/tracetest/server/test" ) const ( @@ -22,7 +22,7 @@ func NewSelectorBasedPoller(innerPoller PollerExecutor, eventEmitter EventEmitte return selectorBasedPollerExecutor{innerPoller, eventEmitter} } -func (pe selectorBasedPollerExecutor) ExecuteRequest(request *PollingRequest) (bool, string, model.Run, error) { +func (pe selectorBasedPollerExecutor) ExecuteRequest(request *PollingRequest) (bool, string, test.Run, error) { ready, reason, run, err := pe.pollerExecutor.ExecuteRequest(request) if !ready { request.SetHeaderInt(selectorBasedPollerExecutorRetryHeader, 0) @@ -85,14 +85,12 @@ func (pe selectorBasedPollerExecutor) getNumberTries(request *PollingRequest) in func (pe selectorBasedPollerExecutor) allSelectorsMatchSpans(request *PollingRequest) bool { allSelectorsHaveMatch := true - request.test.Specs.ForEach(func(selectorQuery model.SpanQuery, _ model.NamedAssertions) error { - spans := selector(selectorQuery).Filter(*request.run.Trace) + for _, spec := range request.test.Specs { + spans := selector(spec.Selector).Filter(*request.run.Trace) if len(spans) == 0 { allSelectorsHaveMatch = false } - - return nil - }) + } return allSelectorsHaveMatch } diff --git a/server/executor/selector_based_poller_executor_test.go b/server/executor/selector_based_poller_executor_test.go index 37fe87ae5a..fb32ad9082 100644 --- a/server/executor/selector_based_poller_executor_test.go +++ b/server/executor/selector_based_poller_executor_test.go @@ -7,7 +7,7 @@ import ( "github.com/kubeshop/tracetest/server/executor" "github.com/kubeshop/tracetest/server/executor/pollingprofile" "github.com/kubeshop/tracetest/server/model" - "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -17,9 +17,9 @@ type defaultPollerMock struct { } // ExecuteRequest implements executor.PollerExecutor -func (m *defaultPollerMock) ExecuteRequest(request *executor.PollingRequest) (bool, string, model.Run, error) { +func (m *defaultPollerMock) ExecuteRequest(request *executor.PollingRequest) (bool, string, test.Run, error) { args := m.Called(request) - return args.Bool(0), args.String(1), args.Get(2).(model.Run), args.Error(3) + return args.Bool(0), args.String(1), args.Get(2).(test.Run), args.Error(3) } var _ executor.PollerExecutor = &defaultPollerMock{} @@ -41,7 +41,7 @@ func TestSelectorBasedPollerExecutor(t *testing.T) { eventEmitter := new(eventEmitterMock) eventEmitter.On("Emit", mock.Anything, mock.Anything).Return(nil) - createRequest := func(test model.Test, run model.Run) *executor.PollingRequest { + createRequest := func(test test.Test, run test.Run) *executor.PollingRequest { pollingProfile := pollingprofile.DefaultPollingProfile pollingProfile.Periodic.SelectorMatchRetries = 3 @@ -53,9 +53,9 @@ func TestSelectorBasedPollerExecutor(t *testing.T) { defaultPoller := new(defaultPollerMock) selectorBasedPoller := executor.NewSelectorBasedPoller(defaultPoller, eventEmitter) - request := createRequest(model.Test{}, model.Run{}) + request := createRequest(test.Test{}, test.Run{}) - defaultPoller.On("ExecuteRequest", mock.Anything).Return(false, "", model.Run{}, nil) + defaultPoller.On("ExecuteRequest", mock.Anything).Return(false, "", test.Run{}, nil) ready, _, _, _ := selectorBasedPoller.ExecuteRequest(request) assert.False(t, ready) @@ -65,15 +65,16 @@ func TestSelectorBasedPollerExecutor(t *testing.T) { defaultPoller := new(defaultPollerMock) selectorBasedPoller := executor.NewSelectorBasedPoller(defaultPoller, eventEmitter) - specs := maps.Ordered[model.SpanQuery, model.NamedAssertions]{}. - MustAdd(`span[name = "Tracetest trigger"]`, model.NamedAssertions{}). - MustAdd(`span[name = "GET /api/tests"]`, model.NamedAssertions{}) - test := model.Test{Specs: specs} + specs := test.Specs{ + {Selector: test.SpanQuery(`span[name = "Tracetest trigger"]`), Assertions: []test.Assertion{}}, + {Selector: test.SpanQuery(`span[name = "GET /api/tests"]`), Assertions: []test.Assertion{}}, + } + testObj := test.Test{Specs: specs} trace := model.NewTrace(randomIDGenerator.TraceID().String(), make([]model.Span, 0)) - run := model.Run{Trace: &trace} + run := test.Run{Trace: &trace} - request := createRequest(test, run) + request := createRequest(testObj, run) defaultPoller.On("ExecuteRequest", mock.Anything).Return(true, "all spans found", run, nil) ready, _, _, _ := selectorBasedPoller.ExecuteRequest(request) @@ -86,15 +87,16 @@ func TestSelectorBasedPollerExecutor(t *testing.T) { defaultPoller := new(defaultPollerMock) selectorBasedPoller := executor.NewSelectorBasedPoller(defaultPoller, eventEmitter) - specs := maps.Ordered[model.SpanQuery, model.NamedAssertions]{}. - MustAdd(`span[name = "Tracetest trigger"]`, model.NamedAssertions{}). - MustAdd(`span[name = "GET /api/tests"]`, model.NamedAssertions{}) - test := model.Test{Specs: specs} + specs := test.Specs{ + {Selector: test.SpanQuery(`span[name = "Tracetest trigger"]`), Assertions: []test.Assertion{}}, + {Selector: test.SpanQuery(`span[name = "GET /api/tests"]`), Assertions: []test.Assertion{}}, + } + testObj := test.Test{Specs: specs} trace := model.NewTrace(randomIDGenerator.TraceID().String(), make([]model.Span, 0)) - run := model.Run{Trace: &trace} + run := test.Run{Trace: &trace} - request := createRequest(test, run) + request := createRequest(testObj, run) defaultPoller.On("ExecuteRequest", mock.Anything).Return(false, "trace not found", run, nil).Once() @@ -123,19 +125,20 @@ func TestSelectorBasedPollerExecutor(t *testing.T) { defaultPoller := new(defaultPollerMock) selectorBasedPoller := executor.NewSelectorBasedPoller(defaultPoller, eventEmitter) - specs := maps.Ordered[model.SpanQuery, model.NamedAssertions]{}. - MustAdd(`span[name = "Tracetest trigger"]`, model.NamedAssertions{}). - MustAdd(`span[name = "GET /api/tests"]`, model.NamedAssertions{}) - test := model.Test{Specs: specs} + specs := test.Specs{ + {Selector: test.SpanQuery(`span[name = "Tracetest trigger"]`), Assertions: []test.Assertion{}}, + {Selector: test.SpanQuery(`span[name = "GET /api/tests"]`), Assertions: []test.Assertion{}}, + } + testObj := test.Test{Specs: specs} rootSpan := model.Span{ID: randomIDGenerator.SpanID(), Name: "Tracetest trigger", Attributes: make(model.Attributes)} trace := model.NewTrace(randomIDGenerator.TraceID().String(), []model.Span{ rootSpan, {ID: randomIDGenerator.SpanID(), Name: "GET /api/tests", Attributes: model.Attributes{model.TracetestMetadataFieldParentID: rootSpan.ID.String()}}, }) - run := model.Run{Trace: &trace} + run := test.Run{Trace: &trace} - request := createRequest(test, run) + request := createRequest(testObj, run) defaultPoller.On("ExecuteRequest", mock.Anything).Return(true, "all spans found", run, nil) diff --git a/server/executor/trace_poller.go b/server/executor/trace_poller.go index 0a4c357997..b047d3e595 100644 --- a/server/executor/trace_poller.go +++ b/server/executor/trace_poller.go @@ -10,9 +10,9 @@ import ( "github.com/kubeshop/tracetest/server/analytics" "github.com/kubeshop/tracetest/server/executor/pollingprofile" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/model/events" "github.com/kubeshop/tracetest/server/subscription" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/tracedb/connection" "go.opentelemetry.io/otel/propagation" v1 "go.opentelemetry.io/proto/otlp/trace/v1" @@ -21,7 +21,7 @@ import ( const PollingRequestStartIteration = 1 type TracePoller interface { - Poll(context.Context, model.Test, model.Run, pollingprofile.PollingProfile) + Poll(context.Context, test.Test, test.Run, pollingprofile.PollingProfile) } type PersistentTracePoller interface { @@ -30,7 +30,7 @@ type PersistentTracePoller interface { } type PollerExecutor interface { - ExecuteRequest(*PollingRequest) (bool, string, model.Run, error) + ExecuteRequest(*PollingRequest) (bool, string, test.Run, error) } type TraceFetcher interface { @@ -74,8 +74,8 @@ type tracePoller struct { } type PollingRequest struct { - test model.Test - run model.Run + test test.Test + run test.Run pollingProfile pollingprofile.PollingProfile count int headers map[string]string @@ -122,7 +122,7 @@ func (pr PollingRequest) IsFirstRequest() bool { return !pr.HeaderBool("requeued") } -func NewPollingRequest(ctx context.Context, test model.Test, run model.Run, count int, pollingProfile pollingprofile.PollingProfile) *PollingRequest { +func NewPollingRequest(ctx context.Context, test test.Test, run test.Run, count int, pollingProfile pollingprofile.PollingProfile) *PollingRequest { propagator := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}) request := &PollingRequest{ @@ -167,7 +167,7 @@ func (tp tracePoller) Stop() { tp.exit <- true } -func (tp tracePoller) Poll(ctx context.Context, test model.Test, run model.Run, pollingProfile pollingprofile.PollingProfile) { +func (tp tracePoller) Poll(ctx context.Context, test test.Test, run test.Run, pollingProfile pollingprofile.PollingProfile) { log.Printf("[TracePoller] Test %s Run %d: Poll\n", test.ID, run.ID) job := NewPollingRequest(ctx, test, run, PollingRequestStartIteration, pollingProfile) diff --git a/server/executor/transaction_runner.go b/server/executor/transaction_runner.go index 991e9a55dd..3d8941e663 100644 --- a/server/executor/transaction_runner.go +++ b/server/executor/transaction_runner.go @@ -5,14 +5,14 @@ import ( "fmt" "github.com/kubeshop/tracetest/server/environment" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/maps" "github.com/kubeshop/tracetest/server/subscription" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/transaction" ) type TransactionRunner interface { - Run(context.Context, transaction.Transaction, model.RunMetadata, environment.Environment) transaction.TransactionRun + Run(context.Context, transaction.Transaction, test.RunMetadata, environment.Environment) transaction.TransactionRun } type PersistentTransactionRunner interface { @@ -27,7 +27,7 @@ type transactionRunRepository interface { func NewTransactionRunner( runner Runner, - db model.Repository, + db test.Repository, transactionRuns transactionRunRepository, subscriptionManager *subscription.Manager, ) persistentTransactionRunner { @@ -54,7 +54,7 @@ type transactionRunJob struct { type persistentTransactionRunner struct { testRunner Runner - db model.Repository + db test.Repository transactionRuns transactionRunRepository updater TransactionRunUpdater subscriptionManager *subscription.Manager @@ -62,7 +62,7 @@ type persistentTransactionRunner struct { exit chan bool } -func (r persistentTransactionRunner) Run(ctx context.Context, transaction transaction.Transaction, metadata model.RunMetadata, environment environment.Environment) transaction.TransactionRun { +func (r persistentTransactionRunner) Run(ctx context.Context, transaction transaction.Transaction, metadata test.RunMetadata, environment environment.Environment) transaction.TransactionRun { run := transaction.NewRun() run.Metadata = metadata run.Environment = environment @@ -133,8 +133,8 @@ func (r persistentTransactionRunner) runTransaction(ctx context.Context, tran tr return r.updater.Update(ctx, run) } -func (r persistentTransactionRunner) runTransactionStep(ctx context.Context, tr transaction.TransactionRun, step int, test model.Test) (transaction.TransactionRun, error) { - testRun := r.testRunner.Run(ctx, test, tr.Metadata, tr.Environment) +func (r persistentTransactionRunner) runTransactionStep(ctx context.Context, tr transaction.TransactionRun, step int, testObj test.Test) (transaction.TransactionRun, error) { + testRun := r.testRunner.Run(ctx, testObj, tr.Metadata, tr.Environment) tr, err := r.updateStepRun(ctx, tr, step, testRun) if err != nil { return transaction.TransactionRun{}, fmt.Errorf("could not update transaction run: %w", err) @@ -144,7 +144,7 @@ func (r persistentTransactionRunner) runTransactionStep(ctx context.Context, tr // listen for updates and propagate them as if they were transaction updates r.subscriptionManager.Subscribe(testRun.ResourceID(), subscription.NewSubscriberFunction( func(m subscription.Message) error { - testRun := m.Content.(model.Run) + testRun := m.Content.(test.Run) if testRun.LastError != nil { tr.State = transaction.TransactionRunStateFailed tr.LastError = testRun.LastError @@ -175,9 +175,9 @@ func (r persistentTransactionRunner) runTransactionStep(ctx context.Context, tr return tr, err } -func (r persistentTransactionRunner) updateStepRun(ctx context.Context, tr transaction.TransactionRun, step int, run model.Run) (transaction.TransactionRun, error) { +func (r persistentTransactionRunner) updateStepRun(ctx context.Context, tr transaction.TransactionRun, step int, run test.Run) (transaction.TransactionRun, error) { if len(tr.Steps) <= step { - tr.Steps = append(tr.Steps, model.Run{}) + tr.Steps = append(tr.Steps, test.Run{}) } tr.Steps[step] = run @@ -189,9 +189,9 @@ func (r persistentTransactionRunner) updateStepRun(ctx context.Context, tr trans return tr, nil } -func mergeOutputsIntoEnv(env environment.Environment, outputs maps.Ordered[string, model.RunOutput]) environment.Environment { +func mergeOutputsIntoEnv(env environment.Environment, outputs maps.Ordered[string, test.RunOutput]) environment.Environment { newEnv := make([]environment.EnvironmentValue, 0, outputs.Len()) - outputs.ForEach(func(key string, val model.RunOutput) error { + outputs.ForEach(func(key string, val test.RunOutput) error { newEnv = append(newEnv, environment.EnvironmentValue{ Key: key, Value: val.Value, diff --git a/server/executor/transaction_runner_test.go b/server/executor/transaction_runner_test.go index d018b0ab87..bf00739311 100644 --- a/server/executor/transaction_runner_test.go +++ b/server/executor/transaction_runner_test.go @@ -14,7 +14,7 @@ import ( "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/pkg/maps" "github.com/kubeshop/tracetest/server/subscription" - "github.com/kubeshop/tracetest/server/testdb" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/testmock" "github.com/kubeshop/tracetest/server/transaction" "github.com/stretchr/testify/assert" @@ -22,17 +22,17 @@ import ( ) type fakeTestRunner struct { - db model.Repository + db test.RunRepository subscriptionManager *subscription.Manager returnErr bool uid int } -func (r *fakeTestRunner) Run(ctx context.Context, test model.Test, metadata model.RunMetadata, env environment.Environment) model.Run { - run := model.NewRun() +func (r *fakeTestRunner) Run(ctx context.Context, testObj test.Test, metadata test.RunMetadata, env environment.Environment) test.Run { + run := test.NewRun() run.Environment = env - run.State = model.RunStateCreated - newRun, err := r.db.CreateRun(ctx, test, run) + run.State = test.RunStateCreated + newRun, err := r.db.CreateRun(ctx, testObj, run) if err != nil { panic(err) } @@ -42,15 +42,15 @@ func (r *fakeTestRunner) Run(ctx context.Context, test model.Test, metadata mode time.Sleep(100 * time.Millisecond) // simulate some real work if r.returnErr { - run.State = model.RunStateTriggerFailed + run.State = test.RunStateTriggerFailed run.LastError = fmt.Errorf("failed to do something") } else { - run.State = model.RunStateFinished + run.State = test.RunStateFinished } r.uid++ - run.Outputs = (maps.Ordered[string, model.RunOutput]{}).MustAdd("USER_ID", model.RunOutput{ + run.Outputs = (maps.Ordered[string, test.RunOutput]{}).MustAdd("USER_ID", test.RunOutput{ Value: strconv.Itoa(r.uid), }) @@ -71,11 +71,11 @@ func TestTransactionRunner(t *testing.T) { runTransactionRunnerTest(t, false, func(t *testing.T, actual transaction.TransactionRun) { assert.Equal(t, transaction.TransactionRunStateFinished, actual.State) assert.Len(t, actual.Steps, 2) - assert.Equal(t, actual.Steps[0].State, model.RunStateFinished) - assert.Equal(t, actual.Steps[1].State, model.RunStateFinished) + assert.Equal(t, actual.Steps[0].State, test.RunStateFinished) + assert.Equal(t, actual.Steps[1].State, test.RunStateFinished) assert.Equal(t, "http://my-service.com", actual.Environment.Get("url")) - assert.Equal(t, model.RunOutput{Name: "", Value: "1", SpanID: ""}, actual.Steps[0].Outputs.Get("USER_ID")) + assert.Equal(t, test.RunOutput{Name: "", Value: "1", SpanID: ""}, actual.Steps[0].Outputs.Get("USER_ID")) // this assertion is supposed to test that the output from the previous step // is injected in the env for the next. In practice, this depends @@ -83,7 +83,7 @@ func TestTransactionRunner(t *testing.T) { // to the test run, like the real test runner would. // see line 27 assert.Equal(t, "1", actual.Steps[1].Environment.Get("USER_ID")) - assert.Equal(t, model.RunOutput{Name: "", Value: "2", SpanID: ""}, actual.Steps[1].Outputs.Get("USER_ID")) + assert.Equal(t, test.RunOutput{Name: "", Value: "2", SpanID: ""}, actual.Steps[1].Outputs.Get("USER_ID")) assert.Equal(t, "2", actual.Environment.Get("USER_ID")) @@ -94,7 +94,7 @@ func TestTransactionRunner(t *testing.T) { runTransactionRunnerTest(t, true, func(t *testing.T, actual transaction.TransactionRun) { assert.Equal(t, transaction.TransactionRunStateFailed, actual.State) require.Len(t, actual.Steps, 1) - assert.Equal(t, model.RunStateTriggerFailed, actual.Steps[0].State) + assert.Equal(t, test.RunStateTriggerFailed, actual.Steps[0].State) }) }) @@ -109,26 +109,27 @@ func getDB() (model.Repository, *sql.DB) { func runTransactionRunnerTest(t *testing.T, withErrors bool, assert func(t *testing.T, actual transaction.TransactionRun)) { ctx := context.Background() - db, rawDB := getDB() + _, rawDB := getDB() subscriptionManager := subscription.NewManager() + testRepo := test.NewRepository(rawDB) + runRepo := test.NewRunRepository(rawDB) testRunner := &fakeTestRunner{ - db, + runRepo, subscriptionManager, withErrors, 0, } - test1, err := db.CreateTest(ctx, model.Test{Name: "Test 1"}) + test1, err := testRepo.Create(ctx, test.Test{Name: "Test 1"}) require.NoError(t, err) - test2, err := db.CreateTest(ctx, model.Test{Name: "Test 2"}) + test2, err := testRepo.Create(ctx, test.Test{Name: "Test 2"}) require.NoError(t, err) - testsRepo, _ := testdb.Postgres(testdb.WithDB(rawDB)) - transactionsRepo := transaction.NewRepository(rawDB, testsRepo) - transactionRunRepo := transaction.NewRunRepository(rawDB, testsRepo) + transactionsRepo := transaction.NewRepository(rawDB, testRepo) + transactionRunRepo := transaction.NewRunRepository(rawDB, runRepo) tran, err := transactionsRepo.Create(ctx, transaction.Transaction{ Name: "transaction", StepIDs: []id.ID{test1.ID, test2.ID}, @@ -138,7 +139,7 @@ func runTransactionRunnerTest(t *testing.T, withErrors bool, assert func(t *test tran, err = transactionsRepo.GetAugmented(context.TODO(), tran.ID) require.NoError(t, err) - metadata := model.RunMetadata{ + metadata := test.RunMetadata{ "environment": "production", "service": "tracetest", } @@ -155,7 +156,7 @@ func runTransactionRunnerTest(t *testing.T, withErrors bool, assert func(t *test }) require.NoError(t, err) - runner := executor.NewTransactionRunner(testRunner, db, transactionRunRepo, subscriptionManager) + runner := executor.NewTransactionRunner(testRunner, testRepo, transactionRunRepo, subscriptionManager) runner.Start(1) ctxWithTimeout, cancel := context.WithTimeout(ctx, 1*time.Second) diff --git a/server/executor/trigger/grpc.go b/server/executor/trigger/grpc.go index 5ef722296f..f7dcb87687 100644 --- a/server/executor/trigger/grpc.go +++ b/server/executor/trigger/grpc.go @@ -12,7 +12,8 @@ import ( "github.com/golang/protobuf/jsonpb" "github.com/golang/protobuf/proto" "github.com/jhump/protoreflect/desc" - "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -27,23 +28,23 @@ func GRPC() Triggerer { type grpcTriggerer struct{} -func (te *grpcTriggerer) Trigger(ctx context.Context, test model.Test, opts *TriggerOptions) (Response, error) { +func (te *grpcTriggerer) Trigger(ctx context.Context, test test.Test, opts *TriggerOptions) (Response, error) { response := Response{ - Result: model.TriggerResult{ + Result: trigger.TriggerResult{ Type: te.Type(), }, } - trigger := test.ServiceUnderTest - if trigger.Type != model.TriggerTypeGRPC { - return response, fmt.Errorf(`trigger type "%s" not supported by GRPC triggerer`, trigger.Type) + triggerObj := test.Trigger + if triggerObj.Type != trigger.TriggerTypeGRPC { + return response, fmt.Errorf(`trigger type "%s" not supported by GRPC triggerer`, triggerObj.Type) } - if trigger.GRPC == nil { + if triggerObj.GRPC == nil { return response, fmt.Errorf("no settings provided for GRPC triggerer") } - tReq := trigger.GRPC + tReq := triggerObj.GRPC conn, err := te.dial(ctx, tReq.Address) if err != nil { @@ -85,7 +86,7 @@ func (te *grpcTriggerer) Trigger(ctx context.Context, test model.Test, opts *Tri return response, err } - response.Result.GRPC = &model.GRPCResponse{ + response.Result.GRPC = &trigger.GRPCResponse{ Metadata: mapHeaders(h.respMD), StatusCode: int(h.respCode), Status: h.respCode.String(), @@ -100,12 +101,12 @@ func (te *grpcTriggerer) Trigger(ctx context.Context, test model.Test, opts *Tri return response, nil } -func (t *grpcTriggerer) Type() model.TriggerType { - return model.TriggerTypeGRPC +func (t *grpcTriggerer) Type() trigger.TriggerType { + return trigger.TriggerTypeGRPC } -func (t *grpcTriggerer) Resolve(ctx context.Context, test model.Test, opts *TriggerOptions) (model.Test, error) { - grpc := test.ServiceUnderTest.GRPC +func (t *grpcTriggerer) Resolve(ctx context.Context, test test.Test, opts *TriggerOptions) (test.Test, error) { + grpc := test.Trigger.GRPC if grpc == nil { return test, fmt.Errorf("no settings provided for GRPC triggerer") @@ -178,11 +179,11 @@ func protoDescription(content string) (grpcurl.DescriptorSource, error) { } -func mapHeaders(md metadata.MD) []model.GRPCHeader { - var mappedHeaders []model.GRPCHeader +func mapHeaders(md metadata.MD) []trigger.GRPCHeader { + var mappedHeaders []trigger.GRPCHeader for key, headers := range md { for _, val := range headers { - val := model.GRPCHeader{ + val := trigger.GRPCHeader{ Key: key, Value: val, } diff --git a/server/executor/trigger/http.go b/server/executor/trigger/http.go index 452f8108f5..428355cb97 100644 --- a/server/executor/trigger/http.go +++ b/server/executor/trigger/http.go @@ -14,7 +14,8 @@ import ( "github.com/goware/urlx" "github.com/kubeshop/tracetest/server/expression" - "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" ) @@ -61,25 +62,25 @@ func newSpanContext(ctx context.Context) trace.SpanContext { }) } -func (te *httpTriggerer) Trigger(ctx context.Context, test model.Test, opts *TriggerOptions) (Response, error) { +func (te *httpTriggerer) Trigger(ctx context.Context, test test.Test, opts *TriggerOptions) (Response, error) { response := Response{ - Result: model.TriggerResult{ + Result: trigger.TriggerResult{ Type: te.Type(), }, } - trigger := test.ServiceUnderTest - if trigger.Type != model.TriggerTypeHTTP { - return response, fmt.Errorf(`trigger type "%s" not supported by HTTP triggerer`, trigger.Type) + triggerObj := test.Trigger + if triggerObj.Type != trigger.TriggerTypeHTTP { + return response, fmt.Errorf(`trigger type "%s" not supported by HTTP triggerer`, triggerObj.Type) } - client := httpClient(trigger.HTTP.SSLVerification) + client := httpClient(triggerObj.HTTP.SSLVerification) ctx = trace.ContextWithSpanContext(ctx, newSpanContext(ctx)) ctx, cncl := context.WithTimeout(ctx, 30*time.Second) defer cncl() - tReq := trigger.HTTP + tReq := triggerObj.HTTP var body io.Reader if tReq.Body != "" { body = bytes.NewBufferString(tReq.Body) @@ -115,12 +116,12 @@ func (te *httpTriggerer) Trigger(ctx context.Context, test model.Test, opts *Tri return response, nil } -func (t *httpTriggerer) Type() model.TriggerType { - return model.TriggerTypeHTTP +func (t *httpTriggerer) Type() trigger.TriggerType { + return trigger.TriggerTypeHTTP } -func (t *httpTriggerer) Resolve(ctx context.Context, test model.Test, opts *TriggerOptions) (model.Test, error) { - http := test.ServiceUnderTest.HTTP +func (t *httpTriggerer) Resolve(ctx context.Context, test test.Test, opts *TriggerOptions) (test.Test, error) { + http := test.Trigger.HTTP if http == nil { return test, fmt.Errorf("no settings provided for HTTP triggerer") @@ -134,7 +135,7 @@ func (t *httpTriggerer) Resolve(ctx context.Context, test model.Test, opts *Trig http.URL = url - headers := []model.HTTPHeader{} + headers := []trigger.HTTPHeader{} for _, h := range http.Headers { h.Key, err = opts.Executor.ResolveStatement(WrapInQuotes(h.Key, "\"")) if err != nil { @@ -162,12 +163,12 @@ func (t *httpTriggerer) Resolve(ctx context.Context, test model.Test, opts *Trig return test, err } - test.ServiceUnderTest.HTTP = http + test.Trigger.HTTP = http return test, nil } -func resolveAuth(auth *model.HTTPAuthenticator, executor expression.Executor) (*model.HTTPAuthenticator, error) { +func resolveAuth(auth *trigger.HTTPAuthenticator, executor expression.Executor) (*trigger.HTTPAuthenticator, error) { if auth == nil { return nil, nil } @@ -183,11 +184,11 @@ func resolveAuth(auth *model.HTTPAuthenticator, executor expression.Executor) (* return &updated, err } -func mapResp(resp *http.Response) model.HTTPResponse { - var mappedHeaders []model.HTTPHeader +func mapResp(resp *http.Response) trigger.HTTPResponse { + var mappedHeaders []trigger.HTTPHeader for key, headers := range resp.Header { for _, val := range headers { - val := model.HTTPHeader{ + val := trigger.HTTPHeader{ Key: key, Value: val, } @@ -201,7 +202,7 @@ func mapResp(resp *http.Response) model.HTTPResponse { fmt.Println(err) } - return model.HTTPResponse{ + return trigger.HTTPResponse{ Status: resp.Status, StatusCode: resp.StatusCode, Headers: mappedHeaders, diff --git a/server/executor/trigger/http_test.go b/server/executor/trigger/http_test.go index b562d3f1c1..a70fa4f959 100644 --- a/server/executor/trigger/http_test.go +++ b/server/executor/trigger/http_test.go @@ -7,9 +7,10 @@ import ( "net/http/httptest" "testing" - "github.com/kubeshop/tracetest/server/executor/trigger" - "github.com/kubeshop/tracetest/server/model" + triggerer "github.com/kubeshop/tracetest/server/executor/trigger" "github.com/kubeshop/tracetest/server/pkg/id" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/trace" ) @@ -44,14 +45,14 @@ func TestTriggerGet(t *testing.T) { })) defer server.Close() - test := model.Test{ + test := test.Test{ Name: "test", - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ + Trigger: trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ URL: server.URL, - Method: model.HTTPMethodGET, - Headers: []model.HTTPHeader{ + Method: trigger.HTTPMethodGET, + Headers: []trigger.HTTPHeader{ {Key: "Key1", Value: "Value1"}, }, Body: "body", @@ -59,7 +60,7 @@ func TestTriggerGet(t *testing.T) { }, } - ex := trigger.HTTP() + ex := triggerer.HTTP() resp, err := ex.Trigger(createContext(), test, nil) assert.NoError(t, err) @@ -91,14 +92,14 @@ func TestTriggerPost(t *testing.T) { })) defer server.Close() - test := model.Test{ + test := test.Test{ Name: "test", - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ + Trigger: trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ URL: server.URL, - Method: model.HTTPMethodPOST, - Headers: []model.HTTPHeader{ + Method: trigger.HTTPMethodPOST, + Headers: []trigger.HTTPHeader{ {Key: "Key1", Value: "Value1"}, }, Body: "body", @@ -106,7 +107,7 @@ func TestTriggerPost(t *testing.T) { }, } - ex := trigger.HTTP() + ex := triggerer.HTTP() resp, err := ex.Trigger(createContext(), test, nil) assert.NoError(t, err) @@ -144,22 +145,22 @@ func TestTriggerPostWithApiKeyAuth(t *testing.T) { })) defer server.Close() - test := model.Test{ + test := test.Test{ Name: "test", - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ + Trigger: trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ URL: server.URL, - Method: model.HTTPMethodPOST, - Headers: []model.HTTPHeader{ + Method: trigger.HTTPMethodPOST, + Headers: []trigger.HTTPHeader{ {Key: "Key1", Value: "Value1"}, }, - Auth: &model.HTTPAuthenticator{ + Auth: &trigger.HTTPAuthenticator{ Type: "apiKey", - APIKey: model.APIKeyAuthenticator{ + APIKey: &trigger.APIKeyAuthenticator{ Key: "key", Value: "value", - In: model.APIKeyPositionHeader, + In: trigger.APIKeyPositionHeader, }, }, Body: "body", @@ -167,7 +168,7 @@ func TestTriggerPostWithApiKeyAuth(t *testing.T) { }, } - ex := trigger.HTTP() + ex := triggerer.HTTP() resp, err := ex.Trigger(createContext(), test, nil) assert.NoError(t, err) @@ -205,19 +206,19 @@ func TestTriggerPostWithBasicAuth(t *testing.T) { })) defer server.Close() - test := model.Test{ + test := test.Test{ Name: "test", - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ + Trigger: trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ URL: server.URL, - Method: model.HTTPMethodPOST, - Headers: []model.HTTPHeader{ + Method: trigger.HTTPMethodPOST, + Headers: []trigger.HTTPHeader{ {Key: "Key1", Value: "Value1"}, }, - Auth: &model.HTTPAuthenticator{ + Auth: &trigger.HTTPAuthenticator{ Type: "basic", - Basic: model.BasicAuthenticator{ + Basic: &trigger.BasicAuthenticator{ Username: "username", Password: "password", }, @@ -227,7 +228,7 @@ func TestTriggerPostWithBasicAuth(t *testing.T) { }, } - ex := trigger.HTTP() + ex := triggerer.HTTP() resp, err := ex.Trigger(createContext(), test, nil) assert.NoError(t, err) @@ -265,19 +266,19 @@ func TestTriggerPostWithBearerAuth(t *testing.T) { })) defer server.Close() - test := model.Test{ + test := test.Test{ Name: "test", - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ + Trigger: trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ URL: server.URL, - Method: model.HTTPMethodPOST, - Headers: []model.HTTPHeader{ + Method: trigger.HTTPMethodPOST, + Headers: []trigger.HTTPHeader{ {Key: "Key1", Value: "Value1"}, }, - Auth: &model.HTTPAuthenticator{ + Auth: &trigger.HTTPAuthenticator{ Type: "bearer", - Bearer: model.BearerAuthenticator{ + Bearer: &trigger.BearerAuthenticator{ Bearer: "token", }, }, @@ -286,7 +287,7 @@ func TestTriggerPostWithBearerAuth(t *testing.T) { }, } - ex := trigger.HTTP() + ex := triggerer.HTTP() resp, err := ex.Trigger(createContext(), test, nil) assert.NoError(t, err) diff --git a/server/executor/trigger/instrument.go b/server/executor/trigger/instrument.go index 254ae0587c..9a4342512a 100644 --- a/server/executor/trigger/instrument.go +++ b/server/executor/trigger/instrument.go @@ -5,6 +5,8 @@ import ( "fmt" "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "go.opentelemetry.io/contrib/propagators/aws/xray" "go.opentelemetry.io/contrib/propagators/b3" "go.opentelemetry.io/contrib/propagators/jaeger" @@ -28,15 +30,15 @@ type instrumentedTriggerer struct { triggerer Triggerer } -func (t *instrumentedTriggerer) Type() model.TriggerType { - return model.TriggerType("instrumented") +func (t *instrumentedTriggerer) Type() trigger.TriggerType { + return trigger.TriggerType("instrumented") } -func (t *instrumentedTriggerer) Resolve(ctx context.Context, test model.Test, opts *TriggerOptions) (model.Test, error) { +func (t *instrumentedTriggerer) Resolve(ctx context.Context, test test.Test, opts *TriggerOptions) (test.Test, error) { return t.triggerer.Resolve(ctx, test, opts) } -func (t *instrumentedTriggerer) Trigger(ctx context.Context, test model.Test, opts *TriggerOptions) (Response, error) { +func (t *instrumentedTriggerer) Trigger(ctx context.Context, test test.Test, opts *TriggerOptions) (Response, error) { _, span := t.tracer.Start(ctx, "Trigger test") defer span.End() diff --git a/server/executor/trigger/traceid.go b/server/executor/trigger/traceid.go index 79b2f67095..887a08a720 100644 --- a/server/executor/trigger/traceid.go +++ b/server/executor/trigger/traceid.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" ) func TRACEID() Triggerer { @@ -13,23 +14,23 @@ func TRACEID() Triggerer { type traceidTriggerer struct{} -func (t *traceidTriggerer) Trigger(ctx context.Context, test model.Test, opts *TriggerOptions) (Response, error) { +func (t *traceidTriggerer) Trigger(ctx context.Context, test test.Test, opts *TriggerOptions) (Response, error) { response := Response{ - Result: model.TriggerResult{ + Result: trigger.TriggerResult{ Type: t.Type(), - TRACEID: &model.TRACEIDResponse{ID: test.ServiceUnderTest.TraceID.ID}, + TraceID: &trigger.TraceIDResponse{ID: test.Trigger.TraceID.ID}, }, } return response, nil } -func (t *traceidTriggerer) Type() model.TriggerType { - return model.TriggerTypeTRACEID +func (t *traceidTriggerer) Type() trigger.TriggerType { + return trigger.TriggerTypeTraceID } -func (t *traceidTriggerer) Resolve(ctx context.Context, test model.Test, opts *TriggerOptions) (model.Test, error) { - traceid := test.ServiceUnderTest.TraceID +func (t *traceidTriggerer) Resolve(ctx context.Context, test test.Test, opts *TriggerOptions) (test.Test, error) { + traceid := test.Trigger.TraceID if traceid == nil { return test, fmt.Errorf("no settings provided for TRACEID triggerer") } @@ -40,7 +41,7 @@ func (t *traceidTriggerer) Resolve(ctx context.Context, test model.Test, opts *T } traceid.ID = id - test.ServiceUnderTest.TraceID = traceid + test.Trigger.TraceID = traceid return test, nil } diff --git a/server/executor/trigger/triggerer.go b/server/executor/trigger/triggerer.go index 58abb22658..3a958880b3 100644 --- a/server/executor/trigger/triggerer.go +++ b/server/executor/trigger/triggerer.go @@ -7,7 +7,8 @@ import ( "sync" "github.com/kubeshop/tracetest/server/expression" - "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "go.opentelemetry.io/otel/trace" ) @@ -17,14 +18,14 @@ type TriggerOptions struct { } type Triggerer interface { - Trigger(context.Context, model.Test, *TriggerOptions) (Response, error) - Type() model.TriggerType - Resolve(context.Context, model.Test, *TriggerOptions) (model.Test, error) + Trigger(context.Context, test.Test, *TriggerOptions) (Response, error) + Type() trigger.TriggerType + Resolve(context.Context, test.Test, *TriggerOptions) (test.Test, error) } type Response struct { SpanAttributes map[string]string - Result model.TriggerResult + Result trigger.TriggerResult TraceID trace.TraceID SpanID trace.SpanID } @@ -33,7 +34,7 @@ func NewRegsitry(tracer, triggerSpanTracer trace.Tracer) *Registry { return &Registry{ tracer: tracer, triggerSpanTracer: triggerSpanTracer, - reg: map[model.TriggerType]Triggerer{}, + reg: map[trigger.TriggerType]Triggerer{}, } } @@ -41,7 +42,7 @@ type Registry struct { sync.Mutex tracer trace.Tracer triggerSpanTracer trace.Tracer - reg map[model.TriggerType]Triggerer + reg map[trigger.TriggerType]Triggerer } func (r *Registry) Add(t Triggerer) { @@ -53,7 +54,7 @@ func (r *Registry) Add(t Triggerer) { var ErrTriggererTypeNotRegistered = errors.New("triggerer type not found") -func (r *Registry) Get(triggererType model.TriggerType) (Triggerer, error) { +func (r *Registry) Get(triggererType trigger.TriggerType) (Triggerer, error) { r.Lock() defer r.Unlock() diff --git a/server/expression/linting/missing_variables_test.go b/server/expression/linting/missing_variables_test.go index ba77686c47..76733a1329 100644 --- a/server/expression/linting/missing_variables_test.go +++ b/server/expression/linting/missing_variables_test.go @@ -5,8 +5,9 @@ import ( "time" "github.com/kubeshop/tracetest/server/expression/linting" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "github.com/stretchr/testify/assert" ) @@ -134,10 +135,10 @@ func TestMissingVariableDetection(t *testing.T) { { name: "should_detect_missing_variables_in_test_http_body", availableVariables: []string{}, - object: model.Test{ - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ + object: test.Test{ + Trigger: trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ Body: `{"id": ${env:pokemonId}}`, }, }, diff --git a/server/go.mod b/server/go.mod index d6c1242390..14ab43f474 100644 --- a/server/go.mod +++ b/server/go.mod @@ -29,6 +29,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/j2gg0s/otsql v0.14.0 github.com/jhump/protoreflect v1.12.0 + github.com/json-iterator/go v1.1.12 github.com/lib/pq v1.10.5 github.com/mitchellh/mapstructure v1.5.0 github.com/ohler55/ojg v1.14.4 @@ -99,6 +100,8 @@ require ( github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mostynb/go-grpc-compression v1.1.16 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect diff --git a/server/go.sum b/server/go.sum index 35fcd101fc..69d97ce81c 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1099,6 +1099,7 @@ github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -1276,9 +1277,11 @@ github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7ID github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= diff --git a/server/http/controller.go b/server/http/controller.go index 57930a01c5..b8ac659e74 100644 --- a/server/http/controller.go +++ b/server/http/controller.go @@ -2,6 +2,7 @@ package http import ( "context" + "database/sql" "encoding/hex" "errors" "fmt" @@ -19,9 +20,9 @@ import ( "github.com/kubeshop/tracetest/server/junit" "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/model/yaml" - "github.com/kubeshop/tracetest/server/model/yaml/yamlconvert" "github.com/kubeshop/tracetest/server/openapi" "github.com/kubeshop/tracetest/server/pkg/id" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/testdb" "github.com/kubeshop/tracetest/server/tracedb" "github.com/kubeshop/tracetest/server/transaction" @@ -41,6 +42,8 @@ type controller struct { testDB model.Repository transactionRepository transactionsRepository transactionRunRepository transactionRunRepository + testRepository test.Repository + testRunRepository test.RunRepository environmentGetter environmentGetter } @@ -64,8 +67,8 @@ type transactionRunRepository interface { type runner interface { StopTest(testID id.ID, runID int) - RunTest(ctx context.Context, test model.Test, rm model.RunMetadata, env environment.Environment) model.Run - RunTransaction(ctx context.Context, tr transaction.Transaction, rm model.RunMetadata, env environment.Environment) transaction.TransactionRun + RunTest(ctx context.Context, test test.Test, rm test.RunMetadata, env environment.Environment) test.Run + RunTransaction(ctx context.Context, tr transaction.Transaction, rm test.RunMetadata, env environment.Environment) transaction.TransactionRun RunAssertions(ctx context.Context, request executor.AssertionRequest) } @@ -77,6 +80,8 @@ func NewController( testDB model.Repository, transactionRepository transactionsRepository, transactionRunRepository transactionRunRepository, + testRepository test.Repository, + testRunRepository test.RunRepository, newTraceDBFn func(ds datastore.DataStore) (tracedb.TraceDB, error), runner runner, mappers mappings.Mappings, @@ -90,6 +95,8 @@ func NewController( testDB: testDB, transactionRepository: transactionRepository, transactionRunRepository: transactionRunRepository, + testRepository: testRepository, + testRunRepository: testRunRepository, environmentGetter: envGetter, runner: runner, newTraceDBFn: newTraceDBFn, @@ -101,75 +108,15 @@ func NewController( func handleDBError(err error) openapi.ImplResponse { switch { - case errors.Is(testdb.ErrNotFound, err): + case errors.Is(testdb.ErrNotFound, err) || errors.Is(sql.ErrNoRows, err): return openapi.Response(http.StatusNotFound, err.Error()) default: return openapi.Response(http.StatusInternalServerError, err.Error()) } } -func (c *controller) CreateTest(ctx context.Context, in openapi.Test) (openapi.ImplResponse, error) { - test, err := c.mappers.In.Test(in) - if err != nil { - return openapi.Response(http.StatusBadRequest, err.Error()), nil - } - - return c.doCreateTest(ctx, test) -} - -var errTestExists = errors.New("test already exists") - -func (c *controller) doCreateTest(ctx context.Context, test model.Test) (openapi.ImplResponse, error) { - // if they try to create a test with preset ID, we need to make sure that ID doesn't exist already - if test.HasID() { - exists, err := c.testDB.TestIDExists(ctx, test.ID) - - if err != nil { - return handleDBError(err), err - } - - if exists { - err := fmt.Errorf(`cannot create test with ID "%s: %w`, test.ID, errTestExists) - r := map[string]string{ - "error": err.Error(), - } - return openapi.Response(http.StatusBadRequest, r), err - } - } - - test, err := c.testDB.CreateTest(ctx, test) - if err != nil { - return openapi.Response(http.StatusInternalServerError, err.Error()), err - } - - return openapi.Response(200, c.mappers.Out.Test(test)), nil -} - -func (c *controller) DeleteTest(ctx context.Context, testID string) (openapi.ImplResponse, error) { - test, err := c.testDB.GetLatestTestVersion(ctx, id.ID(testID)) - if err != nil { - return handleDBError(err), err - } - - err = c.testDB.DeleteTest(ctx, test) - if err != nil { - return openapi.Response(http.StatusInternalServerError, err.Error()), err - } - - return openapi.Response(204, nil), nil -} - -func (c *controller) GetTest(ctx context.Context, testID string) (openapi.ImplResponse, error) { - test, err := c.testDB.GetLatestTestVersion(ctx, id.ID(testID)) - if err != nil { - return handleDBError(err), err - } - - return openapi.Response(200, c.mappers.Out.Test(test)), nil -} - func (c *controller) GetTestSpecs(ctx context.Context, testID string) (openapi.ImplResponse, error) { - test, err := c.testDB.GetLatestTestVersion(ctx, id.ID(testID)) + test, err := c.testRepository.Get(ctx, id.ID(testID)) if err != nil { return handleDBError(err), err } @@ -184,7 +131,7 @@ func (c *controller) GetTestResultSelectedSpans(ctx context.Context, testID stri return handleDBError(err), err } - run, err := c.testDB.GetRun(ctx, id.ID(testID), int(runID)) + run, err := c.testRunRepository.GetRun(ctx, id.ID(testID), int(runID)) if err != nil { return openapi.Response(http.StatusInternalServerError, ""), nil } @@ -201,7 +148,7 @@ func (c *controller) GetTestResultSelectedSpans(ctx context.Context, testID stri } res := openapi.SelectedSpansResult{ - Selector: c.mappers.Out.Selector(model.SpanQuery(selectorQuery)), + Selector: c.mappers.Out.Selector(test.SpanQuery(selectorQuery)), SpanIds: selectedSpanIds, } @@ -209,7 +156,7 @@ func (c *controller) GetTestResultSelectedSpans(ctx context.Context, testID stri } func (c *controller) GetTestRun(ctx context.Context, testID string, runID int32) (openapi.ImplResponse, error) { - run, err := c.testDB.GetRun(ctx, id.ID(testID), int(runID)) + run, err := c.testRunRepository.GetRun(ctx, id.ID(testID), int(runID)) if err != nil { return handleDBError(err), err } @@ -227,12 +174,12 @@ func (c *controller) GetTestRunEvents(ctx context.Context, testID string, runID } func (c *controller) DeleteTestRun(ctx context.Context, testID string, runID int32) (openapi.ImplResponse, error) { - run, err := c.testDB.GetRun(ctx, id.ID(testID), int(runID)) + run, err := c.testRunRepository.GetRun(ctx, id.ID(testID), int(runID)) if err != nil { return handleDBError(err), err } - err = c.testDB.DeleteRun(ctx, run) + err = c.testRunRepository.DeleteRun(ctx, run) if err != nil { return openapi.Response(http.StatusInternalServerError, err.Error()), err } @@ -250,56 +197,40 @@ func (c *controller) GetTestRuns(ctx context.Context, testID string, take, skip take = 20 } - test, err := c.testDB.GetLatestTestVersion(ctx, id.ID(testID)) + test, err := c.testRepository.Get(ctx, id.ID(testID)) if err != nil { return handleDBError(err), err } - runs, err := c.testDB.GetTestRuns(ctx, test, take, skip) + runs, err := c.testRunRepository.GetTestRuns(ctx, test, take, skip) if err != nil { return handleDBError(err), err } return openapi.Response(200, paginated[openapi.TestRun]{ - items: c.mappers.Out.Runs(runs.Items), - count: runs.TotalCount, - }), nil -} - -func (c *controller) GetTests(ctx context.Context, take, skip int32, query string, sortBy string, sortDirection string) (openapi.ImplResponse, error) { - if take == 0 { - take = 20 - } - - tests, err := c.testDB.GetTests(ctx, take, skip, query, sortBy, sortDirection) - if err != nil { - return handleDBError(err), err - } - - return openapi.Response(200, paginated[openapi.Test]{ - items: c.mappers.Out.Tests(tests.Items), - count: tests.TotalCount, + items: c.mappers.Out.Runs(runs), + count: len(runs), // TODO: find a way of returning the proper number }), nil } func (c *controller) RerunTestRun(ctx context.Context, testID string, runID int32) (openapi.ImplResponse, error) { - test, err := c.testDB.GetLatestTestVersion(ctx, id.ID(testID)) + test, err := c.testRepository.GetAugmented(ctx, id.ID(testID)) if err != nil { return handleDBError(err), err } - run, err := c.testDB.GetRun(ctx, id.ID(testID), int(runID)) + run, err := c.testRunRepository.GetRun(ctx, id.ID(testID), int(runID)) if err != nil { return handleDBError(err), err } - newTestRun, err := c.testDB.CreateRun(ctx, test, run.Copy()) + newTestRun, err := c.testRunRepository.CreateRun(ctx, test, run.Copy()) if err != nil { return openapi.Response(http.StatusUnprocessableEntity, err.Error()), err } newTestRun = newTestRun.SuccessfullyPolledTraces(run.Trace) - err = c.testDB.UpdateRun(ctx, newTestRun) + err = c.testRunRepository.UpdateRun(ctx, newTestRun) if err != nil { return openapi.Response(http.StatusInternalServerError, err.Error()), err } @@ -315,7 +246,7 @@ func (c *controller) RerunTestRun(ctx context.Context, testID string, runID int3 } func (c *controller) RunTest(ctx context.Context, testID string, runInformation openapi.RunInformation) (openapi.ImplResponse, error) { - test, err := c.testDB.GetLatestTestVersion(ctx, id.ID(testID)) + test, err := c.testRepository.GetAugmented(ctx, id.ID(testID)) if err != nil { return handleDBError(err), err } @@ -330,7 +261,7 @@ func (c *controller) RunTest(ctx context.Context, testID string, runInformation return handleDBError(err), err } - missingVariablesError, err := validation.ValidateMissingVariables(ctx, c.testDB, test, environment) + missingVariablesError, err := validation.ValidateMissingVariables(ctx, c.testRepository, c.testRunRepository, test, environment) if err != nil { if err == validation.ErrMissingVariables { return openapi.Response(http.StatusUnprocessableEntity, missingVariablesError), nil @@ -350,34 +281,8 @@ func (c *controller) StopTestRun(_ context.Context, testID string, runID int32) return openapi.Response(http.StatusOK, map[string]string{"result": "success"}), nil } -func (c *controller) UpdateTest(ctx context.Context, testID string, in openapi.Test) (openapi.ImplResponse, error) { - updated, err := c.mappers.In.Test(in) - if err != nil { - return openapi.Response(http.StatusBadRequest, err.Error()), nil - } - - return c.doUpdateTest(ctx, id.ID(testID), updated) -} - -func (c *controller) doUpdateTest(ctx context.Context, testID id.ID, updated model.Test) (openapi.ImplResponse, error) { - test, err := c.testDB.GetLatestTestVersion(ctx, testID) - if err != nil { - return handleDBError(err), err - } - - updated.Version = test.Version - updated.ID = test.ID - - _, err = c.testDB.UpdateTest(ctx, updated) - if err != nil { - return handleDBError(err), err - } - - return openapi.Response(204, nil), nil -} - func (c *controller) DryRunAssertion(ctx context.Context, testID string, runID int32, def openapi.TestSpecs) (openapi.ImplResponse, error) { - run, err := c.testDB.GetRun(ctx, id.ID(testID), int(runID)) + run, err := c.testRunRepository.GetRun(ctx, id.ID(testID), int(runID)) if err != nil { return openapi.Response(http.StatusInternalServerError, ""), nil } @@ -386,10 +291,7 @@ func (c *controller) DryRunAssertion(ctx context.Context, testID string, runID i return openapi.Response(http.StatusUnprocessableEntity, fmt.Sprintf(`run "%d" has no trace associated`, runID)), nil } - definition, err := c.mappers.In.Definition(def.Specs) - if err != nil { - return openapi.Response(http.StatusBadRequest, err.Error()), nil - } + definition := c.mappers.In.Definition(def.Specs) ds := []expression.DataStore{expression.EnvironmentDataStore{ Values: run.Environment.Values, @@ -398,7 +300,7 @@ func (c *controller) DryRunAssertion(ctx context.Context, testID string, runID i assertionExecutor := executor.NewAssertionExecutor(c.tracer) results, allPassed := assertionExecutor.Assert(ctx, definition, *run.Trace, ds) - res := c.mappers.Out.Result(&model.RunResults{ + res := c.mappers.Out.Result(&test.RunResults{ AllPassed: allPassed, Results: results, }) @@ -407,12 +309,12 @@ func (c *controller) DryRunAssertion(ctx context.Context, testID string, runID i } func (c *controller) GetRunResultJUnit(ctx context.Context, testID string, runID int32) (openapi.ImplResponse, error) { - run, err := c.testDB.GetRun(ctx, id.ID(testID), int(runID)) + run, err := c.testRunRepository.GetRun(ctx, id.ID(testID), int(runID)) if err != nil { return handleDBError(err), err } - test, err := c.testDB.GetTestVersion(ctx, id.ID(testID), run.TestVersion) + test, err := c.testRepository.GetVersion(ctx, id.ID(testID), run.TestVersion) if err != nil { return handleDBError(err), err } @@ -426,7 +328,7 @@ func (c *controller) GetRunResultJUnit(ctx context.Context, testID string, runID } func (c controller) GetTestVersion(ctx context.Context, testID string, version int32) (openapi.ImplResponse, error) { - test, err := c.testDB.GetTestVersion(ctx, id.ID(testID), int(version)) + test, err := c.testRepository.GetVersion(ctx, id.ID(testID), int(version)) if err != nil { return handleDBError(err), err } @@ -434,27 +336,13 @@ func (c controller) GetTestVersion(ctx context.Context, testID string, version i return openapi.Response(200, c.mappers.Out.Test(test)), nil } -func (c controller) GetTestVersionDefinitionFile(ctx context.Context, testID string, version int32) (openapi.ImplResponse, error) { - test, err := c.testDB.GetTestVersion(ctx, id.ID(testID), int(version)) - if err != nil { - return handleDBError(err), err - } - - enc, err := yaml.Encode(yamlconvert.Test(test)) - if err != nil { - return openapi.Response(http.StatusUnprocessableEntity, err.Error()), err - } - - return openapi.Response(200, enc), nil -} - func (c controller) ExportTestRun(ctx context.Context, testID string, runID int32) (openapi.ImplResponse, error) { - run, err := c.testDB.GetRun(ctx, id.ID(testID), int(runID)) + run, err := c.testRunRepository.GetRun(ctx, id.ID(testID), int(runID)) if err != nil { return handleDBError(err), err } - test, err := c.testDB.GetTestVersion(ctx, id.ID(testID), run.TestVersion) + test, err := c.testRepository.GetVersion(ctx, id.ID(testID), run.TestVersion) if err != nil { return handleDBError(err), err } @@ -478,19 +366,19 @@ func (c controller) ImportTestRun(ctx context.Context, exportedTest openapi.Expo return openapi.Response(http.StatusBadRequest, err.Error()), nil } - createdTest, err := c.testDB.CreateTest(ctx, test) + createdTest, err := c.testRepository.Create(ctx, test) if err != nil { return openapi.Response(http.StatusUnprocessableEntity, err.Error()), err } - createdRun, err := c.testDB.CreateRun(ctx, createdTest, *run) + createdRun, err := c.testRunRepository.CreateRun(ctx, createdTest, *run) if err != nil { return openapi.Response(http.StatusUnprocessableEntity, err.Error()), err } createdRun.State = run.State - err = c.testDB.UpdateRun(ctx, createdRun) + err = c.testRunRepository.UpdateRun(ctx, createdRun) if err != nil { return openapi.Response(http.StatusUnprocessableEntity, err.Error()), err } @@ -503,23 +391,6 @@ func (c controller) ImportTestRun(ctx context.Context, exportedTest openapi.Expo return openapi.Response(http.StatusOK, response), nil } -func (c *controller) UpsertDefinition(ctx context.Context, testDefinition openapi.TextDefinition) (openapi.ImplResponse, error) { - def, err := yaml.Decode([]byte(testDefinition.Content)) - if err != nil { - return openapi.Response(http.StatusUnprocessableEntity, err.Error()), err - } - - if test, err := def.Test(); err == nil { - return c.upsertTest(ctx, test.Model()) - } - - if transaction, err := def.Transaction(); err == nil { - return c.upsertTransaction(ctx, transaction.Model()) - } - - return openapi.Response(http.StatusUnprocessableEntity, nil), nil -} - func (c *controller) ExecuteDefinition(ctx context.Context, testDefinition openapi.TextDefinition) (openapi.ImplResponse, error) { def, err := yaml.Decode([]byte(testDefinition.Content)) if err != nil { @@ -570,33 +441,6 @@ func (c *controller) executeTransaction(ctx context.Context, tran transaction.Tr return openapi.Response(200, res), nil } -func (c *controller) upsertTransaction(ctx context.Context, transaction transaction.Transaction) (openapi.ImplResponse, error) { - resp, err := c.doCreateTransaction(ctx, transaction) - var status int - if err != nil { - if errors.Is(err, errTransactionExists) { - resp, err := c.doUpdateTransaction(ctx, transaction.ID, transaction) - if err != nil { - return resp, err - } - status = http.StatusOK - } else { - return resp, err - } - } else { - status = http.StatusCreated - transaction.ID = id.ID(resp.Body.(openapi.Transaction).Id) - } - - return openapi.ImplResponse{ - Code: status, - Body: openapi.UpsertDefinitionResponse{ - Id: transaction.ID.String(), - Type: yaml.FileTypeTransaction.String(), - }, - }, nil -} - var errTransactionExists = errors.New("transaction already exists") func (c *controller) doCreateTransaction(ctx context.Context, transaction transaction.Transaction) (openapi.ImplResponse, error) { @@ -656,12 +500,12 @@ func (c *controller) doUpdateTransaction(ctx context.Context, transactionID id.I return openapi.Response(204, nil), nil } -func metadata(in *map[string]string) model.RunMetadata { +func metadata(in *map[string]string) test.RunMetadata { if in == nil { return nil } - return model.RunMetadata(*in) + return test.RunMetadata(*in) } func getEnvironment(ctx context.Context, environmentRepository environmentGetter, environmentId string, variablesEnv environment.Environment) (environment.Environment, error) { @@ -678,53 +522,26 @@ func getEnvironment(ctx context.Context, environmentRepository environmentGetter return variablesEnv, nil } -func (c *controller) executeTest(ctx context.Context, test model.Test, runInfo openapi.RunInformation) (openapi.ImplResponse, error) { - resp, err := c.upsertTest(ctx, test) +func (c *controller) executeTest(ctx context.Context, test test.Test, runInfo openapi.RunInformation) (openapi.ImplResponse, error) { + createdTest, err := c.testRepository.Create(ctx, test) if err != nil { - return resp, err + return openapi.Response(http.StatusInternalServerError, err.Error()), err } - testID := id.ID(resp.Body.(openapi.UpsertDefinitionResponse).Id) + // test ready, execute it - resp, err = c.RunTest(ctx, testID.String(), runInfo) + resp, err := c.RunTest(ctx, createdTest.ID.String(), runInfo) if resp.Code != http.StatusOK || err != nil { return resp, err } res := openapi.ExecuteDefinitionResponse{ - Id: testID.String(), + Id: createdTest.ID.String(), RunId: resp.Body.(openapi.TestRun).Id, Type: yaml.FileTypeTest.String(), } return openapi.Response(200, res), nil } -func (c *controller) upsertTest(ctx context.Context, test model.Test) (openapi.ImplResponse, error) { - resp, err := c.doCreateTest(ctx, test) - var status int - if err != nil { - if errors.Is(err, errTestExists) { - resp, err := c.doUpdateTest(ctx, test.ID, test) - if err != nil { - return resp, err - } - status = http.StatusOK - } else { - return resp, err - } - } else { - status = http.StatusCreated - test.ID = id.ID(resp.Body.(openapi.Test).Id) - } - - return openapi.ImplResponse{ - Code: status, - Body: openapi.UpsertDefinitionResponse{ - Id: test.ID.String(), - Type: yaml.FileTypeTest.String(), - }, - }, nil -} - // Expressions func (c *controller) ExpressionResolve(ctx context.Context, in openapi.ResolveRequestInfo) (openapi.ImplResponse, error) { dsList, err := c.buildDataStores(ctx, in) @@ -771,7 +588,7 @@ func (c *controller) buildDataStores(ctx context.Context, info openapi.ResolveRe return [][]expression.DataStore{}, err } - run, err := c.testDB.GetRun(ctx, id.ID(context.TestId), runId) + run, err := c.testRunRepository.GetRun(ctx, id.ID(context.TestId), runId) if err != nil { return [][]expression.DataStore{}, err } @@ -843,7 +660,7 @@ func (c *controller) RunTransaction(ctx context.Context, transactionID string, r return handleDBError(err), err } - missingVariablesError, err := validation.ValidateMissingVariablesFromTransaction(ctx, c.testDB, transaction, environment) + missingVariablesError, err := validation.ValidateMissingVariablesFromTransaction(ctx, c.testRepository, c.testRunRepository, transaction, environment) if err != nil { if err == validation.ErrMissingVariables { return openapi.Response(http.StatusUnprocessableEntity, missingVariablesError), nil @@ -916,15 +733,17 @@ func (c *controller) GetResources(ctx context.Context, take, skip int32, query, return handleDBError(err), err } - getTestsResponse, err := c.GetTests(ctx, newTake, 0, query, sortBy, sortDirection) + tests, err := c.testRepository.ListAugmented(ctx, int(newTake), 0, query, sortBy, sortDirection) if err != nil { - return getTestsResponse, err + return handleDBError(err), err } - testPaginatedResponse := getTestsResponse.Body.(paginated[openapi.Test]) - tests := testPaginatedResponse.items + testCount, err := c.testRepository.Count(ctx, query) + if err != nil { + return handleDBError(err), err + } - totalResources := transactionCount + testPaginatedResponse.count + totalResources := transactionCount + testCount items := takeResources(transactions, tests, take, skip) @@ -936,7 +755,7 @@ func (c *controller) GetResources(ctx context.Context, take, skip int32, query, return openapi.Response(http.StatusOK, paginatedResponse), nil } -func takeResources(transactions []transaction.Transaction, tests []openapi.Test, take, skip int32) []openapi.Resource { +func takeResources(transactions []transaction.Transaction, tests []test.Test, take, skip int32) []openapi.Resource { numItems := len(transactions) + len(tests) items := make([]openapi.Resource, numItems) maxNumItems := len(transactions) + len(tests) @@ -967,7 +786,7 @@ func takeResources(transactions []transaction.Transaction, tests []openapi.Test, transactionInterface := any(transaction) testInterface := any(test) - if transaction.CreatedAt.After(test.CreatedAt) { + if transaction.CreatedAt.After(*test.CreatedAt) { items[currentNumberItens] = openapi.Resource{Type: "transaction", Item: &transactionInterface} i++ } else { diff --git a/server/http/controller_test.go b/server/http/controller_test.go index eff3d1b64f..27784a8b82 100644 --- a/server/http/controller_test.go +++ b/server/http/controller_test.go @@ -11,15 +11,18 @@ import ( "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/openapi" "github.com/kubeshop/tracetest/server/pkg/id" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/mocks" "github.com/kubeshop/tracetest/server/testdb" "github.com/kubeshop/tracetest/server/traces" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace" ) var ( - exampleRun = model.Run{ + exampleRun = test.Run{ ID: 1, TestID: id.ID("abc123"), TraceID: http.IDGen.TraceID(), @@ -113,15 +116,22 @@ func TestContains_Issue617(t *testing.T) { func setupController(t *testing.T) controllerFixture { mdb := new(testdb.MockRepository) mdb.Test(t) + + runRepo := new(mocks.RunRepository) + runRepo.Test(t) + return controllerFixture{ - db: mdb, + db: mdb, + testRunRepo: runRepo, c: http.NewController( mdb, nil, nil, nil, + runRepo, + nil, nil, - mappings.New(traces.NewConversionConfig(), comparator.DefaultRegistry(), mdb), + mappings.New(traces.NewConversionConfig(), comparator.DefaultRegistry()), nil, &trigger.Registry{}, trace.NewNoopTracerProvider().Tracer("tracer"), @@ -131,12 +141,13 @@ func setupController(t *testing.T) controllerFixture { } type controllerFixture struct { - db *testdb.MockRepository - c openapi.ApiApiServicer + db *testdb.MockRepository + testRunRepo *mocks.RunRepository + c openapi.ApiApiServicer } -func (f controllerFixture) expectGetRun(r model.Run) { - f.db. - On("GetRun", r.TestID, r.ID). +func (f controllerFixture) expectGetRun(r test.Run) { + f.testRunRepo. + On("GetRun", mock.Anything, r.TestID, r.ID). Return(r, nil) } diff --git a/server/http/custom_routes.go b/server/http/custom_routes.go index 987516bba0..190b73abbd 100644 --- a/server/http/custom_routes.go +++ b/server/http/custom_routes.go @@ -37,11 +37,9 @@ func (c *customController) Routes() openapi.Routes { routes[c.getRouteIndex("GetTransactionVersionDefinitionFile")].HandlerFunc = c.GetTransactionVersionDefinitionFile routes[c.getRouteIndex("GetRunResultJUnit")].HandlerFunc = c.GetRunResultJUnit - routes[c.getRouteIndex("GetTestVersionDefinitionFile")].HandlerFunc = c.GetTestVersionDefinitionFile routes[c.getRouteIndex("GetTestRuns")].HandlerFunc = c.GetTestRuns - routes[c.getRouteIndex("GetTests")].HandlerFunc = paginatedEndpoint[openapi.Test](c.service.GetTests, c.errorHandler) routes[c.getRouteIndex("GetResources")].HandlerFunc = paginatedEndpoint[openapi.Resource](c.service.GetResources, c.errorHandler) for index, route := range routes { @@ -150,27 +148,6 @@ func (c *customController) GetRunResultJUnit(w http.ResponseWriter, r *http.Requ w.Write(result.Body.([]byte)) } -// GetTestVersionDefinitionFile - Get the test definition as an YAML file -func (c *customController) GetTestVersionDefinitionFile(w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - testIdParam := params["testId"] - - versionParam, err := parseInt32Parameter(params["version"], true) - if err != nil { - c.errorHandler(w, r, &openapi.ParsingError{Err: err}, nil) - return - } - - result, err := c.service.GetTestVersionDefinitionFile(r.Context(), testIdParam, versionParam) - // If an error occurred, encode the error with the status code - if err != nil { - c.errorHandler(w, r, err, &result) - return - } - w.Header().Set("Content-Type", "application/yaml; charset=UTF-8") - w.Write(result.Body.([]byte)) -} - func paginatedEndpoint[T any]( f func(c context.Context, take, skip int32, query string, sortBy string, sortDirection string) (openapi.ImplResponse, error), errorHandler openapi.ErrorHandler, diff --git a/server/http/mappings/grpc.go b/server/http/mappings/grpc.go index 8199a2da92..601fe06c6a 100644 --- a/server/http/mappings/grpc.go +++ b/server/http/mappings/grpc.go @@ -1,13 +1,13 @@ package mappings import ( - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/openapi" + "github.com/kubeshop/tracetest/server/test/trigger" ) // out -func (m OpenAPI) GRPCRequest(in *model.GRPCRequest) openapi.GrpcRequest { +func (m OpenAPI) GRPCRequest(in *trigger.GRPCRequest) openapi.GrpcRequest { if in == nil { return openapi.GrpcRequest{} } @@ -23,7 +23,7 @@ func (m OpenAPI) GRPCRequest(in *model.GRPCRequest) openapi.GrpcRequest { } } -func (m OpenAPI) GRPCResponse(in *model.GRPCResponse) openapi.GrpcResponse { +func (m OpenAPI) GRPCResponse(in *trigger.GRPCResponse) openapi.GrpcResponse { if in == nil { return openapi.GrpcResponse{} } @@ -34,7 +34,7 @@ func (m OpenAPI) GRPCResponse(in *model.GRPCResponse) openapi.GrpcResponse { } } -func (m OpenAPI) GRPCMetadata(in []model.GRPCHeader) []openapi.GrpcHeader { +func (m OpenAPI) GRPCMetadata(in []trigger.GRPCHeader) []openapi.GrpcHeader { headers := make([]openapi.GrpcHeader, len(in)) for i, h := range in { headers[i] = openapi.GrpcHeader{Key: h.Key, Value: h.Value} @@ -45,22 +45,22 @@ func (m OpenAPI) GRPCMetadata(in []model.GRPCHeader) []openapi.GrpcHeader { //in -func (m Model) GRPCHeaders(in []openapi.GrpcHeader) []model.GRPCHeader { - headers := make([]model.GRPCHeader, len(in)) +func (m Model) GRPCHeaders(in []openapi.GrpcHeader) []trigger.GRPCHeader { + headers := make([]trigger.GRPCHeader, len(in)) for i, h := range in { - headers[i] = model.GRPCHeader{Key: h.Key, Value: h.Value} + headers[i] = trigger.GRPCHeader{Key: h.Key, Value: h.Value} } return headers } -func (m Model) GRPCRequest(in openapi.GrpcRequest) *model.GRPCRequest { +func (m Model) GRPCRequest(in openapi.GrpcRequest) *trigger.GRPCRequest { // ignore unset grpc requests if in.Address == "" { return nil } - return &model.GRPCRequest{ + return &trigger.GRPCRequest{ ProtobufFile: in.ProtobufFile, Address: in.Address, Method: in.Method, @@ -70,13 +70,13 @@ func (m Model) GRPCRequest(in openapi.GrpcRequest) *model.GRPCRequest { } } -func (m Model) GRPCResponse(in openapi.GrpcResponse) *model.GRPCResponse { +func (m Model) GRPCResponse(in openapi.GrpcResponse) *trigger.GRPCResponse { // ignore unset grcp responses if in.StatusCode == 0 { return nil } - return &model.GRPCResponse{ + return &trigger.GRPCResponse{ StatusCode: int(in.StatusCode), Metadata: m.GRPCHeaders(in.Metadata), Body: in.Body, diff --git a/server/http/mappings/http.go b/server/http/mappings/http.go index b561999b23..b013faca11 100644 --- a/server/http/mappings/http.go +++ b/server/http/mappings/http.go @@ -1,13 +1,13 @@ package mappings import ( - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/openapi" + "github.com/kubeshop/tracetest/server/test/trigger" ) // out -func (m OpenAPI) HTTPHeaders(in []model.HTTPHeader) []openapi.HttpHeader { +func (m OpenAPI) HTTPHeaders(in []trigger.HTTPHeader) []openapi.HttpHeader { headers := make([]openapi.HttpHeader, len(in)) for i, h := range in { headers[i] = openapi.HttpHeader{Key: h.Key, Value: h.Value} @@ -16,7 +16,7 @@ func (m OpenAPI) HTTPHeaders(in []model.HTTPHeader) []openapi.HttpHeader { return headers } -func (m OpenAPI) HTTPRequest(in *model.HTTPRequest) openapi.HttpRequest { +func (m OpenAPI) HTTPRequest(in *trigger.HTTPRequest) openapi.HttpRequest { if in == nil { return openapi.HttpRequest{} } @@ -31,7 +31,7 @@ func (m OpenAPI) HTTPRequest(in *model.HTTPRequest) openapi.HttpRequest { } } -func (m OpenAPI) HTTPResponse(in *model.HTTPResponse) openapi.HttpResponse { +func (m OpenAPI) HTTPResponse(in *trigger.HTTPResponse) openapi.HttpResponse { if in == nil { return openapi.HttpResponse{} } @@ -43,7 +43,7 @@ func (m OpenAPI) HTTPResponse(in *model.HTTPResponse) openapi.HttpResponse { } } -func (m OpenAPI) Auth(in *model.HTTPAuthenticator) openapi.HttpAuth { +func (m OpenAPI) Auth(in *trigger.HTTPAuthenticator) openapi.HttpAuth { if in == nil { return openapi.HttpAuth{} } @@ -74,24 +74,24 @@ func (m OpenAPI) Auth(in *model.HTTPAuthenticator) openapi.HttpAuth { // in -func (m Model) HTTPHeaders(in []openapi.HttpHeader) []model.HTTPHeader { - headers := make([]model.HTTPHeader, len(in)) +func (m Model) HTTPHeaders(in []openapi.HttpHeader) []trigger.HTTPHeader { + headers := make([]trigger.HTTPHeader, len(in)) for i, h := range in { - headers[i] = model.HTTPHeader{Key: h.Key, Value: h.Value} + headers[i] = trigger.HTTPHeader{Key: h.Key, Value: h.Value} } return headers } -func (m Model) HTTPRequest(in openapi.HttpRequest) *model.HTTPRequest { +func (m Model) HTTPRequest(in openapi.HttpRequest) *trigger.HTTPRequest { // ignore unset http requests if in.Url == "" { return nil } - return &model.HTTPRequest{ + return &trigger.HTTPRequest{ URL: in.Url, - Method: model.HTTPMethod(in.Method), + Method: trigger.HTTPMethod(in.Method), Headers: m.HTTPHeaders(in.Headers), Body: in.Body, Auth: m.Auth(in.Auth), @@ -99,13 +99,13 @@ func (m Model) HTTPRequest(in openapi.HttpRequest) *model.HTTPRequest { } } -func (m Model) HTTPResponse(in openapi.HttpResponse) *model.HTTPResponse { +func (m Model) HTTPResponse(in openapi.HttpResponse) *trigger.HTTPResponse { // ignore unset http responses if in.StatusCode == 0 { return nil } - return &model.HTTPResponse{ + return &trigger.HTTPResponse{ Status: in.Status, StatusCode: int(in.StatusCode), Headers: m.HTTPHeaders(in.Headers), @@ -113,19 +113,19 @@ func (m Model) HTTPResponse(in openapi.HttpResponse) *model.HTTPResponse { } } -func (m Model) Auth(in openapi.HttpAuth) *model.HTTPAuthenticator { - return &model.HTTPAuthenticator{ +func (m Model) Auth(in openapi.HttpAuth) *trigger.HTTPAuthenticator { + return &trigger.HTTPAuthenticator{ Type: in.Type, - APIKey: model.APIKeyAuthenticator{ + APIKey: &trigger.APIKeyAuthenticator{ Key: in.ApiKey.Key, Value: in.ApiKey.Value, - In: model.APIKeyPosition(in.ApiKey.In), + In: trigger.APIKeyPosition(in.ApiKey.In), }, - Basic: model.BasicAuthenticator{ + Basic: &trigger.BasicAuthenticator{ Username: in.Basic.Username, Password: in.Basic.Password, }, - Bearer: model.BearerAuthenticator{ + Bearer: &trigger.BearerAuthenticator{ Bearer: in.Bearer.Token, }, } diff --git a/server/http/mappings/mappings.go b/server/http/mappings/mappings.go index e2886ef1c5..0716415573 100644 --- a/server/http/mappings/mappings.go +++ b/server/http/mappings/mappings.go @@ -2,7 +2,6 @@ package mappings import ( "github.com/kubeshop/tracetest/server/assertions/comparator" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/traces" ) @@ -11,12 +10,11 @@ type Mappings struct { Out OpenAPI } -func New(tcc traces.ConversionConfig, cr comparator.Registry, tr model.TestRepository) Mappings { +func New(tcc traces.ConversionConfig, cr comparator.Registry) Mappings { return Mappings{ In: Model{ comparators: cr, traceConversionConfig: tcc, - testRepository: tr, }, Out: OpenAPI{ traceConversionConfig: tcc, diff --git a/server/http/mappings/mappings_test.go b/server/http/mappings/mappings_test.go index 9b723e4d7e..8a0f1d5a3d 100644 --- a/server/http/mappings/mappings_test.go +++ b/server/http/mappings/mappings_test.go @@ -63,9 +63,8 @@ func TestSpecOrder(t *testing.T) { // try multiple times to hit the map iteration randomization attempts := 50 for i := 0; i < attempts; i++ { - maps := mappings.New(traces.ConversionConfig{}, comparator.DefaultRegistry(), nil) - definition, err := maps.In.Definition(input) - require.NoError(t, err) + maps := mappings.New(traces.ConversionConfig{}, comparator.DefaultRegistry()) + definition := maps.In.Definition(input) actual := maps.Out.Specs(definition) actualJSON, err := json.Marshal(actual) @@ -143,7 +142,7 @@ func TestResultsOrder(t *testing.T) { // try multiple times to hit the map iteration randomization attempts := 50 for i := 0; i < attempts; i++ { - maps := mappings.New(traces.ConversionConfig{}, comparator.DefaultRegistry(), nil) + maps := mappings.New(traces.ConversionConfig{}, comparator.DefaultRegistry()) result, err := maps.In.Result(input) require.NoError(t, err) diff --git a/server/http/mappings/tests.go b/server/http/mappings/tests.go index 81550ea438..e565c88fc1 100644 --- a/server/http/mappings/tests.go +++ b/server/http/mappings/tests.go @@ -8,10 +8,11 @@ import ( "github.com/kubeshop/tracetest/server/assertions/comparator" "github.com/kubeshop/tracetest/server/assertions/selectors" "github.com/kubeshop/tracetest/server/environment" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/openapi" "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "github.com/kubeshop/tracetest/server/traces" "github.com/kubeshop/tracetest/server/transaction" "go.opentelemetry.io/otel/trace" @@ -52,16 +53,16 @@ func (m OpenAPI) TransactionRun(in transaction.TransactionRun) openapi.Transacti } } -func (m OpenAPI) Test(in model.Test) openapi.Test { +func (m OpenAPI) Test(in test.Test) openapi.Test { return openapi.Test{ - Id: string(in.ID), - Name: in.Name, - Description: in.Description, - ServiceUnderTest: m.Trigger(in.ServiceUnderTest), - Specs: m.Specs(in.Specs), - Version: int32(in.Version), - CreatedAt: in.CreatedAt, - Outputs: m.Outputs(in.Outputs), + Id: string(in.ID), + Name: in.Name, + Description: in.Description, + Trigger: m.Trigger(in.Trigger), + Specs: m.Specs(in.Specs), + Version: int32(*in.Version), + CreatedAt: *in.CreatedAt, + Outputs: m.Outputs(in.Outputs), Summary: openapi.TestSummary{ Runs: int32(in.Summary.Runs), LastRun: openapi.TestSummaryLastRun{ @@ -93,43 +94,42 @@ func (m OpenAPI) EnvironmentValues(in []environment.EnvironmentValue) []openapi. return values } -func (m OpenAPI) Outputs(in maps.Ordered[string, model.Output]) []openapi.TestOutput { - res := make([]openapi.TestOutput, 0, in.Len()) - in.ForEach(func(key string, val model.Output) error { +func (m OpenAPI) Outputs(in []test.Output) []openapi.TestOutput { + res := make([]openapi.TestOutput, 0, len(in)) + for _, output := range in { res = append(res, openapi.TestOutput{ - Name: key, - Selector: string(val.Selector), - SelectorParsed: m.Selector(val.Selector), - Value: val.Value, + Name: output.Name, + Selector: string(output.Selector), + SelectorParsed: m.Selector(output.Selector), + Value: output.Value, }) - return nil - }) + } return res } -func (m OpenAPI) Trigger(in model.Trigger) openapi.Trigger { +func (m OpenAPI) Trigger(in trigger.Trigger) openapi.Trigger { return openapi.Trigger{ - TriggerType: string(in.Type), - Http: m.HTTPRequest(in.HTTP), + Type: string(in.Type), + HttpRequest: m.HTTPRequest(in.HTTP), Grpc: m.GRPCRequest(in.GRPC), - Traceid: m.TRACEIDRequest(in.TraceID), + Traceid: m.TraceIDRequest(in.TraceID), } } -func (m OpenAPI) TriggerResult(in model.TriggerResult) openapi.TriggerResult { +func (m OpenAPI) TriggerResult(in trigger.TriggerResult) openapi.TriggerResult { return openapi.TriggerResult{ - TriggerType: string(in.Type), + Type: string(in.Type), TriggerResult: openapi.TriggerResultTriggerResult{ Http: m.HTTPResponse(in.HTTP), Grpc: m.GRPCResponse(in.GRPC), - Traceid: m.TRACEIDResponse(in.TRACEID), + Traceid: m.TraceIDResponse(in.TraceID), }, } } -func (m OpenAPI) Tests(in []model.Test) []openapi.Test { +func (m OpenAPI) Tests(in []test.Test) []openapi.Test { tests := make([]openapi.Test, len(in)) for i, t := range in { tests[i] = m.Test(t) @@ -147,31 +147,27 @@ func (m OpenAPI) Environments(in []environment.Environment) []openapi.Environmen return environments } -func (m OpenAPI) Specs(in maps.Ordered[model.SpanQuery, model.NamedAssertions]) []openapi.TestSpec { - - specs := make([]openapi.TestSpec, in.Len()) +func (m OpenAPI) Specs(in test.Specs) []openapi.TestSpec { + specs := make([]openapi.TestSpec, len(in)) - i := 0 - in.ForEach(func(spanQuery model.SpanQuery, namedAssertions model.NamedAssertions) error { - assertions := make([]string, len(namedAssertions.Assertions)) - for j, a := range namedAssertions.Assertions { + for i, spec := range in { + assertions := make([]string, len(spec.Assertions)) + for j, a := range spec.Assertions { assertions[j] = string(a) } specs[i] = openapi.TestSpec{ - Name: namedAssertions.Name, - Selector: string(spanQuery), - SelectorParsed: m.Selector(spanQuery), + Name: spec.Name, + Selector: string(spec.Selector), + SelectorParsed: m.Selector(test.SpanQuery(spec.Selector)), Assertions: assertions, } - i++ - return nil - }) + } return specs } -func (m OpenAPI) Selector(in model.SpanQuery) openapi.Selector { +func (m OpenAPI) Selector(in test.SpanQuery) openapi.Selector { structuredSelector := selectors.FromSpanQuery(in) spanSelectors := make([]openapi.SpanSelector, 0) @@ -219,7 +215,7 @@ func (m OpenAPI) SpanSelector(in selectors.SpanSelector) openapi.SpanSelector { } } -func (m OpenAPI) Result(in *model.RunResults) openapi.AssertionResults { +func (m OpenAPI) Result(in *test.RunResults) openapi.AssertionResults { if in == nil { return openapi.AssertionResults{} } @@ -227,7 +223,7 @@ func (m OpenAPI) Result(in *model.RunResults) openapi.AssertionResults { results := make([]openapi.AssertionResultsResultsInner, in.Results.Len()) i := 0 - in.Results.ForEach(func(query model.SpanQuery, inRes []model.AssertionResult) error { + in.Results.ForEach(func(query test.SpanQuery, inRes []test.AssertionResult) error { res := make([]openapi.AssertionResult, len(inRes)) for j, r := range inRes { sres := make([]openapi.AssertionSpanResult, len(r.Results)) @@ -264,7 +260,7 @@ func (m OpenAPI) Result(in *model.RunResults) openapi.AssertionResults { } } -func (m OpenAPI) Run(in *model.Run) openapi.TestRun { +func (m OpenAPI) Run(in *test.Run) openapi.TestRun { if in == nil { return openapi.TestRun{} } @@ -295,10 +291,10 @@ func (m OpenAPI) Run(in *model.Run) openapi.TestRun { } } -func (m OpenAPI) RunOutputs(in maps.Ordered[string, model.RunOutput]) []openapi.TestRunOutputsInner { +func (m OpenAPI) RunOutputs(in maps.Ordered[string, test.RunOutput]) []openapi.TestRunOutputsInner { res := make([]openapi.TestRunOutputsInner, 0, in.Len()) - in.ForEach(func(key string, val model.RunOutput) error { + in.ForEach(func(key string, val test.RunOutput) error { res = append(res, openapi.TestRunOutputsInner{ Name: key, Value: val.Value, @@ -311,7 +307,7 @@ func (m OpenAPI) RunOutputs(in maps.Ordered[string, model.RunOutput]) []openapi. return res } -func (m OpenAPI) Runs(in []model.Run) []openapi.TestRun { +func (m OpenAPI) Runs(in []test.Run) []openapi.TestRun { runs := make([]openapi.TestRun, len(in)) for i, t := range in { runs[i] = m.Run(&t) @@ -324,95 +320,84 @@ func (m OpenAPI) Runs(in []model.Run) []openapi.TestRun { type Model struct { comparators comparator.Registry traceConversionConfig traces.ConversionConfig - testRepository model.TestRepository } -func (m Model) Test(in openapi.Test) (model.Test, error) { - definition, err := m.Definition(in.Specs) - if err != nil { - return model.Test{}, fmt.Errorf("could not convert definition: %w", err) - } +func (m Model) Test(in openapi.Test) (test.Test, error) { + definition := m.Definition(in.Specs) + outputs := m.Outputs(in.Outputs) - outputs, err := m.Outputs(in.Outputs) - if err != nil { - return model.Test{}, fmt.Errorf("could not convert outputs: %w", err) - } - - return model.Test{ - ID: id.ID(in.Id), - Name: in.Name, - Description: in.Description, - ServiceUnderTest: m.Trigger(in.ServiceUnderTest), - Specs: definition, - Outputs: outputs, - Version: int(in.Version), + version := int(in.Version) + return test.Test{ + ID: id.ID(in.Id), + Name: in.Name, + Description: in.Description, + Trigger: m.Trigger(in.Trigger), + Specs: definition, + Outputs: outputs, + Version: &version, }, nil } -func (m Model) Outputs(in []openapi.TestOutput) (maps.Ordered[string, model.Output], error) { - res := maps.Ordered[string, model.Output]{} +func (m Model) Outputs(in []openapi.TestOutput) test.Outputs { + res := make(test.Outputs, 0, len(in)) - var err error for _, output := range in { - res, err = res.Add(output.Name, model.Output{ - Selector: model.SpanQuery(output.SelectorParsed.Query), + res = append(res, test.Output{ + Name: output.Name, + Selector: test.SpanQuery(output.SelectorParsed.Query), Value: output.Value, }) - - if err != nil { - return res, fmt.Errorf("cannot parse outputs: %w", err) - } } - return res, nil + return res } -func (m Model) Tests(in []openapi.Test) ([]model.Test, error) { - tests := make([]model.Test, len(in)) +func (m Model) Tests(in []openapi.Test) ([]test.Test, error) { + tests := make([]test.Test, len(in)) for i, t := range in { - test, err := m.Test(t) + testObject, err := m.Test(t) if err != nil { - return []model.Test{}, fmt.Errorf("could not convert test: %w", err) + return []test.Test{}, fmt.Errorf("could not convert test: %w", err) } - tests[i] = test + tests[i] = testObject } return tests, nil } -func (m Model) Definition(in []openapi.TestSpec) (maps.Ordered[model.SpanQuery, model.NamedAssertions], error) { - specs := maps.Ordered[model.SpanQuery, model.NamedAssertions]{} +func (m Model) Definition(in []openapi.TestSpec) test.Specs { + specs := make(test.Specs, 0, len(in)) for _, spec := range in { - asserts := make([]model.Assertion, len(spec.Assertions)) + asserts := make([]test.Assertion, len(spec.Assertions)) for i, a := range spec.Assertions { - assertion := model.Assertion(a) + assertion := test.Assertion(a) asserts[i] = assertion } - namedAssertions := model.NamedAssertions{ + specs = append(specs, test.TestSpec{ + Selector: test.SpanQuery(spec.SelectorParsed.Query), Name: spec.Name, Assertions: asserts, - } - specs, _ = specs.Add(model.SpanQuery(spec.SelectorParsed.Query), namedAssertions) + }) } - return specs, nil + return specs } -func (m Model) Run(in openapi.TestRun) (*model.Run, error) { +func (m Model) Run(in openapi.TestRun) (*test.Run, error) { tid, _ := trace.TraceIDFromHex(in.TraceId) sid, _ := trace.SpanIDFromHex(in.SpanId) id, _ := strconv.Atoi(in.Id) result, err := m.Result(in.Result) if err != nil { - return &model.Run{}, fmt.Errorf("could not convert result: %w", err) + return &test.Run{}, fmt.Errorf("could not convert result: %w", err) } - return &model.Run{ + return &test.Run{ ID: id, TraceID: tid, SpanID: sid, - State: model.RunState(in.State), + State: test.RunState(in.State), LastError: stringToErr(in.LastErrorState), CreatedAt: in.CreatedAt, ServiceTriggeredAt: in.ServiceTriggeredAt, @@ -429,11 +414,11 @@ func (m Model) Run(in openapi.TestRun) (*model.Run, error) { }, nil } -func (m Model) RunOutputs(in []openapi.TestRunOutputsInner) maps.Ordered[string, model.RunOutput] { - res := maps.Ordered[string, model.RunOutput]{} +func (m Model) RunOutputs(in []openapi.TestRunOutputsInner) maps.Ordered[string, test.RunOutput] { + res := maps.Ordered[string, test.RunOutput]{} for _, output := range in { - res.Add(output.Name, model.RunOutput{ + res.Add(output.Name, test.RunOutput{ Value: output.Value, Name: output.Name, SpanID: output.SpanId, @@ -444,68 +429,68 @@ func (m Model) RunOutputs(in []openapi.TestRunOutputsInner) maps.Ordered[string, return res } -func (m Model) Trigger(in openapi.Trigger) model.Trigger { - return model.Trigger{ - Type: model.TriggerType(in.TriggerType), - HTTP: m.HTTPRequest(in.Http), +func (m Model) Trigger(in openapi.Trigger) trigger.Trigger { + return trigger.Trigger{ + Type: trigger.TriggerType(in.Type), + HTTP: m.HTTPRequest(in.HttpRequest), GRPC: m.GRPCRequest(in.Grpc), - TraceID: m.TRACEIDRequest(in.Traceid), + TraceID: m.TraceIDRequest(in.Traceid), } } -func (m Model) TriggerResult(in openapi.TriggerResult) model.TriggerResult { +func (m Model) TriggerResult(in openapi.TriggerResult) trigger.TriggerResult { - return model.TriggerResult{ - Type: model.TriggerType(in.TriggerType), + return trigger.TriggerResult{ + Type: trigger.TriggerType(in.Type), HTTP: m.HTTPResponse(in.TriggerResult.Http), GRPC: m.GRPCResponse(in.TriggerResult.Grpc), - TRACEID: m.TRACEIDResponse(in.TriggerResult.Traceid), + TraceID: m.TraceIDResponse(in.TriggerResult.Traceid), } } -func (m Model) Result(in openapi.AssertionResults) (*model.RunResults, error) { - results := maps.Ordered[model.SpanQuery, []model.AssertionResult]{} +func (m Model) Result(in openapi.AssertionResults) (*test.RunResults, error) { + results := maps.Ordered[test.SpanQuery, []test.AssertionResult]{} for _, res := range in.Results { - ars := make([]model.AssertionResult, len(res.Results)) + ars := make([]test.AssertionResult, len(res.Results)) for i, r := range res.Results { - sars := make([]model.SpanAssertionResult, len(r.SpanResults)) + sars := make([]test.SpanAssertionResult, len(r.SpanResults)) for j, sar := range r.SpanResults { var sid *trace.SpanID if sar.SpanId != "" { s, _ := trace.SpanIDFromHex(sar.SpanId) sid = &s } - sars[j] = model.SpanAssertionResult{ + sars[j] = test.SpanAssertionResult{ SpanID: sid, ObservedValue: sar.ObservedValue, CompareErr: fmt.Errorf(sar.Error), } } - assertion := model.Assertion(r.Assertion) + assertion := test.Assertion(r.Assertion) - ars[i] = model.AssertionResult{ + ars[i] = test.AssertionResult{ Assertion: assertion, AllPassed: r.AllPassed, Results: sars, } } - results, _ = results.Add(model.SpanQuery(res.Selector.Query), ars) + results, _ = results.Add(test.SpanQuery(res.Selector.Query), ars) } - return &model.RunResults{ + return &test.RunResults{ AllPassed: in.AllPassed, Results: results, }, nil } -func (m Model) Runs(in []openapi.TestRun) ([]model.Run, error) { - runs := make([]model.Run, len(in)) +func (m Model) Runs(in []openapi.TestRun) ([]test.Run, error) { + runs := make([]test.Run, len(in)) for i, r := range in { run, err := m.Run(r) if err != nil { - return []model.Run{}, fmt.Errorf("could not convert run: %w", err) + return []test.Run{}, fmt.Errorf("could not convert run: %w", err) } runs[i] = *run } diff --git a/server/http/mappings/tests_test.go b/server/http/mappings/tests_test.go index 2366e3c478..be2b56e793 100644 --- a/server/http/mappings/tests_test.go +++ b/server/http/mappings/tests_test.go @@ -4,9 +4,8 @@ import ( "testing" "github.com/kubeshop/tracetest/server/http/mappings" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/openapi" - "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/traces" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -25,12 +24,15 @@ func Test_OpenApiToModel_Outputs(t *testing.T) { }, } - expected := (maps.Ordered[string, model.Output]{}).MustAdd("OUTPUT", model.Output{ - Selector: `span[name="root"]`, - Value: "attr:tracetest.selected_spans.count", - }) + expected := test.Outputs{ + { + Name: "OUTPUT", + Selector: `span[name="root"]`, + Value: "attr:tracetest.selected_spans.count", + }, + } - m := mappings.New(traces.NewConversionConfig(), nil, nil) + m := mappings.New(traces.NewConversionConfig(), nil) actual, err := m.In.Test(in) require.NoError(t, err) diff --git a/server/http/mappings/traceid.go b/server/http/mappings/traceid.go index 1a383b6059..649fc37982 100644 --- a/server/http/mappings/traceid.go +++ b/server/http/mappings/traceid.go @@ -1,11 +1,11 @@ package mappings import ( - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/openapi" + "github.com/kubeshop/tracetest/server/test/trigger" ) -func (m OpenAPI) TRACEIDRequest(in *model.TRACEIDRequest) openapi.TraceidRequest { +func (m OpenAPI) TraceIDRequest(in *trigger.TraceIDRequest) openapi.TraceidRequest { if in == nil { return openapi.TraceidRequest{} } @@ -15,7 +15,7 @@ func (m OpenAPI) TRACEIDRequest(in *model.TRACEIDRequest) openapi.TraceidRequest } } -func (m OpenAPI) TRACEIDResponse(in *model.TRACEIDResponse) openapi.TraceidResponse { +func (m OpenAPI) TraceIDResponse(in *trigger.TraceIDResponse) openapi.TraceidResponse { if in == nil { return openapi.TraceidResponse{} } @@ -24,22 +24,22 @@ func (m OpenAPI) TRACEIDResponse(in *model.TRACEIDResponse) openapi.TraceidRespo } } -func (m Model) TRACEIDRequest(in openapi.TraceidRequest) *model.TRACEIDRequest { +func (m Model) TraceIDRequest(in openapi.TraceidRequest) *trigger.TraceIDRequest { if in.Id == "" { return nil } - return &model.TRACEIDRequest{ + return &trigger.TraceIDRequest{ ID: in.Id, } } -func (m Model) TRACEIDResponse(in openapi.TraceidResponse) *model.TRACEIDResponse { +func (m Model) TraceIDResponse(in openapi.TraceidResponse) *trigger.TraceIDResponse { if in.Id == "" { return nil } - return &model.TRACEIDResponse{ + return &trigger.TraceIDResponse{ ID: in.Id, } } diff --git a/server/http/validation/variable.go b/server/http/validation/variable.go index 57331d6c81..a73e697b0e 100644 --- a/server/http/validation/variable.go +++ b/server/http/validation/variable.go @@ -2,24 +2,24 @@ package validation import ( "context" + "database/sql" "errors" "github.com/kubeshop/tracetest/server/environment" "github.com/kubeshop/tracetest/server/expression/linting" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/openapi" - "github.com/kubeshop/tracetest/server/testdb" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/transaction" ) var ErrMissingVariables = errors.New("variables are missing") -func ValidateMissingVariables(ctx context.Context, db model.Repository, test model.Test, env environment.Environment) (openapi.MissingVariablesError, error) { +func ValidateMissingVariables(ctx context.Context, testRepo test.Repository, runRepo test.RunRepository, test test.Test, env environment.Environment) (openapi.MissingVariablesError, error) { missingVariables := getMissingVariables(test, env) previousValues := map[string]environment.EnvironmentValue{} var err error if len(missingVariables) > 0 { - previousValues, err = getPreviousEnvironmentValues(ctx, db, test) + previousValues, err = getPreviousEnvironmentValues(ctx, testRepo, runRepo, test) if err != nil { return openapi.MissingVariablesError{}, err } @@ -27,7 +27,7 @@ func ValidateMissingVariables(ctx context.Context, db model.Repository, test mod return buildErrorObject(test, missingVariables, previousValues) } -func getMissingVariables(test model.Test, environment environment.Environment) []string { +func getMissingVariables(test test.Test, environment environment.Environment) []string { availableTestVariables := getAvailableVariables(test, environment) expectedVariables := linting.DetectMissingVariables(test, availableTestVariables) @@ -47,31 +47,30 @@ func getMissingVariables(test model.Test, environment environment.Environment) [ return missingVariables } -func getAvailableVariables(test model.Test, environment environment.Environment) []string { +func getAvailableVariables(test test.Test, environment environment.Environment) []string { availableVariables := make([]string, 0) for _, env := range environment.Values { availableVariables = append(availableVariables, env.Key) } - test.Outputs.ForEach(func(key string, _ model.Output) error { - availableVariables = append(availableVariables, key) - return nil - }) + for _, output := range test.Outputs { + availableVariables = append(availableVariables, output.Name) + } return availableVariables } -func getPreviousEnvironmentValues(ctx context.Context, db model.Repository, test model.Test) (map[string]environment.EnvironmentValue, error) { - latestTestVersion, err := db.GetLatestTestVersion(ctx, test.ID) +func getPreviousEnvironmentValues(ctx context.Context, testRepo test.Repository, runRepo test.RunRepository, test test.Test) (map[string]environment.EnvironmentValue, error) { + latestTestVersion, err := testRepo.GetAugmented(ctx, test.ID) if err != nil { return map[string]environment.EnvironmentValue{}, err } - previousTestRun, err := db.GetLatestRunByTestVersion(ctx, test.ID, latestTestVersion.Version) + previousTestRun, err := runRepo.GetLatestRunByTestVersion(ctx, test.ID, *latestTestVersion.Version) if err != nil { // If error is not found, it means this is the first run. So just ignore this error // and provide empty values in the default values for the missing variables - if err != testdb.ErrNotFound { + if err != sql.ErrNoRows { return map[string]environment.EnvironmentValue{}, err } } else { @@ -86,10 +85,10 @@ func getPreviousEnvironmentValues(ctx context.Context, db model.Repository, test return map[string]environment.EnvironmentValue{}, nil } -func ValidateMissingVariablesFromTransaction(ctx context.Context, db model.Repository, transaction transaction.Transaction, env environment.Environment) (openapi.MissingVariablesError, error) { +func ValidateMissingVariablesFromTransaction(ctx context.Context, testRepo test.Repository, runRepo test.RunRepository, transaction transaction.Transaction, env environment.Environment) (openapi.MissingVariablesError, error) { missingVariables := make([]openapi.MissingVariable, 0) for _, step := range transaction.Steps { - stepValidationResult, err := ValidateMissingVariables(ctx, db, step, env) + stepValidationResult, err := ValidateMissingVariables(ctx, testRepo, runRepo, step, env) if err != ErrMissingVariables { return openapi.MissingVariablesError{}, err } @@ -98,10 +97,9 @@ func ValidateMissingVariablesFromTransaction(ctx context.Context, db model.Repos // update env with this test outputs outputs := make([]environment.EnvironmentValue, 0) - step.Outputs.ForEach(func(key string, val model.Output) error { - outputs = append(outputs, environment.EnvironmentValue{Key: key}) - return nil - }) + for _, output := range step.Outputs { + outputs = append(outputs, environment.EnvironmentValue{Key: output.Name}) + } env.Values = append(env.Values, outputs...) } @@ -113,7 +111,7 @@ func ValidateMissingVariablesFromTransaction(ctx context.Context, db model.Repos return openapi.MissingVariablesError{}, nil } -func buildErrorObject(test model.Test, missingVariables []string, previousValues map[string]environment.EnvironmentValue) (openapi.MissingVariablesError, error) { +func buildErrorObject(test test.Test, missingVariables []string, previousValues map[string]environment.EnvironmentValue) (openapi.MissingVariablesError, error) { if len(missingVariables) > 0 { missingVariableObjects := make([]openapi.Variable, 0, len(missingVariables)) for _, variable := range missingVariables { diff --git a/server/http/websocket/subscribe.go b/server/http/websocket/subscribe.go index 76e09eeabf..2856fe1cd9 100644 --- a/server/http/websocket/subscribe.go +++ b/server/http/websocket/subscribe.go @@ -8,6 +8,7 @@ import ( "github.com/kubeshop/tracetest/server/http/mappings" "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/subscription" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/transaction" ) @@ -59,9 +60,9 @@ func (e subscribeCommandExecutor) Execute(conn *websocket.Conn, message []byte) func (e subscribeCommandExecutor) ResourceUpdatedEvent(resource interface{}) Event { var mapped interface{} switch v := resource.(type) { - case model.Run: + case test.Run: mapped = e.mappers.Out.Run(&v) - case *model.Run: + case *test.Run: mapped = e.mappers.Out.Run(v) case transaction.TransactionRun: mapped = e.mappers.Out.TransactionRun(v) diff --git a/server/integration/ensure_server_prefix_test.go b/server/integration/ensure_server_prefix_test.go index 5fb010f1d5..b17fd5da23 100644 --- a/server/integration/ensure_server_prefix_test.go +++ b/server/integration/ensure_server_prefix_test.go @@ -12,8 +12,8 @@ import ( "github.com/goccy/go-yaml" "github.com/kubeshop/tracetest/server/app" "github.com/kubeshop/tracetest/server/datastore" - "github.com/kubeshop/tracetest/server/openapi" "github.com/kubeshop/tracetest/server/resourcemanager" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/testmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -55,7 +55,7 @@ func TestServerPrefix(t *testing.T) { assert.GreaterOrEqual(t, dataStores.Count, 1) } -func getTests(t *testing.T, endpoint string) []openapi.Test { +func getTests(t *testing.T, endpoint string) resourcemanager.ResourceList[test.Test] { url := fmt.Sprintf("%s/api/tests", endpoint) resp, err := http.Get(url) require.NoError(t, err) @@ -64,7 +64,7 @@ func getTests(t *testing.T, endpoint string) []openapi.Test { bodyJsonBytes, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) - var tests []openapi.Test + var tests resourcemanager.ResourceList[test.Test] err = json.Unmarshal(bodyJsonBytes, &tests) require.NoError(t, err) diff --git a/server/junit/junit.go b/server/junit/junit.go index e8cd6d05e4..9cd15a8619 100644 --- a/server/junit/junit.go +++ b/server/junit/junit.go @@ -6,10 +6,10 @@ import ( "fmt" "github.com/kubeshop/tracetest/server/assertions/comparator" - "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/test" ) -func FromRunResult(test model.Test, run model.Run) ([]byte, error) { +func FromRunResult(t test.Test, run test.Run) ([]byte, error) { assertions := []assertion{} @@ -18,7 +18,7 @@ func FromRunResult(test model.Test, run model.Run) ([]byte, error) { if run.Results == nil { return nil, fmt.Errorf("run has no results") } - run.Results.Results.ForEach(func(selector model.SpanQuery, results []model.AssertionResult) error { + run.Results.Results.ForEach(func(selector test.SpanQuery, results []test.AssertionResult) error { checks := []check{} var total, fails, errs int for _, res := range results { @@ -63,7 +63,7 @@ func FromRunResult(test model.Test, run model.Run) ([]byte, error) { }) r := report{ - TestName: test.Name, + TestName: t.Name, Time: run.ExecutionTime(), Total: testTotals, Failures: testFails, diff --git a/server/junit/junit_test.go b/server/junit/junit_test.go index 0b3aace34a..cdca9b9c61 100644 --- a/server/junit/junit_test.go +++ b/server/junit/junit_test.go @@ -11,23 +11,23 @@ import ( "github.com/kubeshop/tracetest/server/assertions/comparator" "github.com/kubeshop/tracetest/server/junit" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestConversion(t *testing.T) { - test := model.Test{ + sampleTest := test.Test{ Name: "Example test", } - results := model.RunResults{ - Results: (maps.Ordered[model.SpanQuery, []model.AssertionResult]{}).MustAdd( - model.SpanQuery(`span[tracetest.span.type = "database"]`), []model.AssertionResult{ + results := test.RunResults{ + Results: (maps.Ordered[test.SpanQuery, []test.AssertionResult]{}).MustAdd( + test.SpanQuery(`span[tracetest.span.type = "database"]`), []test.AssertionResult{ { Assertion: `attr:db.statement contains "INSERT"`, - Results: []model.SpanAssertionResult{ + Results: []test.SpanAssertionResult{ { ObservedValue: "INSERT into whatever", CompareErr: nil, @@ -36,7 +36,7 @@ func TestConversion(t *testing.T) { }, { Assertion: `attr:tracetest.span.duration < 500`, - Results: []model.SpanAssertionResult{ + Results: []test.SpanAssertionResult{ { ObservedValue: "notANumber", CompareErr: errors.New(`cannot parse "notANumber" as integer`), @@ -45,7 +45,7 @@ func TestConversion(t *testing.T) { }, { Assertion: `attr:tracetest.span.type = "http"`, - Results: []model.SpanAssertionResult{ + Results: []test.SpanAssertionResult{ { ObservedValue: "database", CompareErr: comparator.ErrNoMatch, @@ -55,7 +55,7 @@ func TestConversion(t *testing.T) { }), } - run := model.Run{ + run := test.Run{ CreatedAt: time.Date(2022, 05, 23, 14, 55, 07, 0, time.UTC), CompletedAt: time.Date(2022, 05, 23, 14, 55, 18, 0, time.UTC), Results: &results, @@ -64,7 +64,7 @@ func TestConversion(t *testing.T) { expected, err := os.ReadFile("testdata/junit_result.xml") require.NoError(t, err) - actual, err := junit.FromRunResult(test, run) + actual, err := junit.FromRunResult(sampleTest, run) require.NoError(t, err) t.Log("File:", string(actual)) diff --git a/server/model/grpc.go b/server/model/grpc.go index 2b1492f2a4..64c87f9afa 100644 --- a/server/model/grpc.go +++ b/server/model/grpc.go @@ -2,6 +2,8 @@ package model import "google.golang.org/grpc/metadata" +type TriggerType string + const TriggerTypeGRPC TriggerType = "grpc" type GRPCHeader struct { diff --git a/server/model/json.go b/server/model/json.go deleted file mode 100644 index 735feaf667..0000000000 --- a/server/model/json.go +++ /dev/null @@ -1,229 +0,0 @@ -package model - -import ( - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/kubeshop/tracetest/server/assertions/comparator" - "go.opentelemetry.io/otel/trace" -) - -func (ro RunOutput) MarshalJSON() ([]byte, error) { - return json.Marshal(&struct { - Name string - Value string - SpanID string - Resolved bool - Error string - }{ - Name: ro.Name, - Value: ro.Value, - SpanID: ro.SpanID, - Resolved: ro.Resolved, - Error: errToString(ro.Error), - }) -} - -func (ro *RunOutput) UnmarshalJSON(data []byte) error { - aux := struct { - Name string - Value string - SpanID string - Resolved bool - Error string - }{} - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - - ro.Name = aux.Name - ro.Value = aux.Value - ro.SpanID = aux.SpanID - ro.Resolved = aux.Resolved - if err := stringToErr(aux.Error); err != nil { - ro.Error = err - } - - return nil -} - -func (sar SpanAssertionResult) MarshalJSON() ([]byte, error) { - sid := "" - if sar.SpanID != nil { - sid = sar.SpanID.String() - } - return json.Marshal(&struct { - SpanID *string - ObservedValue string - CompareErr string - }{ - SpanID: &sid, - ObservedValue: sar.ObservedValue, - CompareErr: errToString(sar.CompareErr), - }) -} - -func (sar *SpanAssertionResult) UnmarshalJSON(data []byte) error { - aux := struct { - SpanID string - ObservedValue string - CompareErr string - }{} - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - - var sid *trace.SpanID - if aux.SpanID != "" { - s, err := trace.SpanIDFromHex(aux.SpanID) - if err != nil { - return err - } - sid = &s - } - - sar.SpanID = sid - sar.ObservedValue = aux.ObservedValue - if err := stringToErr(aux.CompareErr); err != nil { - if err.Error() == comparator.ErrNoMatch.Error() { - err = comparator.ErrNoMatch - } - - sar.CompareErr = err - } - - return nil -} - -func (na *NamedAssertions) UnmarshalJSON(data []byte) error { - type encodedNamedAssertions struct { - Name string - Assertions []Assertion - } - - var namedAssertion encodedNamedAssertions - if err := json.Unmarshal(data, &namedAssertion); err != nil { - // this might be an []Assertion from the older format - // try to parse []Assertions instead - - err = json.Unmarshal(data, &namedAssertion.Assertions) - if err != nil { - return err - } - } - - na.Name = namedAssertion.Name - na.Assertions = namedAssertion.Assertions - - return nil -} - -type encodedRun struct { - ID int - ShortID string - TraceID string - SpanID string - State string - LastErrorString string - CreatedAt time.Time - ServiceTriggeredAt time.Time - ServiceTriggerCompletedAt time.Time - ObtainedTraceAt time.Time - CompletedAt time.Time - TriggerResult TriggerResult - Trace *Trace - Results *RunResults - TestVersion int - Metadata map[string]string -} - -func (r Run) MarshalJSON() ([]byte, error) { - return json.Marshal(&encodedRun{ - ID: r.ID, - TraceID: r.TraceID.String(), - SpanID: r.SpanID.String(), - State: string(r.State), - LastErrorString: errToString(r.LastError), - CreatedAt: r.CreatedAt, - ServiceTriggeredAt: r.ServiceTriggeredAt, - ServiceTriggerCompletedAt: r.ServiceTriggerCompletedAt, - ObtainedTraceAt: r.ObtainedTraceAt, - CompletedAt: r.CompletedAt, - TestVersion: r.TestVersion, - Trace: r.Trace, - Results: r.Results, - TriggerResult: r.TriggerResult, - Metadata: r.Metadata, - }) -} - -func (r *Run) UnmarshalJSON(data []byte) error { - aux := encodedRun{} - - if err := json.Unmarshal(data, &aux); err != nil { - return fmt.Errorf("unmarshal run: %w", err) - } - - var ( - tid trace.TraceID - sid trace.SpanID - err error - ) - - if aux.TraceID != "" { - tid, err = trace.TraceIDFromHex(aux.TraceID) - if err != nil { - return fmt.Errorf("unmarshal run: %w", err) - } - } - - if aux.SpanID != "" { - sid, err = trace.SpanIDFromHex(aux.SpanID) - if err != nil { - return fmt.Errorf("unmarshal run: %w", err) - } - } - - triggerResult := TriggerResult{ - Type: aux.TriggerResult.Type, - HTTP: aux.TriggerResult.HTTP, - GRPC: aux.TriggerResult.GRPC, - } - - r.ID = aux.ID - r.TraceID = tid - r.SpanID = sid - r.State = RunState(aux.State) - r.LastError = stringToErr(aux.LastErrorString) - r.CreatedAt = aux.CreatedAt - r.ServiceTriggeredAt = aux.ServiceTriggeredAt - r.ServiceTriggerCompletedAt = aux.ServiceTriggerCompletedAt - r.ObtainedTraceAt = aux.ObtainedTraceAt - r.CompletedAt = aux.CompletedAt - r.TestVersion = aux.TestVersion - r.TriggerResult = triggerResult - - r.Trace = aux.Trace - r.Results = aux.Results - r.Metadata = aux.Metadata - - return nil -} - -func errToString(err error) string { - if err != nil { - return err.Error() - } - - return "" -} - -func stringToErr(s string) error { - if s == "" { - return nil - } - - return errors.New(s) -} diff --git a/server/model/json_test.go b/server/model/json_test.go deleted file mode 100644 index 13e15706cb..0000000000 --- a/server/model/json_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package model_test - -import ( - "encoding/json" - "errors" - "testing" - "time" - - "github.com/brianvoe/gofakeit/v6" - "github.com/kubeshop/tracetest/server/model" - "github.com/kubeshop/tracetest/server/model/modeltest" - "github.com/kubeshop/tracetest/server/pkg/id" - "github.com/kubeshop/tracetest/server/pkg/maps" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" -) - -func TestTestEncoding(t *testing.T) { - t1 := time.Date(2022, 06, 07, 13, 03, 24, 100, time.UTC) - - test := model.Test{ - ID: id.ID("test1"), - CreatedAt: t1, - Name: "the name", - Description: "the description", - Version: 1, - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ - URL: "http://localhost:11633/list", - Method: model.HTTPMethodGET, - }, - }, - Specs: (maps.Ordered[model.SpanQuery, model.NamedAssertions]{}). - MustAdd(model.SpanQuery(`span[name="test"]`), model.NamedAssertions{ - Name: "test", - Assertions: []model.Assertion{ - model.Assertion(`attr:name = "test"`), - }, - }), - } - - encoded, err := json.Marshal(test) - require.NoError(t, err) - - var actual model.Test - err = json.Unmarshal(encoded, &actual) - require.NoError(t, err) - - assert.Equal(t, test, actual) -} - -func TestRunEncoding(t *testing.T) { - tid, _ := trace.TraceIDFromHex("83c7f2fb8b556416e12e1d18c05a30c3") - sid, _ := trace.SpanIDFromHex("9ed1382a48be2649") - - t1 := time.Date(2022, 06, 07, 13, 03, 24, 100, time.UTC) - t2 := time.Date(2022, 06, 07, 13, 03, 25, 100, time.UTC) - t3 := time.Date(2022, 06, 07, 13, 03, 27, 100, time.UTC) - t4 := time.Date(2022, 06, 07, 13, 03, 28, 100, time.UTC) - - rootSpan := model.Span{ - ID: sid, - Name: "Root Span", - StartTime: t1, - EndTime: t2, - Attributes: model.Attributes{ - "tracetest.span.duration": "200", - "tracetest.span.type": "http", - }, - } - exampleTrace := &model.Trace{ - ID: tid, - RootSpan: rootSpan, - Flat: map[trace.SpanID]*model.Span{ - sid: &rootSpan, - }, - } - - cases := []struct { - name string - run model.Run - }{ - { - name: "Errors", - run: model.Run{ - ID: 1, - TraceID: tid, - SpanID: sid, - State: model.RunStateTriggerFailed, - LastError: errors.New("some error"), - CreatedAt: t1, - ServiceTriggeredAt: t1, - CompletedAt: t1, - TestVersion: 1, - Metadata: map[string]string{"key": "value"}, - }, - }, - { - name: "Success", - run: model.Run{ - ID: 1, - TraceID: tid, - SpanID: sid, - State: model.RunStateFinished, - LastError: nil, - CreatedAt: t1, - ServiceTriggeredAt: t1, - ServiceTriggerCompletedAt: t2, - ObtainedTraceAt: t3, - CompletedAt: t4, - TriggerResult: model.TriggerResult{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPResponse{ - Status: "OK", - StatusCode: 200, - Headers: []model.HTTPHeader{ - {"Content-Type", "application/json"}, - {"Length", "9"}, - }, - Body: `{"id":52}`, - }, - }, - Trace: exampleTrace, - TestVersion: 2, - Metadata: map[string]string{"another_key": "value"}, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - cl := c - t.Parallel() - - run := cl.run - - encoded, err := json.Marshal(run) - require.NoError(t, err) - - var actual model.Run - err = json.Unmarshal(encoded, &actual) - require.NoError(t, err) - - modeltest.AssertRunEqual(t, cl.run, actual) - }) - } -} - -func TestOldAssertionSpecsFormatWithoutNames(t *testing.T) { - type OldTest struct { - ID id.ID - CreatedAt time.Time - Name string - Description string - Version int - ServiceUnderTest model.Trigger - Specs maps.Ordered[model.SpanQuery, []model.Assertion] - Summary model.Summary - } - - expectedSpecs := maps.Ordered[model.SpanQuery, model.NamedAssertions]{} - expectedSpecs = expectedSpecs.MustAdd(model.SpanQuery(`span[tracetest.span.type = "http"]`), model.NamedAssertions{ - Name: "", - Assertions: []model.Assertion{ - model.Assertion(`attr:http.status = 200`), - }, - }) - - specs := maps.Ordered[model.SpanQuery, []model.Assertion]{} - specs = specs.MustAdd(model.SpanQuery(`span[tracetest.span.type = "http"]`), []model.Assertion{ - model.Assertion(`attr:http.status = 200`), - }) - oldTest := OldTest{ - ID: id.NewRandGenerator().ID(), - CreatedAt: time.Now(), - Name: "my test name", - Description: "this is an old test using the old test format from version <= 0.7.2", - Version: 1, - ServiceUnderTest: model.Trigger{}, - Specs: specs, - Summary: model.Summary{}, - } - - oldTestJson, err := json.Marshal(oldTest) - require.NoError(t, err) - t.Log(string(oldTestJson)) - - var test model.Test - err = json.Unmarshal(oldTestJson, &test) - require.NoError(t, err) - - assert.Equal(t, expectedSpecs, test.Specs) -} - -func TestNewAssertionSpecFormat(t *testing.T) { - test := model.Test{ - ID: id.NewRandGenerator().ID(), - CreatedAt: time.Now(), - Name: gofakeit.Name(), - Description: gofakeit.AdjectiveDescriptive(), - Version: 1, - ServiceUnderTest: model.Trigger{}, - Specs: maps.Ordered[model.SpanQuery, model.NamedAssertions]{}.MustAdd( - model.SpanQuery(`span[tracetest.span.type = "http"`), model.NamedAssertions{ - Name: "my test", - Assertions: []model.Assertion{ - model.Assertion(`attr:http.status = 200`), - }, - }, - ), - Summary: model.Summary{}, - } - - bytes, err := json.Marshal(test) - require.NoError(t, err) - - var newTest model.Test - err = json.Unmarshal(bytes, &newTest) - - require.NoError(t, err) - assert.Equal(t, test.Specs, newTest.Specs) -} diff --git a/server/model/modeltest/run.go b/server/model/modeltest/run.go index 2b1761736e..021306c772 100644 --- a/server/model/modeltest/run.go +++ b/server/model/modeltest/run.go @@ -4,13 +4,13 @@ import ( "testing" "time" - "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/test" "github.com/stretchr/testify/assert" ) var resetTime = time.Date(2022, 06, 07, 13, 03, 24, 100, time.UTC) -func AssertRunEqual(t *testing.T, expected, actual model.Run) bool { +func AssertRunEqual(t *testing.T, expected, actual test.Run) bool { t.Helper() // assert.Equal doesn't work on time.Time vars (see https://stackoverflow.com/a/69362528) diff --git a/server/model/repository.go b/server/model/repository.go index 87573e5dd4..77e8b85615 100644 --- a/server/model/repository.go +++ b/server/model/repository.go @@ -4,7 +4,6 @@ import ( "context" "github.com/kubeshop/tracetest/server/pkg/id" - "go.opentelemetry.io/otel/trace" ) type List[T any] struct { @@ -12,35 +11,12 @@ type List[T any] struct { TotalCount int } -type TestRepository interface { - CreateTest(context.Context, Test) (Test, error) - UpdateTest(context.Context, Test) (Test, error) - DeleteTest(context.Context, Test) error - TestIDExists(context.Context, id.ID) (bool, error) - GetLatestTestVersion(context.Context, id.ID) (Test, error) - GetTestVersion(_ context.Context, _ id.ID, version int) (Test, error) - GetTests(_ context.Context, take, skip int32, query, sortBy, sortDirection string) (List[Test], error) -} - -type RunRepository interface { - CreateRun(context.Context, Test, Run) (Run, error) - UpdateRun(context.Context, Run) error - DeleteRun(context.Context, Run) error - GetRun(_ context.Context, testID id.ID, runID int) (Run, error) - GetTestRuns(_ context.Context, _ Test, take, skip int32) (List[Run], error) - GetRunByTraceID(context.Context, trace.TraceID) (Run, error) - GetLatestRunByTestVersion(context.Context, id.ID, int) (Run, error) -} - type TestRunEventRepository interface { CreateTestRunEvent(context.Context, TestRunEvent) error GetTestRunEvents(context.Context, id.ID, int) ([]TestRunEvent, error) } type Repository interface { - TestRepository - RunRepository - TestRunEventRepository ServerID() (id string, isNew bool, _ error) diff --git a/server/model/spans.go b/server/model/spans.go index 9c84e10b4c..7d117ba8b9 100644 --- a/server/model/spans.go +++ b/server/model/spans.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "github.com/kubeshop/tracetest/server/test/trigger" "go.opentelemetry.io/otel/trace" ) @@ -203,15 +204,15 @@ func (span Span) setMetadataAttributes() Span { return span } -func (span Span) setTriggerResultAttributes(result TriggerResult) Span { +func (span Span) setTriggerResultAttributes(result trigger.TriggerResult) Span { switch result.Type { - case TriggerTypeHTTP: + case trigger.TriggerTypeHTTP: resp := result.HTTP jsonheaders, _ := json.Marshal(resp.Headers) span.Attributes["tracetest.response.status"] = fmt.Sprintf("%d", resp.StatusCode) span.Attributes["tracetest.response.body"] = resp.Body span.Attributes["tracetest.response.headers"] = string(jsonheaders) - case TriggerTypeGRPC: + case trigger.TriggerTypeGRPC: resp := result.GRPC jsonheaders, _ := json.Marshal(resp.Metadata) span.Attributes["tracetest.response.status"] = fmt.Sprintf("%d", resp.StatusCode) @@ -221,20 +222,3 @@ func (span Span) setTriggerResultAttributes(result TriggerResult) Span { return span } - -func AugmentRootSpan(span Span, result TriggerResult) Span { - return span. - setMetadataAttributes(). - setTriggerResultAttributes(result) -} - -func NewTracetestRootSpan(run Run) Span { - return AugmentRootSpan(Span{ - ID: IDGen.SpanID(), - Name: TriggerSpanName, - StartTime: run.ServiceTriggeredAt, - EndTime: run.ServiceTriggerCompletedAt, - Attributes: Attributes{}, - Children: []*Span{}, - }, run.TriggerResult) -} diff --git a/server/model/test_changes.go b/server/model/test_changes.go deleted file mode 100644 index 7491253510..0000000000 --- a/server/model/test_changes.go +++ /dev/null @@ -1,54 +0,0 @@ -package model - -import ( - "encoding/json" -) - -func BumpTestVersionIfNeeded(in, updated Test) (Test, error) { - testHasChanged, err := testHasChanged(in, updated) - if err != nil { - return Test{}, err - } - - if testHasChanged { - updated.Version = in.Version + 1 - } - - return updated, nil -} - -func testHasChanged(oldTest Test, newTest Test) (bool, error) { - outputsHaveChanged, err := testFieldHasChanged(oldTest.Outputs, newTest.Outputs) - if err != nil { - return false, err - } - - definitionHasChanged, err := testFieldHasChanged(oldTest.Specs, newTest.Specs) - if err != nil { - return false, err - } - - serviceUnderTestHasChanged, err := testFieldHasChanged(oldTest.ServiceUnderTest, newTest.ServiceUnderTest) - if err != nil { - return false, err - } - - nameHasChanged := oldTest.Name != newTest.Name - descriptionHasChanged := oldTest.Description != newTest.Description - - return outputsHaveChanged || definitionHasChanged || serviceUnderTestHasChanged || nameHasChanged || descriptionHasChanged, nil -} - -func testFieldHasChanged(oldField interface{}, newField interface{}) (bool, error) { - oldFieldJSON, err := json.Marshal(oldField) - if err != nil { - return false, err - } - - newFieldJSON, err := json.Marshal(newField) - if err != nil { - return false, err - } - - return string(oldFieldJSON) != string(newFieldJSON), nil -} diff --git a/server/model/test_json.go b/server/model/test_json.go deleted file mode 100644 index 40d0cf876f..0000000000 --- a/server/model/test_json.go +++ /dev/null @@ -1,166 +0,0 @@ -package model - -import ( - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/fluidtruck/deepcopy" - "github.com/kubeshop/tracetest/server/pkg/id" - "github.com/kubeshop/tracetest/server/pkg/maps" -) - -type jsonSpec struct { - Name string `json:"name"` - Selector string `json:"selector"` - Assertions []Assertion `json:"assertions"` -} - -type jsonOutput struct { - Name string `json:"name"` - Selector string `json:"selector"` - Value string `json:"value"` -} - -type jsonTest struct { - ID id.ID `json:"id"` - CreatedAt time.Time `json:"createdAt,omitempty"` - Name string `json:"name"` - Description string `json:"description"` - Version int `json:"version,omitempty"` - ServiceUnderTest Trigger `json:"serviceUnderTest"` - SpecsJSON []jsonSpec `json:"specs"` - OutputsJSON []jsonOutput `json:"outputs"` - Summary Summary `json:"summary,omitempty"` -} - -func (t Test) MarshalJSON() ([]byte, error) { - jt := jsonTest{} - err := deepcopy.DeepCopy(t, &jt) - if err != nil { - return nil, err - } - - jt.SpecsJSON = make([]jsonSpec, 0, t.Specs.Len()) - t.Specs.ForEach(func(key SpanQuery, val NamedAssertions) error { - jt.SpecsJSON = append(jt.SpecsJSON, jsonSpec{ - Name: val.Name, - Assertions: val.Assertions, - Selector: string(key), - }) - - return nil - }) - - jt.OutputsJSON = make([]jsonOutput, 0, t.Outputs.Len()) - t.Outputs.ForEach(func(key string, val Output) error { - jt.OutputsJSON = append(jt.OutputsJSON, jsonOutput{ - Name: key, - Selector: string(val.Selector), - Value: val.Value, - }) - - return nil - }) - - return json.Marshal(jt) -} - -func (t *Test) UnmarshalJSON(data []byte) error { - jt := jsonTest{} - err := json.Unmarshal(data, &jt) - if err != nil { - return err - } - - specs, err := unmarshalSpecs(jt) - if err != nil { - return err - } - - if oldSpecs, shouldReplace := checkForOldSpecs(specs, data); shouldReplace { - specs = oldSpecs - } - - outputs := maps.Ordered[string, Output]{} - for _, output := range jt.OutputsJSON { - outputs, err = outputs.Add(output.Name, Output{ - Selector: SpanQuery(output.Selector), - Value: output.Value, - }) - if err != nil { - return err - } - } - - err = deepcopy.DeepCopy(jt, t) - if err != nil { - return err - } - - t.Specs = specs - t.Outputs = outputs - - return nil -} - -func unmarshalSpecs(jt jsonTest) (specs maps.Ordered[SpanQuery, NamedAssertions], err error) { - for _, spec := range jt.SpecsJSON { - specs, err = specs.Add(SpanQuery(spec.Selector), NamedAssertions{ - Name: spec.Name, - Assertions: spec.Assertions, - }) - if err != nil { - return - } - } - - return -} - -var errOldFormatDetected = fmt.Errorf("old format detected") - -func checkForOldSpecs(newFormatSpecs maps.Ordered[SpanQuery, NamedAssertions], data []byte) (specs maps.Ordered[SpanQuery, NamedAssertions], shouldReplace bool) { - err := newFormatSpecs.ForEach(func(key SpanQuery, val NamedAssertions) error { - // assertions is nil for the old format - if val.Assertions == nil { - // dumb error, used for signaling the caller function - return errOldFormatDetected - } - return nil - }) - - if !errors.Is(err, errOldFormatDetected) { - shouldReplace = false - return - } - shouldReplace = true - - testWithOldFormat := struct { - Specs maps.Ordered[SpanQuery, []Assertion] - }{} - - err = json.Unmarshal(data, &testWithOldFormat) - if err != nil { - return - } - - err = testWithOldFormat.Specs.ForEach(func(key SpanQuery, val []Assertion) error { - specs, err = specs.Add(key, NamedAssertions{ - Name: "", - Assertions: val, - }) - return err - }) - - return -} - -func (t Test) MarshalYAML() ([]byte, error) { - return t.MarshalJSON() -} - -func (t *Test) UnmarshalYAML(data []byte) error { - return t.UnmarshalJSON(data) -} diff --git a/server/model/test_json_test.go b/server/model/test_json_test.go deleted file mode 100644 index 4a785ee0ff..0000000000 --- a/server/model/test_json_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package model_test - -import ( - "encoding/json" - "testing" - - "github.com/kubeshop/tracetest/server/model" - "github.com/kubeshop/tracetest/server/pkg/maps" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestJSONEncoding(t *testing.T) { - t.Parallel() - - original := model.Test{ - ID: "ezMn7bE4g", - Name: "first test", - Description: "description", - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ - URL: "http://localhost:3030/hello-instrumented", - }, - }, - Specs: (maps.Ordered[model.SpanQuery, model.NamedAssertions]{}). - MustAdd("query", model.NamedAssertions{ - Name: "some assertion", - Assertions: []model.Assertion{ - "attr:some_attr = 1", - }, - }). - MustAdd("unnamed", model.NamedAssertions{ // unnamed - Assertions: []model.Assertion{ - "attr:some_attr = 1", - }, - }), - Outputs: (maps.Ordered[string, model.Output]{}). - MustAdd("output", model.Output{ - Selector: "selector", - Value: "value", - }), - } - - encoded, err := json.Marshal(original) - require.NoError(t, err) - - decoded := model.Test{} - err = json.Unmarshal(encoded, &decoded) - require.NoError(t, err) - - assert.Equal(t, original, decoded) -} diff --git a/server/model/tests_test.go b/server/model/tests_test.go deleted file mode 100644 index 22f2400d48..0000000000 --- a/server/model/tests_test.go +++ /dev/null @@ -1,172 +0,0 @@ -package model_test - -import ( - "encoding/json" - "testing" - - "github.com/kubeshop/tracetest/server/model" - "github.com/kubeshop/tracetest/server/pkg/maps" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSpec(t *testing.T) { - t.Run("Add", func(t *testing.T) { - spec := (model.Test{}).Specs - - spec, err := spec.Add(model.SpanQuery("1"), model.NamedAssertions{ - Assertions: []model.Assertion{model.Assertion(`1 = 1`)}, - }) - require.NoError(t, err) - - spec, err = spec.Add(model.SpanQuery("2"), model.NamedAssertions{ - Assertions: []model.Assertion{model.Assertion(`2 = 2`)}, - }) - require.NoError(t, err) - assert.Equal(t, 2, spec.Len()) - - spec, err = spec.Add(model.SpanQuery("2"), model.NamedAssertions{ - Assertions: []model.Assertion{model.Assertion(`2 = 2`)}, - }) - assert.ErrorContains(t, err, "selector already exists") - assert.Equal(t, 0, spec.Len()) - - }) - - generateSpec := func() maps.Ordered[model.SpanQuery, model.NamedAssertions] { - spec := (model.Test{}).Specs - - spec, _ = spec.Add(model.SpanQuery("1"), model.NamedAssertions{ - Assertions: []model.Assertion{model.Assertion(`1 = 1`)}, - }) - - spec, _ = spec.Add(model.SpanQuery("2"), model.NamedAssertions{ - Assertions: []model.Assertion{model.Assertion(`2 = 2`)}, - }) - - return spec - } - - t.Run("Map", func(t *testing.T) { - - spec := generateSpec() - - expected := map[string]model.NamedAssertions{ - "1": {Assertions: []model.Assertion{model.Assertion(`1 = 1`)}}, - "2": {Assertions: []model.Assertion{model.Assertion(`2 = 2`)}}, - } - - actual := make(map[string]model.NamedAssertions) - spec.ForEach(func(spanQuery model.SpanQuery, asserts model.NamedAssertions) error { - actual[string(spanQuery)] = asserts - return nil - }) - - assert.Equal(t, expected, actual) - - }) - - t.Run("Get", func(t *testing.T) { - - spec := generateSpec() - - expected := model.NamedAssertions{ - Assertions: []model.Assertion{model.Assertion(`1 = 1`)}, - } - actual := spec.Get(model.SpanQuery("1")) - - assert.Equal(t, expected, actual) - - assert.Empty(t, spec.Get(model.SpanQuery("3"))) - - }) - - t.Run("JSON", func(t *testing.T) { - - spec := generateSpec() - - encoded, err := json.Marshal(spec) - require.NoError(t, err) - - decoded := maps.Ordered[model.SpanQuery, model.NamedAssertions]{} - err = json.Unmarshal(encoded, &decoded) - require.NoError(t, err) - - assert.Equal(t, spec, decoded) - }) - -} - -func TestResults(t *testing.T) { - t.Run("Add", func(t *testing.T) { - def := (model.RunResults{}).Results - - def, err := def.Add(model.SpanQuery("1"), []model.AssertionResult{{Assertion: model.Assertion(`1 = 1`)}}) - require.NoError(t, err) - - def, err = def.Add(model.SpanQuery("2"), []model.AssertionResult{{Assertion: model.Assertion(`2 = 2`)}}) - require.NoError(t, err) - assert.Equal(t, 2, def.Len()) - - def, err = def.Add(model.SpanQuery("2"), []model.AssertionResult{{Assertion: model.Assertion(`2 = 2`)}}) - assert.ErrorContains(t, err, "selector already exists") - assert.Equal(t, 0, def.Len()) - - }) - - generateDef := func() maps.Ordered[model.SpanQuery, []model.AssertionResult] { - def := (model.RunResults{}).Results - - def, _ = def.Add(model.SpanQuery("1"), []model.AssertionResult{{Assertion: model.Assertion(`1 = 1`)}}) - def, _ = def.Add(model.SpanQuery("2"), []model.AssertionResult{{Assertion: model.Assertion(`2 = 2`)}}) - - return def - } - - t.Run("Map", func(t *testing.T) { - - def := generateDef() - - expected := map[string][]model.AssertionResult{ - "1": {{Assertion: model.Assertion(`1 = 1`)}}, - "2": {{Assertion: model.Assertion(`2 = 2`)}}, - } - - actual := map[string][]model.AssertionResult{} - def.ForEach(func(spanQuery model.SpanQuery, asserts []model.AssertionResult) error { - actual[string(spanQuery)] = asserts - return nil - }) - - assert.Equal(t, expected, actual) - - }) - - t.Run("Get", func(t *testing.T) { - - def := generateDef() - - expected := []model.AssertionResult{{Assertion: model.Assertion(`1 = 1`)}} - actual := def.Get(model.SpanQuery("1")) - - assert.Equal(t, expected, actual) - - assert.Empty(t, def.Get(model.SpanQuery("3"))) - - }) - - t.Run("JSON", func(t *testing.T) { - - def := generateDef() - - encoded, err := json.Marshal(def) - require.NoError(t, err) - - decoded := maps.Ordered[model.SpanQuery, []model.AssertionResult]{} - err = json.Unmarshal(encoded, &decoded) - require.NoError(t, err) - - assert.Equal(t, def, decoded) - }) - -} diff --git a/server/model/traces.go b/server/model/traces.go index c8e3876ebb..e0572da5b2 100644 --- a/server/model/traces.go +++ b/server/model/traces.go @@ -6,6 +6,9 @@ import ( "sort" "strings" + "github.com/kubeshop/tracetest/server/pkg/id" + "github.com/kubeshop/tracetest/server/pkg/timing" + "github.com/kubeshop/tracetest/server/test/trigger" "go.opentelemetry.io/otel/trace" ) @@ -69,7 +72,7 @@ func getRootSpan(allRoots []*Span) *Span { } if root == nil { - root = &Span{ID: IDGen.SpanID(), Name: TemporaryRootSpanName, Attributes: make(Attributes), Children: []*Span{}} + root = &Span{ID: id.NewRandGenerator().SpanID(), Name: TemporaryRootSpanName, Attributes: make(Attributes), Children: []*Span{}} } for _, span := range allRoots { @@ -105,8 +108,8 @@ func spanType(attrs Attributes) string { } func spanDuration(span Span) string { - timeDifference := timeDiff(span.StartTime, span.EndTime) - return fmt.Sprintf("%d", durationInNanoseconds(timeDifference)) + timeDifference := timing.TimeDiff(span.StartTime, span.EndTime) + return fmt.Sprintf("%d", timing.DurationInNanoseconds(timeDifference)) } func (t *Trace) Sort() Trace { @@ -238,3 +241,9 @@ func flattenSpans(res map[trace.SpanID]*Span, root Span) { // Remove children and parent because they are now part of the flatten structure rootPtr.Children = nil } + +func AugmentRootSpan(span Span, result trigger.TriggerResult) Span { + return span. + setMetadataAttributes(). + setTriggerResultAttributes(result) +} diff --git a/server/model/traces_test.go b/server/model/traces_test.go index 583a67a183..2156b71725 100644 --- a/server/model/traces_test.go +++ b/server/model/traces_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/traces" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -178,7 +179,7 @@ func TestTriggerSpanShouldBeRootWhenTemporaryRootExistsToo(t *testing.T) { func newSpan(name string, parent *model.Span) model.Span { span := model.Span{ - ID: model.IDGen.SpanID(), + ID: id.NewRandGenerator().SpanID(), Name: name, Parent: parent, Attributes: make(model.Attributes), @@ -194,7 +195,7 @@ func newSpan(name string, parent *model.Span) model.Span { } func newOtelSpan(name string, parent *v1.Span) *v1.Span { - id := model.IDGen.SpanID() + id := id.NewRandGenerator().SpanID() var parentId []byte = nil if parent != nil { parentId = parent.SpanId diff --git a/server/model/transaction_changes.go b/server/model/transaction_changes.go deleted file mode 100644 index 147b0f76d7..0000000000 --- a/server/model/transaction_changes.go +++ /dev/null @@ -1,45 +0,0 @@ -package model - -import "encoding/json" - -func BumpTransactionVersionIfNeeded(in, updated Transaction) Transaction { - transactionHasChanged := transactionHasChanged(in, updated) - if transactionHasChanged { - updated.Version = in.Version + 1 - } - - return updated -} - -func transactionHasChanged(in, updated Transaction) bool { - jsons := []struct { - Name string - Description string - Steps []string - }{ - { - Name: in.Name, - Description: in.Description, - Steps: getStepIds(in), - }, - { - Name: updated.Name, - Description: updated.Description, - Steps: getStepIds(updated), - }, - } - - inJson, _ := json.Marshal(jsons[0]) - updatedJson, _ := json.Marshal(jsons[1]) - - return string(inJson) != string(updatedJson) -} - -func getStepIds(in Transaction) []string { - steps := make([]string, len(in.Steps)) - for i, step := range in.Steps { - steps[i] = step.ID.String() - } - - return steps -} diff --git a/server/model/transactions.go b/server/model/transactions.go deleted file mode 100644 index b820ad667c..0000000000 --- a/server/model/transactions.go +++ /dev/null @@ -1,96 +0,0 @@ -package model - -import ( - "fmt" - "time" - - "github.com/kubeshop/tracetest/server/environment" - "github.com/kubeshop/tracetest/server/pkg/id" -) - -type ( - Transaction struct { - ID id.ID - CreatedAt time.Time - Name string - Description string - Version int - Steps []Test - Summary Summary - } - - TransactionRunState string - - TransactionRun struct { - ID int - TransactionID id.ID - TransactionVersion int - - // Timestamps - CreatedAt time.Time - CompletedAt time.Time - - // - Steps []Run - - // trigger params - State TransactionRunState - CurrentTest int - - // result info - LastError error - Pass int - Fail int - - Metadata RunMetadata - - // environment - Environment environment.Environment - } -) - -const ( - TransactionRunStateCreated TransactionRunState = "CREATED" - TransactionRunStateExecuting TransactionRunState = "EXECUTING" - TransactionRunStateFailed TransactionRunState = "FAILED" - TransactionRunStateFinished TransactionRunState = "FINISHED" -) - -func (rs TransactionRunState) IsFinal() bool { - return rs == TransactionRunStateFailed || rs == TransactionRunStateFinished -} - -func (t Transaction) HasID() bool { - return t.ID != "" -} - -func (t Transaction) NewRun() TransactionRun { - - return TransactionRun{ - TransactionID: t.ID, - TransactionVersion: t.Version, - CreatedAt: time.Now(), - State: TransactionRunStateCreated, - Steps: make([]Run, 0, len(t.Steps)), - CurrentTest: 0, - } -} - -func (tr TransactionRun) ResourceID() string { - return fmt.Sprintf("transaction/%s/run/%d", tr.TransactionID, tr.ID) -} - -func (tr TransactionRun) ResultsCount() (pass, fail int) { - if tr.Steps == nil { - return - } - - for _, step := range tr.Steps { - stepPass, stepFail := step.ResultsCount() - - pass += stepPass - fail += stepFail - } - - return -} diff --git a/server/model/trigger_json_test.go b/server/model/trigger_json_test.go deleted file mode 100644 index f9a6b8ae24..0000000000 --- a/server/model/trigger_json_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package model_test - -import ( - "encoding/json" - "testing" - - "github.com/kubeshop/tracetest/server/model" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestTriggerFormatV1(t *testing.T) { - v1 := struct { - Type model.TriggerType - HTTP *model.HTTPRequest - GRPC *model.GRPCRequest - TraceID *model.TRACEIDRequest - }{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ - Method: model.HTTPMethodGET, - URL: "http://example.com/list", - }, - } - - expected := model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ - Method: model.HTTPMethodGET, - URL: "http://example.com/list", - }, - } - - v1Json, err := json.Marshal(v1) - require.NoError(t, err) - - current := model.Trigger{} - err = json.Unmarshal(v1Json, ¤t) - require.NoError(t, err) - - assert.Equal(t, expected, current) -} - -func TestTriggerFormatV2(t *testing.T) { - v2 := struct { - Type model.TriggerType `json:"triggerType"` - HTTP *model.HTTPRequest `json:"http,omitempty"` - GRPC *model.GRPCRequest `json:"grpc,omitempty"` - TraceID *model.TRACEIDRequest `json:"traceid,omitempty"` - }{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ - Method: model.HTTPMethodGET, - URL: "http://example.com/list", - }, - } - - expected := model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ - Method: model.HTTPMethodGET, - URL: "http://example.com/list", - }, - } - - v2Json, err := json.Marshal(v2) - require.NoError(t, err) - - current := model.Trigger{} - err = json.Unmarshal(v2Json, ¤t) - require.NoError(t, err) - - assert.Equal(t, expected, current) -} diff --git a/server/model/yaml/file_test.go b/server/model/yaml/file_test.go deleted file mode 100644 index e79d5edaa5..0000000000 --- a/server/model/yaml/file_test.go +++ /dev/null @@ -1,371 +0,0 @@ -package yaml_test - -import ( - "testing" - - "github.com/kubeshop/tracetest/server/model" - "github.com/kubeshop/tracetest/server/model/yaml" - "github.com/kubeshop/tracetest/server/pkg/id" - "github.com/kubeshop/tracetest/server/pkg/maps" - "github.com/kubeshop/tracetest/server/transaction" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestTestModel(t *testing.T) { - cases := []struct { - name string - in yaml.Test - expected model.Test - }{ - { - name: "HTTP", - in: yaml.Test{ - Name: "A test", - Description: "A test description", - Trigger: yaml.TestTrigger{ - Type: "http", - HTTPRequest: yaml.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - Headers: []yaml.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: "", - }, - }, - }, - expected: model.Test{ - Name: "A test", - Description: "A test description", - ServiceUnderTest: model.Trigger{ - Type: "http", - HTTP: &model.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - Headers: []model.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: "", - }, - }, - }, - }, - { - name: "HTTPBasicAuth", - in: yaml.Test{ - Name: "A test", - Description: "A test description", - Trigger: yaml.TestTrigger{ - Type: "http", - HTTPRequest: yaml.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - Headers: []yaml.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: "", - Authentication: &yaml.HTTPAuthentication{ - Type: "basic", - Basic: &yaml.HTTPBasicAuth{ - Username: "matheus", - Password: "pikachu", - }, - }, - }, - }, - }, - expected: model.Test{ - Name: "A test", - Description: "A test description", - ServiceUnderTest: model.Trigger{ - Type: "http", - HTTP: &model.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - Headers: []model.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: "", - Auth: &model.HTTPAuthenticator{ - Type: "basic", - Basic: model.BasicAuthenticator{ - Username: "matheus", - Password: "pikachu", - }, - }, - }, - }, - }, - }, - { - name: "HTTPApiKey", - in: yaml.Test{ - Name: "A test", - Description: "A test description", - Trigger: yaml.TestTrigger{ - Type: "http", - HTTPRequest: yaml.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - Headers: []yaml.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: "", - Authentication: &yaml.HTTPAuthentication{ - Type: "apiKey", - APIKey: &yaml.HTTPAPIKeyAuth{ - Key: "X-Key", - Value: "my-api-key", - In: "header", - }, - }, - }, - }, - }, - expected: model.Test{ - Name: "A test", - Description: "A test description", - ServiceUnderTest: model.Trigger{ - Type: "http", - HTTP: &model.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - Headers: []model.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: "", - Auth: &model.HTTPAuthenticator{ - Type: "apiKey", - APIKey: model.APIKeyAuthenticator{ - Key: "X-Key", - Value: "my-api-key", - In: "header", - }, - }, - }, - }, - }, - }, - { - name: "HTTPBearer", - in: yaml.Test{ - Name: "A test", - Description: "A test description", - Trigger: yaml.TestTrigger{ - Type: "http", - HTTPRequest: yaml.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - Headers: []yaml.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: "", - Authentication: &yaml.HTTPAuthentication{ - Type: "bearer", - Bearer: &yaml.HTTPBearerAuth{ - Token: "my-token", - }, - }, - }, - }, - }, - expected: model.Test{ - Name: "A test", - Description: "A test description", - ServiceUnderTest: model.Trigger{ - Type: "http", - HTTP: &model.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - Headers: []model.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: "", - Auth: &model.HTTPAuthenticator{ - Type: "bearer", - Bearer: model.BearerAuthenticator{ - Bearer: "my-token", - }, - }, - }, - }, - }, - }, - { - name: "HTTPRequestBody", - in: yaml.Test{ - Name: "A test", - Description: "A test description", - Trigger: yaml.TestTrigger{ - Type: "http", - HTTPRequest: yaml.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - Headers: []yaml.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: `{ "message": "hello" }`, - }, - }, - }, - expected: model.Test{ - Name: "A test", - Description: "A test description", - ServiceUnderTest: model.Trigger{ - Type: "http", - HTTP: &model.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - Headers: []model.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: `{ "message": "hello" }`, - }, - }, - }, - }, - { - name: "Definitions", - in: yaml.Test{ - Name: "A test", - Description: "A test description", - Trigger: yaml.TestTrigger{ - Type: "http", - HTTPRequest: yaml.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - Body: "", - }, - }, - Specs: []yaml.TestSpec{ - { - Selector: `span[tracetest.span.type="http"]`, - Assertions: []string{ - "attr:tracetest.span.duration <= 200ms", - "attr:http.status_code = 200", - }, - }, - }, - }, - expected: model.Test{ - Name: "A test", - Description: "A test description", - ServiceUnderTest: model.Trigger{ - Type: "http", - HTTP: &model.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - }, - }, - Specs: (maps.Ordered[model.SpanQuery, model.NamedAssertions]{}). - MustAdd(model.SpanQuery(`span[tracetest.span.type="http"]`), model.NamedAssertions{ - Name: "", - Assertions: []model.Assertion{ - `attr:tracetest.span.duration <= 200ms`, - `attr:http.status_code = 200`, - }, - }), - }, - }, - { - name: "Outputs", - in: yaml.Test{ - Name: "A test", - Description: "A test description", - Trigger: yaml.TestTrigger{ - Type: "http", - HTTPRequest: yaml.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - }, - }, - Outputs: yaml.Outputs{ - { - Name: "USER_ID", - Selector: `span[name = "create user"]`, - Value: `attr:myapp.user_id`, - }, - }, - }, - expected: model.Test{ - Name: "A test", - Description: "A test description", - ServiceUnderTest: model.Trigger{ - Type: "http", - HTTP: &model.HTTPRequest{ - URL: "http://localhost:1234", - Method: "POST", - }, - }, - Outputs: (maps.Ordered[string, model.Output]{}). - MustAdd("USER_ID", model.Output{ - Selector: `span[name = "create user"]`, - Value: `attr:myapp.user_id`, - }), - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - cl := c - t.Parallel() - - file := yaml.File{ - Type: yaml.FileTypeTest, - Spec: cl.in, - } - - test, err := file.Test() - require.NoError(t, err) - - actual := test.Model() - - assert.Equal(t, cl.expected, actual) - }) - } -} - -func TestTransactionModel(t *testing.T) { - cases := []struct { - name string - in yaml.Transaction - expected transaction.Transaction - }{ - { - name: "Basic", - in: yaml.Transaction{ - ID: "123", - Name: "Transaction", - Description: "Some transaction", - Steps: []string{"345"}, - }, - expected: transaction.Transaction{ - ID: id.ID("123"), - Name: "Transaction", - Description: "Some transaction", - StepIDs: []id.ID{"345"}, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - cl := c - t.Parallel() - - file := yaml.File{ - Type: yaml.FileTypeTransaction, - Spec: cl.in, - } - - transaction, err := file.Transaction() - require.NoError(t, err) - - actual := transaction.Model() - - assert.Equal(t, cl.expected, actual) - }) - } -} diff --git a/server/model/yaml/test.go b/server/model/yaml/test.go index 33c7e3b8cd..841574728c 100644 --- a/server/model/yaml/test.go +++ b/server/model/yaml/test.go @@ -4,46 +4,47 @@ import ( "fmt" dc "github.com/fluidtruck/deepcopy" - "github.com/kubeshop/tracetest/server/model" - "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/test" ) type TestSpecs []TestSpec -func (ts TestSpecs) Model() maps.Ordered[model.SpanQuery, model.NamedAssertions] { - mts := maps.Ordered[model.SpanQuery, model.NamedAssertions]{} +func (ts TestSpecs) Model() test.Specs { + specs := make(test.Specs, 0, len(ts)) for _, spec := range ts { - assertions := make([]model.Assertion, 0, len(spec.Assertions)) + assertions := make([]test.Assertion, 0, len(spec.Assertions)) for _, a := range spec.Assertions { - assertions = append(assertions, model.Assertion(a)) + assertions = append(assertions, test.Assertion(a)) } - mts, _ = mts.Add(model.SpanQuery(spec.Selector), model.NamedAssertions{ + specs = append(specs, test.TestSpec{ Name: spec.Name, + Selector: test.SpanQuery(spec.Selector), Assertions: assertions, }) } - return mts + return specs } type Outputs []Output -func (outs Outputs) Model() maps.Ordered[string, model.Output] { - mos := maps.Ordered[string, model.Output]{} +func (outs Outputs) Model() test.Outputs { + outputs := make(test.Outputs, 0, len(outs)) for _, output := range outs { - mos, _ = mos.Add(output.Name, model.Output{ - Selector: model.SpanQuery(output.Selector), + outputs = append(outputs, test.Output{ + Name: output.Name, + Selector: test.SpanQuery(output.Selector), Value: output.Value, }) } - return mos + return outputs } type Test struct { ID string `mapstructure:"id"` Name string `mapstructure:"name"` Description string `mapstructure:"description" yaml:",omitempty"` - Trigger TestTrigger `mapstructure:"trigger" dc:"serviceUnderTest"` + Trigger TestTrigger `mapstructure:"trigger"` Specs TestSpecs `mapstructure:"specs" yaml:",omitempty"` Outputs Outputs `mapstructure:"outputs,omitempty" yaml:",omitempty"` } @@ -102,8 +103,8 @@ type TestSpec struct { Assertions []string `mapstructure:"assertions"` } -func (t Test) Model() model.Test { - mt := model.Test{} +func (t Test) Model() test.Test { + mt := test.Test{} dc.DeepCopy(t, &mt) mt.Specs = t.Specs.Model() mt.Outputs = t.Outputs.Model() diff --git a/server/model/yaml/yamlconvert/tests.go b/server/model/yaml/yamlconvert/tests.go deleted file mode 100644 index dd24615c51..0000000000 --- a/server/model/yaml/yamlconvert/tests.go +++ /dev/null @@ -1,39 +0,0 @@ -package yamlconvert - -import ( - dc "github.com/fluidtruck/deepcopy" - "github.com/kubeshop/tracetest/server/model" - "github.com/kubeshop/tracetest/server/model/yaml" -) - -func Test(in model.Test) yaml.File { - out := yaml.Test{} - dc.DeepCopy(in, &out) - dc.DeepCopy(in.ServiceUnderTest, &out.Trigger) - - in.Specs.ForEach(func(key model.SpanQuery, val model.NamedAssertions) error { - spec := yaml.TestSpec{ - Selector: string(key), - Name: val.Name, - } - dc.DeepCopy(val.Assertions, &spec.Assertions) - out.Specs = append(out.Specs, spec) - - return nil - }) - - in.Outputs.ForEach(func(key string, val model.Output) error { - out.Outputs = append(out.Outputs, yaml.Output{ - Name: key, - Selector: string(val.Selector), - Value: val.Value, - }) - - return nil - }) - - return yaml.File{ - Type: yaml.FileTypeTest, - Spec: out, - } -} diff --git a/server/model/yaml/yamlconvert/tests_test.go b/server/model/yaml/yamlconvert/tests_test.go deleted file mode 100644 index a898bcaaad..0000000000 --- a/server/model/yaml/yamlconvert/tests_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package yamlconvert_test - -import ( - "testing" - - "github.com/kubeshop/tracetest/server/model" - "github.com/kubeshop/tracetest/server/model/yaml" - "github.com/kubeshop/tracetest/server/model/yaml/yamlconvert" - "github.com/kubeshop/tracetest/server/pkg/maps" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestConverter(t *testing.T) { - in := model.Test{ - ID: "123", - Name: "The Name", - Description: "Description", - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ - Method: model.HTTPMethodPOST, - URL: "http://google.com", - Headers: []model.HTTPHeader{ - {Key: "Content-Type", Value: "application/json"}, - }, - Body: `{"id":123}`, - Auth: &model.HTTPAuthenticator{ - Type: "basic", - Basic: model.BasicAuthenticator{ - Username: "user", - Password: "passwd", - }, - }, - }, - }, - Specs: (maps.Ordered[model.SpanQuery, model.NamedAssertions]{}). - MustAdd(model.SpanQuery(`span[name="Test Span"]`), model.NamedAssertions{ - Name: "count test spans", - Assertions: []model.Assertion{ - "attr:tracetest.selected_spans.count = 2", - }, - }), - Outputs: (maps.Ordered[string, model.Output]{}). - MustAdd("user_id", model.Output{ - Selector: model.SpanQuery(`span[name="Create User"]`), - Value: "attr:myapp.user_id", - }), - } - - expected := `type: Test -spec: - id: "123" - name: The Name - description: Description - trigger: - type: http - httpRequest: - url: http://google.com - method: POST - headers: - - key: Content-Type - value: application/json - authentication: - type: basic - basic: - username: user - password: passwd - body: '{"id":123}' - specs: - - name: count test spans - selector: span[name="Test Span"] - assertions: - - attr:tracetest.selected_spans.count = 2 - outputs: - - name: user_id - selector: span[name="Create User"] - value: attr:myapp.user_id -` - - mapped := yamlconvert.Test(in) - actual, err := yaml.Encode(mapped) - require.NoError(t, err) - - assert.Equal(t, expected, string(actual)) -} diff --git a/server/openapi/api.go b/server/openapi/api.go index a8181e207b..fb926e040f 100644 --- a/server/openapi/api.go +++ b/server/openapi/api.go @@ -18,8 +18,6 @@ import ( // The ApiApiRouter implementation should parse necessary information from the http request, // pass the data to a ApiApiServicer to perform the required actions, then write the service results to the http response. type ApiApiRouter interface { - CreateTest(http.ResponseWriter, *http.Request) - DeleteTest(http.ResponseWriter, *http.Request) DeleteTestRun(http.ResponseWriter, *http.Request) DeleteTransactionRun(http.ResponseWriter, *http.Request) DryRunAssertion(http.ResponseWriter, *http.Request) @@ -28,15 +26,12 @@ type ApiApiRouter interface { ExpressionResolve(http.ResponseWriter, *http.Request) GetResources(http.ResponseWriter, *http.Request) GetRunResultJUnit(http.ResponseWriter, *http.Request) - GetTest(http.ResponseWriter, *http.Request) GetTestResultSelectedSpans(http.ResponseWriter, *http.Request) GetTestRun(http.ResponseWriter, *http.Request) GetTestRunEvents(http.ResponseWriter, *http.Request) GetTestRuns(http.ResponseWriter, *http.Request) GetTestSpecs(http.ResponseWriter, *http.Request) GetTestVersion(http.ResponseWriter, *http.Request) - GetTestVersionDefinitionFile(http.ResponseWriter, *http.Request) - GetTests(http.ResponseWriter, *http.Request) GetTransactionRun(http.ResponseWriter, *http.Request) GetTransactionRuns(http.ResponseWriter, *http.Request) GetTransactionVersion(http.ResponseWriter, *http.Request) @@ -48,8 +43,6 @@ type ApiApiRouter interface { RunTransaction(http.ResponseWriter, *http.Request) StopTestRun(http.ResponseWriter, *http.Request) TestConnection(http.ResponseWriter, *http.Request) - UpdateTest(http.ResponseWriter, *http.Request) - UpsertDefinition(http.ResponseWriter, *http.Request) } // ResourceApiApiRouter defines the required methods for binding the api requests to a responses for the ResourceApiApi @@ -59,11 +52,13 @@ type ResourceApiApiRouter interface { CreateDemo(http.ResponseWriter, *http.Request) CreateEnvironment(http.ResponseWriter, *http.Request) CreateLinter(http.ResponseWriter, *http.Request) + CreateTest(http.ResponseWriter, *http.Request) CreateTransaction(http.ResponseWriter, *http.Request) DeleteDataStore(http.ResponseWriter, *http.Request) DeleteDemo(http.ResponseWriter, *http.Request) DeleteEnvironment(http.ResponseWriter, *http.Request) DeleteLinter(http.ResponseWriter, *http.Request) + DeleteTest(http.ResponseWriter, *http.Request) DeleteTransaction(http.ResponseWriter, *http.Request) GetConfiguration(http.ResponseWriter, *http.Request) GetDataStore(http.ResponseWriter, *http.Request) @@ -71,6 +66,7 @@ type ResourceApiApiRouter interface { GetEnvironment(http.ResponseWriter, *http.Request) GetLinter(http.ResponseWriter, *http.Request) GetPollingProfile(http.ResponseWriter, *http.Request) + GetTests(http.ResponseWriter, *http.Request) GetTransaction(http.ResponseWriter, *http.Request) GetTransactions(http.ResponseWriter, *http.Request) ListConfiguration(http.ResponseWriter, *http.Request) @@ -79,12 +75,14 @@ type ResourceApiApiRouter interface { ListEnvironments(http.ResponseWriter, *http.Request) ListLinters(http.ResponseWriter, *http.Request) ListPollingProfile(http.ResponseWriter, *http.Request) + TestsTestIdGet(http.ResponseWriter, *http.Request) UpdateConfiguration(http.ResponseWriter, *http.Request) UpdateDataStore(http.ResponseWriter, *http.Request) UpdateDemo(http.ResponseWriter, *http.Request) UpdateEnvironment(http.ResponseWriter, *http.Request) UpdateLinter(http.ResponseWriter, *http.Request) UpdatePollingProfile(http.ResponseWriter, *http.Request) + UpdateTest(http.ResponseWriter, *http.Request) UpdateTransaction(http.ResponseWriter, *http.Request) } @@ -93,8 +91,6 @@ type ResourceApiApiRouter interface { // while the service implementation can be ignored with the .openapi-generator-ignore file // and updated with the logic required for the API. type ApiApiServicer interface { - CreateTest(context.Context, Test) (ImplResponse, error) - DeleteTest(context.Context, string) (ImplResponse, error) DeleteTestRun(context.Context, string, int32) (ImplResponse, error) DeleteTransactionRun(context.Context, string, int32) (ImplResponse, error) DryRunAssertion(context.Context, string, int32, TestSpecs) (ImplResponse, error) @@ -103,15 +99,12 @@ type ApiApiServicer interface { ExpressionResolve(context.Context, ResolveRequestInfo) (ImplResponse, error) GetResources(context.Context, int32, int32, string, string, string) (ImplResponse, error) GetRunResultJUnit(context.Context, string, int32) (ImplResponse, error) - GetTest(context.Context, string) (ImplResponse, error) GetTestResultSelectedSpans(context.Context, string, int32, string) (ImplResponse, error) GetTestRun(context.Context, string, int32) (ImplResponse, error) GetTestRunEvents(context.Context, string, int32) (ImplResponse, error) GetTestRuns(context.Context, string, int32, int32) (ImplResponse, error) GetTestSpecs(context.Context, string) (ImplResponse, error) GetTestVersion(context.Context, string, int32) (ImplResponse, error) - GetTestVersionDefinitionFile(context.Context, string, int32) (ImplResponse, error) - GetTests(context.Context, int32, int32, string, string, string) (ImplResponse, error) GetTransactionRun(context.Context, string, int32) (ImplResponse, error) GetTransactionRuns(context.Context, string, int32, int32) (ImplResponse, error) GetTransactionVersion(context.Context, string, int32) (ImplResponse, error) @@ -123,8 +116,6 @@ type ApiApiServicer interface { RunTransaction(context.Context, string, RunInformation) (ImplResponse, error) StopTestRun(context.Context, string, int32) (ImplResponse, error) TestConnection(context.Context, DataStore) (ImplResponse, error) - UpdateTest(context.Context, string, Test) (ImplResponse, error) - UpsertDefinition(context.Context, TextDefinition) (ImplResponse, error) } // ResourceApiApiServicer defines the api actions for the ResourceApiApi service @@ -135,11 +126,13 @@ type ResourceApiApiServicer interface { CreateDemo(context.Context, Demo) (ImplResponse, error) CreateEnvironment(context.Context, EnvironmentResource) (ImplResponse, error) CreateLinter(context.Context, LinterResource) (ImplResponse, error) + CreateTest(context.Context, Test) (ImplResponse, error) CreateTransaction(context.Context, TransactionResource) (ImplResponse, error) DeleteDataStore(context.Context, string) (ImplResponse, error) DeleteDemo(context.Context, string) (ImplResponse, error) DeleteEnvironment(context.Context, string) (ImplResponse, error) DeleteLinter(context.Context, string) (ImplResponse, error) + DeleteTest(context.Context, string) (ImplResponse, error) DeleteTransaction(context.Context, string) (ImplResponse, error) GetConfiguration(context.Context, string) (ImplResponse, error) GetDataStore(context.Context, string) (ImplResponse, error) @@ -147,6 +140,7 @@ type ResourceApiApiServicer interface { GetEnvironment(context.Context, string) (ImplResponse, error) GetLinter(context.Context, string) (ImplResponse, error) GetPollingProfile(context.Context, string) (ImplResponse, error) + GetTests(context.Context, int32, int32, string, string, string) (ImplResponse, error) GetTransaction(context.Context, string) (ImplResponse, error) GetTransactions(context.Context, int32, int32, string, string, string) (ImplResponse, error) ListConfiguration(context.Context, int32, int32, string, string) (ImplResponse, error) @@ -155,11 +149,13 @@ type ResourceApiApiServicer interface { ListEnvironments(context.Context, int32, int32, string, string) (ImplResponse, error) ListLinters(context.Context, int32, int32, string, string) (ImplResponse, error) ListPollingProfile(context.Context, int32, int32, string, string) (ImplResponse, error) + TestsTestIdGet(context.Context, string) (ImplResponse, error) UpdateConfiguration(context.Context, string, ConfigurationResource) (ImplResponse, error) UpdateDataStore(context.Context, string, DataStore) (ImplResponse, error) UpdateDemo(context.Context, string, Demo) (ImplResponse, error) UpdateEnvironment(context.Context, string, EnvironmentResource) (ImplResponse, error) UpdateLinter(context.Context, string, LinterResource) (ImplResponse, error) UpdatePollingProfile(context.Context, string, PollingProfile) (ImplResponse, error) + UpdateTest(context.Context, string, Test) (ImplResponse, error) UpdateTransaction(context.Context, string, TransactionResource) (ImplResponse, error) } diff --git a/server/openapi/api_api.go b/server/openapi/api_api.go index e67605d3dd..81841bdbef 100644 --- a/server/openapi/api_api.go +++ b/server/openapi/api_api.go @@ -50,18 +50,6 @@ func NewApiApiController(s ApiApiServicer, opts ...ApiApiOption) Router { // Routes returns all the api routes for the ApiApiController func (c *ApiApiController) Routes() Routes { return Routes{ - { - "CreateTest", - strings.ToUpper("Post"), - "/api/tests", - c.CreateTest, - }, - { - "DeleteTest", - strings.ToUpper("Delete"), - "/api/tests/{testId}", - c.DeleteTest, - }, { "DeleteTestRun", strings.ToUpper("Delete"), @@ -110,12 +98,6 @@ func (c *ApiApiController) Routes() Routes { "/api/tests/{testId}/run/{runId}/junit.xml", c.GetRunResultJUnit, }, - { - "GetTest", - strings.ToUpper("Get"), - "/api/tests/{testId}", - c.GetTest, - }, { "GetTestResultSelectedSpans", strings.ToUpper("Get"), @@ -152,18 +134,6 @@ func (c *ApiApiController) Routes() Routes { "/api/tests/{testId}/version/{version}", c.GetTestVersion, }, - { - "GetTestVersionDefinitionFile", - strings.ToUpper("Get"), - "/api/tests/{testId}/version/{version}/definition.yaml", - c.GetTestVersionDefinitionFile, - }, - { - "GetTests", - strings.ToUpper("Get"), - "/api/tests", - c.GetTests, - }, { "GetTransactionRun", strings.ToUpper("Get"), @@ -230,59 +200,7 @@ func (c *ApiApiController) Routes() Routes { "/api/config/connection", c.TestConnection, }, - { - "UpdateTest", - strings.ToUpper("Put"), - "/api/tests/{testId}", - c.UpdateTest, - }, - { - "UpsertDefinition", - strings.ToUpper("Put"), - "/api/definition.yaml", - c.UpsertDefinition, - }, - } -} - -// CreateTest - Create new test -func (c *ApiApiController) CreateTest(w http.ResponseWriter, r *http.Request) { - testParam := Test{} - d := json.NewDecoder(r.Body) - d.DisallowUnknownFields() - if err := d.Decode(&testParam); err != nil { - c.errorHandler(w, r, &ParsingError{Err: err}, nil) - return - } - if err := AssertTestRequired(testParam); err != nil { - c.errorHandler(w, r, err, nil) - return - } - result, err := c.service.CreateTest(r.Context(), testParam) - // If an error occurred, encode the error with the status code - if err != nil { - c.errorHandler(w, r, err, &result) - return - } - // If no error, encode the body and the result code - EncodeJSONResponse(result.Body, &result.Code, w) - -} - -// DeleteTest - delete a test -func (c *ApiApiController) DeleteTest(w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - testIdParam := params["testId"] - - result, err := c.service.DeleteTest(r.Context(), testIdParam) - // If an error occurred, encode the error with the status code - if err != nil { - c.errorHandler(w, r, err, &result) - return } - // If no error, encode the body and the result code - EncodeJSONResponse(result.Body, &result.Code, w) - } // DeleteTestRun - delete a test run @@ -481,22 +399,6 @@ func (c *ApiApiController) GetRunResultJUnit(w http.ResponseWriter, r *http.Requ } -// GetTest - get test -func (c *ApiApiController) GetTest(w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - testIdParam := params["testId"] - - result, err := c.service.GetTest(r.Context(), testIdParam) - // If an error occurred, encode the error with the status code - if err != nil { - c.errorHandler(w, r, err, &result) - return - } - // If no error, encode the body and the result code - EncodeJSONResponse(result.Body, &result.Code, w) - -} - // GetTestResultSelectedSpans - retrieve spans that will be selected by selector func (c *ApiApiController) GetTestResultSelectedSpans(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) @@ -630,55 +532,6 @@ func (c *ApiApiController) GetTestVersion(w http.ResponseWriter, r *http.Request } -// GetTestVersionDefinitionFile - Get the test definition as an YAML file -func (c *ApiApiController) GetTestVersionDefinitionFile(w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - testIdParam := params["testId"] - - versionParam, err := parseInt32Parameter(params["version"], true) - if err != nil { - c.errorHandler(w, r, &ParsingError{Err: err}, nil) - return - } - - result, err := c.service.GetTestVersionDefinitionFile(r.Context(), testIdParam, versionParam) - // If an error occurred, encode the error with the status code - if err != nil { - c.errorHandler(w, r, err, &result) - return - } - // If no error, encode the body and the result code - EncodeJSONResponse(result.Body, &result.Code, w) - -} - -// GetTests - Get tests -func (c *ApiApiController) GetTests(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - takeParam, err := parseInt32Parameter(query.Get("take"), false) - if err != nil { - c.errorHandler(w, r, &ParsingError{Err: err}, nil) - return - } - skipParam, err := parseInt32Parameter(query.Get("skip"), false) - if err != nil { - c.errorHandler(w, r, &ParsingError{Err: err}, nil) - return - } - queryParam := query.Get("query") - sortByParam := query.Get("sortBy") - sortDirectionParam := query.Get("sortDirection") - result, err := c.service.GetTests(r.Context(), takeParam, skipParam, queryParam, sortByParam, sortDirectionParam) - // If an error occurred, encode the error with the status code - if err != nil { - c.errorHandler(w, r, err, &result) - return - } - // If no error, encode the body and the result code - EncodeJSONResponse(result.Body, &result.Code, w) - -} - // GetTransactionRun - Get a specific run from a particular transaction func (c *ApiApiController) GetTransactionRun(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) @@ -930,54 +783,3 @@ func (c *ApiApiController) TestConnection(w http.ResponseWriter, r *http.Request EncodeJSONResponse(result.Body, &result.Code, w) } - -// UpdateTest - update test -func (c *ApiApiController) UpdateTest(w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - testIdParam := params["testId"] - - testParam := Test{} - d := json.NewDecoder(r.Body) - d.DisallowUnknownFields() - if err := d.Decode(&testParam); err != nil { - c.errorHandler(w, r, &ParsingError{Err: err}, nil) - return - } - if err := AssertTestRequired(testParam); err != nil { - c.errorHandler(w, r, err, nil) - return - } - result, err := c.service.UpdateTest(r.Context(), testIdParam, testParam) - // If an error occurred, encode the error with the status code - if err != nil { - c.errorHandler(w, r, err, &result) - return - } - // If no error, encode the body and the result code - EncodeJSONResponse(result.Body, &result.Code, w) - -} - -// UpsertDefinition - Upsert a definition -func (c *ApiApiController) UpsertDefinition(w http.ResponseWriter, r *http.Request) { - textDefinitionParam := TextDefinition{} - d := json.NewDecoder(r.Body) - d.DisallowUnknownFields() - if err := d.Decode(&textDefinitionParam); err != nil { - c.errorHandler(w, r, &ParsingError{Err: err}, nil) - return - } - if err := AssertTextDefinitionRequired(textDefinitionParam); err != nil { - c.errorHandler(w, r, err, nil) - return - } - result, err := c.service.UpsertDefinition(r.Context(), textDefinitionParam) - // If an error occurred, encode the error with the status code - if err != nil { - c.errorHandler(w, r, err, &result) - return - } - // If no error, encode the body and the result code - EncodeJSONResponse(result.Body, &result.Code, w) - -} diff --git a/server/openapi/api_resource_api.go b/server/openapi/api_resource_api.go index 630bb497a4..6f02b4f86d 100644 --- a/server/openapi/api_resource_api.go +++ b/server/openapi/api_resource_api.go @@ -68,6 +68,12 @@ func (c *ResourceApiApiController) Routes() Routes { "/api/linters", c.CreateLinter, }, + { + "CreateTest", + strings.ToUpper("Post"), + "/api/tests", + c.CreateTest, + }, { "CreateTransaction", strings.ToUpper("Post"), @@ -98,6 +104,12 @@ func (c *ResourceApiApiController) Routes() Routes { "/api/linters/{LinterId}", c.DeleteLinter, }, + { + "DeleteTest", + strings.ToUpper("Delete"), + "/api/tests/{testId}", + c.DeleteTest, + }, { "DeleteTransaction", strings.ToUpper("Delete"), @@ -140,6 +152,12 @@ func (c *ResourceApiApiController) Routes() Routes { "/api/pollingprofiles/{pollingProfileId}", c.GetPollingProfile, }, + { + "GetTests", + strings.ToUpper("Get"), + "/api/tests", + c.GetTests, + }, { "GetTransaction", strings.ToUpper("Get"), @@ -188,6 +206,12 @@ func (c *ResourceApiApiController) Routes() Routes { "/api/pollingprofiles", c.ListPollingProfile, }, + { + "TestsTestIdGet", + strings.ToUpper("Get"), + "/api/tests/{testId}", + c.TestsTestIdGet, + }, { "UpdateConfiguration", strings.ToUpper("Put"), @@ -224,6 +248,12 @@ func (c *ResourceApiApiController) Routes() Routes { "/api/pollingprofiles/{pollingProfileId}", c.UpdatePollingProfile, }, + { + "UpdateTest", + strings.ToUpper("Put"), + "/api/tests/{testId}", + c.UpdateTest, + }, { "UpdateTransaction", strings.ToUpper("Put"), @@ -305,6 +335,30 @@ func (c *ResourceApiApiController) CreateLinter(w http.ResponseWriter, r *http.R } +// CreateTest - Create new test +func (c *ResourceApiApiController) CreateTest(w http.ResponseWriter, r *http.Request) { + testParam := Test{} + d := json.NewDecoder(r.Body) + d.DisallowUnknownFields() + if err := d.Decode(&testParam); err != nil { + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + if err := AssertTestRequired(testParam); err != nil { + c.errorHandler(w, r, err, nil) + return + } + result, err := c.service.CreateTest(r.Context(), testParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) + +} + // CreateTransaction - Create new transaction func (c *ResourceApiApiController) CreateTransaction(w http.ResponseWriter, r *http.Request) { transactionResourceParam := TransactionResource{} @@ -393,6 +447,22 @@ func (c *ResourceApiApiController) DeleteLinter(w http.ResponseWriter, r *http.R } +// DeleteTest - delete a test +func (c *ResourceApiApiController) DeleteTest(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + testIdParam := params["testId"] + + result, err := c.service.DeleteTest(r.Context(), testIdParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) + +} + // DeleteTransaction - delete a transaction func (c *ResourceApiApiController) DeleteTransaction(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) @@ -505,6 +575,33 @@ func (c *ResourceApiApiController) GetPollingProfile(w http.ResponseWriter, r *h } +// GetTests - Get tests +func (c *ResourceApiApiController) GetTests(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + takeParam, err := parseInt32Parameter(query.Get("take"), false) + if err != nil { + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + skipParam, err := parseInt32Parameter(query.Get("skip"), false) + if err != nil { + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + queryParam := query.Get("query") + sortByParam := query.Get("sortBy") + sortDirectionParam := query.Get("sortDirection") + result, err := c.service.GetTests(r.Context(), takeParam, skipParam, queryParam, sortByParam, sortDirectionParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) + +} + // GetTransaction - get transaction func (c *ResourceApiApiController) GetTransaction(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) @@ -704,6 +801,22 @@ func (c *ResourceApiApiController) ListPollingProfile(w http.ResponseWriter, r * } +// TestsTestIdGet - get test +func (c *ResourceApiApiController) TestsTestIdGet(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + testIdParam := params["testId"] + + result, err := c.service.TestsTestIdGet(r.Context(), testIdParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) + +} + // UpdateConfiguration - Update Tracetest configuration func (c *ResourceApiApiController) UpdateConfiguration(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) @@ -866,6 +979,33 @@ func (c *ResourceApiApiController) UpdatePollingProfile(w http.ResponseWriter, r } +// UpdateTest - update test +func (c *ResourceApiApiController) UpdateTest(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + testIdParam := params["testId"] + + testParam := Test{} + d := json.NewDecoder(r.Body) + d.DisallowUnknownFields() + if err := d.Decode(&testParam); err != nil { + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + if err := AssertTestRequired(testParam); err != nil { + c.errorHandler(w, r, err, nil) + return + } + result, err := c.service.UpdateTest(r.Context(), testIdParam, testParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) + +} + // UpdateTransaction - update transaction func (c *ResourceApiApiController) UpdateTransaction(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) diff --git a/server/openapi/api_resource_api_service.go b/server/openapi/api_resource_api_service.go index 1ea9c8f88a..8d98fa7c20 100644 --- a/server/openapi/api_resource_api_service.go +++ b/server/openapi/api_resource_api_service.go @@ -68,6 +68,20 @@ func (s *ResourceApiApiService) CreateLinter(ctx context.Context, linterResource return Response(http.StatusNotImplemented, nil), errors.New("CreateLinter method not implemented") } +// CreateTest - Create new test +func (s *ResourceApiApiService) CreateTest(ctx context.Context, test Test) (ImplResponse, error) { + // TODO - update CreateTest with the required logic for this service method. + // Add api_resource_api_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + + //TODO: Uncomment the next line to return response Response(200, Test{}) or use other options such as http.Ok ... + //return Response(200, Test{}), nil + + //TODO: Uncomment the next line to return response Response(400, {}) or use other options such as http.Ok ... + //return Response(400, nil),nil + + return Response(http.StatusNotImplemented, nil), errors.New("CreateTest method not implemented") +} + // CreateTransaction - Create new transaction func (s *ResourceApiApiService) CreateTransaction(ctx context.Context, transactionResource TransactionResource) (ImplResponse, error) { // TODO - update CreateTransaction with the required logic for this service method. @@ -156,6 +170,17 @@ func (s *ResourceApiApiService) DeleteLinter(ctx context.Context, linterId strin return Response(http.StatusNotImplemented, nil), errors.New("DeleteLinter method not implemented") } +// DeleteTest - delete a test +func (s *ResourceApiApiService) DeleteTest(ctx context.Context, testId string) (ImplResponse, error) { + // TODO - update DeleteTest with the required logic for this service method. + // Add api_resource_api_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + + //TODO: Uncomment the next line to return response Response(204, {}) or use other options such as http.Ok ... + //return Response(204, nil),nil + + return Response(http.StatusNotImplemented, nil), errors.New("DeleteTest method not implemented") +} + // DeleteTransaction - delete a transaction func (s *ResourceApiApiService) DeleteTransaction(ctx context.Context, transactionId string) (ImplResponse, error) { // TODO - update DeleteTransaction with the required logic for this service method. @@ -272,6 +297,23 @@ func (s *ResourceApiApiService) GetPollingProfile(ctx context.Context, pollingPr return Response(http.StatusNotImplemented, nil), errors.New("GetPollingProfile method not implemented") } +// GetTests - Get tests +func (s *ResourceApiApiService) GetTests(ctx context.Context, take int32, skip int32, query string, sortBy string, sortDirection string) (ImplResponse, error) { + // TODO - update GetTests with the required logic for this service method. + // Add api_resource_api_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + + //TODO: Uncomment the next line to return response Response(200, TestResourceList{}) or use other options such as http.Ok ... + //return Response(200, TestResourceList{}), nil + + //TODO: Uncomment the next line to return response Response(400, {}) or use other options such as http.Ok ... + //return Response(400, nil),nil + + //TODO: Uncomment the next line to return response Response(500, {}) or use other options such as http.Ok ... + //return Response(500, nil),nil + + return Response(http.StatusNotImplemented, nil), errors.New("GetTests method not implemented") +} + // GetTransaction - get transaction func (s *ResourceApiApiService) GetTransaction(ctx context.Context, transactionId string) (ImplResponse, error) { // TODO - update GetTransaction with the required logic for this service method. @@ -399,6 +441,20 @@ func (s *ResourceApiApiService) ListPollingProfile(ctx context.Context, take int return Response(http.StatusNotImplemented, nil), errors.New("ListPollingProfile method not implemented") } +// TestsTestIdGet - get test +func (s *ResourceApiApiService) TestsTestIdGet(ctx context.Context, testId string) (ImplResponse, error) { + // TODO - update TestsTestIdGet with the required logic for this service method. + // Add api_resource_api_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + + //TODO: Uncomment the next line to return response Response(200, TestResource{}) or use other options such as http.Ok ... + //return Response(200, TestResource{}), nil + + //TODO: Uncomment the next line to return response Response(500, {}) or use other options such as http.Ok ... + //return Response(500, nil),nil + + return Response(http.StatusNotImplemented, nil), errors.New("TestsTestIdGet method not implemented") +} + // UpdateConfiguration - Update Tracetest configuration func (s *ResourceApiApiService) UpdateConfiguration(ctx context.Context, configId string, configurationResource ConfigurationResource) (ImplResponse, error) { // TODO - update UpdateConfiguration with the required logic for this service method. @@ -510,6 +566,20 @@ func (s *ResourceApiApiService) UpdatePollingProfile(ctx context.Context, pollin return Response(http.StatusNotImplemented, nil), errors.New("UpdatePollingProfile method not implemented") } +// UpdateTest - update test +func (s *ResourceApiApiService) UpdateTest(ctx context.Context, testId string, test Test) (ImplResponse, error) { + // TODO - update UpdateTest with the required logic for this service method. + // Add api_resource_api_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. + + //TODO: Uncomment the next line to return response Response(204, {}) or use other options such as http.Ok ... + //return Response(204, nil),nil + + //TODO: Uncomment the next line to return response Response(500, {}) or use other options such as http.Ok ... + //return Response(500, nil),nil + + return Response(http.StatusNotImplemented, nil), errors.New("UpdateTest method not implemented") +} + // UpdateTransaction - update transaction func (s *ResourceApiApiService) UpdateTransaction(ctx context.Context, transactionId string, transactionResource TransactionResource) (ImplResponse, error) { // TODO - update UpdateTransaction with the required logic for this service method. diff --git a/server/openapi/model_test_.go b/server/openapi/model_test_.go index ba1406284d..170a7da47f 100644 --- a/server/openapi/model_test_.go +++ b/server/openapi/model_test_.go @@ -25,7 +25,7 @@ type Test struct { CreatedAt time.Time `json:"createdAt,omitempty"` - ServiceUnderTest Trigger `json:"serviceUnderTest,omitempty"` + Trigger Trigger `json:"trigger,omitempty"` // specification of assertions that are going to be made Specs []TestSpec `json:"specs,omitempty"` @@ -38,7 +38,7 @@ type Test struct { // AssertTestRequired checks if the required fields are not zero-ed func AssertTestRequired(obj Test) error { - if err := AssertTriggerRequired(obj.ServiceUnderTest); err != nil { + if err := AssertTriggerRequired(obj.Trigger); err != nil { return err } for _, el := range obj.Specs { diff --git a/server/openapi/model_test_resource.go b/server/openapi/model_test_resource.go new file mode 100644 index 0000000000..47e0d85a54 --- /dev/null +++ b/server/openapi/model_test_resource.go @@ -0,0 +1,39 @@ +/* + * TraceTest + * + * OpenAPI definition for TraceTest endpoint and resources + * + * API version: 0.2.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +// TestResource - Represents a test structured into the Resources format. +type TestResource struct { + + // Represents the type of this resource. It should always be set as 'Test'. + Type string `json:"type,omitempty"` + + Spec Test `json:"spec,omitempty"` +} + +// AssertTestResourceRequired checks if the required fields are not zero-ed +func AssertTestResourceRequired(obj TestResource) error { + if err := AssertTestRequired(obj.Spec); err != nil { + return err + } + return nil +} + +// AssertRecurseTestResourceRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of TestResource (e.g. [][]TestResource), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseTestResourceRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aTestResource, ok := obj.(TestResource) + if !ok { + return ErrTypeAssertionError + } + return AssertTestResourceRequired(aTestResource) + }) +} diff --git a/server/openapi/model_test_resource_list.go b/server/openapi/model_test_resource_list.go new file mode 100644 index 0000000000..620b329fdd --- /dev/null +++ b/server/openapi/model_test_resource_list.go @@ -0,0 +1,38 @@ +/* + * TraceTest + * + * OpenAPI definition for TraceTest endpoint and resources + * + * API version: 0.2.1 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +type TestResourceList struct { + Count int32 `json:"count,omitempty"` + + Items []TestResource `json:"items,omitempty"` +} + +// AssertTestResourceListRequired checks if the required fields are not zero-ed +func AssertTestResourceListRequired(obj TestResourceList) error { + for _, el := range obj.Items { + if err := AssertTestResourceRequired(el); err != nil { + return err + } + } + return nil +} + +// AssertRecurseTestResourceListRequired recursively checks if required fields are not zero-ed in a nested slice. +// Accepts only nested slice of TestResourceList (e.g. [][]TestResourceList), otherwise ErrTypeAssertionError is thrown. +func AssertRecurseTestResourceListRequired(objSlice interface{}) error { + return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { + aTestResourceList, ok := obj.(TestResourceList) + if !ok { + return ErrTypeAssertionError + } + return AssertTestResourceListRequired(aTestResourceList) + }) +} diff --git a/server/openapi/model_trigger.go b/server/openapi/model_trigger.go index d757f4a1ae..8f584d58db 100644 --- a/server/openapi/model_trigger.go +++ b/server/openapi/model_trigger.go @@ -10,9 +10,9 @@ package openapi type Trigger struct { - TriggerType string `json:"triggerType,omitempty"` + Type string `json:"type,omitempty"` - Http HttpRequest `json:"http,omitempty"` + HttpRequest HttpRequest `json:"httpRequest,omitempty"` Grpc GrpcRequest `json:"grpc,omitempty"` @@ -21,7 +21,7 @@ type Trigger struct { // AssertTriggerRequired checks if the required fields are not zero-ed func AssertTriggerRequired(obj Trigger) error { - if err := AssertHttpRequestRequired(obj.Http); err != nil { + if err := AssertHttpRequestRequired(obj.HttpRequest); err != nil { return err } if err := AssertGrpcRequestRequired(obj.Grpc); err != nil { diff --git a/server/openapi/model_trigger_result.go b/server/openapi/model_trigger_result.go index 3108b614d1..e50675e673 100644 --- a/server/openapi/model_trigger_result.go +++ b/server/openapi/model_trigger_result.go @@ -10,7 +10,7 @@ package openapi type TriggerResult struct { - TriggerType string `json:"triggerType,omitempty"` + Type string `json:"type,omitempty"` TriggerResult TriggerResultTriggerResult `json:"triggerResult,omitempty"` } diff --git a/server/openapi/model_upsert_definition_response.go b/server/openapi/model_upsert_definition_response.go deleted file mode 100644 index 8bdc4f1d50..0000000000 --- a/server/openapi/model_upsert_definition_response.go +++ /dev/null @@ -1,36 +0,0 @@ -/* - * TraceTest - * - * OpenAPI definition for TraceTest endpoint and resources - * - * API version: 0.2.1 - * Generated by: OpenAPI Generator (https://openapi-generator.tech) - */ - -package openapi - -type UpsertDefinitionResponse struct { - - // resource ID - Id string `json:"id,omitempty"` - - // resource type - Type string `json:"type,omitempty"` -} - -// AssertUpsertDefinitionResponseRequired checks if the required fields are not zero-ed -func AssertUpsertDefinitionResponseRequired(obj UpsertDefinitionResponse) error { - return nil -} - -// AssertRecurseUpsertDefinitionResponseRequired recursively checks if required fields are not zero-ed in a nested slice. -// Accepts only nested slice of UpsertDefinitionResponse (e.g. [][]UpsertDefinitionResponse), otherwise ErrTypeAssertionError is thrown. -func AssertRecurseUpsertDefinitionResponseRequired(objSlice interface{}) error { - return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { - aUpsertDefinitionResponse, ok := obj.(UpsertDefinitionResponse) - if !ok { - return ErrTypeAssertionError - } - return AssertUpsertDefinitionResponseRequired(aUpsertDefinitionResponse) - }) -} diff --git a/server/otlp/ingester.go b/server/otlp/ingester.go index 41ab988974..90a0847060 100644 --- a/server/otlp/ingester.go +++ b/server/otlp/ingester.go @@ -9,6 +9,7 @@ import ( "github.com/kubeshop/tracetest/server/executor" "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/model/events" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/traces" "go.opentelemetry.io/otel/trace" pb "go.opentelemetry.io/proto/otlp/collector/trace/v1" @@ -16,16 +17,16 @@ import ( ) type ingester struct { - db model.Repository - eventEmitter executor.EventEmitter - dsRepo *datastore.Repository + runRepository test.RunRepository + eventEmitter executor.EventEmitter + dsRepo *datastore.Repository } -func NewIngester(db model.Repository, eventEmitter executor.EventEmitter, dsRepo *datastore.Repository) ingester { +func NewIngester(runRepository test.RunRepository, eventEmitter executor.EventEmitter, dsRepo *datastore.Repository) ingester { return ingester{ - db: db, - eventEmitter: eventEmitter, - dsRepo: dsRepo, + runRepository: runRepository, + eventEmitter: eventEmitter, + dsRepo: dsRepo, } } @@ -81,7 +82,7 @@ func (i ingester) getSpansByTrace(request *pb.ExportTraceServiceRequest) map[tra } func (e ingester) saveSpansIntoTest(ctx context.Context, traceID trace.TraceID, spans []model.Span, requestType string) error { - run, err := e.db.GetRunByTraceID(ctx, traceID) + run, err := e.runRepository.GetRunByTraceID(ctx, traceID) if err != nil && strings.Contains(err.Error(), "record not found") { // span is not part of any known test run. So it will be ignored return nil @@ -91,7 +92,7 @@ func (e ingester) saveSpansIntoTest(ctx context.Context, traceID trace.TraceID, return fmt.Errorf("could not find test run with traceID %s: %w", traceID.String(), err) } - if run.State != model.RunStateAwaitingTrace { + if run.State != test.RunStateAwaitingTrace { // test is not waiting for trace, so we can completely ignore those as they might // mess up with the test integrity. // @@ -114,7 +115,7 @@ func (e ingester) saveSpansIntoTest(ctx context.Context, traceID trace.TraceID, e.eventEmitter.Emit(ctx, events.TraceOtlpServerReceivedSpans(run.TestID, run.ID, len(newSpans), requestType)) run.Trace = &newTrace - err = e.db.UpdateRun(ctx, run) + err = e.runRepository.UpdateRun(ctx, run) if err != nil { return fmt.Errorf("could not update run: %w", err) } diff --git a/server/pkg/timing/timing.go b/server/pkg/timing/timing.go new file mode 100644 index 0000000000..dfb181c7f5 --- /dev/null +++ b/server/pkg/timing/timing.go @@ -0,0 +1,36 @@ +package timing + +import ( + "math" + "time" +) + +var Now = func() time.Time { + return time.Now().UTC() +} + +func TimeDiff(start, end time.Time) time.Duration { + var endDate time.Time + if !dateIsZero(end) { + endDate = end + } else { + endDate = Now() + } + return endDate.Sub(start) +} + +func DurationInMillieconds(d time.Duration) int { + return int(d.Milliseconds()) +} + +func DurationInNanoseconds(d time.Duration) int { + return int(d.Nanoseconds()) +} + +func DurationInSeconds(d time.Duration) int { + return int(math.Ceil(d.Seconds())) +} + +func dateIsZero(in time.Time) bool { + return in.IsZero() || in.Unix() == 0 +} diff --git a/server/pkg/validation/error.go b/server/pkg/validation/error.go new file mode 100644 index 0000000000..867bce9351 --- /dev/null +++ b/server/pkg/validation/error.go @@ -0,0 +1,5 @@ +package validation + +import "errors" + +var ErrValidation error = errors.New("validation error") diff --git a/server/resourcemanager/resource_manager.go b/server/resourcemanager/resource_manager.go index ac698f03fd..ff5ffe2382 100644 --- a/server/resourcemanager/resource_manager.go +++ b/server/resourcemanager/resource_manager.go @@ -18,6 +18,7 @@ import ( "github.com/gorilla/mux" "github.com/kubeshop/tracetest/server/pkg/id" + "github.com/kubeshop/tracetest/server/pkg/validation" ) type ResourceSpec interface { @@ -511,6 +512,10 @@ func (m *manager[T]) handleResourceHandlerError(w http.ResponseWriter, verb stri return } + if errors.Is(err, validation.ErrValidation) { + writeError(w, encoder, http.StatusBadRequest, err) + } + // 500 - internal server error err = fmt.Errorf("error %s resource %s: %w", verb, m.resourceTypeSingular, err) writeError(w, encoder, http.StatusInternalServerError, err) diff --git a/server/resourcemanager/testutil/operations_list.go b/server/resourcemanager/testutil/operations_list.go index bca371697c..ab796d32b4 100644 --- a/server/resourcemanager/testutil/operations_list.go +++ b/server/resourcemanager/testutil/operations_list.go @@ -207,7 +207,9 @@ func buildPaginationOperationStep(sortDirection, sortField string) operationTest asserted++ prevVal = itemSpec[sortField] } - assert.Equal(t, len(parsedJsonBody.Items), asserted) + + msg := fmt.Sprintf("incorrect number of items asserted for field '%s' direction '%s'", sortField, sortDirection) + assert.Equal(t, len(parsedJsonBody.Items), asserted, msg) }, } } diff --git a/server/test/main_test.go b/server/test/main_test.go new file mode 100644 index 0000000000..5a8a88c307 --- /dev/null +++ b/server/test/main_test.go @@ -0,0 +1,18 @@ +package test_test + +import ( + "os" + "testing" + + "github.com/kubeshop/tracetest/server/testmock" +) + +func TestMain(m *testing.M) { + testmock.StartTestEnvironment() + + exitVal := m.Run() + + testmock.StopTestEnvironment() + + os.Exit(exitVal) +} diff --git a/server/test/mocks/run_repository_mock.go b/server/test/mocks/run_repository_mock.go new file mode 100644 index 0000000000..a2bed74e83 --- /dev/null +++ b/server/test/mocks/run_repository_mock.go @@ -0,0 +1,55 @@ +package mocks + +import ( + "context" + + "github.com/kubeshop/tracetest/server/pkg/id" + "github.com/kubeshop/tracetest/server/test" + "github.com/stretchr/testify/mock" + "go.opentelemetry.io/otel/trace" +) + +type RunRepository struct { + mock.Mock + T mock.TestingT +} + +func (m *RunRepository) CreateRun(ctx context.Context, t test.Test, r test.Run) (test.Run, error) { + args := m.Called(ctx, t, r) + return args.Get(0).(test.Run), args.Error(1) +} + +func (m *RunRepository) UpdateRun(ctx context.Context, r test.Run) error { + args := m.Called(ctx, r) + return args.Error(0) +} + +func (m *RunRepository) DeleteRun(ctx context.Context, r test.Run) error { + args := m.Called(ctx, r) + return args.Error(0) +} + +func (m *RunRepository) GetRun(ctx context.Context, testID id.ID, runID int) (test.Run, error) { + args := m.Called(ctx, testID, runID) + return args.Get(0).(test.Run), args.Error(1) +} + +func (m *RunRepository) GetTestRuns(ctx context.Context, t test.Test, take, skip int32) ([]test.Run, error) { + args := m.Called(ctx, t, take, skip) + return args.Get(0).([]test.Run), args.Error(1) +} + +func (m *RunRepository) GetRunByTraceID(ctx context.Context, traceID trace.TraceID) (test.Run, error) { + args := m.Called(ctx, traceID) + return args.Get(0).(test.Run), args.Error(1) +} + +func (m *RunRepository) GetLatestRunByTestVersion(ctx context.Context, id id.ID, version int) (test.Run, error) { + args := m.Called(ctx, id, version) + return args.Get(0).(test.Run), args.Error(1) +} + +func (m *RunRepository) GetTransactionRunSteps(ctx context.Context, id id.ID, runID int) ([]test.Run, error) { + args := m.Called(ctx, id, runID) + return args.Get(0).([]test.Run), args.Error(1) +} diff --git a/server/test/mocks/test_repository_mock.go b/server/test/mocks/test_repository_mock.go new file mode 100644 index 0000000000..8d8f712409 --- /dev/null +++ b/server/test/mocks/test_repository_mock.go @@ -0,0 +1,79 @@ +package mocks + +import ( + "context" + + "github.com/kubeshop/tracetest/server/pkg/id" + "github.com/kubeshop/tracetest/server/test" + "github.com/stretchr/testify/mock" +) + +type TestRepository struct { + mock.Mock + T mock.TestingT +} + +func (m *TestRepository) List(ctx context.Context, take, skip int, query, sortBy, sortDirection string) ([]test.Test, error) { + args := m.Called(ctx, take, skip, query, sortBy, sortDirection) + return args.Get(0).([]test.Test), args.Error(1) +} + +func (m *TestRepository) ListAugmented(ctx context.Context, take, skip int, query, sortBy, sortDirection string) ([]test.Test, error) { + args := m.Called(ctx, take, skip, query, sortBy, sortDirection) + return args.Get(0).([]test.Test), args.Error(1) +} + +func (m *TestRepository) Count(ctx context.Context, query string) (int, error) { + args := m.Called(ctx, query) + return args.Int(0), args.Error(1) +} + +func (m *TestRepository) SortingFields() []string { + args := m.Called() + return args.Get(0).([]string) +} + +func (m *TestRepository) Provision(ctx context.Context, t test.Test) error { + args := m.Called(ctx, t) + return args.Error(0) +} + +func (m *TestRepository) SetID(t test.Test, id id.ID) test.Test { + args := m.Called(t, id) + return args.Get(0).(test.Test) +} + +func (m *TestRepository) Get(ctx context.Context, id id.ID) (test.Test, error) { + args := m.Called(ctx, id) + return args.Get(0).(test.Test), args.Error(1) +} + +func (m *TestRepository) GetAugmented(ctx context.Context, id id.ID) (test.Test, error) { + args := m.Called(ctx, id) + return args.Get(0).(test.Test), args.Error(1) +} + +func (m *TestRepository) Exists(ctx context.Context, id id.ID) (bool, error) { + args := m.Called(ctx, id) + return args.Bool(0), args.Error(1) +} + +func (m *TestRepository) GetVersion(ctx context.Context, id id.ID, version int) (test.Test, error) { + args := m.Called(ctx, id, version) + return args.Get(0).(test.Test), args.Error(1) +} + +func (m *TestRepository) Create(ctx context.Context, t test.Test) (test.Test, error) { + args := m.Called(ctx, t) + return args.Get(0).(test.Test), args.Error(1) +} + +func (m *TestRepository) Update(ctx context.Context, t test.Test) (test.Test, error) { + args := m.Called(ctx, t) + return args.Get(0).(test.Test), args.Error(1) +} + +func (m *TestRepository) Delete(ctx context.Context, id id.ID) error { + args := m.Called(ctx, id) + return args.Error(0) +} diff --git a/server/model/run.go b/server/test/run.go similarity index 81% rename from server/model/run.go rename to server/test/run.go index 5291f034f0..2641e36a45 100644 --- a/server/model/run.go +++ b/server/test/run.go @@ -1,4 +1,4 @@ -package model +package test import ( "fmt" @@ -6,8 +6,10 @@ import ( "time" "github.com/kubeshop/tracetest/server/environment" + "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/test/trigger" ) var ( @@ -70,10 +72,6 @@ func durationInMillieconds(d time.Duration) int { return int(d.Milliseconds()) } -func durationInNanoseconds(d time.Duration) int { - return int(d.Nanoseconds()) -} - func durationInSeconds(d time.Duration) int { return int(math.Ceil(d.Seconds())) } @@ -88,7 +86,7 @@ func (r Run) Start() Run { return r } -func (r Run) TriggerCompleted(tr TriggerResult) Run { +func (r Run) TriggerCompleted(tr trigger.TriggerResult) Run { r.ServiceTriggerCompletedAt = Now() r.TriggerResult = tr return r @@ -99,7 +97,7 @@ func (r Run) SuccessfullyTriggered() Run { return r } -func (r Run) SuccessfullyPolledTraces(t *Trace) Run { +func (r Run) SuccessfullyPolledTraces(t *model.Trace) Run { r.State = RunStateAnalyzingTrace r.Trace = t r.ObtainedTraceAt = time.Now() @@ -156,9 +154,20 @@ func (r Run) LinterError(err error) Run { return r.Finish() } -func (r Run) SuccessfulLinterExecution(linter LinterResult) Run { +func (r Run) SuccessfulLinterExecution(linter model.LinterResult) Run { r.State = RunStateAwaitingTestResults r.Linter = linter return r } + +func NewTracetestRootSpan(run Run) model.Span { + return model.AugmentRootSpan(model.Span{ + ID: id.NewRandGenerator().SpanID(), + Name: model.TriggerSpanName, + StartTime: run.ServiceTriggeredAt, + EndTime: run.ServiceTriggerCompletedAt, + Attributes: model.Attributes{}, + Children: []*model.Span{}, + }, run.TriggerResult) +} diff --git a/server/testdb/runs.go b/server/test/run_repository.go similarity index 51% rename from server/testdb/runs.go rename to server/test/run_repository.go index 326d8c90fa..48dab7ec4a 100644 --- a/server/testdb/runs.go +++ b/server/test/run_repository.go @@ -1,23 +1,41 @@ -package testdb +package test import ( "context" - "crypto/md5" "database/sql" - "encoding/hex" "encoding/json" "fmt" - "strings" "time" "github.com/kubeshop/tracetest/server/environment" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" - "github.com/kubeshop/tracetest/server/transaction" "go.opentelemetry.io/otel/trace" ) -var _ model.RunRepository = &postgresDB{} +type RunRepository interface { + CreateRun(context.Context, Test, Run) (Run, error) + UpdateRun(context.Context, Run) error + DeleteRun(context.Context, Run) error + GetRun(_ context.Context, testID id.ID, runID int) (Run, error) + GetTestRuns(_ context.Context, _ Test, take, skip int32) ([]Run, error) + GetRunByTraceID(context.Context, trace.TraceID) (Run, error) + GetLatestRunByTestVersion(context.Context, id.ID, int) (Run, error) + + GetTransactionRunSteps(_ context.Context, _ id.ID, runID int) ([]Run, error) +} + +type runRepository struct { + db *sql.DB +} + +func NewRunRepository(db *sql.DB) RunRepository { + return &runRepository{db} +} + +const ( + createSequeceQuery = `CREATE SEQUENCE IF NOT EXISTS "` + runSequenceName + `";` + dropSequeceQuery = `DROP SEQUENCE IF EXISTS "` + runSequenceName + `";` +) const createRunQuery = ` INSERT INTO test_runs ( @@ -85,77 +103,48 @@ INSERT INTO test_runs ( ) RETURNING "id"` -const ( - createSequeceQuery = `CREATE SEQUENCE IF NOT EXISTS "` + runSequenceName + `";` - dropSequeceQuery = `DROP SEQUENCE IF EXISTS "` + runSequenceName + `";` - runSequenceName = "%sequence_name%" -) - -func dropSequece(ctx context.Context, tx *sql.Tx, testID id.ID) error { - _, err := tx.ExecContext( - ctx, - replaceRunSequenceName(createSequeceQuery, testID), - ) - - return err -} - -func md5Hash(text string) string { - hash := md5.Sum([]byte(text)) - return hex.EncodeToString(hash[:]) -} - -func replaceRunSequenceName(sql string, testID id.ID) string { - // postgres doesn't like uppercase chars in sequence names. - // testID might contain uppercase chars, and we cannot lowercase them - // because they might lose their uniqueness. - // md5 creates a unique, lowercase hash. - seqName := "runs_test_" + md5Hash(testID.String()) + "_seq" - return strings.ReplaceAll(sql, runSequenceName, seqName) -} - -func (td *postgresDB) CreateRun(ctx context.Context, test model.Test, run model.Run) (model.Run, error) { +func (r *runRepository) CreateRun(ctx context.Context, test Test, run Run) (Run, error) { run.TestID = test.ID - run.State = model.RunStateCreated - run.TestVersion = test.Version + run.State = RunStateCreated + run.TestVersion = test.SafeVersion() if run.CreatedAt.IsZero() { run.CreatedAt = time.Now() } jsonTriggerResults, err := json.Marshal(run.TriggerResult) if err != nil { - return model.Run{}, fmt.Errorf("trigger results encoding error: %w", err) + return Run{}, fmt.Errorf("trigger results encoding error: %w", err) } jsonTrace, err := json.Marshal(run.Trace) if err != nil { - return model.Run{}, fmt.Errorf("trace encoding error: %w", err) + return Run{}, fmt.Errorf("trace encoding error: %w", err) } jsonMetadata, err := json.Marshal(run.Metadata) if err != nil { - return model.Run{}, fmt.Errorf("metadata encoding error: %w", err) + return Run{}, fmt.Errorf("metadata encoding error: %w", err) } jsonEnvironment, err := json.Marshal(run.Environment) if err != nil { - return model.Run{}, fmt.Errorf("environment encoding error: %w", err) + return Run{}, fmt.Errorf("environment encoding error: %w", err) } jsonlinter, err := json.Marshal(run.Linter) if err != nil { - return model.Run{}, fmt.Errorf("environment encoding error: %w", err) + return Run{}, fmt.Errorf("environment encoding error: %w", err) } - tx, err := td.db.BeginTx(ctx, nil) + tx, err := r.db.BeginTx(ctx, nil) if err != nil { - return model.Run{}, fmt.Errorf("sql beginTx: %w", err) + return Run{}, fmt.Errorf("sql beginTx: %w", err) } _, err = tx.ExecContext(ctx, replaceRunSequenceName(createSequeceQuery, test.ID)) if err != nil { tx.Rollback() - return model.Run{}, fmt.Errorf("sql exec: %w", err) + return Run{}, fmt.Errorf("sql exec: %w", err) } var runID int @@ -163,7 +152,7 @@ func (td *postgresDB) CreateRun(ctx context.Context, test model.Test, run model. ctx, replaceRunSequenceName(createRunQuery, test.ID), test.ID, - test.Version, + test.SafeVersion(), run.CreatedAt, run.ServiceTriggeredAt, run.ServiceTriggerCompletedAt, @@ -179,12 +168,12 @@ func (td *postgresDB) CreateRun(ctx context.Context, test model.Test, run model. ).Scan(&runID) if err != nil { tx.Rollback() - return model.Run{}, fmt.Errorf("sql exec: %w", err) + return Run{}, fmt.Errorf("sql exec: %w", err) } tx.Commit() - return td.GetRun(ctx, test.ID, runID) + return r.GetRun(ctx, test.ID, runID) } const updateRunQuery = ` @@ -219,65 +208,65 @@ UPDATE test_runs SET WHERE id = $16 AND test_id = $17 ` -func (td *postgresDB) UpdateRun(ctx context.Context, r model.Run) error { - stmt, err := td.db.Prepare(updateRunQuery) +func (r *runRepository) UpdateRun(ctx context.Context, run Run) error { + stmt, err := r.db.Prepare(updateRunQuery) if err != nil { return fmt.Errorf("prepare: %w", err) } defer stmt.Close() - jsonTriggerResults, err := json.Marshal(r.TriggerResult) + jsonTriggerResults, err := json.Marshal(run.TriggerResult) if err != nil { return fmt.Errorf("trigger results encoding error: %w", err) } - jsonTestResults, err := json.Marshal(r.Results) + jsonTestResults, err := json.Marshal(run.Results) if err != nil { return fmt.Errorf("test results encoding error: %w", err) } - jsonTrace, err := json.Marshal(r.Trace) + jsonTrace, err := json.Marshal(run.Trace) if err != nil { return fmt.Errorf("trace encoding error: %w", err) } - jsonOutputs, err := json.Marshal(r.Outputs) + jsonOutputs, err := json.Marshal(run.Outputs) if err != nil { return fmt.Errorf("outputs encoding error: %w", err) } - jsonMetadata, err := json.Marshal(r.Metadata) + jsonMetadata, err := json.Marshal(run.Metadata) if err != nil { return fmt.Errorf("encoding error: %w", err) } - jsonEnvironment, err := json.Marshal(r.Environment) + jsonEnvironment, err := json.Marshal(run.Environment) if err != nil { return fmt.Errorf("encoding error: %w", err) } - jsonlinter, err := json.Marshal(r.Linter) + jsonlinter, err := json.Marshal(run.Linter) if err != nil { return fmt.Errorf("encoding error: %w", err) } var lastError *string - if r.LastError != nil { - e := r.LastError.Error() + if run.LastError != nil { + e := run.LastError.Error() lastError = &e } - pass, fail := r.ResultsCount() + pass, fail := run.ResultsCount() _, err = stmt.ExecContext( ctx, - r.ServiceTriggeredAt, - r.ServiceTriggerCompletedAt, - r.ObtainedTraceAt, - r.CompletedAt, - r.State, - r.TraceID.String(), - r.SpanID.String(), + run.ServiceTriggeredAt, + run.ServiceTriggerCompletedAt, + run.ObtainedTraceAt, + run.CompletedAt, + run.State, + run.TraceID.String(), + run.SpanID.String(), jsonTriggerResults, jsonTestResults, jsonTrace, @@ -286,8 +275,8 @@ func (td *postgresDB) UpdateRun(ctx context.Context, r model.Run) error { pass, fail, jsonMetadata, - r.ID, - r.TestID, + run.ID, + run.TestID, jsonEnvironment, jsonlinter, ) @@ -298,19 +287,19 @@ func (td *postgresDB) UpdateRun(ctx context.Context, r model.Run) error { return nil } -func (td *postgresDB) DeleteRun(ctx context.Context, r model.Run) error { +func (r *runRepository) DeleteRun(ctx context.Context, run Run) error { queries := []string{ "DELETE FROM transaction_run_steps WHERE test_run_id = $1 AND test_run_test_id = $2", "DELETE FROM test_runs WHERE id = $1 AND test_id = $2", } - tx, err := td.db.BeginTx(ctx, nil) + tx, err := r.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("sql BeginTx: %w", err) } for _, sql := range queries { - _, err := tx.ExecContext(ctx, sql, r.ID, r.TestID) + _, err := tx.ExecContext(ctx, sql, run.ID, run.TestID) if err != nil { tx.Rollback() return fmt.Errorf("sql error: %w", err) @@ -363,88 +352,85 @@ FROM ON transaction_run_steps.test_run_id = test_runs.id AND transaction_run_steps.test_run_test_id = test_runs.test_id ` -func (td *postgresDB) GetRun(ctx context.Context, testID id.ID, runID int) (model.Run, error) { - stmt, err := td.db.Prepare(selectRunQuery + " WHERE id = $1 AND test_id = $2") +func (r *runRepository) GetRun(ctx context.Context, testID id.ID, runID int) (Run, error) { + stmt, err := r.db.Prepare(selectRunQuery + " WHERE id = $1 AND test_id = $2") if err != nil { - return model.Run{}, err + return Run{}, err } defer stmt.Close() run, err := readRunRow(stmt.QueryRowContext(ctx, runID, testID.String())) if err != nil { - return model.Run{}, fmt.Errorf("cannot read row: %w", err) + return Run{}, fmt.Errorf("cannot read row: %w", err) } return run, nil } -func (td *postgresDB) GetTestRuns(ctx context.Context, test model.Test, take, skip int32) (model.List[model.Run], error) { +func (r *runRepository) GetTestRuns(ctx context.Context, test Test, take, skip int32) ([]Run, error) { const condition = " WHERE test_id = $1" - stmt, err := td.db.Prepare(selectRunQuery + condition + " ORDER BY created_at DESC LIMIT $2 OFFSET $3") + stmt, err := r.db.Prepare(selectRunQuery + condition + " ORDER BY created_at DESC LIMIT $2 OFFSET $3") if err != nil { - return model.List[model.Run]{}, err + return []Run{}, err } defer stmt.Close() rows, err := stmt.QueryContext(ctx, test.ID, take, skip) if err != nil { - return model.List[model.Run]{}, err + return []Run{}, err } - runs, err := td.readRunRows(ctx, rows) + runs, err := r.readRunRows(ctx, rows) if err != nil { - return model.List[model.Run]{}, err + return []Run{}, err } var count int - err = td.db. + err = r.db. QueryRowContext(ctx, "SELECT COUNT(*) FROM test_runs"+condition, test.ID). Scan(&count) if err != nil { - return model.List[model.Run]{}, err + return []Run{}, err } - return model.List[model.Run]{ - Items: runs, - TotalCount: count, - }, nil + return runs, nil } -func (td *postgresDB) GetRunByTraceID(ctx context.Context, traceID trace.TraceID) (model.Run, error) { - stmt, err := td.db.Prepare(selectRunQuery + " WHERE trace_id = $1") +func (r *runRepository) GetRunByTraceID(ctx context.Context, traceID trace.TraceID) (Run, error) { + stmt, err := r.db.Prepare(selectRunQuery + " WHERE trace_id = $1") if err != nil { - return model.Run{}, err + return Run{}, err } defer stmt.Close() run, err := readRunRow(stmt.QueryRowContext(ctx, traceID.String())) if err != nil { - return model.Run{}, fmt.Errorf("cannot read row: %w", err) + return Run{}, fmt.Errorf("cannot read row: %w", err) } return run, nil } -func (td *postgresDB) GetLatestRunByTestVersion(ctx context.Context, testID id.ID, version int) (model.Run, error) { - stmt, err := td.db.Prepare(selectRunQuery + " WHERE test_id = $1 AND test_version = $2 ORDER BY created_at DESC LIMIT 1") +func (r *runRepository) GetLatestRunByTestVersion(ctx context.Context, testID id.ID, version int) (Run, error) { + stmt, err := r.db.Prepare(selectRunQuery + " WHERE test_id = $1 AND test_version = $2 ORDER BY created_at DESC LIMIT 1") if err != nil { - return model.Run{}, err + return Run{}, err } defer stmt.Close() run, err := readRunRow(stmt.QueryRowContext(ctx, testID.String(), version)) if err != nil { - return model.Run{}, err + return Run{}, err } return run, nil } -func (td *postgresDB) readRunRows(ctx context.Context, rows *sql.Rows) ([]model.Run, error) { - var runs []model.Run +func (r *runRepository) readRunRows(ctx context.Context, rows *sql.Rows) ([]Run, error) { + var runs []Run for rows.Next() { run, err := readRunRow(rows) if err != nil { - return []model.Run{}, fmt.Errorf("cannot read row: %w", err) + return []Run{}, fmt.Errorf("cannot read row: %w", err) } runs = append(runs, run) } @@ -452,8 +438,8 @@ func (td *postgresDB) readRunRows(ctx context.Context, rows *sql.Rows) ([]model. return runs, nil } -func readRunRow(row scanner) (model.Run, error) { - r := model.Run{} +func readRunRow(row scanner) (Run, error) { + r := Run{} var ( jsonTriggerResults, @@ -496,113 +482,109 @@ func readRunRow(row scanner) (model.Run, error) { &jsonLinter, ) - switch err { - case sql.ErrNoRows: - return model.Run{}, ErrNotFound - case nil: - err = json.Unmarshal(jsonTriggerResults, &r.TriggerResult) - if err != nil { - return model.Run{}, fmt.Errorf("cannot parse TriggerResult: %w", err) - } + if err != nil { + return Run{}, err + } - err = json.Unmarshal(jsonTestResults, &r.Results) - if err != nil { - return model.Run{}, fmt.Errorf("cannot parse Results: %w", err) - } + err = json.Unmarshal(jsonTriggerResults, &r.TriggerResult) + if err != nil { + return Run{}, fmt.Errorf("cannot parse TriggerResult: %w", err) + } - if jsonTrace != nil { - err = json.Unmarshal(jsonTrace, &r.Trace) - if err != nil { - return model.Run{}, fmt.Errorf("cannot parse Trace: %w", err) - } - } + err = json.Unmarshal(jsonTestResults, &r.Results) + if err != nil { + return Run{}, fmt.Errorf("cannot parse Results: %w", err) + } - if jsonLinter != nil { - err = json.Unmarshal(jsonLinter, &r.Linter) - if err != nil { - return model.Run{}, fmt.Errorf("cannot parse linter: %w", err) - } + if jsonTrace != nil { + err = json.Unmarshal(jsonTrace, &r.Trace) + if err != nil { + return Run{}, fmt.Errorf("cannot parse Trace: %w", err) } + } - err = json.Unmarshal(jsonOutputs, &r.Outputs) + if jsonLinter != nil { + err = json.Unmarshal(jsonLinter, &r.Linter) if err != nil { - // try with raw outputs - var rawOutputs []environment.EnvironmentValue - err = json.Unmarshal(jsonOutputs, &rawOutputs) - - for _, value := range rawOutputs { - r.Outputs.Add(value.Key, model.RunOutput{ - Name: value.Key, - Value: value.Value, - SpanID: "", - }) - } - - if err != nil { - return model.Run{}, fmt.Errorf("cannot parse Outputs: %w", err) - } + return Run{}, fmt.Errorf("cannot parse linter: %w", err) } + } - err = json.Unmarshal(jsonMetadata, &r.Metadata) - if err != nil { - return model.Run{}, fmt.Errorf("cannot parse Metadata: %w", err) + err = json.Unmarshal(jsonOutputs, &r.Outputs) + if err != nil { + // try with raw outputs + var rawOutputs []environment.EnvironmentValue + err = json.Unmarshal(jsonOutputs, &rawOutputs) + + for _, value := range rawOutputs { + r.Outputs.Add(value.Key, RunOutput{ + Name: value.Key, + Value: value.Value, + SpanID: "", + }) } - err = json.Unmarshal(jsonEnvironment, &r.Environment) if err != nil { - return model.Run{}, fmt.Errorf("cannot parse Environment: %w", err) + return Run{}, fmt.Errorf("cannot parse Outputs: %w", err) } + } - if traceID != "" { - r.TraceID, err = trace.TraceIDFromHex(traceID) - if err != nil { - return model.Run{}, fmt.Errorf("cannot parse TraceID: %w", err) - } - } + err = json.Unmarshal(jsonMetadata, &r.Metadata) + if err != nil { + return Run{}, fmt.Errorf("cannot parse Metadata: %w", err) + } - if spanID != "" { - r.SpanID, err = trace.SpanIDFromHex(spanID) - if err != nil { - return model.Run{}, fmt.Errorf("cannot parse SpanID: %w", err) - } - } + err = json.Unmarshal(jsonEnvironment, &r.Environment) + if err != nil { + return Run{}, fmt.Errorf("cannot parse Environment: %w", err) + } - if lastError != nil && *lastError != "" { - r.LastError = fmt.Errorf(*lastError) + if traceID != "" { + r.TraceID, err = trace.TraceIDFromHex(traceID) + if err != nil { + return Run{}, fmt.Errorf("cannot parse TraceID: %w", err) } + } - if transactionID.Valid && transactionRunID.Valid { - r.TransactionID = transactionID.String - r.TransactionRunID = transactionRunID.String + if spanID != "" { + r.SpanID, err = trace.SpanIDFromHex(spanID) + if err != nil { + return Run{}, fmt.Errorf("cannot parse SpanID: %w", err) } + } - return r, nil + if lastError != nil && *lastError != "" { + r.LastError = fmt.Errorf(*lastError) + } - default: - return model.Run{}, fmt.Errorf("read run row: %w", err) + if transactionID.Valid && transactionRunID.Valid { + r.TransactionID = transactionID.String + r.TransactionRunID = transactionRunID.String } + + return r, nil } -func (td *postgresDB) GetTransactionRunSteps(ctx context.Context, tr transaction.TransactionRun) ([]model.Run, error) { +func (r *runRepository) GetTransactionRunSteps(ctx context.Context, id id.ID, runID int) ([]Run, error) { query := selectRunQuery + ` WHERE transaction_run_steps.transaction_run_id = $1 AND transaction_run_steps.transaction_run_transaction_id = $2 ORDER BY test_runs.completed_at ASC ` - stmt, err := td.db.Prepare(query) + stmt, err := r.db.Prepare(query) if err != nil { - return []model.Run{}, fmt.Errorf("prepare: %w", err) + return []Run{}, fmt.Errorf("prepare: %w", err) } defer stmt.Close() - rows, err := stmt.QueryContext(ctx, tr.ID, tr.TransactionID) + rows, err := stmt.QueryContext(ctx, runID, id) if err != nil { - return []model.Run{}, fmt.Errorf("query context: %w", err) + return []Run{}, fmt.Errorf("query context: %w", err) } - steps, err := td.readRunRows(ctx, rows) + steps, err := r.readRunRows(ctx, rows) if err != nil { - return []model.Run{}, fmt.Errorf("read row: %w", err) + return []Run{}, fmt.Errorf("read row: %w", err) } return steps, nil diff --git a/server/model/run_test.go b/server/test/run_test.go similarity index 87% rename from server/model/run_test.go rename to server/test/run_test.go index 33e0753049..d07c2d46fc 100644 --- a/server/model/run_test.go +++ b/server/test/run_test.go @@ -1,23 +1,23 @@ -package model_test +package test_test import ( "testing" "time" - "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/test" "github.com/stretchr/testify/assert" ) func TestRunExecutionTime(t *testing.T) { cases := []struct { name string - run model.Run + run test.Run now time.Time expected int }{ { name: "CompletedOk", - run: model.Run{ + run: test.Run{ CreatedAt: time.Date(2022, 01, 25, 12, 45, 33, int(100*time.Millisecond), time.UTC), CompletedAt: time.Date(2022, 01, 25, 12, 45, 36, int(400*time.Millisecond), time.UTC), }, @@ -25,7 +25,7 @@ func TestRunExecutionTime(t *testing.T) { }, { name: "LessThan1Sec", - run: model.Run{ + run: test.Run{ CreatedAt: time.Date(2022, 01, 25, 12, 45, 33, int(100*time.Millisecond), time.UTC), CompletedAt: time.Date(2022, 01, 25, 12, 45, 33, int(400*time.Millisecond), time.UTC), }, @@ -33,7 +33,7 @@ func TestRunExecutionTime(t *testing.T) { }, { name: "StillRunning", - run: model.Run{ + run: test.Run{ CreatedAt: time.Date(2022, 01, 25, 12, 45, 33, int(100*time.Millisecond), time.UTC), }, now: time.Date(2022, 01, 25, 12, 45, 34, int(300*time.Millisecond), time.UTC), @@ -41,7 +41,7 @@ func TestRunExecutionTime(t *testing.T) { }, { name: "ZeroedDate", - run: model.Run{ + run: test.Run{ CreatedAt: time.Date(2022, 01, 25, 12, 45, 33, int(100*time.Millisecond), time.UTC), CompletedAt: time.Unix(0, 0), }, @@ -52,15 +52,15 @@ func TestRunExecutionTime(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - now := model.Now + now := test.Now if c.now.Unix() > 0 { - model.Now = func() time.Time { + test.Now = func() time.Time { return c.now } } assert.Equal(t, c.expected, c.run.ExecutionTime()) - model.Now = now + test.Now = now }) } } @@ -68,13 +68,13 @@ func TestRunExecutionTime(t *testing.T) { func TestRunTriggerTime(t *testing.T) { cases := []struct { name string - run model.Run + run test.Run now time.Time expected int }{ { name: "CompletedOk", - run: model.Run{ + run: test.Run{ ServiceTriggeredAt: time.Date(2022, 01, 25, 12, 45, 33, int(100*time.Millisecond), time.UTC), ServiceTriggerCompletedAt: time.Date(2022, 01, 25, 12, 45, 36, int(400*time.Millisecond), time.UTC), }, @@ -82,7 +82,7 @@ func TestRunTriggerTime(t *testing.T) { }, { name: "LessThan1Sec", - run: model.Run{ + run: test.Run{ ServiceTriggeredAt: time.Date(2022, 01, 25, 12, 45, 33, int(100*time.Millisecond), time.UTC), ServiceTriggerCompletedAt: time.Date(2022, 01, 25, 12, 45, 33, int(400*time.Millisecond), time.UTC), }, @@ -90,7 +90,7 @@ func TestRunTriggerTime(t *testing.T) { }, { name: "StillRunning", - run: model.Run{ + run: test.Run{ ServiceTriggeredAt: time.Date(2022, 01, 25, 12, 45, 33, int(100*time.Millisecond), time.UTC), }, now: time.Date(2022, 01, 25, 12, 45, 34, int(300*time.Millisecond), time.UTC), @@ -98,7 +98,7 @@ func TestRunTriggerTime(t *testing.T) { }, { name: "ZeroedDate", - run: model.Run{ + run: test.Run{ ServiceTriggeredAt: time.Date(2022, 01, 25, 12, 45, 33, int(100*time.Millisecond), time.UTC), ServiceTriggerCompletedAt: time.Unix(0, 0), }, @@ -109,15 +109,15 @@ func TestRunTriggerTime(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - now := model.Now + now := test.Now if c.now.Unix() > 0 { - model.Now = func() time.Time { + test.Now = func() time.Time { return c.now } } assert.Equal(t, c.expected, c.run.TriggerTime()) - model.Now = now + test.Now = now }) } } diff --git a/server/model/tests.go b/server/test/test_entities.go similarity index 62% rename from server/model/tests.go rename to server/test/test_entities.go index fade12cb4e..f7a3882752 100644 --- a/server/model/tests.go +++ b/server/test/test_entities.go @@ -1,37 +1,67 @@ -package model +package test import ( "fmt" "time" "github.com/kubeshop/tracetest/server/environment" + "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/test/trigger" "go.opentelemetry.io/otel/trace" ) +const ( + ResourceName string = "Test" + ResourceNamePlural string = "Tests" +) + type ( // this struct yaml/json encoding is handled at ./test_json.go for custom encodings Test struct { - ID id.ID - CreatedAt time.Time - Name string - Description string - Version int - ServiceUnderTest Trigger - Specs maps.Ordered[SpanQuery, NamedAssertions] - Outputs maps.Ordered[string, Output] - Summary Summary + ID id.ID `json:"id,omitempty"` + CreatedAt *time.Time `json:"createdAt,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Version *int `json:"version,omitempty"` + Trigger trigger.Trigger `json:"trigger,omitempty"` + Specs Specs `json:"specs,omitempty"` + Outputs Outputs `json:"outputs,omitempty"` + Summary *Summary `json:"summary,omitempty"` } + Specs []TestSpec + + Outputs []Output + Output struct { - Selector SpanQuery - Value string `expr_enabled:"true"` + Name string `json:"name,omitempty"` + Selector SpanQuery `json:"selector,omitempty"` + Value string `json:"value,omitempty" expr_enabled:"true"` + } + + TestSpec struct { + Selector SpanQuery `json:"selector,omitempty"` + Name string `json:"name,omitempty"` + Assertions []Assertion `json:"assertions,omitempty"` } - NamedAssertions struct { - Name string `expr_enabled:"true"` - Assertions []Assertion `stmt_enabled:"true"` + SpanSelector struct { + Filters []SelectorFilter `json:"filters,omitempty"` + PseudoClass *SelectorPseudoClass `json:"pseudoClass,omitempty"` + ChildSelector *SpanSelector `json:"childSelector,omitempty"` + } + + SelectorFilter struct { + Property string `json:"property,omitempty"` + Operator string `json:"operator,omitempty"` + Value string `json:"value,omitempty"` + } + + SelectorPseudoClass struct { + Name string `json:"name,omitempty"` + Argument *int32 `json:"argument,omitempty"` } Summary struct { @@ -40,29 +70,12 @@ type ( } LastRun struct { - Time time.Time `json:"time"` + Time time.Time `json:"time,omitempty"` Passes int `json:"passes"` Fails int `json:"fails"` AnalyzerScore int `json:"analyzerScore"` } - TriggerType string - - // this struct yaml/json encoding is handled at ./trigger_json.go for custom encodings - Trigger struct { - Type TriggerType - HTTP *HTTPRequest - GRPC *GRPCRequest - TraceID *TRACEIDRequest - } - - TriggerResult struct { - Type TriggerType - HTTP *HTTPResponse - GRPC *GRPCResponse - TRACEID *TRACEIDResponse - } - SpanQuery string Assertion string @@ -98,9 +111,9 @@ type ( SpanID trace.SpanID // result info - TriggerResult TriggerResult + TriggerResult trigger.TriggerResult Results *RunResults - Trace *Trace + Trace *model.Trace Outputs maps.Ordered[string, RunOutput] LastError error Pass int @@ -115,7 +128,7 @@ type ( TransactionID string TransactionRunID string - Linter LinterResult + Linter model.LinterResult } RunResults struct { @@ -144,7 +157,23 @@ type ( } ) -func (sar SpanAssertionResult) SafeSpanIDString() string { +func (t Test) GetID() id.ID { + return t.ID +} + +func (t Test) SafeVersion() int { + if t.Version != nil { + return *t.Version + } + + return 1 +} + +func (t Test) Validate() error { + return nil +} + +func (sar SpanAssertionResult) SpanIDString() string { if sar.SpanID == nil { return "" } @@ -161,15 +190,15 @@ func (e *AssertionExpression) String() string { return "" } - if e.Expression == nil { - if e.LiteralValue.Type == "attribute" { - return fmt.Sprintf("attr:%s", e.LiteralValue.Value) - } + if e.Expression != nil { + return fmt.Sprintf("%s %s %s", e.LiteralValue.Value, e.Operation, e.Expression.String()) + } - return e.LiteralValue.Value + if e.LiteralValue.Type == "attribute" { + return fmt.Sprintf("attr:%s", e.LiteralValue.Value) } - return fmt.Sprintf("%s %s %s", e.LiteralValue.Value, e.Operation, e.Expression.String()) + return e.LiteralValue.Value } func (e *AssertionExpression) Type() string { diff --git a/server/test/test_json.go b/server/test/test_json.go new file mode 100644 index 0000000000..a33ef2dd1b --- /dev/null +++ b/server/test/test_json.go @@ -0,0 +1,220 @@ +package test + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/fluidtruck/deepcopy" + "github.com/kubeshop/tracetest/server/assertions/comparator" + "github.com/kubeshop/tracetest/server/pkg/maps" + "go.opentelemetry.io/otel/trace" +) + +type testSpecV1 maps.Ordered[SpanQuery, namedAssertions] + +func (v1 testSpecV1) valid() bool { + valid := true + specs := maps.Ordered[SpanQuery, namedAssertions](v1) + specs.ForEach(func(key SpanQuery, val namedAssertions) error { + if key == "" { + valid = false + } + return nil + }) + + return valid +} + +type testSpecV2 []TestSpec + +func (v2 testSpecV2) valid() bool { + for _, spec := range v2 { + // since we can have an empty selector, to check if go + // sent an empty struct we need to see if we have an empty selector and no assertions + if spec.Selector == "" && len(spec.Assertions) == 0 { + return false + } + } + + return true +} + +type namedAssertions struct { + Name string + Assertions []Assertion +} + +func (ts *Specs) UnmarshalJSON(data []byte) error { + v2 := testSpecV2{} + err := json.Unmarshal(data, &v2) + if err != nil { + return err + } + + if v2.valid() { + return deepcopy.DeepCopy(v2, ts) + } + + v1Map := maps.Ordered[SpanQuery, namedAssertions]{} + v1Map.UnmarshalJSON(data) + + v1 := testSpecV1(v1Map) + if v1.valid() { + specs := maps.Ordered[SpanQuery, namedAssertions](v1Map) + *ts = make([]TestSpec, 0, specs.Len()) + specs.ForEach(func(key SpanQuery, val namedAssertions) error { + *ts = append(*ts, TestSpec{ + Selector: SpanQuery(key), + Name: val.Name, + Assertions: val.Assertions, + }) + return nil + }) + + return nil + } + + return fmt.Errorf("test json version is not supported. Expecting version 1 or 2") +} + +func (s *SpanQuery) UnmarshalJSON(data []byte) error { + selectorStruct := struct { + Query string `json:"query"` + ParsedSelector SpanSelector `json:"parsedSelector"` + }{} + + err := json.Unmarshal(data, &selectorStruct) + if err != nil { + // This is only the query string + var query string + err = json.Unmarshal(data, &query) + if err != nil { + return err + } + + *s = SpanQuery(query) + return nil + } + + *s = SpanQuery(selectorStruct.Query) + return nil +} + +type testOutputV1 maps.Ordered[string, Output] + +func (v1 testOutputV1) valid() bool { + orderedMap := maps.Ordered[string, Output](v1) + for name, item := range orderedMap.Unordered() { + if name == "" || string(item.Selector) == "" || item.Value == "" { + return false + } + } + return true +} + +type testOutputV2 []Output + +func (v2 testOutputV2) valid() bool { + for _, item := range v2 { + if item.Name == "" || string(item.Selector) == "" || item.Value == "" { + return false + } + } + return true +} + +func (o *Outputs) UnmarshalJSON(data []byte) error { + v2 := testOutputV2{} + err := json.Unmarshal(data, &v2) + if err == nil && v2.valid() { + *o = []Output(v2) + return nil + } + + v1 := maps.Ordered[string, Output]{} + err = json.Unmarshal(data, &v1) + if err == nil && testOutputV1(v1).valid() { + newOutputs := make(Outputs, 0) + v1Map := maps.Ordered[string, Output](v1) + + v1Map.ForEach(func(key string, val Output) error { + newOutputs = append(newOutputs, Output{ + Name: key, + Selector: val.Selector, + Value: val.Value, + }) + + return nil + }) + + *o = newOutputs + return nil + } + + return fmt.Errorf("test output json version is not supported. Expecting version 1 or 2") +} + +func (sar SpanAssertionResult) MarshalJSON() ([]byte, error) { + sid := "" + if sar.SpanID != nil { + sid = sar.SpanID.String() + } + return json.Marshal(&struct { + SpanID *string + ObservedValue string + CompareErr string + }{ + SpanID: &sid, + ObservedValue: sar.ObservedValue, + CompareErr: errToString(sar.CompareErr), + }) +} + +func (sar *SpanAssertionResult) UnmarshalJSON(data []byte) error { + aux := struct { + SpanID string + ObservedValue string + CompareErr string + }{} + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + var sid *trace.SpanID + if aux.SpanID != "" { + s, err := trace.SpanIDFromHex(aux.SpanID) + if err != nil { + return err + } + sid = &s + } + + sar.SpanID = sid + sar.ObservedValue = aux.ObservedValue + if err := stringToErr(aux.CompareErr); err != nil { + if err.Error() == comparator.ErrNoMatch.Error() { + err = comparator.ErrNoMatch + } + + sar.CompareErr = err + } + + return nil +} + +func errToString(err error) string { + if err != nil { + return err.Error() + } + + return "" +} + +func stringToErr(s string) error { + if s == "" { + return nil + } + + return errors.New(s) +} diff --git a/server/test/test_json_test.go b/server/test/test_json_test.go new file mode 100644 index 0000000000..4dab191de2 --- /dev/null +++ b/server/test/test_json_test.go @@ -0,0 +1,149 @@ +package test_test + +import ( + "encoding/json" + "testing" + + "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSpecV1(t *testing.T) { + oldSpecFormat := ` + [ + { + "Key": "span[tracetest.span.type=\"general\" name=\"Tracetest trigger\"]", + "Value": { + "Name": "my check", + "Assertions": [ + "attr:name = \"Tracetest trigger\"" + ] + } + }, + { + "Key": "span[name=\"GET /api/tests\"]", + "Value": { + "Name": "validate status", + "Assertions": [ + "attr:http.status = 200" + ] + } + } + ] + ` + + testObject := test.Test{} + err := json.Unmarshal([]byte(oldSpecFormat), &testObject.Specs) + + require.NoError(t, err) + require.Len(t, testObject.Specs, 2) + + assert.Equal(t, test.SpanQuery("span[tracetest.span.type=\"general\" name=\"Tracetest trigger\"]"), testObject.Specs[0].Selector) + assert.Equal(t, "my check", testObject.Specs[0].Name) + assert.Len(t, testObject.Specs[0].Assertions, 1) + assert.Equal(t, test.Assertion("attr:name = \"Tracetest trigger\""), testObject.Specs[0].Assertions[0]) + + assert.Equal(t, test.SpanQuery("span[name=\"GET /api/tests\"]"), testObject.Specs[1].Selector) + assert.Equal(t, "validate status", testObject.Specs[1].Name) + assert.Len(t, testObject.Specs[1].Assertions, 1) + assert.Equal(t, test.Assertion("attr:http.status = 200"), testObject.Specs[1].Assertions[0]) +} + +func TestSpecV2(t *testing.T) { + specFormat := ` + [ + { + "selector": "span[tracetest.span.type=\"general\" name=\"Tracetest trigger\"]", + "name": "my check", + "assertions": [ + "attr:name = \"Tracetest trigger\"" + ] + }, + { + "selector": "span[name=\"GET /api/tests\"]", + "name": "validate status", + "assertions": [ + "attr:http.status = 200" + ] + } + ] + ` + + testObject := test.Test{} + err := json.Unmarshal([]byte(specFormat), &testObject.Specs) + + require.NoError(t, err) + require.Len(t, testObject.Specs, 2) + + assert.Equal(t, test.SpanQuery("span[tracetest.span.type=\"general\" name=\"Tracetest trigger\"]"), testObject.Specs[0].Selector) + assert.Equal(t, "my check", testObject.Specs[0].Name) + assert.Len(t, testObject.Specs[0].Assertions, 1) + assert.Equal(t, test.Assertion("attr:name = \"Tracetest trigger\""), testObject.Specs[0].Assertions[0]) + + assert.Equal(t, test.SpanQuery("span[name=\"GET /api/tests\"]"), testObject.Specs[1].Selector) + assert.Equal(t, "validate status", testObject.Specs[1].Name) + assert.Len(t, testObject.Specs[1].Assertions, 1) + assert.Equal(t, test.Assertion("attr:http.status = 200"), testObject.Specs[1].Assertions[0]) +} + +func TestOutputsV1(t *testing.T) { + v1Format := maps.Ordered[string, test.Output]{} + v1Format = v1Format. + MustAdd("USER_ID", test.Output{ + Selector: test.SpanQuery(`span[name = "user creation"]`), + Value: `attr:user_id`, + }). + MustAdd("USER_NAME", test.Output{ + Selector: test.SpanQuery(`span[name = "user creation"]`), + Value: `attr:user_name`, + }) + + v1Json, err := json.Marshal(v1Format) + require.NoError(t, err) + + testObject := test.Test{} + err = json.Unmarshal([]byte(v1Json), &testObject.Outputs) + + require.NoError(t, err) + require.Len(t, testObject.Outputs, 2) + + assert.Equal(t, "USER_ID", testObject.Outputs[0].Name) + assert.Equal(t, test.SpanQuery(`span[name = "user creation"]`), testObject.Outputs[0].Selector) + assert.Equal(t, `attr:user_id`, testObject.Outputs[0].Value) + + assert.Equal(t, "USER_NAME", testObject.Outputs[1].Name) + assert.Equal(t, test.SpanQuery(`span[name = "user creation"]`), testObject.Outputs[1].Selector) + assert.Equal(t, `attr:user_name`, testObject.Outputs[1].Value) +} + +func TestOutputsV2(t *testing.T) { + v2Format := make([]test.Output, 0) + v2Format = append(v2Format, test.Output{ + Name: "USER_ID", + Selector: test.SpanQuery(`span[name = "user creation"]`), + Value: `attr:user_id`, + }) + v2Format = append(v2Format, test.Output{ + Name: "USER_NAME", + Selector: test.SpanQuery(`span[name = "user creation"]`), + Value: `attr:user_name`, + }) + + v2Json, err := json.Marshal(v2Format) + require.NoError(t, err) + + testObject := test.Test{} + err = json.Unmarshal([]byte(v2Json), &testObject.Outputs) + + require.NoError(t, err) + require.Len(t, testObject.Outputs, 2) + assert.Equal(t, "USER_ID", testObject.Outputs[0].Name) + assert.Equal(t, test.SpanQuery(`span[name = "user creation"]`), testObject.Outputs[0].Selector) + assert.Equal(t, `attr:user_id`, testObject.Outputs[0].Value) + + assert.Equal(t, "USER_NAME", testObject.Outputs[1].Name) + assert.Equal(t, test.SpanQuery(`span[name = "user creation"]`), testObject.Outputs[1].Selector) + assert.Equal(t, `attr:user_name`, testObject.Outputs[1].Value) +} diff --git a/server/test/test_repository.go b/server/test/test_repository.go new file mode 100644 index 0000000000..01f23eef31 --- /dev/null +++ b/server/test/test_repository.go @@ -0,0 +1,577 @@ +package test + +import ( + "context" + "crypto/md5" + "database/sql" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/kubeshop/tracetest/server/pkg/id" + "github.com/kubeshop/tracetest/server/pkg/sqlutil" + "github.com/kubeshop/tracetest/server/pkg/validation" +) + +type Repository interface { + List(_ context.Context, take, skip int, query, sortBy, sortDirection string) ([]Test, error) + ListAugmented(_ context.Context, take, skip int, query, sortBy, sortDirection string) ([]Test, error) + Count(context.Context, string) (int, error) + SortingFields() []string + + Provision(context.Context, Test) error + SetID(test Test, id id.ID) Test + + Get(context.Context, id.ID) (Test, error) + GetAugmented(ctx context.Context, id id.ID) (Test, error) + Exists(context.Context, id.ID) (bool, error) + GetVersion(_ context.Context, _ id.ID, version int) (Test, error) + + Create(context.Context, Test) (Test, error) + Update(context.Context, Test) (Test, error) + Delete(context.Context, id.ID) error + + GetTransactionSteps(_ context.Context, _ id.ID, version int) ([]Test, error) + DB() *sql.DB +} + +type repository struct { + db *sql.DB +} + +func NewRepository(db *sql.DB) Repository { + return &repository{db} +} + +// needed for test +func (r *repository) DB() *sql.DB { + return r.db +} + +func (r *repository) SetID(test Test, id id.ID) Test { + test.ID = id + return test +} + +func (r *repository) Provision(ctx context.Context, test Test) error { + return nil +} + +func (r *repository) SortingFields() []string { + return []string{"created", "name", "last_run"} +} + +const ( + getTestSQL = ` + SELECT + t.id, + t.version, + t.name, + t.description, + t.service_under_test, + t.specs, + t.outputs, + t.created_at, + (SELECT COUNT(*) FROM test_runs tr WHERE tr.test_id = t.id) as total_runs, + last_test_run.created_at as last_test_run_time, + last_test_run.pass as last_test_run_pass, + last_test_run.fail as last_test_run_fail + FROM tests t + LEFT OUTER JOIN ( + SELECT MAX(id) as id, test_id FROM test_runs GROUP BY test_id + ) as ltr ON ltr.test_id = t.id + LEFT OUTER JOIN + test_runs last_test_run + ON last_test_run.test_id = ltr.test_id AND last_test_run.id = ltr.id +` + + testMaxVersionQuery = ` + INNER JOIN ( + SELECT id as idx, max(version) as latest_version FROM tests GROUP BY idx + ) as latest_tests ON latest_tests.idx = t.id AND t.version = latest_tests.latest_version + ` +) + +func (r *repository) List(ctx context.Context, take, skip int, query, sortBy, sortDirection string) ([]Test, error) { + tests, err := r.list(ctx, take, skip, query, sortBy, sortDirection) + if err != nil { + return []Test{}, err + } + + for i, test := range tests { + r.removeNonAugmentedFields(&test) + tests[i] = test + } + + return tests, err +} + +func (r *repository) removeNonAugmentedFields(test *Test) { + test.CreatedAt = nil + test.Summary = nil + test.Version = nil +} + +func (r *repository) ListAugmented(ctx context.Context, take, skip int, query, sortBy, sortDirection string) ([]Test, error) { + return r.list(ctx, take, skip, query, sortBy, sortDirection) +} + +func (r *repository) list(ctx context.Context, take, skip int, query, sortBy, sortDirection string) ([]Test, error) { + sql := getTestSQL + testMaxVersionQuery + params := []any{take, skip} + + const condition = " AND (t.name ilike $3 OR t.description ilike $3)" + q, params := sqlutil.Search(sql, condition, query, params) + + sortingFields := map[string]string{ + "created": "t.created_at", + "name": "t.name", + "last_run": "last_test_run_time", + } + + q = sqlutil.Sort(q, sortBy, sortDirection, "created", sortingFields) + q += ` LIMIT $1 OFFSET $2 ` + + stmt, err := r.db.Prepare(q) + if err != nil { + return []Test{}, err + } + defer stmt.Close() + + rows, err := stmt.QueryContext(ctx, params...) + if err != nil { + return []Test{}, err + } + + tests, err := r.readRows(ctx, rows) + if err != nil { + return []Test{}, err + } + + return tests, nil +} + +func (r *repository) Count(ctx context.Context, query string) (int, error) { + countQuery := "SELECT COUNT(*) FROM tests t" + testMaxVersionQuery + + if query != "" { + countQuery = fmt.Sprintf("%s WHERE %s", countQuery, query) + } + + count := 0 + + err := r.db. + QueryRowContext(ctx, countQuery). + Scan(&count) + + if err != nil { + return 0, fmt.Errorf("sql query: %w", err) + } + + return count, nil +} + +func (r *repository) Get(ctx context.Context, id id.ID) (Test, error) { + test, err := r.get(ctx, id) + if err != nil { + return test, err + } + + r.removeNonAugmentedFields(&test) + return test, nil +} + +func (r *repository) GetAugmented(ctx context.Context, id id.ID) (Test, error) { + return r.get(ctx, id) +} + +func (r *repository) get(ctx context.Context, id id.ID) (Test, error) { + stmt, err := r.db.Prepare(getTestSQL + " WHERE t.id = $1 ORDER BY t.version DESC LIMIT 1") + if err != nil { + return Test{}, fmt.Errorf("prepare: %w", err) + } + defer stmt.Close() + + test, err := r.readRow(ctx, stmt.QueryRowContext(ctx, id)) + if err != nil { + return Test{}, err + } + + return test, nil +} + +func (r *repository) GetTransactionSteps(ctx context.Context, id id.ID, version int) ([]Test, error) { + stmt, err := r.db.Prepare(getTestSQL + testMaxVersionQuery + ` INNER JOIN transaction_steps ts ON t.id = ts.test_id + WHERE ts.transaction_id = $1 AND ts.transaction_version = $2 ORDER BY ts.step_number ASC`) + if err != nil { + return []Test{}, fmt.Errorf("prepare 2: %w", err) + } + defer stmt.Close() + + rows, err := stmt.QueryContext(ctx, id, version) + if err != nil { + return []Test{}, fmt.Errorf("query context: %w", err) + } + + steps, err := r.readRows(ctx, rows) + if err != nil { + return []Test{}, fmt.Errorf("read row: %w", err) + } + + return steps, nil +} + +type scanner interface { + Scan(dest ...interface{}) error +} + +func (r *repository) readRows(ctx context.Context, rows *sql.Rows) ([]Test, error) { + tests := []Test{} + + for rows.Next() { + test, err := r.readRow(ctx, rows) + if err != nil { + return []Test{}, err + } + + tests = append(tests, test) + } + + return tests, nil +} + +func (r *repository) readRow(ctx context.Context, row scanner) (Test, error) { + version := 0 + createdAt := time.Now() + + test := Test{ + CreatedAt: &createdAt, + Version: &version, + Summary: &Summary{}, + } + + var ( + jsonServiceUnderTest, + jsonSpecs, + jsonOutputs []byte + + lastRunTime *time.Time + + pass, fail *int + ) + err := row.Scan( + &test.ID, + &test.Version, + &test.Name, + &test.Description, + &jsonServiceUnderTest, + &jsonSpecs, + &jsonOutputs, + &test.CreatedAt, + &test.Summary.Runs, + &lastRunTime, + &pass, + &fail, + ) + + if err != nil { + if err == sql.ErrNoRows { + return Test{}, err + } + + return Test{}, fmt.Errorf("cannot read row: %w", err) + } + + err = json.Unmarshal(jsonServiceUnderTest, &test.Trigger) + if err != nil { + return Test{}, fmt.Errorf("cannot parse trigger: %w", err) + } + + err = json.Unmarshal(jsonSpecs, &test.Specs) + if err != nil { + return Test{}, fmt.Errorf("cannot parse specs: %w", err) + } + + err = json.Unmarshal(jsonOutputs, &test.Outputs) + if err != nil { + return Test{}, fmt.Errorf("cannot parse outputs: %w", err) + } + + if lastRunTime != nil { + test.Summary.LastRun.Time = *lastRunTime + } + if pass != nil { + test.Summary.LastRun.Passes = *pass + } + if fail != nil { + test.Summary.LastRun.Fails = *fail + } + + return test, nil +} + +const insertQuery = ` +INSERT INTO tests ( + "id", + "version", + "name", + "description", + "service_under_test", + "specs", + "outputs", + "created_at" +) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)` + +func (r *repository) Create(ctx context.Context, test Test) (Test, error) { + if test.HasID() { + exists, err := r.Exists(ctx, test.ID) + if err != nil { + return Test{}, fmt.Errorf("error checking if a test exists: %w", err) + } + + if exists { + return Test{}, fmt.Errorf("%w: test with same ID already exists", validation.ErrValidation) + } + } + if !test.HasID() { + test.ID = IDGen.ID() + } + + version := 1 + now := time.Now() + test.Version = &version + test.CreatedAt = &now + + insertedTest, err := r.insertTest(ctx, test) + if err != nil { + return Test{}, err + } + + r.removeNonAugmentedFields(&insertedTest) + + return insertedTest, nil +} + +func (r *repository) insertTest(ctx context.Context, test Test) (Test, error) { + stmt, err := r.db.Prepare(insertQuery) + if err != nil { + return Test{}, fmt.Errorf("sql prepare: %w", err) + } + defer stmt.Close() + + triggerJson, err := json.Marshal(test.Trigger) + if err != nil { + return Test{}, fmt.Errorf("encoding error: %w", err) + } + + specsJson, err := json.Marshal(test.Specs) + if err != nil { + return Test{}, fmt.Errorf("encoding error: %w", err) + } + + outputsJson, err := json.Marshal(test.Outputs) + if err != nil { + return Test{}, fmt.Errorf("encoding error: %w", err) + } + + _, err = stmt.ExecContext( + ctx, + test.ID, + test.Version, + test.Name, + test.Description, + triggerJson, + specsJson, + outputsJson, + test.CreatedAt, + ) + if err != nil { + return Test{}, fmt.Errorf("sql exec: %w", err) + } + + return test, nil +} + +func (r *repository) Update(ctx context.Context, test Test) (Test, error) { + oldTest, err := r.get(ctx, test.ID) + if err != nil { + return Test{}, fmt.Errorf("could not get latest test version while updating test: %w", err) + } + + // keep the same creation date to keep sort order + test.CreatedAt = oldTest.CreatedAt + test.Version = oldTest.Version + + testToUpdate, err := bumpTestVersionIfNeeded(oldTest, test) + if err != nil { + return Test{}, fmt.Errorf("could not bump test version: %w", err) + } + + if oldTest.SafeVersion() == testToUpdate.SafeVersion() { + // No change in the version. Nothing changed so no need to persist it + r.removeNonAugmentedFields(&testToUpdate) + return testToUpdate, nil + } + + updatedTest, err := r.insertTest(ctx, testToUpdate) + if err != nil { + return Test{}, fmt.Errorf("could not create test with new version while updating test: %w", err) + } + + r.removeNonAugmentedFields(&updatedTest) + + return updatedTest, nil +} + +func bumpTestVersionIfNeeded(in, updated Test) (Test, error) { + testHasChanged, err := testHasChanged(in, updated) + if err != nil { + return Test{}, err + } + + version := in.SafeVersion() + if testHasChanged { + version = version + 1 + } + updated.Version = &version + + return updated, nil +} + +func testHasChanged(oldTest Test, newTest Test) (bool, error) { + outputsHaveChanged, err := testFieldHasChanged(oldTest.Outputs, newTest.Outputs) + if err != nil { + return false, err + } + + definitionHasChanged, err := testFieldHasChanged(oldTest.Specs, newTest.Specs) + if err != nil { + return false, err + } + + triggerHasChanged, err := testFieldHasChanged(oldTest.Trigger, newTest.Trigger) + if err != nil { + return false, err + } + + nameHasChanged := oldTest.Name != newTest.Name + descriptionHasChanged := oldTest.Description != newTest.Description + + return outputsHaveChanged || definitionHasChanged || triggerHasChanged || nameHasChanged || descriptionHasChanged, nil +} + +func testFieldHasChanged(oldField interface{}, newField interface{}) (bool, error) { + oldFieldJSON, err := json.Marshal(oldField) + if err != nil { + return false, err + } + + newFieldJSON, err := json.Marshal(newField) + if err != nil { + return false, err + } + + return string(oldFieldJSON) != string(newFieldJSON), nil +} + +func intPtr(in int) *int { + return &in +} + +func (r *repository) Delete(ctx context.Context, id id.ID) error { + exists, err := r.Exists(ctx, id) + if err != nil { + return fmt.Errorf("error checking if a test exists: %w", err) + } + if !exists { + return sql.ErrNoRows // propagate no row error to the API, to emit a 404 + } + + queries := []string{ + "DELETE FROM transaction_run_steps WHERE test_run_test_id = $1", + "DELETE FROM transaction_steps WHERE test_id = $1", + "DELETE FROM test_runs WHERE test_id = $1", + "DELETE FROM tests WHERE id = $1", + } + + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("sql BeginTx: %w", err) + } + defer tx.Rollback() + + for _, sql := range queries { + _, err := tx.ExecContext(ctx, sql, id) + if err != nil { + return fmt.Errorf("sql error: %w", err) + } + } + + dropSequence(ctx, tx, id) + + err = tx.Commit() + if err != nil { + return fmt.Errorf("sql Commit: %w", err) + } + + return nil +} + +const ( + createSequenceQuery = `CREATE SEQUENCE IF NOT EXISTS "` + runSequenceName + `";` + dropSequenceQuery = `DROP SEQUENCE IF EXISTS "` + runSequenceName + `";` + runSequenceName = "%sequence_name%" +) + +func dropSequence(ctx context.Context, tx *sql.Tx, testID id.ID) error { + _, err := tx.ExecContext( + ctx, + replaceRunSequenceName(dropSequenceQuery, testID), + ) + + return err +} + +func md5Hash(text string) string { + hash := md5.Sum([]byte(text)) + return hex.EncodeToString(hash[:]) +} + +func replaceRunSequenceName(sql string, testID id.ID) string { + // postgres doesn't like uppercase chars in sequence names. + // testID might contain uppercase chars, and we cannot lowercase them + // because they might lose their uniqueness. + // md5 creates a unique, lowercase hash. + seqName := "runs_test_" + md5Hash(testID.String()) + "_seq" + return strings.ReplaceAll(sql, runSequenceName, seqName) +} + +func (r *repository) Exists(ctx context.Context, id id.ID) (bool, error) { + _, err := r.get(ctx, id) + if err != nil { + if err == sql.ErrNoRows { + return false, nil + } + + return false, err + } + + return true, nil +} + +func (r *repository) GetVersion(ctx context.Context, id id.ID, version int) (Test, error) { + stmt, err := r.db.Prepare(getTestSQL + " WHERE t.id = $1 AND t.version = $2") + if err != nil { + return Test{}, fmt.Errorf("prepare: %w", err) + } + defer stmt.Close() + + test, err := r.readRow(ctx, stmt.QueryRowContext(ctx, id, version)) + if err != nil { + return Test{}, err + } + + return test, nil +} diff --git a/server/test/test_repository_test.go b/server/test/test_repository_test.go new file mode 100644 index 0000000000..2e67339a95 --- /dev/null +++ b/server/test/test_repository_test.go @@ -0,0 +1,765 @@ +package test_test + +import ( + "context" + "database/sql" + "fmt" + "testing" + + "github.com/gorilla/mux" + "github.com/kubeshop/tracetest/server/pkg/id" + "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/resourcemanager" + rmtest "github.com/kubeshop/tracetest/server/resourcemanager/testutil" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" + "github.com/kubeshop/tracetest/server/testmock" + "github.com/kubeshop/tracetest/server/transaction" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + excludedOperations = rmtest.ExcludeOperations( + // List + rmtest.OperationListSortSuccess, // we need to think how to deal with augmented fields on sorting + ) + jsonComparer = rmtest.JSONComparer(testJsonComparer) +) + +func testJsonComparer(t require.TestingT, operation rmtest.Operation, firstValue, secondValue string) { + expected := firstValue + expected = rmtest.RemoveFieldFromJSONResource("createdAt", expected) + expected = rmtest.RemoveFieldFromJSONResource("specs.0.selector.parsedSelector", expected) + expected = rmtest.RemoveFieldFromJSONResource("summary.lastRun.time", expected) + + actual := secondValue + actual = rmtest.RemoveFieldFromJSONResource("createdAt", actual) + actual = rmtest.RemoveFieldFromJSONResource("specs.0.selector.parsedSelector", actual) + actual = rmtest.RemoveFieldFromJSONResource("summary.lastRun.time", actual) + + require.JSONEq(t, expected, actual) +} + +func createRun(runRepository test.RunRepository, t test.Test) (test.Run, error) { + run := test.Run{ + State: test.RunStateFinished, + TraceID: id.NewRandGenerator().TraceID(), + SpanID: id.NewRandGenerator().SpanID(), + } + run, err := runRepository.CreateRun(context.TODO(), t, run) + if err != nil { + return test.NewRun(), err + } + + run.Results.Results = (maps.Ordered[test.SpanQuery, []test.AssertionResult]{}). + MustAdd("query", []test.AssertionResult{ + { + Results: []test.SpanAssertionResult{ + {CompareErr: nil}, + {CompareErr: nil}, + {CompareErr: fmt.Errorf("some error")}, + }, + }, + }) + + err = runRepository.UpdateRun(context.TODO(), run) + if err != nil { + return test.NewRun(), err + } + + return run, nil +} + +func registerManagerFn(router *mux.Router, db *sql.DB) resourcemanager.Manager { + testRepo := test.NewRepository(db) + + manager := resourcemanager.New[test.Test]( + test.ResourceName, + test.ResourceNamePlural, + testRepo, + resourcemanager.CanBeAugmented(), + ) + manager.RegisterRoutes(router) + + return manager +} + +func getScenarioPreparation(sample, secondSample, thirdSample test.Test) func(t *testing.T, op rmtest.Operation, manager resourcemanager.Manager) { + return func(t *testing.T, op rmtest.Operation, manager resourcemanager.Manager) { + testRepo := manager.Handler().(test.Repository) + testRunRepo := test.NewRunRepository(testRepo.DB()) + + switch op { + case rmtest.OperationGetSuccess, + rmtest.OperationUpdateSuccess, + rmtest.OperationListSuccess: + testRepo.Create(context.TODO(), sample) + _, err := createRun(testRunRepo, sample) + require.NoError(t, err) + + case rmtest.OperationDeleteSuccess: + testRepo.Create(context.TODO(), sample) + + case rmtest.OperationListAugmentedSuccess, + rmtest.OperationGetAugmentedSuccess: + testRepo.Create(context.TODO(), sample) + _, err := createRun(testRunRepo, sample) + require.NoError(t, err) + + case rmtest.OperationListSortSuccess: + testRepo.Create(context.TODO(), sample) + testRepo.Create(context.TODO(), secondSample) + testRepo.Create(context.TODO(), thirdSample) + } + } +} + +func TestIfDeleteTestsCascadeDeletes(t *testing.T) { + testmock.StartTestEnvironment() + + var testSample = test.Test{ + ID: "NiWVnxP4R", + Name: "Verify Import", + Description: "check the working of the import flow", + Trigger: trigger.Trigger{ + Type: "http", + HTTP: &trigger.HTTPRequest{ + Method: "GET", + URL: "http://localhost:11633/api/tests", + }, + }, + Specs: test.Specs{ + { + Name: "check user id exists", + Selector: `span[name = "span name"]`, + Assertions: []test.Assertion{`attr:user_id != ""`}, + }, + }, + Outputs: test.Outputs{ + { + Name: "USER_ID", + Selector: test.SpanQuery(`span[name = "span name"]`), + Value: `attr:user_id`, + }, + }, + } + + var secondTestSample = test.Test{ + ID: "NiWVnjahsdvR", + Name: "Another Test", + Description: "another test description", + Trigger: trigger.Trigger{ + Type: "http", + HTTP: &trigger.HTTPRequest{ + Method: "GET", + URL: "http://localhost:11633/api/tests", + }, + }, + Specs: test.Specs{ + { + Name: "check user id exists", + Selector: `span[name = "span name"]`, + Assertions: []test.Assertion{`attr:user_id != ""`}, + }, + }, + Outputs: test.Outputs{}, + } + + var transactionSample = transaction.Transaction{ + ID: "a98s76de", + Name: "Verify Import", + Description: "check the working of the import flow", + StepIDs: []id.ID{ + testSample.ID, + secondTestSample.ID, + }, + } + + db := testmock.CreateMigratedDatabase() + defer db.Close() + + testRepository := test.NewRepository(db) + runRepository := test.NewRunRepository(db) + transactionRepository := transaction.NewRepository(db, testRepository) + transactionRunRepository := transaction.NewRunRepository(db, runRepository) + + _, err := testRepository.Create(context.TODO(), testSample) + require.NoError(t, err) + + run, err := createRun(runRepository, testSample) + require.NoError(t, err) + + _, err = testRepository.Create(context.TODO(), secondTestSample) + require.NoError(t, err) + + secondRun, err := createRun(runRepository, secondTestSample) + require.NoError(t, err) + + _, err = transactionRepository.Create(context.TODO(), transactionSample) + require.NoError(t, err) + + updatedTransactionSample, err := transactionRepository.GetAugmented(context.TODO(), transactionSample.ID) + require.NoError(t, err) + + transactionRun, err := transactionRunRepository.CreateRun(context.TODO(), updatedTransactionSample.NewRun()) + require.NoError(t, err) + + transactionRun.Steps = []test.Run{run, secondRun} + + err = transactionRunRepository.UpdateRun(context.TODO(), transactionRun) + require.NoError(t, err) + + err = testRepository.Delete(context.TODO(), testSample.ID) + require.NoError(t, err) + + recentTransactionRun, err := transactionRunRepository.GetTransactionRun(context.TODO(), transactionSample.ID, transactionRun.ID) + require.NoError(t, err) + assert.Len(t, recentTransactionRun.Steps, 1) + + recentTransaction, err := transactionRepository.Get(context.TODO(), transactionSample.ID) + require.NoError(t, err) + assert.Len(t, recentTransaction.StepIDs, 1) + + _, err = runRepository.GetRun(context.TODO(), run.TestID, run.ID) + assert.ErrorIs(t, err, sql.ErrNoRows) + + _, err = testRepository.Get(context.TODO(), testSample.ID) + assert.ErrorIs(t, err, sql.ErrNoRows) +} + +func TestTestResourceWithHTTPTrigger(t *testing.T) { + var testSample = test.Test{ + ID: "NiWVnxP4R", + Name: "Verify Import", + Description: "check the working of the import flow", + Trigger: trigger.Trigger{ + Type: "http", + HTTP: &trigger.HTTPRequest{ + Method: "GET", + URL: "http://localhost:11633/api/tests", + }, + }, + Specs: test.Specs{ + { + Name: "check user id exists", + Selector: `span[name = "span name"]`, + Assertions: []test.Assertion{`attr:user_id != ""`}, + }, + }, + Outputs: test.Outputs{ + { + Name: "USER_ID", + Selector: test.SpanQuery(`span[name = "span name"]`), + Value: `attr:user_id`, + }, + }, + } + + var secondTestSample = test.Test{ + ID: "NiWVnjahsdvR", + Name: "Another Test", + Description: "another test description", + Trigger: trigger.Trigger{ + Type: "http", + HTTP: &trigger.HTTPRequest{ + Method: "GET", + URL: "http://localhost:11633/api/tests", + }, + }, + Specs: test.Specs{ + { + Name: "check user id exists", + Selector: `span[name = "span name"]`, + Assertions: []test.Assertion{`attr:user_id != ""`}, + }, + }, + Outputs: test.Outputs{}, + } + + var thirdTestSample = test.Test{ + ID: "oau3si2y6d", + Name: "One More Test", + Description: "one more test description", + Trigger: trigger.Trigger{ + Type: "http", + HTTP: &trigger.HTTPRequest{ + Method: "GET", + URL: "http://localhost:11633/api/tests", + }, + }, + Specs: test.Specs{ + { + Name: "check user id exists", + Selector: `span[name = "span name"]`, + Assertions: []test.Assertion{`attr:user_id != ""`}, + }, + }, + Outputs: test.Outputs{}, + } + + testSpec := rmtest.ResourceTypeTest{ + ResourceTypeSingular: test.ResourceName, + ResourceTypePlural: test.ResourceNamePlural, + RegisterManagerFn: registerManagerFn, + Prepare: getScenarioPreparation(testSample, secondTestSample, thirdTestSample), + SampleJSON: `{ + "type": "Test", + "spec": { + "id": "NiWVnxP4R", + "name": "Verify Import", + "description": "check the working of the import flow", + "trigger": { + "type": "http", + "httpRequest": { + "method": "GET", + "url": "http://localhost:11633/api/tests" + } + }, + "specs": [ + { + "name": "check user id exists", + "selector": "span[name = \"span name\"]", + "assertions": [ "attr:user_id != \"\"" ] + } + ], + "outputs": [ + { + "name": "USER_ID", + "selector": "span[name = \"span name\"]", + "value": "attr:user_id" + } + ] + } + }`, + SampleJSONAugmented: `{ + "type": "Test", + "spec": { + "id": "NiWVnxP4R", + "name": "Verify Import", + "description": "check the working of the import flow", + "trigger": { + "type": "http", + "httpRequest": { + "method": "GET", + "url": "http://localhost:11633/api/tests" + } + }, + "specs": [ + { + "name": "check user id exists", + "selector": "span[name = \"span name\"]", + "assertions": [ "attr:user_id != \"\"" ] + } + ], + "outputs": [ + { + "name": "USER_ID", + "selector": "span[name = \"span name\"]", + "value": "attr:user_id" + } + ], + "version": 1, + "summary": { + "runs": 1, + "lastRun": { + "analyzerScore": 0, + "fails": 1, + "passes": 2 + } + } + } + }`, + SampleJSONUpdated: `{ + "type": "Test", + "spec": { + "id": "NiWVnxP4R", + "name": "Verify Import Updated", + "description": "check the working of the import flow updated", + "trigger": { + "type": "http", + "httpRequest": { + "method": "GET", + "url": "http://localhost:11633/api/tests" + } + }, + "specs": [ + { + "name": "check user id exists updated", + "selector": "span[name = \"span name updated\"]", + "assertions": [ "attr:user_id != \"\"" ] + } + ] + } + }`, + } + + rmtest.TestResourceType(t, testSpec, excludedOperations, jsonComparer) +} + +func TestTestResourceWithGRPCTrigger(t *testing.T) { + protobufFile := `syntax = "proto3"; + +option java_multiple_files = true; +option java_outer_classname = "PokeshopProto"; +option objc_class_prefix = "PKS"; + +package pokeshop; + +service Pokeshop { + rpc getPokemonList (GetPokemonRequest) returns (GetPokemonListResponse) {} +} + +message GetPokemonRequest { + optional int32 skip = 1; + optional int32 take = 2; + optional bool isFixed = 3; +} + +message GetPokemonListResponse { + repeated Pokemon items = 1; + int32 totalCount = 2; +}` + + var testSample = test.Test{ + ID: "NiWVnxP4R", + Name: "Verify Import", + Description: "check the working of the import flow", + Trigger: trigger.Trigger{ + Type: "grpc", + GRPC: &trigger.GRPCRequest{ + Address: "someadress:8080", + Method: "service.method", + Request: `{"hello":"world"}`, + ProtobufFile: protobufFile, + }, + }, + Specs: test.Specs{ + { + Name: "check user id exists", + Selector: `span[name = "span name"]`, + Assertions: []test.Assertion{`attr:user_id != ""`}, + }, + }, + Outputs: test.Outputs{ + { + Name: "USER_ID", + Selector: test.SpanQuery(`span[name = "span name"]`), + Value: `attr:user_id`, + }, + }, + } + + var secondTestSample = test.Test{ + ID: "NiWVnjahsdvR", + Name: "Another Test", + Description: "another test description", + Trigger: trigger.Trigger{ + Type: "grpc", + GRPC: &trigger.GRPCRequest{ + Address: "someadress:8080", + Method: "service.method", + Request: `{"hello":"world"}`, + ProtobufFile: protobufFile, + }, + }, + Specs: test.Specs{ + { + Name: "check user id exists", + Selector: `span[name = "span name"]`, + Assertions: []test.Assertion{`attr:user_id != ""`}, + }, + }, + Outputs: test.Outputs{}, + } + + var thirdTestSample = test.Test{ + ID: "oau3si2y6d", + Name: "One More Test", + Description: "one more test description", + Trigger: trigger.Trigger{ + Type: "grpc", + GRPC: &trigger.GRPCRequest{ + Address: "someadress:8080", + Method: "service.method", + Request: `{"hello":"world"}`, + ProtobufFile: protobufFile, + }, + }, + Specs: test.Specs{ + { + Name: "check user id exists", + Selector: `span[name = "span name"]`, + Assertions: []test.Assertion{`attr:user_id != ""`}, + }, + }, + Outputs: test.Outputs{}, + } + + testSpec := rmtest.ResourceTypeTest{ + ResourceTypeSingular: test.ResourceName, + ResourceTypePlural: test.ResourceNamePlural, + RegisterManagerFn: registerManagerFn, + Prepare: getScenarioPreparation(testSample, secondTestSample, thirdTestSample), + SampleJSON: `{ + "type": "Test", + "spec": { + "id": "NiWVnxP4R", + "name": "Verify Import", + "description": "check the working of the import flow", + "trigger": { + "type": "grpc", + "grpc": { + "address": "someadress:8080", + "method": "service.method", + "request": "{\"hello\":\"world\"}", + "protobufFile": "syntax = \"proto3\";\n\noption java_multiple_files = true;\noption java_outer_classname = \"PokeshopProto\";\noption objc_class_prefix = \"PKS\";\n\npackage pokeshop;\n\nservice Pokeshop {\n rpc getPokemonList (GetPokemonRequest) returns (GetPokemonListResponse) {}\n}\n\nmessage GetPokemonRequest {\n optional int32 skip = 1;\n optional int32 take = 2;\n optional bool isFixed = 3;\n}\n\nmessage GetPokemonListResponse {\n repeated Pokemon items = 1;\n int32 totalCount = 2;\n}" + } + }, + "specs": [ + { + "name": "check user id exists", + "selector": "span[name = \"span name\"]", + "assertions": [ "attr:user_id != \"\"" ] + } + ], + "outputs": [ + { + "name": "USER_ID", + "selector": "span[name = \"span name\"]", + "value": "attr:user_id" + } + ] + } + }`, + SampleJSONAugmented: `{ + "type": "Test", + "spec": { + "id": "NiWVnxP4R", + "name": "Verify Import", + "description": "check the working of the import flow", + "trigger": { + "type": "grpc", + "grpc": { + "address": "someadress:8080", + "method": "service.method", + "request": "{\"hello\":\"world\"}", + "protobufFile": "syntax = \"proto3\";\n\noption java_multiple_files = true;\noption java_outer_classname = \"PokeshopProto\";\noption objc_class_prefix = \"PKS\";\n\npackage pokeshop;\n\nservice Pokeshop {\n rpc getPokemonList (GetPokemonRequest) returns (GetPokemonListResponse) {}\n}\n\nmessage GetPokemonRequest {\n optional int32 skip = 1;\n optional int32 take = 2;\n optional bool isFixed = 3;\n}\n\nmessage GetPokemonListResponse {\n repeated Pokemon items = 1;\n int32 totalCount = 2;\n}" + } + }, + "specs": [ + { + "name": "check user id exists", + "selector": "span[name = \"span name\"]", + "assertions": [ "attr:user_id != \"\"" ] + } + ], + "outputs": [ + { + "name": "USER_ID", + "selector": "span[name = \"span name\"]", + "value": "attr:user_id" + } + ], + "version": 1, + "summary": { + "runs": 1, + "lastRun": { + "analyzerScore": 0, + "fails": 1, + "passes": 2 + } + } + } + }`, + SampleJSONUpdated: `{ + "type": "Test", + "spec": { + "id": "NiWVnxP4R", + "name": "Verify Import Updated", + "description": "check the working of the import flow updated", + "trigger": { + "type": "grpc", + "grpc": { + "address": "someadress:8080", + "method": "service.method", + "request": "{\"hello\":\"world\"}", + "protobufFile": "syntax = \"proto3\";\n\noption java_multiple_files = true;\noption java_outer_classname = \"PokeshopProto\";\noption objc_class_prefix = \"PKS\";\n\npackage pokeshop;\n\nservice Pokeshop {\n rpc getPokemonList (GetPokemonRequest) returns (GetPokemonListResponse) {}\n}\n\nmessage GetPokemonRequest {\n optional int32 skip = 1;\n optional int32 take = 2;\n optional bool isFixed = 3;\n}\n\nmessage GetPokemonListResponse {\n repeated Pokemon items = 1;\n int32 totalCount = 2;\n}" + } + }, + "specs": [ + { + "name": "check user id exists updated", + "selector": "span[name = \"span name updated\"]", + "assertions": [ "attr:user_id != \"\"" ] + } + ] + } + }`, + } + + rmtest.TestResourceType(t, testSpec, excludedOperations, jsonComparer) +} + +func TestTestResourceWithTraceIDTrigger(t *testing.T) { + var testSample = test.Test{ + ID: "NiWVnxP4R", + Name: "Verify Import", + Description: "check the working of the import flow", + Trigger: trigger.Trigger{ + Type: "traceid", + TraceID: &trigger.TraceIDRequest{ + ID: "some-trace-id", + }, + }, + Specs: test.Specs{ + { + Name: "check user id exists", + Selector: `span[name = "span name"]`, + Assertions: []test.Assertion{`attr:user_id != ""`}, + }, + }, + Outputs: test.Outputs{ + { + Name: "USER_ID", + Selector: test.SpanQuery(`span[name = "span name"]`), + Value: `attr:user_id`, + }, + }, + } + + var secondTestSample = test.Test{ + ID: "NiWVnjahsdvR", + Name: "Another Test", + Description: "another test description", + Trigger: trigger.Trigger{ + Type: "traceid", + TraceID: &trigger.TraceIDRequest{ + ID: "some-trace-id", + }, + }, + Specs: test.Specs{ + { + Name: "check user id exists", + Selector: `span[name = "span name"]`, + Assertions: []test.Assertion{`attr:user_id != ""`}, + }, + }, + Outputs: test.Outputs{}, + } + + var thirdTestSample = test.Test{ + ID: "oau3si2y6d", + Name: "One More Test", + Description: "one more test description", + Trigger: trigger.Trigger{ + Type: "traceid", + TraceID: &trigger.TraceIDRequest{ + ID: "some-trace-id", + }, + }, + Specs: test.Specs{ + { + Name: "check user id exists", + Selector: `span[name = "span name"]`, + Assertions: []test.Assertion{`attr:user_id != ""`}, + }, + }, + Outputs: test.Outputs{}, + } + + testSpec := rmtest.ResourceTypeTest{ + ResourceTypeSingular: test.ResourceName, + ResourceTypePlural: test.ResourceNamePlural, + RegisterManagerFn: registerManagerFn, + Prepare: getScenarioPreparation(testSample, secondTestSample, thirdTestSample), + SampleJSON: `{ + "type": "Test", + "spec": { + "id": "NiWVnxP4R", + "name": "Verify Import", + "description": "check the working of the import flow", + "trigger": { + "type": "traceid", + "traceid": { + "id": "some-trace-id" + } + }, + "specs": [ + { + "name": "check user id exists", + "selector": "span[name = \"span name\"]", + "assertions": [ "attr:user_id != \"\"" ] + } + ], + "outputs": [ + { + "name": "USER_ID", + "selector": "span[name = \"span name\"]", + "value": "attr:user_id" + } + ] + } + }`, + SampleJSONAugmented: `{ + "type": "Test", + "spec": { + "id": "NiWVnxP4R", + "name": "Verify Import", + "description": "check the working of the import flow", + "trigger": { + "type": "traceid", + "traceid": { + "id": "some-trace-id" + } + }, + "specs": [ + { + "name": "check user id exists", + "selector": "span[name = \"span name\"]", + "assertions": [ "attr:user_id != \"\"" ] + } + ], + "outputs": [ + { + "name": "USER_ID", + "selector": "span[name = \"span name\"]", + "value": "attr:user_id" + } + ], + "version": 1, + "summary": { + "runs": 1, + "lastRun": { + "analyzerScore": 0, + "fails": 1, + "passes": 2 + } + } + } + }`, + SampleJSONUpdated: `{ + "type": "Test", + "spec": { + "id": "NiWVnxP4R", + "name": "Verify Import Updated", + "description": "check the working of the import flow updated", + "trigger": { + "type": "traceid", + "traceid": { + "id": "some-trace-id" + } + }, + "specs": [ + { + "name": "check user id exists updated", + "selector": "span[name = \"span name updated\"]", + "assertions": [ "attr:user_id != \"\"" ] + } + ] + } + }`, + } + + rmtest.TestResourceType(t, testSpec, excludedOperations, jsonComparer) +} diff --git a/server/test/trigger/grpc.go b/server/test/trigger/grpc.go new file mode 100644 index 0000000000..19d418787b --- /dev/null +++ b/server/test/trigger/grpc.go @@ -0,0 +1,65 @@ +package trigger + +import "google.golang.org/grpc/metadata" + +const TriggerTypeGRPC TriggerType = "grpc" + +type GRPCHeader struct { + Key string `expr_enabled:"true"` + Value string `expr_enabled:"true"` +} + +type GRPCRequest struct { + ProtobufFile string `json:"protobufFile,omitempty" expr_enabled:"true"` + Address string `json:"address,omitempty" expr_enabled:"true"` + Service string `json:"service,omitempty" expr_enabled:"true"` + Method string `json:"method,omitempty" expr_enabled:"true"` + Request string `json:"request,omitempty" expr_enabled:"true"` + Metadata []GRPCHeader `json:"metadata,omitempty"` + Auth *HTTPAuthenticator `json:"auth,omitempty"` +} + +func (a GRPCRequest) Headers() []string { + h := []string{} + + for _, md := range a.Metadata { + // ignore invalid values + if md.Key == "" { + continue + } + + h = append(h, md.Key+": "+md.Value) + } + + return h +} + +func (a GRPCRequest) MD() *metadata.MD { + md := metadata.MD{} + + for _, header := range a.Metadata { + // ignore invalid values + if header.Key == "" { + continue + } + + md[header.Key] = []string{header.Value} + } + + return &md +} + +func (a GRPCRequest) Authenticate() { + if a.Auth == nil { + return + } + + a.Auth.AuthenticateGRPC() +} + +type GRPCResponse struct { + Status string + StatusCode int + Metadata []GRPCHeader + Body string +} diff --git a/server/test/trigger/http.go b/server/test/trigger/http.go new file mode 100644 index 0000000000..c3ce3bc09e --- /dev/null +++ b/server/test/trigger/http.go @@ -0,0 +1,224 @@ +package trigger + +import ( + "encoding/json" + "net/http" +) + +const TriggerTypeHTTP TriggerType = "http" + +type HTTPMethod string + +var ( + HTTPMethodGET HTTPMethod = "GET" + HTTPMethodPUT HTTPMethod = "PUT" + HTTPMethodPOST HTTPMethod = "POST" + HTTPMethodPATCH HTTPMethod = "PATCH" + HTTPMethodDELETE HTTPMethod = "DELETE" + HTTPMethodCOPY HTTPMethod = "COPY" + HTTPMethodHEAD HTTPMethod = "HEAD" + HTTPMethodOPTIONS HTTPMethod = "OPTIONS" + HTTPMethodLINK HTTPMethod = "LINK" + HTTPMethodUNLINK HTTPMethod = "UNLINK" + HTTPMethodPURGE HTTPMethod = "PURGE" + HTTPMethodLOCK HTTPMethod = "LOCK" + HTTPMethodUNLOCK HTTPMethod = "UNLOCK" + HTTPMethodPROPFIND HTTPMethod = "PROPFIND" + HTTPMethodVIEW HTTPMethod = "VIEW" +) + +type HTTPHeader struct { + Key string `expr_enabled:"true" json:"key,omitempty"` + Value string `expr_enabled:"true" json:"value,omitempty"` +} + +type HTTPRequest struct { + Method HTTPMethod `expr_enabled:"true" json:"method,omitempty"` + URL string `expr_enabled:"true" json:"url,omitempty"` + Body string `expr_enabled:"true" json:"body,omitempty"` + Headers []HTTPHeader `json:"headers,omitempty"` + Auth *HTTPAuthenticator `json:"auth,omitempty"` + SSLVerification bool `json:"sslVerification,omitempty"` +} + +type request struct { + Method HTTPMethod `expr_enabled:"true" json:"method,omitempty"` + URL string `expr_enabled:"true" json:"url,omitempty"` + Body string `expr_enabled:"true" json:"body,omitempty"` + Headers []HTTPHeader `json:"headers,omitempty"` + Auth *HTTPAuthenticator `json:"auth,omitempty"` + SSLVerification bool `json:"sslVerification,omitempty"` +} + +func (r *HTTPRequest) UnmarshalJSON(data []byte) error { + request := request{} + err := json.Unmarshal(data, &request) + if err != nil { + return err + } + + r.Method = request.Method + r.URL = request.URL + r.Body = request.Body + r.Headers = request.Headers + r.SSLVerification = request.SSLVerification + if request.Auth != nil && request.Auth.Type != "" { + r.Auth = request.Auth + } + + return nil +} + +func (a HTTPRequest) Authenticate(req *http.Request) { + if a.Auth == nil { + return + } + + a.Auth.AuthenticateHTTP(req) +} + +type HTTPResponse struct { + Status string + StatusCode int + Headers []HTTPHeader + Body string +} + +type HTTPAuthenticator struct { + Type string `json:"type,omitempty" expr_enabled:"true"` + APIKey *APIKeyAuthenticator `json:"apiKey,omitempty"` + Basic *BasicAuthenticator `json:"basic,omitempty"` + Bearer *BearerAuthenticator `json:"bearer,omitempty"` +} + +type auth struct { + Type string `json:"type,omitempty" expr_enabled:"true"` + APIKey *APIKeyAuthenticator `json:"apiKey,omitempty"` + Basic *BasicAuthenticator `json:"basic,omitempty"` + Bearer *BearerAuthenticator `json:"bearer,omitempty"` +} + +func (a *HTTPAuthenticator) UnmarshalJSON(data []byte) error { + auth := auth{} + err := json.Unmarshal(data, &auth) + if err != nil { + return err + } + + a.Type = auth.Type + if auth.Type == "apiKey" { + a.APIKey = auth.APIKey + } + + if auth.Type == "basic" { + a.Basic = auth.Basic + } + + if auth.Type == "bearer" { + a.Bearer = auth.Bearer + } + + return nil +} + +func (a HTTPAuthenticator) Map(mapFn func(current string) (string, error)) (HTTPAuthenticator, error) { + var err error + switch a.Type { + case "apiKey": + in := string(a.APIKey.In) + in, err = mapFn(in) + if err != nil { + return a, err + } + a.APIKey.In = APIKeyPosition(in) + a.APIKey.Key, err = mapFn(a.APIKey.Key) + if err != nil { + return a, err + } + a.APIKey.Value, err = mapFn(a.APIKey.Value) + if err != nil { + return a, err + } + case "basic": + a.Basic.Username, err = mapFn(a.Basic.Username) + if err != nil { + return a, err + } + a.Basic.Password, err = mapFn(a.Basic.Password) + if err != nil { + return a, err + } + case "bearer": + a.Bearer.Bearer, err = mapFn(a.Bearer.Bearer) + if err != nil { + return a, err + } + } + return a, nil +} + +func (a HTTPAuthenticator) AuthenticateGRPC() {} +func (a HTTPAuthenticator) AuthenticateHTTP(req *http.Request) { + var auth authenticator + switch a.Type { + case "apiKey": + auth = a.APIKey + case "basic": + auth = a.Basic + case "bearer": + auth = a.Bearer + default: + return + } + + auth.AuthenticateHTTP(req) +} + +type APIKeyPosition string + +const ( + APIKeyPositionHeader APIKeyPosition = "header" + APIKeyPositionQuery APIKeyPosition = "query" +) + +type authenticator interface { + AuthenticateHTTP(req *http.Request) + AuthenticateGRPC() +} + +type APIKeyAuthenticator struct { + Key string `json:"key,omitempty" expr_enabled:"true"` + Value string `json:"value,omitempty" expr_enabled:"true"` + In APIKeyPosition `json:"in,omitempty" expr_enabled:"true"` +} + +func (a APIKeyAuthenticator) AuthenticateGRPC() {} +func (a APIKeyAuthenticator) AuthenticateHTTP(req *http.Request) { + switch a.In { + case APIKeyPositionHeader: + req.Header.Set(a.Key, a.Value) + case APIKeyPositionQuery: + q := req.URL.Query() + q.Add(a.Key, a.Value) + req.URL.RawQuery = q.Encode() + } +} + +type BasicAuthenticator struct { + Username string `json:"username,omitempty" expr_enabled:"true"` + Password string `json:"password,omitempty" expr_enabled:"true"` +} + +func (a BasicAuthenticator) AuthenticateGRPC() {} +func (a BasicAuthenticator) AuthenticateHTTP(req *http.Request) { + req.SetBasicAuth(a.Username, a.Password) +} + +type BearerAuthenticator struct { + Bearer string `json:"bearer,omitempty" expr_enabled:"true"` +} + +func (a BearerAuthenticator) AuthenticateGRPC() {} +func (a BearerAuthenticator) AuthenticateHTTP(req *http.Request) { + req.Header.Add("Authorization", a.Bearer) +} diff --git a/server/test/trigger/traceid.go b/server/test/trigger/traceid.go new file mode 100644 index 0000000000..84d40173c5 --- /dev/null +++ b/server/test/trigger/traceid.go @@ -0,0 +1,11 @@ +package trigger + +const TriggerTypeTraceID TriggerType = "traceid" + +type TraceIDRequest struct { + ID string `json:"id,omitempty" expr_enabled:"true"` +} + +type TraceIDResponse struct { + ID string +} diff --git a/server/test/trigger/trigger.go b/server/test/trigger/trigger.go new file mode 100644 index 0000000000..faa20e63bb --- /dev/null +++ b/server/test/trigger/trigger.go @@ -0,0 +1,19 @@ +package trigger + +type ( + TriggerType string + + Trigger struct { + Type TriggerType `json:"type"` + HTTP *HTTPRequest `json:"httpRequest,omitempty"` + GRPC *GRPCRequest `json:"grpc,omitempty"` + TraceID *TraceIDRequest `json:"traceid,omitempty"` + } + + TriggerResult struct { + Type TriggerType `json:"type"` + HTTP *HTTPResponse `json:"httpRequest,omitempty"` + GRPC *GRPCResponse `json:"grpc,omitempty"` + TraceID *TraceIDResponse `json:"traceid,omitempty"` + } +) diff --git a/server/model/trigger_json.go b/server/test/trigger/trigger_json.go similarity index 57% rename from server/model/trigger_json.go rename to server/test/trigger/trigger_json.go index f0899387d3..f11ca0da9d 100644 --- a/server/model/trigger_json.go +++ b/server/test/trigger/trigger_json.go @@ -1,16 +1,32 @@ -package model +package trigger import ( "encoding/json" "github.com/fluidtruck/deepcopy" + jsoniter "github.com/json-iterator/go" ) +type triggerJSONV3 struct { + Type TriggerType `json:"type"` + HTTP *HTTPRequest `json:"httpRequest,omitempty"` + GRPC *GRPCRequest `json:"grpc,omitempty"` + TraceID *TraceIDRequest `json:"traceid,omitempty"` +} + +func (v3 triggerJSONV3) valid() bool { + // has a valid type and at least one not nil trigger type settings + return v3.Type != "" && + (v3.HTTP != nil || + v3.GRPC != nil || + v3.TraceID != nil) +} + type triggerJSONV2 struct { Type TriggerType `json:"triggerType"` HTTP *HTTPRequest `json:"http,omitempty"` GRPC *GRPCRequest `json:"grpc,omitempty"` - TraceID *TRACEIDRequest `json:"traceid,omitempty"` + TraceID *TraceIDRequest `json:"traceid,omitempty"` } func (v2 triggerJSONV2) valid() bool { @@ -25,7 +41,7 @@ type triggerJSONV1 struct { Type TriggerType HTTP *HTTPRequest GRPC *GRPCRequest - TraceID *TRACEIDRequest + TraceID *TraceIDResponse } func (v1 triggerJSONV1) valid() bool { @@ -37,7 +53,7 @@ func (v1 triggerJSONV1) valid() bool { } func (t Trigger) MarshalJSON() ([]byte, error) { - jt := triggerJSONV2{} + jt := triggerJSONV3{} err := deepcopy.DeepCopy(t, &jt) if err != nil { return nil, err @@ -50,6 +66,14 @@ func (t *Trigger) UnmarshalJSON(data []byte) error { var err error // start with older versions and move up to the latest v1 := triggerJSONV1{} + + // DO NOT USE encoding/json here. the match is case insensitive, and can lead to unexpected results. + // see https://stackoverflow.com/a/49006601 + var json = jsoniter.Config{ + EscapeHTML: true, + CaseSensitive: true, + }.Froze() + err = json.Unmarshal(data, &v1) if err != nil { return err @@ -68,5 +92,15 @@ func (t *Trigger) UnmarshalJSON(data []byte) error { return deepcopy.DeepCopy(v2, t) } + // v3 + v3 := triggerJSONV3{} + err = json.Unmarshal(data, &v3) + if err != nil { + return err + } + if v3.valid() { + return deepcopy.DeepCopy(v3, t) + } + return nil } diff --git a/server/test/trigger/trigger_json_test.go b/server/test/trigger/trigger_json_test.go new file mode 100644 index 0000000000..248b91fa7d --- /dev/null +++ b/server/test/trigger/trigger_json_test.go @@ -0,0 +1,106 @@ +package trigger_test + +import ( + "encoding/json" + "testing" + + "github.com/kubeshop/tracetest/server/test/trigger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTriggerFormatV1(t *testing.T) { + v1 := struct { + Type trigger.TriggerType + HTTP *trigger.HTTPRequest + GRPC *trigger.GRPCRequest + TraceID *trigger.TraceIDRequest + }{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ + Method: trigger.HTTPMethodGET, + URL: "http://example.com/list", + }, + } + + expected := trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ + Method: trigger.HTTPMethodGET, + URL: "http://example.com/list", + }, + } + + v1Json, err := json.Marshal(v1) + require.NoError(t, err) + + current := trigger.Trigger{} + err = json.Unmarshal(v1Json, ¤t) + require.NoError(t, err) + + assert.Equal(t, expected, current) +} + +func TestTriggerFormatV2(t *testing.T) { + v2 := struct { + Type trigger.TriggerType `json:"triggerType"` + HTTP *trigger.HTTPRequest `json:"http,omitempty"` + GRPC *trigger.GRPCRequest `json:"grpc,omitempty"` + TraceID *trigger.TraceIDRequest `json:"traceid,omitempty"` + }{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ + Method: trigger.HTTPMethodGET, + URL: "http://example.com/list", + }, + } + + expected := trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ + Method: trigger.HTTPMethodGET, + URL: "http://example.com/list", + }, + } + + v2Json, err := json.Marshal(v2) + require.NoError(t, err) + + current := trigger.Trigger{} + err = json.Unmarshal(v2Json, ¤t) + require.NoError(t, err) + + assert.Equal(t, expected, current) +} + +func TestTriggerFormatV3(t *testing.T) { + v3 := struct { + Type trigger.TriggerType `json:"type"` + HTTP *trigger.HTTPRequest `json:"httpRequest,omitempty"` + GRPC *trigger.GRPCRequest `json:"grpc,omitempty"` + TraceID *trigger.TraceIDRequest `json:"traceid,omitempty"` + }{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ + Method: trigger.HTTPMethodGET, + URL: "http://example.com/list", + }, + } + + expected := trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ + Method: trigger.HTTPMethodGET, + URL: "http://example.com/list", + }, + } + + v3Json, err := json.Marshal(v3) + require.NoError(t, err) + + current := trigger.Trigger{} + err = json.Unmarshal(v3Json, ¤t) + require.NoError(t, err) + + assert.Equal(t, expected, current) +} diff --git a/server/testdb/mock.go b/server/testdb/mock.go index 6d7980eb04..26b3d81f70 100644 --- a/server/testdb/mock.go +++ b/server/testdb/mock.go @@ -6,6 +6,7 @@ import ( "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/pkg/maps" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/transaction" "github.com/stretchr/testify/mock" "go.opentelemetry.io/otel/trace" @@ -34,94 +35,90 @@ func (m *MockRepository) TestIDExists(_ context.Context, id id.ID) (bool, error) return args.Bool(0), args.Error(1) } -func (m *MockRepository) CreateTest(_ context.Context, test model.Test) (model.Test, error) { - args := m.Called(test) - return args.Get(0).(model.Test), args.Error(1) +func (m *MockRepository) CreateTest(_ context.Context, t test.Test) (test.Test, error) { + args := m.Called(t) + return args.Get(0).(test.Test), args.Error(1) } -func (m *MockRepository) UpdateTest(_ context.Context, test model.Test) (model.Test, error) { - args := m.Called(test) - return args.Get(0).(model.Test), args.Error(1) +func (m *MockRepository) UpdateTest(_ context.Context, t test.Test) (test.Test, error) { + args := m.Called(t) + return args.Get(0).(test.Test), args.Error(1) } -func (m *MockRepository) UpdateTestVersion(_ context.Context, test model.Test) error { +func (m *MockRepository) UpdateTestVersion(_ context.Context, test test.Test) error { args := m.Called(test) return args.Error(0) } -func (m *MockRepository) DeleteTest(_ context.Context, test model.Test) error { +func (m *MockRepository) DeleteTest(_ context.Context, test test.Test) error { args := m.Called(test) return args.Error(0) } -func (m *MockRepository) GetTestVersion(_ context.Context, id id.ID, version int) (model.Test, error) { +func (m *MockRepository) GetTestVersion(_ context.Context, id id.ID, version int) (test.Test, error) { args := m.Called(id, version) - return args.Get(0).(model.Test), args.Error(1) + return args.Get(0).(test.Test), args.Error(1) } -func (m *MockRepository) GetLatestTestVersion(_ context.Context, id id.ID) (model.Test, error) { +func (m *MockRepository) GetLatestTestVersion(_ context.Context, id id.ID) (test.Test, error) { args := m.Called(id) - return args.Get(0).(model.Test), args.Error(1) + return args.Get(0).(test.Test), args.Error(1) } -func (m *MockRepository) GetTests(_ context.Context, take, skip int32, query, sortBy, sortDirection string) (model.List[model.Test], error) { +func (m *MockRepository) GetTests(_ context.Context, take, skip int32, query, sortBy, sortDirection string) (model.List[test.Test], error) { args := m.Called(take, skip, query, sortBy, sortDirection) - tests := args.Get(0).([]model.Test) - list := model.List[model.Test]{ + tests := args.Get(0).([]test.Test) + list := model.List[test.Test]{ Items: tests, TotalCount: len(tests), } return list, args.Error(1) } -func (m *MockRepository) GetSpec(_ context.Context, test model.Test) (maps.Ordered[model.SpanQuery, []model.Assertion], error) { - args := m.Called(test) - return args.Get(0).(maps.Ordered[model.SpanQuery, []model.Assertion]), args.Error(1) +func (m *MockRepository) GetSpec(_ context.Context, t test.Test) (maps.Ordered[test.SpanQuery, []test.Assertion], error) { + args := m.Called(t) + return args.Get(0).(maps.Ordered[test.SpanQuery, []test.Assertion]), args.Error(1) } -func (m *MockRepository) SetSpec(_ context.Context, test model.Test, def maps.Ordered[model.SpanQuery, []model.Assertion]) error { +func (m *MockRepository) SetSpec(_ context.Context, test test.Test, def maps.Ordered[test.SpanQuery, []test.Assertion]) error { args := m.Called(test, def) return args.Error(0) } -func (m *MockRepository) CreateRun(_ context.Context, test model.Test, run model.Run) (model.Run, error) { - args := m.Called(test, run) - return args.Get(0).(model.Run), args.Error(1) +func (m *MockRepository) CreateRun(_ context.Context, t test.Test, run test.Run) (test.Run, error) { + args := m.Called(t, run) + return args.Get(0).(test.Run), args.Error(1) } -func (m *MockRepository) UpdateRun(_ context.Context, run model.Run) error { +func (m *MockRepository) UpdateRun(_ context.Context, run test.Run) error { args := m.Called(run) return args.Error(0) } -func (m *MockRepository) DeleteRun(_ context.Context, run model.Run) error { +func (m *MockRepository) DeleteRun(_ context.Context, run test.Run) error { args := m.Called(run) return args.Error(0) } -func (m *MockRepository) GetRun(_ context.Context, testID id.ID, id int) (model.Run, error) { +func (m *MockRepository) GetRun(_ context.Context, testID id.ID, id int) (test.Run, error) { args := m.Called(testID, id) - return args.Get(0).(model.Run), args.Error(1) + return args.Get(0).(test.Run), args.Error(1) } -func (m *MockRepository) GetLatestRunByTestVersion(_ context.Context, testID id.ID, version int) (model.Run, error) { +func (m *MockRepository) GetLatestRunByTestVersion(_ context.Context, testID id.ID, version int) (test.Run, error) { args := m.Called(testID, version) - return args.Get(0).(model.Run), args.Error(1) + return args.Get(0).(test.Run), args.Error(1) } -func (m *MockRepository) GetTestRuns(_ context.Context, test model.Test, take int32, skip int32) (model.List[model.Run], error) { - args := m.Called(test, take, skip) - runs := args.Get(0).([]model.Run) - list := model.List[model.Run]{ - Items: runs, - TotalCount: len(runs), - } - return list, args.Error(1) +func (m *MockRepository) GetTestRuns(_ context.Context, t test.Test, take int32, skip int32) ([]test.Run, error) { + args := m.Called(t, take, skip) + runs := args.Get(0).([]test.Run) + return runs, args.Error(1) } -func (m *MockRepository) GetRunByTraceID(_ context.Context, tid trace.TraceID) (model.Run, error) { +func (m *MockRepository) GetRunByTraceID(_ context.Context, tid trace.TraceID) (test.Run, error) { args := m.Called(tid) - return args.Get(0).(model.Run), args.Error(1) + return args.Get(0).(test.Run), args.Error(1) } func (m *MockRepository) Drop() error { diff --git a/server/testdb/postgres_test.go b/server/testdb/postgres_test.go index 791112bb9a..bb984d324c 100644 --- a/server/testdb/postgres_test.go +++ b/server/testdb/postgres_test.go @@ -6,6 +6,8 @@ import ( "time" "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "github.com/kubeshop/tracetest/server/testdb" "github.com/kubeshop/tracetest/server/testmock" ) @@ -24,38 +26,38 @@ func getDB() (model.Repository, func()) { return db, clean } -func createTestWithName(t *testing.T, db model.Repository, name string) model.Test { +func createTestWithName(t *testing.T, db test.Repository, name string) test.Test { t.Helper() - test := model.Test{ + test := test.Test{ Name: name, Description: "description", - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ + Trigger: trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ URL: "http://localhost:3030/hello-instrumented", }, }, } - updated, err := db.CreateTest(context.TODO(), test) + updated, err := db.Create(context.TODO(), test) if err != nil { panic(err) } return updated } -func createTest(t *testing.T, db model.Repository) model.Test { +func createTest(t *testing.T, db test.Repository) test.Test { return createTestWithName(t, db, "first test") } -func createRun(t *testing.T, db model.Repository, test model.Test) model.Run { +func createRun(t *testing.T, db test.RunRepository, testObj test.Test) test.Run { t.Helper() - run := model.Run{ + run := test.Run{ TraceID: testdb.IDGen.TraceID(), SpanID: testdb.IDGen.SpanID(), CreatedAt: time.Now(), } - updated, err := db.CreateRun(context.TODO(), test, run) + updated, err := db.CreateRun(context.TODO(), testObj, run) if err != nil { panic(err) } diff --git a/server/testdb/runs_test.go b/server/testdb/runs_test.go deleted file mode 100644 index 79940d879b..0000000000 --- a/server/testdb/runs_test.go +++ /dev/null @@ -1,189 +0,0 @@ -package testdb_test - -import ( - "context" - "testing" - - "github.com/kubeshop/tracetest/server/environment" - "github.com/kubeshop/tracetest/server/model" - "github.com/kubeshop/tracetest/server/model/modeltest" - "github.com/kubeshop/tracetest/server/pkg/maps" - "github.com/kubeshop/tracetest/server/testdb" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" -) - -func TestCreateRun(t *testing.T) { - db, clean := getDB() - defer clean() - - test := createTest(t, db) - - run := model.Run{ - TraceID: testdb.IDGen.TraceID(), - SpanID: testdb.IDGen.SpanID(), - Metadata: model.RunMetadata{ - "key": "Value", - }, - Environment: environment.Environment{ - Name: "env1", - Description: "env1", - Values: []environment.EnvironmentValue{{ - Key: "key", - Value: "value", - }}}, - } - - updated, err := db.CreateRun(context.TODO(), test, run) - require.NoError(t, err) - - actual, err := db.GetRun(context.TODO(), test.ID, updated.ID) - require.NoError(t, err) - - assert.NotEmpty(t, actual.ID) - assert.Equal(t, test.ID, actual.TestID) - assert.Equal(t, test.Version, actual.TestVersion) - assert.Equal(t, model.RunStateCreated, actual.State) - assert.Equal(t, run.TraceID, actual.TraceID) - assert.Equal(t, run.SpanID, actual.SpanID) - assert.Equal(t, run.Metadata, actual.Metadata) - assert.Equal(t, run.Environment, actual.Environment) -} - -func TestCreateRunIDsIncrementForTest(t *testing.T) { - db, clean := getDB() - defer clean() - - run := model.Run{ - TraceID: testdb.IDGen.TraceID(), - SpanID: testdb.IDGen.SpanID(), - } - - test1 := createTest(t, db) - test2 := createTest(t, db) - - t1r1, err := db.CreateRun(context.TODO(), test1, run) - require.NoError(t, err) - - t2r1, err := db.CreateRun(context.TODO(), test2, run) - require.NoError(t, err) - - t1r2, err := db.CreateRun(context.TODO(), test1, run) - require.NoError(t, err) - - t2r2, err := db.CreateRun(context.TODO(), test2, run) - require.NoError(t, err) - - assert.Equal(t, 1, t1r1.ID) - assert.Equal(t, 2, t1r2.ID) - assert.Equal(t, 1, t2r1.ID) - assert.Equal(t, 2, t2r2.ID) -} - -func TestUpdateRun(t *testing.T) { - db, clean := getDB() - defer clean() - - test := createTest(t, db) - run := createRun(t, db, test) - - run.State = model.RunStateFinished - run.Trace = &model.Trace{ - ID: testdb.IDGen.TraceID(), - RootSpan: model.Span{ - ID: testdb.IDGen.SpanID(), - Attributes: model.Attributes{ - "service.name": "Pokeshop", - "tracetest.span.duration": "2000", - }, - }, - } - run.Trace.Flat = map[trace.SpanID]*model.Span{ - run.Trace.RootSpan.ID: &run.Trace.RootSpan, - } - run.Results = &model.RunResults{ - AllPassed: true, - Results: (maps.Ordered[model.SpanQuery, []model.AssertionResult]{}).MustAdd(`span[service.name="Pokeshop"]`, []model.AssertionResult{ - { - Assertion: model.Assertion(`attr:tracetest.span.duration = 2000`), - Results: []model.SpanAssertionResult{ - { - SpanID: &run.Trace.RootSpan.ID, - ObservedValue: "2000", - CompareErr: nil, - }, - }, - }, - }), - } - - run.Outputs = (maps.Ordered[string, model.RunOutput]{}). - MustAdd("key", model.RunOutput{ - Value: "value", - }) - - err := db.UpdateRun(context.TODO(), run) - require.NoError(t, err) - - actual, err := db.GetRun(context.TODO(), test.ID, run.ID) - require.NoError(t, err) - - modeltest.AssertRunEqual(t, run, actual) - - updatedList, err := db.GetTestRuns(context.TODO(), test, 20, 0) - require.NoError(t, err) - - assert.Len(t, updatedList.Items, 1) - assert.Equal(t, 1, updatedList.TotalCount) - modeltest.AssertRunEqual(t, updatedList.Items[0], actual) -} - -func TestUpdateRunWithNewIDs(t *testing.T) { - db, clean := getDB() - defer clean() - - t1r1 := createRun(t, db, createTest(t, db)) - t2r1 := createRun(t, db, createTest(t, db)) - - t1r1.Metadata = model.RunMetadata{"key": "val"} - db.UpdateRun(context.TODO(), t1r1) - - t1r1Updated, err := db.GetRun(context.TODO(), t1r1.TestID, t1r1.ID) - require.NoError(t, err) - - t2r1Updated, err := db.GetRun(context.TODO(), t2r1.TestID, t2r1.ID) - require.NoError(t, err) - - assert.Equal(t, t1r1.Metadata, t1r1Updated.Metadata) - assert.Equal(t, t2r1.Metadata, t2r1Updated.Metadata) -} - -func TestDeleteRun(t *testing.T) { - db, clean := getDB() - defer clean() - - t1r1 := createRun(t, db, createTest(t, db)) - t2r1 := createRun(t, db, createTest(t, db)) - - db.DeleteRun(context.TODO(), t2r1) - - _, err := db.GetRun(context.TODO(), t1r1.TestID, t1r1.ID) - require.NoError(t, err) - - _, err = db.GetRun(context.TODO(), t2r1.TestID, t2r1.ID) - require.ErrorIs(t, err, testdb.ErrNotFound) -} - -func TestGetRunByTraceID(t *testing.T) { - db, clean := getDB() - defer clean() - - test := createTest(t, db) - expected := createRun(t, db, test) - - actual, err := db.GetRunByTraceID(context.TODO(), expected.TraceID) - require.NoError(t, err) - - modeltest.AssertRunEqual(t, expected, actual) -} diff --git a/server/testdb/test_run_event_test.go b/server/testdb/test_run_event_test.go index 54e53a0d1c..798e6823a2 100644 --- a/server/testdb/test_run_event_test.go +++ b/server/testdb/test_run_event_test.go @@ -5,18 +5,24 @@ import ( "testing" "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/testmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRunEvents(t *testing.T) { - db, clean := getDB() - defer clean() + rawDB := testmock.GetRawTestingDatabase() + db := testmock.GetTestingDatabaseFromRawDB(rawDB) + defer rawDB.Close() - test1 := createTestWithName(t, db, "test 1") + testRepo := test.NewRepository(rawDB) + testRunRepo := test.NewRunRepository(rawDB) - run1 := createRun(t, db, test1) - run2 := createRun(t, db, test1) + test1 := createTestWithName(t, testRepo, "test 1") + + run1 := createRun(t, testRunRepo, test1) + run2 := createRun(t, testRunRepo, test1) events := []model.TestRunEvent{ {TestID: test1.ID, RunID: run1.ID, Type: "EVENT_1", Stage: model.StageTrigger, Title: "OP 1", Description: "This happened"}, diff --git a/server/testdb/tests.go b/server/testdb/tests.go deleted file mode 100644 index 5847809dd4..0000000000 --- a/server/testdb/tests.go +++ /dev/null @@ -1,403 +0,0 @@ -package testdb - -import ( - "context" - "database/sql" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/kubeshop/tracetest/server/model" - "github.com/kubeshop/tracetest/server/pkg/id" - "github.com/kubeshop/tracetest/server/transaction" -) - -var _ model.TestRepository = &postgresDB{} - -func (td *postgresDB) TestIDExists(ctx context.Context, id id.ID) (bool, error) { - exists := false - - row := td.db.QueryRowContext( - ctx, - "SELECT COUNT(*) > 0 as exists FROM tests WHERE id = $1", - id, - ) - - err := row.Scan(&exists) - - return exists, err -} - -const insertIntoTestsQuery = ` -INSERT INTO tests ( - "id", - "version", - "name", - "description", - "service_under_test", - "specs", - "outputs", - "created_at" -) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)` - -func (td *postgresDB) CreateTest(ctx context.Context, test model.Test) (model.Test, error) { - if !test.HasID() { - test.ID = IDGen.ID() - } - - test.Version = 1 - test.CreatedAt = time.Now() - - return td.insertIntoTests(ctx, test) -} - -func (td *postgresDB) insertIntoTests(ctx context.Context, test model.Test) (model.Test, error) { - stmt, err := td.db.Prepare(insertIntoTestsQuery) - if err != nil { - return model.Test{}, fmt.Errorf("sql prepare: %w", err) - } - defer stmt.Close() - - jsonServiceUnderTest, err := json.Marshal(test.ServiceUnderTest) - if err != nil { - return model.Test{}, fmt.Errorf("encoding error: %w", err) - } - - jsonSpecs, err := json.Marshal(test.Specs) - if err != nil { - return model.Test{}, fmt.Errorf("encoding error: %w", err) - } - - jsonOutputs, err := json.Marshal(test.Outputs) - if err != nil { - return model.Test{}, fmt.Errorf("encoding error: %w", err) - } - - _, err = stmt.ExecContext( - ctx, - test.ID, - test.Version, - test.Name, - test.Description, - jsonServiceUnderTest, - jsonSpecs, - jsonOutputs, - test.CreatedAt, - ) - if err != nil { - return model.Test{}, fmt.Errorf("sql exec: %w", err) - } - - return test, nil -} - -func (td *postgresDB) UpdateTest(ctx context.Context, test model.Test) (model.Test, error) { - if test.Version == 0 { - test.Version = 1 - } - - oldTest, err := td.GetLatestTestVersion(ctx, test.ID) - if err != nil { - return model.Test{}, fmt.Errorf("could not get latest test version while updating test: %w", err) - } - - // keep the same creation date to keep sort order - test.CreatedAt = oldTest.CreatedAt - - testToUpdate, err := model.BumpTestVersionIfNeeded(oldTest, test) - if err != nil { - return model.Test{}, fmt.Errorf("could not bump test version: %w", err) - } - - if oldTest.Version == testToUpdate.Version { - // No change in the version. Nothing changed so no need to persist it - return testToUpdate, nil - } - - return td.insertIntoTests(ctx, testToUpdate) -} - -func (td *postgresDB) DeleteTest(ctx context.Context, test model.Test) error { - queries := []string{ - "DELETE FROM transaction_run_steps WHERE test_run_test_id = $1", - "DELETE FROM transaction_steps WHERE test_id = $1", - "DELETE FROM test_runs WHERE test_id = $1", - "DELETE FROM tests WHERE id = $1", - } - - tx, err := td.db.BeginTx(ctx, nil) - if err != nil { - return fmt.Errorf("sql BeginTx: %w", err) - } - - for _, sql := range queries { - _, err := tx.ExecContext(ctx, sql, test.ID) - if err != nil { - tx.Rollback() - return fmt.Errorf("sql error: %w", err) - } - } - - dropSequece(ctx, tx, test.ID) - - err = tx.Commit() - if err != nil { - return fmt.Errorf("sql Commit: %w", err) - } - - return nil -} - -const ( - getTestSQL = ` - SELECT - t.id, - t.version, - t.name, - t.description, - t.service_under_test, - t.specs, - t.outputs, - t.created_at, - (SELECT COUNT(*) FROM test_runs tr WHERE tr.test_id = t.id) as total_runs, - last_test_run.created_at as last_test_run_time, - last_test_run.pass as last_test_run_pass, - last_test_run.fail as last_test_run_fail, - last_test_run.linter as last_test_run_linter - FROM tests t - LEFT OUTER JOIN ( - SELECT MAX(id) as id, test_id FROM test_runs GROUP BY test_id - ) as ltr ON ltr.test_id = t.id - LEFT OUTER JOIN - test_runs last_test_run - ON last_test_run.test_id = ltr.test_id AND last_test_run.id = ltr.id -` - - testMaxVersionQuery = ` - INNER JOIN ( - SELECT id as idx, max(version) as latest_version FROM tests GROUP BY idx - ) as latest_tests ON latest_tests.idx = t.id AND t.version = latest_tests.latest_version - ` -) - -func sortQuery(sql, sortBy, sortDirection string, sortingFields map[string]string) string { - sortField, ok := sortingFields[sortBy] - - if !ok { - sortField = sortingFields["created"] - } - - dir := "DESC" - if strings.ToLower(sortDirection) == "asc" { - dir = "ASC" - } - - return fmt.Sprintf("%s ORDER BY %s %s", sql, sortField, dir) -} - -func (td *postgresDB) GetTestVersion(ctx context.Context, id id.ID, version int) (model.Test, error) { - stmt, err := td.db.Prepare(getTestSQL + " WHERE t.id = $1 AND t.version = $2") - if err != nil { - return model.Test{}, fmt.Errorf("prepare: %w", err) - } - defer stmt.Close() - - test, err := td.readTestRow(ctx, stmt.QueryRowContext(ctx, id, version)) - if err != nil { - return model.Test{}, err - } - - return test, nil -} - -func (td *postgresDB) GetLatestTestVersion(ctx context.Context, id id.ID) (model.Test, error) { - stmt, err := td.db.Prepare(getTestSQL + " WHERE t.id = $1 ORDER BY t.version DESC LIMIT 1") - if err != nil { - return model.Test{}, fmt.Errorf("prepare: %w", err) - } - defer stmt.Close() - - test, err := td.readTestRow(ctx, stmt.QueryRowContext(ctx, id)) - if err != nil { - return model.Test{}, err - } - - return test, nil -} - -func (td *postgresDB) GetTests(ctx context.Context, take, skip int32, query, sortBy, sortDirection string) (model.List[model.Test], error) { - hasSearchQuery := query != "" - cleanSearchQuery := "%" + strings.ReplaceAll(query, " ", "%") + "%" - params := []any{take, skip} - - sql := getTestSQL + testMaxVersionQuery - - const condition = " AND (t.name ilike $3 OR t.description ilike $3)" - if hasSearchQuery { - params = append(params, cleanSearchQuery) - sql += condition - } - - sortingFields := map[string]string{ - "created": "t.created_at", - "name": "t.name", - "last_run": "last_test_run_time", - } - - sql = sortQuery(sql, sortBy, sortDirection, sortingFields) - sql += ` LIMIT $1 OFFSET $2 ` - - stmt, err := td.db.Prepare(sql) - if err != nil { - return model.List[model.Test]{}, err - } - defer stmt.Close() - - rows, err := stmt.QueryContext(ctx, params...) - if err != nil { - return model.List[model.Test]{}, err - } - - tests, err := td.readTestRows(ctx, rows) - if err != nil { - return model.List[model.Test]{}, err - } - - count, err := td.count(ctx, condition, cleanSearchQuery) - if err != nil { - return model.List[model.Test]{}, err - } - - return model.List[model.Test]{ - Items: tests, - TotalCount: count, - }, nil -} - -func (td *postgresDB) count(ctx context.Context, condition, cleanSearchQuery string) (int, error) { - var ( - count int - params []any - ) - countQuery := "SELECT COUNT(*) FROM tests t" + testMaxVersionQuery - if cleanSearchQuery != "" { - params = []any{cleanSearchQuery} - countQuery += strings.ReplaceAll(condition, "$3", "$1") - } - - err := td.db. - QueryRowContext(ctx, countQuery, params...). - Scan(&count) - - if err != nil { - return 0, err - } - return count, nil -} - -func (td *postgresDB) readTestRows(ctx context.Context, rows *sql.Rows) ([]model.Test, error) { - tests := []model.Test{} - - for rows.Next() { - test, err := td.readTestRow(ctx, rows) - if err != nil { - return []model.Test{}, err - } - - tests = append(tests, test) - } - - return tests, nil -} - -func (td *postgresDB) readTestRow(ctx context.Context, row scanner) (model.Test, error) { - test := model.Test{} - - var ( - jsonServiceUnderTest, - jsonSpecs, - jsonOutputs, - jsonLinter []byte - - lastRunTime *time.Time - - pass, fail *int - ) - err := row.Scan( - &test.ID, - &test.Version, - &test.Name, - &test.Description, - &jsonServiceUnderTest, - &jsonSpecs, - &jsonOutputs, - &test.CreatedAt, - &test.Summary.Runs, - &lastRunTime, - &pass, - &fail, - &jsonLinter, - ) - - switch err { - case nil: - err = json.Unmarshal(jsonServiceUnderTest, &test.ServiceUnderTest) - if err != nil { - return model.Test{}, fmt.Errorf("cannot parse trigger: %w", err) - } - - err = json.Unmarshal(jsonSpecs, &test.Specs) - if err != nil { - return model.Test{}, fmt.Errorf("cannot parse specs: %w", err) - } - - err = json.Unmarshal(jsonOutputs, &test.Outputs) - if err != nil { - return model.Test{}, fmt.Errorf("cannot parse outputs: %w", err) - } - - if lastRunTime != nil { - test.Summary.LastRun.Time = *lastRunTime - } - if pass != nil { - test.Summary.LastRun.Passes = *pass - } - if fail != nil { - test.Summary.LastRun.Fails = *fail - } - - var linter model.LinterResult - err = json.Unmarshal(jsonLinter, &linter) - if err == nil { - test.Summary.LastRun.AnalyzerScore = linter.Score - } - - return test, nil - case sql.ErrNoRows: - return model.Test{}, ErrNotFound - default: - return model.Test{}, err - } -} - -func (td *postgresDB) GetTransactionSteps(ctx context.Context, transaction transaction.Transaction) ([]model.Test, error) { - stmt, err := td.db.Prepare(getTestSQL + testMaxVersionQuery + ` INNER JOIN transaction_steps ts ON t.id = ts.test_id - WHERE ts.transaction_id = $1 AND ts.transaction_version = $2 ORDER BY ts.step_number ASC`) - if err != nil { - return []model.Test{}, fmt.Errorf("prepare 2: %w", err) - } - defer stmt.Close() - - rows, err := stmt.QueryContext(ctx, transaction.ID, transaction.Version) - if err != nil { - return []model.Test{}, fmt.Errorf("query context: %w", err) - } - - steps, err := td.readTestRows(ctx, rows) - if err != nil { - return []model.Test{}, fmt.Errorf("read row: %w", err) - } - - return steps, nil -} diff --git a/server/testdb/tests_test.go b/server/testdb/tests_test.go deleted file mode 100644 index 666bcf8c68..0000000000 --- a/server/testdb/tests_test.go +++ /dev/null @@ -1,266 +0,0 @@ -package testdb_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/kubeshop/tracetest/server/model" - "github.com/kubeshop/tracetest/server/pkg/maps" - "github.com/kubeshop/tracetest/server/testdb" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCreateTest(t *testing.T) { - db, clean := getDB() - defer clean() - - test := model.Test{ - Name: "first test", - Description: "description", - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ - URL: "http://localhost:3030/hello-instrumented", - }, - }, - Outputs: (maps.Ordered[string, model.Output]{}). - MustAdd("output1", model.Output{ - Selector: model.SpanQuery(`span[name="root"]`), - Value: "${attr:myapp.some_attribute}", - }), - } - - updated, err := db.CreateTest(context.TODO(), test) - require.NoError(t, err) - - actual, err := db.GetLatestTestVersion(context.TODO(), updated.ID) - require.NoError(t, err) - assert.Equal(t, test.Name, actual.Name) - assert.Equal(t, test.Description, actual.Description) - assert.Equal(t, test.ServiceUnderTest, actual.ServiceUnderTest) - assert.Equal(t, test.Specs, actual.Specs) - assert.Equal(t, test.Outputs, actual.Outputs) - assert.False(t, actual.CreatedAt.IsZero()) -} - -func TestDeleteTest(t *testing.T) { - db, clean := getDB() - defer clean() - - test := createTest(t, db) - - err := db.DeleteTest(context.TODO(), test) - require.NoError(t, err) - - actual, err := db.GetLatestTestVersion(context.TODO(), test.ID) - assert.ErrorIs(t, err, testdb.ErrNotFound) - assert.Empty(t, actual) - -} - -func TestGetLatestTestVersion(t *testing.T) { - db, clean := getDB() - defer clean() - - test := createTestWithName(t, db, "1") - test.Name = "1 v2" - test.Version = 2 - - _, err := db.UpdateTest(context.TODO(), test) - require.NoError(t, err) - - latestTest, err := db.GetLatestTestVersion(context.TODO(), test.ID) - assert.NoError(t, err) - assert.Equal(t, "1 v2", latestTest.Name) - assert.Equal(t, 2, latestTest.Version) -} - -func TestGetTests(t *testing.T) { - db, clean := getDB() - defer clean() - - createTestWithName(t, db, "one") - createTestWithName(t, db, "two") - createTestWithName(t, db, "three") - - t.Run("Order", func(t *testing.T) { - actual, err := db.GetTests(context.TODO(), 20, 0, "", "", "") - require.NoError(t, err) - - assert.Len(t, actual.Items, 3) - assert.Equal(t, actual.TotalCount, 3) - - // test order - assert.Equal(t, actual.TotalCount, 3) - assert.Equal(t, "three", actual.Items[0].Name) - assert.Equal(t, "two", actual.Items[1].Name) - assert.Equal(t, "one", actual.Items[2].Name) - }) - - t.Run("Pagination", func(t *testing.T) { - actual, err := db.GetTests(context.TODO(), 20, 10, "", "", "") - require.NoError(t, err) - - assert.Equal(t, actual.TotalCount, 3) - assert.Len(t, actual.Items, 0) - }) - - t.Run("SortByCreated", func(t *testing.T) { - actual, err := db.GetTests(context.TODO(), 20, 0, "", "created", "") - require.NoError(t, err) - - // test order - assert.Equal(t, "three", actual.Items[0].Name) - assert.Equal(t, "two", actual.Items[1].Name) - assert.Equal(t, "one", actual.Items[2].Name) - }) - - t.Run("SortByNameAsc", func(t *testing.T) { - actual, err := db.GetTests(context.TODO(), 20, 0, "", "name", "asc") - require.NoError(t, err) - - // test order - assert.Equal(t, "one", actual.Items[0].Name) - assert.Equal(t, "three", actual.Items[1].Name) - assert.Equal(t, "two", actual.Items[2].Name) - }) - - t.Run("SortByNameDesc", func(t *testing.T) { - actual, err := db.GetTests(context.TODO(), 20, 0, "", "name", "desc") - require.NoError(t, err) - - // test order - assert.Equal(t, "two", actual.Items[0].Name) - assert.Equal(t, "three", actual.Items[1].Name) - assert.Equal(t, "one", actual.Items[2].Name) - }) - - t.Run("SearchByName", func(t *testing.T) { - _, _ = db.CreateTest(context.TODO(), model.Test{Name: "VerySpecificName"}) - actual, err := db.GetTests(context.TODO(), 10, 0, "specif", "", "") - require.NoError(t, err) - assert.Len(t, actual.Items, 1) - assert.Equal(t, actual.TotalCount, 1) - - assert.Equal(t, "VerySpecificName", actual.Items[0].Name) - }) - - t.Run("SearchByDescription", func(t *testing.T) { - _, _ = db.CreateTest(context.TODO(), model.Test{Description: "VeryUniqueText"}) - - actual, err := db.GetTests(context.TODO(), 10, 0, "nique", "", "") - require.NoError(t, err) - assert.Len(t, actual.Items, 1) - assert.Equal(t, actual.TotalCount, 1) - - assert.Equal(t, "VeryUniqueText", actual.Items[0].Description) - }) -} - -func TestGetTestsWithMultipleVersions(t *testing.T) { - db, clean := getDB() - defer clean() - - test1 := createTestWithName(t, db, "1") - test1.Name = "1 v2" - - _, err := db.UpdateTest(context.TODO(), test1) - require.NoError(t, err) - - test2 := createTestWithName(t, db, "2") - test2.Name = "2 v2" - - _, err = db.UpdateTest(context.TODO(), test2) - require.NoError(t, err) - - tests, err := db.GetTests(context.TODO(), 20, 0, "", "", "") - assert.NoError(t, err) - assert.Len(t, tests.Items, 2) - assert.Equal(t, 2, tests.TotalCount) - - for _, test := range tests.Items { - assert.Equal(t, 2, test.Version) - } -} - -func TestSummary(t *testing.T) { - db, clean := getDB() - defer clean() - - createRunWithResult := func(t *testing.T, db model.Repository, test model.Test, d time.Time, pass, fail int) model.Run { - t.Helper() - run := model.Run{ - TraceID: testdb.IDGen.TraceID(), - SpanID: testdb.IDGen.SpanID(), - CreatedAt: d, - } - - run, err := db.CreateRun(context.TODO(), test, run) - if err != nil { - panic(err) - } - - result := []model.AssertionResult{ - { - Results: []model.SpanAssertionResult{}, - }, - } - for i := 0; i < pass; i++ { - // CompareErr: nil means passed - result[0].Results = append(result[0].Results, model.SpanAssertionResult{CompareErr: nil}) - } - for i := 0; i < fail; i++ { - result[0].Results = append(result[0].Results, model.SpanAssertionResult{CompareErr: fmt.Errorf("err")}) - } - run.Results = &model.RunResults{ - Results: (maps.Ordered[model.SpanQuery, []model.AssertionResult]{}). - MustAdd("span", result), - } - - err = db.UpdateRun(context.TODO(), run) - if err != nil { - panic(err) - } - - return run - } - - test := createTest(t, db) - - // 1 run - { - t1 := time.Date(2022, time.July, 01, 12, 23, 00, 0, time.UTC) - createRunWithResult(t, db, test, t1, 2, 0) - - tests, err := db.GetTests(context.TODO(), 20, 0, "", "", "") - require.NoError(t, err) - - require.Len(t, tests.Items, 1) - assert.Equal(t, tests.Items[0].ID, test.ID) - - assert.Equal(t, 1, tests.Items[0].Summary.Runs) - assert.WithinDuration(t, t1, tests.Items[0].Summary.LastRun.Time, 0) // hack for comparing times - assert.Equal(t, 2, tests.Items[0].Summary.LastRun.Passes) - assert.Equal(t, 0, tests.Items[0].Summary.LastRun.Fails) - } - - { - // 2 runs - t2 := time.Date(2022, time.July, 01, 12, 23, 30, 0, time.UTC) - createRunWithResult(t, db, test, t2, 1, 1) - - tests, err := db.GetTests(context.TODO(), 20, 0, "", "", "") - require.NoError(t, err) - - require.Len(t, tests.Items, 1) - assert.Equal(t, tests.Items[0].ID, test.ID) - - assert.Equal(t, 2, tests.Items[0].Summary.Runs) - assert.WithinDuration(t, t2, tests.Items[0].Summary.LastRun.Time, 0) // hack for comparing times - assert.Equal(t, 1, tests.Items[0].Summary.LastRun.Passes) - assert.Equal(t, 1, tests.Items[0].Summary.LastRun.Fails) - } -} diff --git a/server/testmock/database.go b/server/testmock/database.go index 3c33e3b495..33d69a593f 100644 --- a/server/testmock/database.go +++ b/server/testmock/database.go @@ -1,6 +1,7 @@ package testmock import ( + "context" "database/sql" "fmt" "math/rand" @@ -170,3 +171,37 @@ func randomInt() int { max := 1000000 return rand.Intn(max-min) + min } + +func DropDatabase(db *sql.DB) error { + return dropTables( + db, + "transaction_run_steps", + "transaction_runs", + "transaction_steps", + "transactions", + "test_runs", + "tests", + "environments", + "data_stores", + "server", + "schema_migrations", + ) +} + +func dropTables(db *sql.DB, tables ...string) error { + tx, err := db.BeginTx(context.Background(), &sql.TxOptions{}) + if err != nil { + return fmt.Errorf("could not start transaction: %w", err) + } + + defer tx.Rollback() + + for _, table := range tables { + _, err := tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s;", table)) + if err != nil { + return err + } + } + + return tx.Commit() +} diff --git a/server/tracedb/otlp.go b/server/tracedb/otlp.go index e461befbe1..97d5fd370a 100644 --- a/server/tracedb/otlp.go +++ b/server/tracedb/otlp.go @@ -5,16 +5,17 @@ import ( "strings" "github.com/kubeshop/tracetest/server/model" + "github.com/kubeshop/tracetest/server/test" "github.com/kubeshop/tracetest/server/tracedb/connection" "github.com/kubeshop/tracetest/server/traces" ) type OTLPTraceDB struct { realTraceDB - db model.RunRepository + db test.RunRepository } -func newCollectorDB(repository model.RunRepository) (TraceDB, error) { +func newCollectorDB(repository test.RunRepository) (TraceDB, error) { return &OTLPTraceDB{ db: repository, }, nil diff --git a/server/tracedb/tracedb.go b/server/tracedb/tracedb.go index 2078c60948..f8891a6957 100644 --- a/server/tracedb/tracedb.go +++ b/server/tracedb/tracedb.go @@ -7,6 +7,7 @@ import ( "github.com/kubeshop/tracetest/server/datastore" "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" + "github.com/kubeshop/tracetest/server/test" "go.opentelemetry.io/otel/trace" ) @@ -46,10 +47,10 @@ func (db *noopTraceDB) TestConnection(ctx context.Context) model.ConnectionResul } type traceDBFactory struct { - runRepository model.RunRepository + runRepository test.RunRepository } -func Factory(runRepository model.RunRepository) func(ds datastore.DataStore) (TraceDB, error) { +func Factory(runRepository test.RunRepository) func(ds datastore.DataStore) (TraceDB, error) { f := traceDBFactory{ runRepository: runRepository, } diff --git a/server/transaction/transaction_entities.go b/server/transaction/transaction_entities.go index 23a4df98d0..629edb1af7 100644 --- a/server/transaction/transaction_entities.go +++ b/server/transaction/transaction_entities.go @@ -3,8 +3,8 @@ package transaction import ( "time" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" + "github.com/kubeshop/tracetest/server/test" ) const ( @@ -13,14 +13,14 @@ const ( ) type Transaction struct { - ID id.ID `json:"id"` - CreatedAt *time.Time `json:"createdAt,omitempty"` - Name string `json:"name"` - Description string `json:"description"` - Version *int `json:"version,omitempty"` - StepIDs []id.ID `json:"steps"` - Steps []model.Test `json:"fullSteps,omitempty"` - Summary *model.Summary `json:"summary,omitempty"` + ID id.ID `json:"id"` + CreatedAt *time.Time `json:"createdAt,omitempty"` + Name string `json:"name"` + Description string `json:"description"` + Version *int `json:"version,omitempty"` + StepIDs []id.ID `json:"steps"` + Steps []test.Test `json:"fullSteps,omitempty"` + Summary *test.Summary `json:"summary,omitempty"` } func setVersion(t *Transaction, v int) { @@ -64,7 +64,7 @@ func (t Transaction) NewRun() TransactionRun { TransactionVersion: t.GetVersion(), CreatedAt: time.Now().UTC(), State: TransactionRunStateCreated, - Steps: make([]model.Run, 0, len(t.StepIDs)), + Steps: make([]test.Run, 0, len(t.StepIDs)), CurrentTest: 0, } } diff --git a/server/transaction/transaction_repository.go b/server/transaction/transaction_repository.go index 406e1f9594..eb8db1d382 100644 --- a/server/transaction/transaction_repository.go +++ b/server/transaction/transaction_repository.go @@ -9,28 +9,31 @@ import ( "strings" "time" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/pkg/sqlutil" + "github.com/kubeshop/tracetest/server/test" ) -func NewRepository(db *sql.DB, stepsRepository stepsRepository) *Repository { +func NewRepository(db *sql.DB, stepRepository transactionStepRepository) *Repository { repo := &Repository{ - db: db, - stepsRepository: stepsRepository, + db: db, + stepRepository: stepRepository, } return repo } -type stepsRepository interface { - GetTransactionSteps(context.Context, Transaction) ([]model.Test, error) - GetTransactionRunSteps(context.Context, TransactionRun) ([]model.Run, error) +type transactionStepRepository interface { + GetTransactionSteps(_ context.Context, _ id.ID, version int) ([]test.Test, error) +} + +type transactionStepRunRepository interface { + GetTransactionRunSteps(_ context.Context, _ id.ID, runID int) ([]test.Run, error) } type Repository struct { - db *sql.DB - stepsRepository stepsRepository + db *sql.DB + stepRepository transactionStepRepository } // needed for test @@ -418,7 +421,7 @@ type scanner interface { func (r *Repository) readRow(ctx context.Context, row scanner, augmented bool) (Transaction, error) { transaction := Transaction{ - Summary: &model.Summary{}, + Summary: &test.Summary{}, } var ( @@ -443,6 +446,10 @@ func (r *Repository) readRow(ctx context.Context, row scanner, augmented bool) ( ) if err != nil { + if err == sql.ErrNoRows { + return transaction, err + } + return Transaction{}, fmt.Errorf("cannot read row: %w", err) } @@ -471,7 +478,7 @@ func (r *Repository) readRow(ctx context.Context, row scanner, augmented bool) ( if !augmented { removeNonAugmentedFields(&transaction) } else { - steps, err := r.stepsRepository.GetTransactionSteps(ctx, transaction) + steps, err := r.stepRepository.GetTransactionSteps(ctx, transaction.ID, *transaction.Version) if err != nil { return Transaction{}, fmt.Errorf("cannot read row: %w", err) } diff --git a/server/transaction/transaction_repository_test.go b/server/transaction/transaction_repository_test.go index 10ede6892e..7a8406f4c3 100644 --- a/server/transaction/transaction_repository_test.go +++ b/server/transaction/transaction_repository_test.go @@ -7,12 +7,12 @@ import ( "testing" "github.com/gorilla/mux" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" "github.com/kubeshop/tracetest/server/pkg/maps" "github.com/kubeshop/tracetest/server/resourcemanager" rmtests "github.com/kubeshop/tracetest/server/resourcemanager/testutil" - "github.com/kubeshop/tracetest/server/testdb" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "github.com/kubeshop/tracetest/server/testmock" "github.com/kubeshop/tracetest/server/transaction" "github.com/stretchr/testify/assert" @@ -20,33 +20,33 @@ import ( ) type transactionFixture struct { - t1 model.Test - t2 model.Test - testRun model.Run + t1 test.Test + t2 test.Test + testRun test.Run } -func copyRun(testsDB model.RunRepository, run model.Run) model.Run { - return createRun(testsDB, model.Test{ +func copyRun(testsDB test.RunRepository, run test.Run) test.Run { + return createRun(testsDB, test.Test{ ID: run.TestID, - Version: run.TestVersion, + Version: &run.TestVersion, }) } -func createRun(testsDB model.RunRepository, test model.Test) model.Run { - run := model.Run{ - State: model.RunStateFinished, +func createRun(runRepository test.RunRepository, t test.Test) test.Run { + run := test.Run{ + State: test.RunStateFinished, TraceID: id.NewRandGenerator().TraceID(), SpanID: id.NewRandGenerator().SpanID(), } - run, err := testsDB.CreateRun(context.TODO(), test, run) + run, err := runRepository.CreateRun(context.TODO(), t, run) if err != nil { panic(err) } - run.Results.Results = (maps.Ordered[model.SpanQuery, []model.AssertionResult]{}). - MustAdd("query", []model.AssertionResult{ + run.Results.Results = (maps.Ordered[test.SpanQuery, []test.AssertionResult]{}). + MustAdd("query", []test.AssertionResult{ { - Results: []model.SpanAssertionResult{ + Results: []test.SpanAssertionResult{ {CompareErr: nil}, {CompareErr: nil}, {CompareErr: fmt.Errorf("some error")}, @@ -54,7 +54,7 @@ func createRun(testsDB model.RunRepository, test model.Test) model.Run { }, }) - err = testsDB.UpdateRun(context.TODO(), run) + err = runRepository.UpdateRun(context.TODO(), run) if err != nil { panic(err) } @@ -62,72 +62,76 @@ func createRun(testsDB model.RunRepository, test model.Test) model.Run { } func setupTransactionFixture(t *testing.T, db *sql.DB) transactionFixture { - testsDB, err := testdb.Postgres(testdb.WithDB(db)) - require.NoError(t, err) + testsDB := test.NewRepository(db) + runDB := test.NewRunRepository(db) fixture := transactionFixture{} - test := model.Test{ + createdTest := test.Test{ ID: "ezMn7bE4g", Name: "first test", Description: "description", - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ + Trigger: trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ URL: "http://localhost:3030/hello-instrumented", }, }, - Specs: (maps.Ordered[model.SpanQuery, model.NamedAssertions]{}). - MustAdd("query", model.NamedAssertions{ - Name: "some assertion", - Assertions: []model.Assertion{ + Specs: test.Specs{ + { + Name: "some assertion", + Selector: "query", + Assertions: []test.Assertion{ "attr:some_attr = 1", }, - }), - Outputs: (maps.Ordered[string, model.Output]{}). - MustAdd("output", model.Output{ - Selector: "selector", - Value: "value", - }), + }, + }, + Outputs: []test.Output{ + {Name: "output", Selector: "selector", Value: "value"}, + }, } - test, err = testsDB.CreateTest(context.TODO(), test) + createdTest, err := testsDB.Create(context.TODO(), createdTest) require.NoError(t, err) - fixture.t1 = test + fixture.t1 = createdTest - fixture.testRun = createRun(testsDB, test) + fixture.testRun = createRun(runDB, createdTest) - test = model.Test{ + createdTest = test.Test{ ID: "2qOn7xPVg", Name: "second test", Description: "description", - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ + Trigger: trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ URL: "http://localhost:3030/hello-instrumented", }, }, - Specs: (maps.Ordered[model.SpanQuery, model.NamedAssertions]{}). - MustAdd("query", model.NamedAssertions{ - Name: "some assertion", - Assertions: []model.Assertion{ + Specs: test.Specs{ + { + Name: "some assertion", + Selector: "query", + Assertions: []test.Assertion{ "attr:some_attr = 1", }, - }), - Outputs: (maps.Ordered[string, model.Output]{}). - MustAdd("output", model.Output{ + }, + }, + Outputs: []test.Output{ + { + Name: "output", Selector: "selector", Value: "value", - }), + }, + }, } - _, err = testsDB.CreateTest(context.TODO(), test) + _, err = testsDB.Create(context.TODO(), createdTest) require.NoError(t, err) - fixture.t2 = test + fixture.t2 = createdTest return fixture } -func setupTests(t *testing.T, db *sql.DB) model.Run { +func setupTests(t *testing.T, db *sql.DB) test.Run { f := setupTransactionFixture(t, db) return f.testRun @@ -137,20 +141,18 @@ func TestDeleteTestsRelatedToTransactions(t *testing.T) { db := testmock.CreateMigratedDatabase() defer db.Close() - testsDB, err := testdb.Postgres(testdb.WithDB(db)) - if err != nil { - panic(err) - } - transactionRepo := transaction.NewRepository(db, testsDB) - transactionRunRepo := transaction.NewRunRepository(db, testsDB) + testRepository := test.NewRepository(db) + runRepository := test.NewRunRepository(db) + transactionRepo := transaction.NewRepository(db, testRepository) + transactionRunRepo := transaction.NewRunRepository(db, runRepository) transactionRepo.Create(context.TODO(), transactionSample) f := setupTransactionFixture(t, db) createTransactionRun(transactionRepo, transactionRunRepo, transactionSample, f.testRun) - testsDB.DeleteTest(context.TODO(), f.t1) - testsDB.DeleteTest(context.TODO(), f.t2) + testRepository.Delete(context.TODO(), f.t1.ID) + testRepository.Delete(context.TODO(), f.t2.ID) actual, err := transactionRepo.Get(context.TODO(), transactionSample.ID) assert.NoError(t, err) @@ -191,10 +193,7 @@ func TestTransactions(t *testing.T) { ResourceTypeSingular: transaction.TransactionResourceName, ResourceTypePlural: transaction.TransactionResourceNamePlural, RegisterManagerFn: func(router *mux.Router, db *sql.DB) resourcemanager.Manager { - testsDB, err := testdb.Postgres(testdb.WithDB(db)) - if err != nil { - panic(err) - } + testsDB := test.NewRepository(db) transactionsRepo := transaction.NewRepository(db, testsDB) manager := resourcemanager.New[transaction.Transaction]( @@ -209,12 +208,9 @@ func TestTransactions(t *testing.T) { }, Prepare: func(t *testing.T, op rmtests.Operation, manager resourcemanager.Manager) { transactionRepo := manager.Handler().(*transaction.Repository) - testsDB, err := testdb.Postgres(testdb.WithDB(transactionRepo.DB())) - runRepo := transaction.NewRunRepository(transactionRepo.DB(), testsDB) + runRepository := test.NewRunRepository(transactionRepo.DB()) + runRepo := transaction.NewRunRepository(transactionRepo.DB(), runRepository) - if err != nil { - panic(err) - } switch op { case rmtests.OperationGetSuccess, rmtests.OperationUpdateSuccess, @@ -228,7 +224,7 @@ func TestTransactions(t *testing.T) { run := setupTests(t, transactionRepo.DB()) createTransactionRun(transactionRepo, runRepo, transactionSample, run) - run = copyRun(testsDB, run) + run = copyRun(runRepository, run) createTransactionRun(transactionRepo, runRepo, transactionSample, run) case rmtests.OperationListAugmentedSuccess, @@ -279,9 +275,9 @@ func TestTransactions(t *testing.T) { "description": "description", "version": 1, "createdAt": "REMOVEME", - "serviceUnderTest": { - "triggerType": "http", - "http": { + "trigger": { + "type": "http", + "httpRequest": { "url": "http://localhost:3030/hello-instrumented" } }, @@ -317,9 +313,9 @@ func TestTransactions(t *testing.T) { "description": "description", "version": 1, "createdAt": "REMOVEME", - "serviceUnderTest": { - "triggerType": "http", - "http": { + "trigger": { + "type": "http", + "httpRequest": { "url": "http://localhost:3030/hello-instrumented" } }, @@ -398,7 +394,7 @@ func compareJSON(t require.TestingT, operation rmtests.Operation, firstValue, se require.JSONEq(t, expected, actual) } -func createTransactionRun(transactionRepo *transaction.Repository, runRepo *transaction.RunRepository, tran transaction.Transaction, run model.Run) { +func createTransactionRun(transactionRepo *transaction.Repository, runRepo *transaction.RunRepository, tran transaction.Transaction, run test.Run) { updated, err := transactionRepo.GetAugmented(context.TODO(), tran.ID) if err != nil { panic(err) @@ -408,7 +404,7 @@ func createTransactionRun(transactionRepo *transaction.Repository, runRepo *tran if err != nil { panic(err) } - tr.Steps = []model.Run{run} + tr.Steps = []test.Run{run} err = runRepo.UpdateRun(context.TODO(), tr) if err != nil { diff --git a/server/transaction/transaction_run_entities.go b/server/transaction/transaction_run_entities.go index af17922c71..cfce1f53bd 100644 --- a/server/transaction/transaction_run_entities.go +++ b/server/transaction/transaction_run_entities.go @@ -5,8 +5,8 @@ import ( "time" "github.com/kubeshop/tracetest/server/environment" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" + "github.com/kubeshop/tracetest/server/test" ) type TransactionRun struct { @@ -20,7 +20,7 @@ type TransactionRun struct { // steps StepIDs []int - Steps []model.Run + Steps []test.Run // trigger params State TransactionRunState @@ -31,7 +31,7 @@ type TransactionRun struct { Pass int Fail int - Metadata model.RunMetadata + Metadata test.RunMetadata // environment Environment environment.Environment diff --git a/server/transaction/transaction_run_repository.go b/server/transaction/transaction_run_repository.go index 5933ff4fda..be819c71bc 100644 --- a/server/transaction/transaction_run_repository.go +++ b/server/transaction/transaction_run_repository.go @@ -12,7 +12,7 @@ import ( "github.com/kubeshop/tracetest/server/pkg/id" ) -func NewRunRepository(db *sql.DB, stepsRepository stepsRepository) *RunRepository { +func NewRunRepository(db *sql.DB, stepsRepository transactionStepRunRepository) *RunRepository { return &RunRepository{ db: db, stepsRepository: stepsRepository, @@ -21,7 +21,7 @@ func NewRunRepository(db *sql.DB, stepsRepository stepsRepository) *RunRepositor type RunRepository struct { db *sql.DB - stepsRepository stepsRepository + stepsRepository transactionStepRunRepository } const createTransactionRunQuery = ` @@ -308,7 +308,7 @@ func (td *RunRepository) GetTransactionRun(ctx context.Context, transactionID id if err != nil { return TransactionRun{}, err } - run.Steps, err = td.stepsRepository.GetTransactionRunSteps(ctx, run) + run.Steps, err = td.stepsRepository.GetTransactionRunSteps(ctx, run.TransactionID, run.ID) if err != nil { return TransactionRun{}, err } @@ -325,7 +325,7 @@ func (td *RunRepository) GetLatestRunByTransactionVersion(ctx context.Context, t if err != nil { return TransactionRun{}, err } - run.Steps, err = td.stepsRepository.GetTransactionRunSteps(ctx, run) + run.Steps, err = td.stepsRepository.GetTransactionRunSteps(ctx, run.TransactionID, run.ID) if err != nil { return TransactionRun{}, err } diff --git a/server/transaction/transaction_run_repository_test.go b/server/transaction/transaction_run_repository_test.go index 5e6e0babe4..c2fe148ce0 100644 --- a/server/transaction/transaction_run_repository_test.go +++ b/server/transaction/transaction_run_repository_test.go @@ -4,51 +4,48 @@ import ( "context" "testing" - "github.com/kubeshop/tracetest/server/model" "github.com/kubeshop/tracetest/server/pkg/id" - "github.com/kubeshop/tracetest/server/testdb" + "github.com/kubeshop/tracetest/server/test" + "github.com/kubeshop/tracetest/server/test/trigger" "github.com/kubeshop/tracetest/server/testmock" "github.com/kubeshop/tracetest/server/transaction" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func createTestWithName(t *testing.T, db model.TestRepository, name string) model.Test { +func createTestWithName(t *testing.T, db test.Repository, name string) test.Test { t.Helper() - test := model.Test{ + test := test.Test{ Name: name, Description: "description", - ServiceUnderTest: model.Trigger{ - Type: model.TriggerTypeHTTP, - HTTP: &model.HTTPRequest{ + Trigger: trigger.Trigger{ + Type: trigger.TriggerTypeHTTP, + HTTP: &trigger.HTTPRequest{ URL: "http://localhost:3030/hello-instrumented", }, }, } - updated, err := db.CreateTest(context.TODO(), test) + updated, err := db.Create(context.TODO(), test) if err != nil { panic(err) } return updated } -func getRepos() (*transaction.Repository, *transaction.RunRepository, model.Repository) { - db := testmock.GetRawTestingDatabase() +func getRepos() (*transaction.Repository, *transaction.RunRepository, test.Repository) { + db := testmock.CreateMigratedDatabase() - testsRepo, err := testdb.Postgres(testdb.WithDB(db)) - if err != nil { - panic(err) - } - - transactionRepo := transaction.NewRepository(db, testsRepo) + testRepo := test.NewRepository(db) + testRunRepo := test.NewRunRepository(db) - runRepo := transaction.NewRunRepository(db, testsRepo) + transactionRepo := transaction.NewRepository(db, testRepo) + runRepo := transaction.NewRunRepository(db, testRunRepo) - return transactionRepo, runRepo, testsRepo + return transactionRepo, runRepo, testRepo } -func getTransaction(t *testing.T, transactionRepo *transaction.Repository, testsRepo model.TestRepository) (transaction.Transaction, transactionFixture) { +func getTransaction(t *testing.T, transactionRepo *transaction.Repository, testsRepo test.Repository) (transaction.Transaction, transactionFixture) { f := setupTransactionFixture(t, transactionRepo.DB()) transaction := transaction.Transaction{ @@ -90,7 +87,7 @@ func TestUpdateTransactionRun(t *testing.T) { require.NoError(t, err) tr.State = transaction.TransactionRunStateExecuting - tr.Steps = []model.Run{fixture.testRun} + tr.Steps = []test.Run{fixture.testRun} err = transactionRunRepo.UpdateRun(context.TODO(), tr) require.NoError(t, err) @@ -138,7 +135,7 @@ func TestListTransactionRun(t *testing.T) { t1 := createTransaction(t, transactionRepo, transaction.Transaction{ Name: "first test", Description: "description", - Steps: []model.Test{ + Steps: []test.Test{ createTestWithName(t, testsRepo, "first step"), createTestWithName(t, testsRepo, "second step"), }, @@ -147,7 +144,7 @@ func TestListTransactionRun(t *testing.T) { t2 := createTransaction(t, transactionRepo, transaction.Transaction{ Name: "second transaction", Description: "description", - Steps: []model.Test{ + Steps: []test.Test{ createTestWithName(t, testsRepo, "first step"), createTestWithName(t, testsRepo, "second step"), }, @@ -178,7 +175,7 @@ func TestBug(t *testing.T) { transaction := createTransaction(t, transactionRepo, transaction.Transaction{ Name: "first test", Description: "description", - Steps: []model.Test{ + Steps: []test.Test{ createTestWithName(t, testsRepo, "first step"), createTestWithName(t, testsRepo, "second step"), }, diff --git a/testing/cli-e2etest/README.md b/testing/cli-e2etest/README.md index 3889bc0d21..ae0e4bcd05 100644 --- a/testing/cli-e2etest/README.md +++ b/testing/cli-e2etest/README.md @@ -30,7 +30,6 @@ The main idea is to test every CLI command against the Tracetest server with dif | CLI Command | Test scenarios | | ------------------------------------------------------------------ | -------------- | -| `test list` | | | `test run -d [test-definition]` | [RunTestWithGrpcTrigger](./testscenarios/test/run_test_with_grpc_trigger_test.go) | | `test run -d [test-definition] -e [environment-id]` | [RunTestWithHttpTriggerAndEnvironmentFile](./testscenarios/test/run_test_with_http_trigger_and_environment_file_test.go) | | `test run -d [test-definition] -e [environment-definition]` | [RunTestWithHttpTriggerAndEnvironmentFile](./testscenarios/test/run_test_with_http_trigger_and_environment_file_test.go) | @@ -148,16 +147,16 @@ The main idea is to test every CLI command against the Tracetest server with dif | CLI Command | Test scenarios | | ----------------------------------------------------------- | -------------- | -| `apply test -f [new-test-file]` | | -| `apply test -f [existing-test-file]` | | -| `delete test --id [existing-id]` | | -| `delete test --id [non-existing-id]` | | -| `get test --id [non-existing-id]` | | -| `get test --id [existing-id] --output pretty` | | -| `get test --id [existing-id] --output json` | | -| `get test --id [existing-id] --output yaml` | | -| `list test --output pretty` | | -| `list test --output json` | | -| `list test --output yaml` | | -| `list test --skip 1 --take 2` | | -| `list test --sortBy name --sortDirection asc` | | +| `apply test -f [new-test-file]` | [ApplyTest](./testscenarios/test/apply_test_test.go) | +| `apply test -f [existing-test-file]` | [ApplyTest](./testscenarios/test/apply_test_test.go) | +| `delete test --id [existing-id]` | [DeleteTest](./testscenarios/test/delete_test_test.go) | +| `delete test --id [non-existing-id]` | [DeleteTest](./testscenarios/test/delete_test_test.go) | +| `get test --id [non-existing-id]` | [GetTest](./testscenarios/test/get_test_test.go), [DeleteTest](./testscenarios/test/delete_test_test.go) | +| `get test --id [existing-id] --output pretty` | [GetTest](./testscenarios/test/get_test_test.go) | +| `get test --id [existing-id] --output json` | [GetTest](./testscenarios/test/get_test_test.go) | +| `get test --id [existing-id] --output yaml` | [GetTest](./testscenarios/test/get_test_test.go) | +| `list test --output pretty` | [ListTest](./testscenarios/test/list_test_test.go) | +| `list test --output json` | [ListTest](./testscenarios/test/list_test_test.go) | +| `list test --output yaml` | [ListTest](./testscenarios/test/list_test_test.go) | +| `list test --skip 1 --take 2` | [ListTest](./testscenarios/test/list_test_test.go) | +| `list test --sortBy name --sortDirection asc` | [ListTest](./testscenarios/test/list_test_test.go) | diff --git a/testing/cli-e2etest/testscenarios/test/apply_test_test.go b/testing/cli-e2etest/testscenarios/test/apply_test_test.go new file mode 100644 index 0000000000..4f196b1edd --- /dev/null +++ b/testing/cli-e2etest/testscenarios/test/apply_test_test.go @@ -0,0 +1,52 @@ +package test + +import ( + "fmt" + "testing" + + "atomicgo.dev/assert" + "github.com/kubeshop/tracetest/cli-e2etest/environment" + "github.com/kubeshop/tracetest/cli-e2etest/helpers" + "github.com/kubeshop/tracetest/cli-e2etest/testscenarios/types" + "github.com/kubeshop/tracetest/cli-e2etest/tracetestcli" + "github.com/stretchr/testify/require" +) + +func TestApplyTest(t *testing.T) { + // instantiate require with testing helper + require := require.New(t) + + // setup isolated e2e environment + env := environment.CreateAndStart(t) + defer env.Close(t) + + cliConfig := env.GetCLIConfigPath(t) + + // Given I am a Tracetest CLI user + // And I have my server recently created + + // When I try to set up a new test + // Then it should be applied with success + testPath := env.GetTestResourcePath(t, "list") + + result := tracetestcli.Exec(t, fmt.Sprintf("apply test --file %s", testPath), tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + // When I try to get a test + // Then it should return the test applied on the last step + result = tracetestcli.Exec(t, "get test --id fH_8AulVR", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + listTest := helpers.UnmarshalYAML[types.TestResource](t, result.StdOut) + assert.Equal("Test", listTest.Type) + assert.Equal("fH_8AulVR", listTest.Spec.ID) + assert.Equal("Pokeshop - List", listTest.Spec.Name) + assert.Equal("List Pokemon", listTest.Spec.Description) + assert.Equal("http", listTest.Spec.Trigger.Type) + assert.Equal("http://demo-api:8081/pokemon?take=20&skip=0", listTest.Spec.Trigger.HTTPRequest.URL) + assert.Equal("GET", listTest.Spec.Trigger.HTTPRequest.Method) + assert.Equal("", listTest.Spec.Trigger.HTTPRequest.Body) + require.Len(listTest.Spec.Trigger.HTTPRequest.Headers, 1) + assert.Equal("Content-Type", listTest.Spec.Trigger.HTTPRequest.Headers[0].Key) + assert.Equal("application/json", listTest.Spec.Trigger.HTTPRequest.Headers[0].Value) +} diff --git a/testing/cli-e2etest/testscenarios/test/delete_test_test.go b/testing/cli-e2etest/testscenarios/test/delete_test_test.go new file mode 100644 index 0000000000..697deebfd8 --- /dev/null +++ b/testing/cli-e2etest/testscenarios/test/delete_test_test.go @@ -0,0 +1,55 @@ +package test + +import ( + "fmt" + "testing" + + "github.com/kubeshop/tracetest/cli-e2etest/environment" + "github.com/kubeshop/tracetest/cli-e2etest/helpers" + "github.com/kubeshop/tracetest/cli-e2etest/testscenarios/types" + "github.com/kubeshop/tracetest/cli-e2etest/tracetestcli" + "github.com/stretchr/testify/require" +) + +func TestDeleteTest(t *testing.T) { + // instantiate require with testing helper + require := require.New(t) + + // setup isolated e2e environment + env := environment.CreateAndStart(t) + defer env.Close(t) + + cliConfig := env.GetCLIConfigPath(t) + + // Given I am a Tracetest CLI user + // And I have my server recently created + + // When I try to delete an test that don't exist + // Then it should return an error and say that this resource does not exist + result := tracetestcli.Exec(t, "delete test --id .env", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 1) + require.Contains(result.StdErr, "Resource test with ID .env not found") + + // When I try to set up a new test + // Then it should be applied with success + newTestPath := env.GetTestResourcePath(t, "list") + + result = tracetestcli.Exec(t, fmt.Sprintf("apply test --file %s", newTestPath), tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + testVars := helpers.UnmarshalYAML[types.TestResource](t, result.StdOut) + require.Equal("Test", testVars.Type) + require.Equal("fH_8AulVR", testVars.Spec.ID) + + // When I try to delete the test + // Then it should delete with success + result = tracetestcli.Exec(t, "delete test --id fH_8AulVR", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + require.Contains(result.StdOut, "✔ Test successfully deleted") + + // When I try to get an test again + // Then it should return a message saying that the test was not found + result = tracetestcli.Exec(t, "get test --id fH_8AulVR", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + require.Contains(result.StdOut, "Resource test with ID fH_8AulVR not found") +} diff --git a/testing/cli-e2etest/testscenarios/test/get_test_test.go b/testing/cli-e2etest/testscenarios/test/get_test_test.go new file mode 100644 index 0000000000..3bdfde0fc2 --- /dev/null +++ b/testing/cli-e2etest/testscenarios/test/get_test_test.go @@ -0,0 +1,125 @@ +package test + +import ( + "fmt" + "testing" + + "atomicgo.dev/assert" + "github.com/kubeshop/tracetest/cli-e2etest/environment" + "github.com/kubeshop/tracetest/cli-e2etest/helpers" + "github.com/kubeshop/tracetest/cli-e2etest/testscenarios/types" + "github.com/kubeshop/tracetest/cli-e2etest/tracetestcli" + "github.com/stretchr/testify/require" +) + +func addGetTestPreReqs(t *testing.T, env environment.Manager) { + cliConfig := env.GetCLIConfigPath(t) + + // Given I am a Tracetest CLI user + // And I have my server recently created + + // When I try to set up a new test + // Then it should be applied with success + newTestPath := env.GetTestResourcePath(t, "list") + + result := tracetestcli.Exec(t, fmt.Sprintf("apply test --file %s", newTestPath), tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) +} + +func TestGetTest(t *testing.T) { + // instantiate require with testing helper + require := require.New(t) + + env := environment.CreateAndStart(t) + defer env.Close(t) + + cliConfig := env.GetCLIConfigPath(t) + + t.Run("get with no test initialized", func(t *testing.T) { + // Given I am a Tracetest CLI user + // And I have my server recently created + // And no test registered + + // When I try to get a test on yaml mode + // Then it should return a error message + result := tracetestcli.Exec(t, "get test --id fH_8AulVR --output yaml", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + require.Contains(result.StdOut, "Resource test with ID fH_8AulVR not found") + }) + + addGetTestPreReqs(t, env) + + t.Run("get with YAML format", func(t *testing.T) { + // Given I am a Tracetest CLI user + // And I have my server recently created + // And I have an test already set + + // When I try to get an test on yaml mode + // Then it should print a YAML + result := tracetestcli.Exec(t, "get test --id fH_8AulVR --output yaml", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + listTest := helpers.UnmarshalYAML[types.TestResource](t, result.StdOut) + assert.Equal("Test", listTest.Type) + assert.Equal("fH_8AulVR", listTest.Spec.ID) + assert.Equal("Pokeshop - List", listTest.Spec.Name) + assert.Equal("List Pokemon", listTest.Spec.Description) + assert.Equal("http", listTest.Spec.Trigger.Type) + assert.Equal("http://demo-api:8081/pokemon?take=20&skip=0", listTest.Spec.Trigger.HTTPRequest.URL) + assert.Equal("GET", listTest.Spec.Trigger.HTTPRequest.Method) + assert.Equal("", listTest.Spec.Trigger.HTTPRequest.Body) + require.Len(listTest.Spec.Trigger.HTTPRequest.Headers, 1) + assert.Equal("Content-Type", listTest.Spec.Trigger.HTTPRequest.Headers[0].Key) + assert.Equal("application/json", listTest.Spec.Trigger.HTTPRequest.Headers[0].Value) + }) + + t.Run("get with JSON format", func(t *testing.T) { + // Given I am a Tracetest CLI user + // And I have my server recently created + // And I have an test already set + + // When I try to get an test on json mode + // Then it should print a json + result := tracetestcli.Exec(t, "get test --id fH_8AulVR --output json", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + listTest := helpers.UnmarshalJSON[types.TestResource](t, result.StdOut) + assert.Equal("Test", listTest.Type) + assert.Equal("fH_8AulVR", listTest.Spec.ID) + assert.Equal("Pokeshop - List", listTest.Spec.Name) + assert.Equal("List Pokemon", listTest.Spec.Description) + assert.Equal("http", listTest.Spec.Trigger.Type) + assert.Equal("http://demo-api:8081/pokemon?take=20&skip=0", listTest.Spec.Trigger.HTTPRequest.URL) + assert.Equal("GET", listTest.Spec.Trigger.HTTPRequest.Method) + assert.Equal("", listTest.Spec.Trigger.HTTPRequest.Body) + require.Len(listTest.Spec.Trigger.HTTPRequest.Headers, 1) + assert.Equal("Content-Type", listTest.Spec.Trigger.HTTPRequest.Headers[0].Key) + assert.Equal("application/json", listTest.Spec.Trigger.HTTPRequest.Headers[0].Value) + }) + + t.Run("get with pretty format", func(t *testing.T) { + // Given I am a Tracetest CLI user + // And I have my server recently created + // And I have an test already set + + // When I try to get an test on pretty mode + // Then it should print a table with 4 lines printed: header, separator, test item and empty line + result := tracetestcli.Exec(t, "get test --id fH_8AulVR --output pretty", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + parsedTable := helpers.UnmarshalTable(t, result.StdOut) + require.Len(parsedTable, 1) + + singleLine := parsedTable[0] + + require.Equal("fH_8AulVR", singleLine["ID"]) + require.Equal("Pokeshop - List", singleLine["NAME"]) + require.Equal("1", singleLine["VERSION"]) + require.Equal("http", singleLine["TRIGGER TYPE"]) + require.Equal("0", singleLine["RUNS"]) + require.Equal("", singleLine["LAST RUN TIME"]) + require.Equal("0", singleLine["LAST RUN SUCCESSES"]) + require.Equal("0", singleLine["LAST RUN FAILURES"]) + require.Equal("http://localhost:11633/test/fH_8AulVR", singleLine["URL"]) + }) +} diff --git a/testing/cli-e2etest/testscenarios/test/list_test_test.go b/testing/cli-e2etest/testscenarios/test/list_test_test.go new file mode 100644 index 0000000000..0984d885fc --- /dev/null +++ b/testing/cli-e2etest/testscenarios/test/list_test_test.go @@ -0,0 +1,211 @@ +package test + +import ( + "fmt" + "testing" + + "github.com/kubeshop/tracetest/cli-e2etest/environment" + "github.com/kubeshop/tracetest/cli-e2etest/helpers" + "github.com/kubeshop/tracetest/cli-e2etest/testscenarios/types" + "github.com/kubeshop/tracetest/cli-e2etest/tracetestcli" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func addListTestsPreReqs(t *testing.T, env environment.Manager) { + cliConfig := env.GetCLIConfigPath(t) + + // Given I am a Tracetest CLI user + // And I have my server recently created + + // When I try to set up a new test + // Then it should be applied with success + newTestPath := env.GetTestResourcePath(t, "list") + + result := tracetestcli.Exec(t, fmt.Sprintf("apply test --file %s", newTestPath), tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + // When I try to set up a another test + // Then it should be applied with success + anotherTestPath := env.GetTestResourcePath(t, "import") + + result = tracetestcli.Exec(t, fmt.Sprintf("apply test --file %s", anotherTestPath), tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) +} + +func TestListTests(t *testing.T) { + // instantiate require with testing helper + require := require.New(t) + assert := assert.New(t) + + // setup isolated e2e test + env := environment.CreateAndStart(t) + defer env.Close(t) + + cliConfig := env.GetCLIConfigPath(t) + + t.Run("list no tests", func(t *testing.T) { + // Given I am a Tracetest CLI user + // And I have my server recently created + // And there is no envs + result := tracetestcli.Exec(t, "list test --sortBy name --sortDirection asc --output yaml", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + testVarsList := helpers.UnmarshalYAMLSequence[types.TestResource](t, result.StdOut) + require.Len(testVarsList, 0) + }) + + addListTestsPreReqs(t, env) + + t.Run("list with invalid sortBy field", func(t *testing.T) { + // Given I am a Tracetest CLI user + // And I have my server recently created + + // When I try to list these tests by an invalid field + // Then I should receive an error + result := tracetestcli.Exec(t, "list test --sortBy id --output yaml", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 1) + require.Contains(result.StdErr, "invalid sort field: id") // TODO: think on how to improve this error handling + }) + + t.Run("list with YAML format", func(t *testing.T) { + // Given I am a Tracetest CLI user + // And I have 2 existing tests + + // When I try to list these tests by a valid field and in YAML format + // Then I should receive 2 tests + result := tracetestcli.Exec(t, "list test --sortBy name --sortDirection desc --output yaml", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + testVarsList := helpers.UnmarshalYAMLSequence[types.TestResource](t, result.StdOut) + require.Len(testVarsList, 2) + + listTest := testVarsList[0] + assert.Equal("Test", listTest.Type) + assert.Equal("fH_8AulVR", listTest.Spec.ID) + assert.Equal("Pokeshop - List", listTest.Spec.Name) + assert.Equal("List Pokemon", listTest.Spec.Description) + assert.Equal("http", listTest.Spec.Trigger.Type) + assert.Equal("http://demo-api:8081/pokemon?take=20&skip=0", listTest.Spec.Trigger.HTTPRequest.URL) + assert.Equal("GET", listTest.Spec.Trigger.HTTPRequest.Method) + assert.Equal("", listTest.Spec.Trigger.HTTPRequest.Body) + require.Len(listTest.Spec.Trigger.HTTPRequest.Headers, 1) + assert.Equal("Content-Type", listTest.Spec.Trigger.HTTPRequest.Headers[0].Key) + assert.Equal("application/json", listTest.Spec.Trigger.HTTPRequest.Headers[0].Value) + + importTest := testVarsList[1] + assert.Equal("Test", importTest.Type) + assert.Equal("RXrbV__4g", importTest.Spec.ID) + assert.Equal("Pokeshop - Import", importTest.Spec.Name) + assert.Equal("Import a Pokemon", importTest.Spec.Description) + assert.Equal("http", importTest.Spec.Trigger.Type) + assert.Equal("http://demo-api:8081/pokemon/import", importTest.Spec.Trigger.HTTPRequest.URL) + assert.Equal("POST", importTest.Spec.Trigger.HTTPRequest.Method) + assert.Equal(`{"id":52}`, importTest.Spec.Trigger.HTTPRequest.Body) + require.Len(importTest.Spec.Trigger.HTTPRequest.Headers, 1) + assert.Equal("Content-Type", importTest.Spec.Trigger.HTTPRequest.Headers[0].Key) + assert.Equal("application/json", importTest.Spec.Trigger.HTTPRequest.Headers[0].Value) + }) + + t.Run("list with JSON format", func(t *testing.T) { + // Given I am a Tracetest CLI user + // And I have my server recently created + + // When I try to list these tests by a valid field and in JSON format + // Then I should receive three tests + result := tracetestcli.Exec(t, "list test --sortBy name --sortDirection asc --output json", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + testVarsList := helpers.UnmarshalJSON[types.ResourceList[types.TestResource]](t, result.StdOut) + require.Len(testVarsList.Items, 2) + require.Equal(len(testVarsList.Items), testVarsList.Count) + + importTest := testVarsList.Items[0] + assert.Equal("Test", importTest.Type) + assert.Equal("RXrbV__4g", importTest.Spec.ID) + assert.Equal("Pokeshop - Import", importTest.Spec.Name) + assert.Equal("Import a Pokemon", importTest.Spec.Description) + assert.Equal("http", importTest.Spec.Trigger.Type) + assert.Equal("http://demo-api:8081/pokemon/import", importTest.Spec.Trigger.HTTPRequest.URL) + assert.Equal("POST", importTest.Spec.Trigger.HTTPRequest.Method) + assert.Equal(`{"id":52}`, importTest.Spec.Trigger.HTTPRequest.Body) + require.Len(importTest.Spec.Trigger.HTTPRequest.Headers, 1) + assert.Equal("Content-Type", importTest.Spec.Trigger.HTTPRequest.Headers[0].Key) + assert.Equal("application/json", importTest.Spec.Trigger.HTTPRequest.Headers[0].Value) + + listTest := testVarsList.Items[1] + assert.Equal("Test", listTest.Type) + assert.Equal("fH_8AulVR", listTest.Spec.ID) + assert.Equal("Pokeshop - List", listTest.Spec.Name) + assert.Equal("List Pokemon", listTest.Spec.Description) + assert.Equal("http", listTest.Spec.Trigger.Type) + assert.Equal("http://demo-api:8081/pokemon?take=20&skip=0", listTest.Spec.Trigger.HTTPRequest.URL) + assert.Equal("GET", listTest.Spec.Trigger.HTTPRequest.Method) + assert.Equal("", listTest.Spec.Trigger.HTTPRequest.Body) + require.Len(listTest.Spec.Trigger.HTTPRequest.Headers, 1) + assert.Equal("Content-Type", listTest.Spec.Trigger.HTTPRequest.Headers[0].Key) + assert.Equal("application/json", listTest.Spec.Trigger.HTTPRequest.Headers[0].Value) + }) + + t.Run("list with pretty format", func(t *testing.T) { + // Given I am a Tracetest CLI user + // And I have my server recently created + + // When I try to list these tests by a valid field and in pretty format + // Then it should print a table with 6 lines printed: header, separator, three envs and empty line + result := tracetestcli.Exec(t, "list test --sortBy name --sortDirection asc --output pretty", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + parsedTable := helpers.UnmarshalTable(t, result.StdOut) + require.Len(parsedTable, 2) + + line := parsedTable[0] + require.Equal("RXrbV__4g", line["ID"]) + require.Equal("Pokeshop - Import", line["NAME"]) + require.Equal("1", line["VERSION"]) + require.Equal("http", line["TRIGGER TYPE"]) + require.Equal("0", line["RUNS"]) + require.Equal("", line["LAST RUN TIME"]) + require.Equal("0", line["LAST RUN SUCCESSES"]) + require.Equal("0", line["LAST RUN FAILURES"]) + require.Equal("http://localhost:11633/test/RXrbV__4g", line["URL"]) + + line = parsedTable[1] + require.Equal("fH_8AulVR", line["ID"]) + require.Equal("Pokeshop - List", line["NAME"]) + require.Equal("1", line["VERSION"]) + require.Equal("http", line["TRIGGER TYPE"]) + require.Equal("0", line["RUNS"]) + require.Equal("", line["LAST RUN TIME"]) + require.Equal("0", line["LAST RUN SUCCESSES"]) + require.Equal("0", line["LAST RUN FAILURES"]) + require.Equal("http://localhost:11633/test/fH_8AulVR", line["URL"]) + }) + + t.Run("list with YAML format skipping the first and taking two items", func(t *testing.T) { + // Given I am a Tracetest CLI user + // And I have my server recently created + + // When I try to list these tests by a valid field, paging options and in YAML format + // Then I should receive two tests + result := tracetestcli.Exec(t, "list test --sortBy name --sortDirection desc --skip 1 --take 2 --output json", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + testVarsList := helpers.UnmarshalJSON[types.ResourceList[types.TestResource]](t, result.StdOut) + require.Len(testVarsList.Items, 1) + require.Equal(2, testVarsList.Count) // items is the total number of items in the server + + importTest := testVarsList.Items[0] + assert.Equal("Test", importTest.Type) + assert.Equal("RXrbV__4g", importTest.Spec.ID) + assert.Equal("Pokeshop - Import", importTest.Spec.Name) + assert.Equal("Import a Pokemon", importTest.Spec.Description) + assert.Equal("http", importTest.Spec.Trigger.Type) + assert.Equal("http://demo-api:8081/pokemon/import", importTest.Spec.Trigger.HTTPRequest.URL) + assert.Equal("POST", importTest.Spec.Trigger.HTTPRequest.Method) + assert.Equal(`{"id":52}`, importTest.Spec.Trigger.HTTPRequest.Body) + require.Len(importTest.Spec.Trigger.HTTPRequest.Headers, 1) + assert.Equal("Content-Type", importTest.Spec.Trigger.HTTPRequest.Headers[0].Key) + assert.Equal("application/json", importTest.Spec.Trigger.HTTPRequest.Headers[0].Value) + }) +} diff --git a/testing/cli-e2etest/testscenarios/test/resources/import.yaml b/testing/cli-e2etest/testscenarios/test/resources/import.yaml new file mode 100644 index 0000000000..0bcd61422b --- /dev/null +++ b/testing/cli-e2etest/testscenarios/test/resources/import.yaml @@ -0,0 +1,14 @@ +type: Test +spec: + id: RXrbV__4g + name: Pokeshop - Import + description: Import a Pokemon + trigger: + type: http + httpRequest: + url: http://demo-api:8081/pokemon/import + method: POST + headers: + - key: Content-Type + value: application/json + body: '{"id":52}' diff --git a/testing/cli-e2etest/testscenarios/test/resources/list.yaml b/testing/cli-e2etest/testscenarios/test/resources/list.yaml new file mode 100644 index 0000000000..06fc9ff36c --- /dev/null +++ b/testing/cli-e2etest/testscenarios/test/resources/list.yaml @@ -0,0 +1,13 @@ +type: Test +spec: + id: fH_8AulVR + name: Pokeshop - List + description: List Pokemon + trigger: + type: http + httpRequest: + url: http://demo-api:8081/pokemon?take=20&skip=0 + method: GET + headers: + - key: Content-Type + value: application/json diff --git a/testing/cli-e2etest/testscenarios/types/test.go b/testing/cli-e2etest/testscenarios/types/test.go new file mode 100644 index 0000000000..d892d61b8b --- /dev/null +++ b/testing/cli-e2etest/testscenarios/types/test.go @@ -0,0 +1,84 @@ +package types + +import ( + "time" + + "github.com/kubeshop/tracetest/server/pkg/maps" +) + +type TestResource struct { + Type string `json:"type"` + Spec Test `json:"spec"` +} + +type Test struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Version int `json:"version"` + Trigger Trigger `json:"trigger"` + Specs []TestSpec `json:"specs"` + Outputs maps.Ordered[string, Output] `json:"outputs"` + Summary Summary `json:"summary"` +} + +type Trigger struct { + Type string `json:"type"` + HTTPRequest HTTPRequest `json:"httpRequest"` +} + +type HTTPRequest struct { + Method string `json:"method,omitempty"` + URL string `json:"url"` + Body string `json:"body,omitempty"` + Headers []HTTPHeader `json:"headers,omitempty"` +} + +type HTTPHeader struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type Output struct { + Selector string `json:"selector"` + Value string `json:"value"` +} + +type TestSpec struct { + Selector Selector `json:"selector"` + Name string `json:"name,omitempty"` + Assertions []string `json:"assertions"` +} + +type Selector struct { + Query string `json:"query"` + ParsedSelector SpanSelector `json:"parsedSelector"` +} + +type SpanSelector struct { + Filters []SelectorFilter `json:"filters"` + PseudoClass *SelectorPseudoClass `json:"pseudoClass,omitempty"` + ChildSelector *SpanSelector `json:"childSelector,omitempty"` +} + +type SelectorFilter struct { + Property string `json:"property"` + Operator string `json:"operator"` + Value string `json:"value"` +} + +type SelectorPseudoClass struct { + Name string `json:"name"` + Argument *int32 `json:"argument,omitempty"` +} + +type Summary struct { + Runs int `json:"runs"` + LastRun LastRun `json:"lastRun"` +} + +type LastRun struct { + Time time.Time `json:"time"` + Passes int `json:"passes"` + Fails int `json:"fails"` +} diff --git a/testing/server-tracetesting/features/grpc_test/01_create_grpc_test.yml b/testing/server-tracetesting/features/grpc_test/01_create_grpc_test.yml index 9bb52ec4cd..d384310761 100644 --- a/testing/server-tracetesting/features/grpc_test/01_create_grpc_test.yml +++ b/testing/server-tracetesting/features/grpc_test/01_create_grpc_test.yml @@ -13,30 +13,31 @@ spec: value: application/json body: | { - "name": "gRPC pokemon list", - "serviceUnderTest": { - "triggerType": "grpc", - "grpc": { - "protobufFile": "syntax = \"proto3\";\n\noption java_multiple_files = true;\noption java_outer_classname = \"PokeshopProto\";\noption objc_class_prefix = \"PKS\";\n\npackage pokeshop;\n\nservice Pokeshop {\n rpc getPokemonList (GetPokemonRequest) returns (GetPokemonListResponse) {}\n rpc createPokemon (Pokemon) returns (Pokemon) {}\n rpc importPokemon (ImportPokemonRequest) returns (ImportPokemonRequest) {}\n}\n\nmessage ImportPokemonRequest {\n int32 id = 1;\n optional bool isFixed = 2;\n}\n\nmessage GetPokemonRequest {\n optional int32 skip = 1;\n optional int32 take = 2;\n optional bool isFixed = 3;\n}\n\nmessage GetPokemonListResponse {\n repeated Pokemon items = 1;\n int32 totalCount = 2;\n}\n\nmessage Pokemon {\n optional int32 id = 1;\n string name = 2;\n string type = 3;\n bool isFeatured = 4;\n optional string imageUrl = 5;\n}", - "address": "${env:DEMO_APP_GRPC_URL}", - "method": "pokeshop.Pokeshop.importPokemon", - "request": "{\"id\": 52}" - } - }, - "specs": [ - { - "selectorParsed": { - "query": "span[name = \"queue.synchronizePokemon publish\"]" - }, - "assertions": ["attr:tracetest.selected_spans.count > 0"] - } - ] + "type": "Test", + "spec": { + "name": "gRPC pokemon list", + "trigger": { + "type": "grpc", + "grpc": { + "protobufFile": "syntax = \"proto3\";\n\noption java_multiple_files = true;\noption java_outer_classname = \"PokeshopProto\";\noption objc_class_prefix = \"PKS\";\n\npackage pokeshop;\n\nservice Pokeshop {\n rpc getPokemonList (GetPokemonRequest) returns (GetPokemonListResponse) {}\n rpc createPokemon (Pokemon) returns (Pokemon) {}\n rpc importPokemon (ImportPokemonRequest) returns (ImportPokemonRequest) {}\n}\n\nmessage ImportPokemonRequest {\n int32 id = 1;\n optional bool isFixed = 2;\n}\n\nmessage GetPokemonRequest {\n optional int32 skip = 1;\n optional int32 take = 2;\n optional bool isFixed = 3;\n}\n\nmessage GetPokemonListResponse {\n repeated Pokemon items = 1;\n int32 totalCount = 2;\n}\n\nmessage Pokemon {\n optional int32 id = 1;\n string name = 2;\n string type = 3;\n bool isFeatured = 4;\n optional string imageUrl = 5;\n}", + "address": "${env:DEMO_APP_GRPC_URL}", + "method": "pokeshop.Pokeshop.importPokemon", + "request": "{\"id\": 52}" + } + }, + "specs": [ + { + "selector":"span[name = \"queue.synchronizePokemon publish\"]", + "assertions": ["attr:tracetest.selected_spans.count > 0"] + } + ] + } } specs: - selector: span[name = "Tracetest trigger"] assertions: - attr:tracetest.selected_spans.count = 1 - - attr:tracetest.response.status = 200 + - attr:tracetest.response.status = 201 - selector: span[name="POST /api/tests" tracetest.span.type="http"] assertions: - attr:tracetest.selected_spans.count = 1 @@ -49,4 +50,4 @@ spec: outputs: - name: GRPC_TEST_ID selector: span[name = "Tracetest trigger"] - value: attr:tracetest.response.body | json_path '$.id' + value: attr:tracetest.response.body | json_path '$.spec.id' diff --git a/testing/server-tracetesting/features/grpc_test/02_list_grpc_test.yml b/testing/server-tracetesting/features/grpc_test/02_list_grpc_test.yml index 92f877d8de..555b93f4ff 100644 --- a/testing/server-tracetesting/features/grpc_test/02_list_grpc_test.yml +++ b/testing/server-tracetesting/features/grpc_test/02_list_grpc_test.yml @@ -16,7 +16,7 @@ spec: assertions: - attr:tracetest.selected_spans.count = 1 - attr:tracetest.response.status = 200 - - attr:tracetest.response.body | json_path '$[*].id' contains env:GRPC_TEST_ID # check if the test is listed + - attr:tracetest.response.body | json_path '$.items[*].spec.id' contains env:GRPC_TEST_ID # check if the test is listed - selector: span[name="GET /api/tests" tracetest.span.type="http"] assertions: - attr:tracetest.selected_spans.count = 1 diff --git a/testing/server-tracetesting/features/grpc_test/04_delete_grpc_test.yml b/testing/server-tracetesting/features/grpc_test/04_delete_grpc_test.yml index 0ce4019daa..db1bb12e86 100644 --- a/testing/server-tracetesting/features/grpc_test/04_delete_grpc_test.yml +++ b/testing/server-tracetesting/features/grpc_test/04_delete_grpc_test.yml @@ -17,7 +17,7 @@ spec: assertions: - attr:tracetest.selected_spans.count = 1 - attr:tracetest.response.status = 204 - - selector: span[name="DELETE /api/tests/{testId}" tracetest.span.type="http"] + - selector: span[name="DELETE /api/tests/{id}" tracetest.span.type="http"] assertions: - attr:tracetest.selected_spans.count = 1 - selector: span[name = "exec DELETE"] diff --git a/testing/server-tracetesting/features/grpc_test/05_create_grpc_test_with_invalid_metadata.yml b/testing/server-tracetesting/features/grpc_test/05_create_grpc_test_with_invalid_metadata.yml index 168d4c645e..9a70c6201a 100644 --- a/testing/server-tracetesting/features/grpc_test/05_create_grpc_test_with_invalid_metadata.yml +++ b/testing/server-tracetesting/features/grpc_test/05_create_grpc_test_with_invalid_metadata.yml @@ -13,31 +13,32 @@ spec: value: application/json body: | { - "name": "gRPC pokemon list", - "serviceUnderTest": { - "triggerType": "grpc", - "grpc": { - "protobufFile": "syntax = \"proto3\";\n\noption java_multiple_files = true;\noption java_outer_classname = \"PokeshopProto\";\noption objc_class_prefix = \"PKS\";\n\npackage pokeshop;\n\nservice Pokeshop {\n rpc getPokemonList (GetPokemonRequest) returns (GetPokemonListResponse) {}\n rpc createPokemon (Pokemon) returns (Pokemon) {}\n rpc importPokemon (ImportPokemonRequest) returns (ImportPokemonRequest) {}\n}\n\nmessage ImportPokemonRequest {\n int32 id = 1;\n optional bool isFixed = 2;\n}\n\nmessage GetPokemonRequest {\n optional int32 skip = 1;\n optional int32 take = 2;\n optional bool isFixed = 3;\n}\n\nmessage GetPokemonListResponse {\n repeated Pokemon items = 1;\n int32 totalCount = 2;\n}\n\nmessage Pokemon {\n optional int32 id = 1;\n string name = 2;\n string type = 3;\n bool isFeatured = 4;\n optional string imageUrl = 5;\n}", - "address": "${env:DEMO_APP_GRPC_URL}", - "method": "pokeshop.Pokeshop.importPokemon", - "request": "{\"id\": 52}", - "metadata": [{}] - } - }, - "specs": [ - { - "selectorParsed": { - "query": "span[name = \"queue.synchronizePokemon publish\"]" - }, - "assertions": ["attr:tracetest.selected_spans.count > 0"] - } - ] + "type": "Test", + "spec": { + "name": "gRPC pokemon list", + "trigger": { + "type": "grpc", + "grpc": { + "protobufFile": "syntax = \"proto3\";\n\noption java_multiple_files = true;\noption java_outer_classname = \"PokeshopProto\";\noption objc_class_prefix = \"PKS\";\n\npackage pokeshop;\n\nservice Pokeshop {\n rpc getPokemonList (GetPokemonRequest) returns (GetPokemonListResponse) {}\n rpc createPokemon (Pokemon) returns (Pokemon) {}\n rpc importPokemon (ImportPokemonRequest) returns (ImportPokemonRequest) {}\n}\n\nmessage ImportPokemonRequest {\n int32 id = 1;\n optional bool isFixed = 2;\n}\n\nmessage GetPokemonRequest {\n optional int32 skip = 1;\n optional int32 take = 2;\n optional bool isFixed = 3;\n}\n\nmessage GetPokemonListResponse {\n repeated Pokemon items = 1;\n int32 totalCount = 2;\n}\n\nmessage Pokemon {\n optional int32 id = 1;\n string name = 2;\n string type = 3;\n bool isFeatured = 4;\n optional string imageUrl = 5;\n}", + "address": "${env:DEMO_APP_GRPC_URL}", + "method": "pokeshop.Pokeshop.importPokemon", + "request": "{\"id\": 52}", + "metadata": [{}] + } + }, + "specs": [ + { + "selector": "span[name = \"queue.synchronizePokemon publish\"]", + "assertions": ["attr:tracetest.selected_spans.count > 0"] + } + ] + } } specs: - selector: span[name = "Tracetest trigger"] assertions: - attr:tracetest.selected_spans.count = 1 - - attr:tracetest.response.status = 200 + - attr:tracetest.response.status = 201 - selector: span[name="POST /api/tests" tracetest.span.type="http"] assertions: - attr:tracetest.selected_spans.count = 1 @@ -50,4 +51,4 @@ spec: outputs: - name: GRPC_TEST_INVALID_METADATA_ID selector: span[name = "Tracetest trigger"] - value: attr:tracetest.response.body | json_path '$.id' + value: attr:tracetest.response.body | json_path '$.spec.id' diff --git a/testing/server-tracetesting/features/http_test/01_delete_http_test_with_non_existing_id.yml b/testing/server-tracetesting/features/http_test/01_delete_http_test_with_non_existing_id.yml index f242de989f..65805d70bf 100644 --- a/testing/server-tracetesting/features/http_test/01_delete_http_test_with_non_existing_id.yml +++ b/testing/server-tracetesting/features/http_test/01_delete_http_test_with_non_existing_id.yml @@ -16,7 +16,7 @@ spec: assertions: - attr:tracetest.selected_spans.count = 1 - attr:tracetest.response.status = 404 - - selector: span[name="DELETE /api/tests/{testId}" tracetest.span.type="http"] + - selector: span[name="DELETE /api/tests/{id}" tracetest.span.type="http"] assertions: - attr:tracetest.selected_spans.count = 1 - selector: span[name = "exec DELETE"] diff --git a/testing/server-tracetesting/features/http_test/02_create_http_test_with_non_existing_id.yml b/testing/server-tracetesting/features/http_test/02_create_http_test_with_non_existing_id.yml index 46de98f187..2a8ab5a653 100644 --- a/testing/server-tracetesting/features/http_test/02_create_http_test_with_non_existing_id.yml +++ b/testing/server-tracetesting/features/http_test/02_create_http_test_with_non_existing_id.yml @@ -13,20 +13,23 @@ spec: value: application/json body: | { - "id": "${env:EXAMPLE_TEST_ID}", - "name": "Pokemon - List - Get a Pokemon", - "serviceUnderTest": { - "triggerType": "http", - "http": { - "url": "${env:DEMO_APP_URL}/pokemon?take=20&skip=0", - "method": "GET", - "body": "", - "headers": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] + "type": "Test", + "spec": { + "id": "${env:EXAMPLE_TEST_ID}", + "name": "Pokemon - List - Get a Pokemon", + "trigger": { + "type": "http", + "httpRequest": { + "url": "${env:DEMO_APP_URL}/pokemon?take=20&skip=0", + "method": "GET", + "body": "", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } } } } @@ -34,8 +37,8 @@ spec: - selector: span[name = "Tracetest trigger"] assertions: - attr:tracetest.selected_spans.count = 1 - - attr:tracetest.response.status = 200 - - attr:tracetest.response.body contains '"id":"${env:EXAMPLE_TEST_ID}"' + - attr:tracetest.response.status = 201 + - attr:tracetest.response.body | json_path '$.spec.id' = env:EXAMPLE_TEST_ID - selector: span[name="POST /api/tests" tracetest.span.type="http"] assertions: - attr:tracetest.selected_spans.count = 1 diff --git a/testing/server-tracetesting/features/http_test/03_create_http_test_with_existent_id.yml b/testing/server-tracetesting/features/http_test/03_create_http_test_with_existent_id.yml index 131f9fc6c7..1ce9c18ddf 100644 --- a/testing/server-tracetesting/features/http_test/03_create_http_test_with_existent_id.yml +++ b/testing/server-tracetesting/features/http_test/03_create_http_test_with_existent_id.yml @@ -13,20 +13,23 @@ spec: value: application/json body: | { - "id": "${env:EXAMPLE_TEST_ID}", - "name": "Pokemon - List - Get a Pokemon", - "serviceUnderTest": { - "triggerType": "http", - "http": { - "url": "${env:DEMO_APP_URL}/pokemon?take=20&skip=0", - "method": "GET", - "body": "", - "headers": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] + "type": "Test", + "spec": { + "id": "${env:EXAMPLE_TEST_ID}", + "name": "Pokemon - List - Get a Pokemon", + "trigger": { + "type": "http", + "httpRequest": { + "url": "${env:DEMO_APP_URL}/pokemon?take=20&skip=0", + "method": "GET", + "body": "", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } } } } diff --git a/testing/server-tracetesting/features/http_test/04_delete_http_test_with_existing_id.yml b/testing/server-tracetesting/features/http_test/04_delete_http_test_with_existing_id.yml index b1df8b1d92..498404a53c 100644 --- a/testing/server-tracetesting/features/http_test/04_delete_http_test_with_existing_id.yml +++ b/testing/server-tracetesting/features/http_test/04_delete_http_test_with_existing_id.yml @@ -16,7 +16,7 @@ spec: assertions: - attr:tracetest.selected_spans.count = 1 - attr:tracetest.response.status = 204 - - selector: span[name="DELETE /api/tests/{testId}" tracetest.span.type="http"] + - selector: span[name="DELETE /api/tests/{id}" tracetest.span.type="http"] assertions: - attr:tracetest.selected_spans.count = 1 - selector: span[name = "exec DELETE"] diff --git a/testing/server-tracetesting/features/http_test/05_create_http_test.yml b/testing/server-tracetesting/features/http_test/05_create_http_test.yml index 6aa391af7b..5d6bb6dfe5 100644 --- a/testing/server-tracetesting/features/http_test/05_create_http_test.yml +++ b/testing/server-tracetesting/features/http_test/05_create_http_test.yml @@ -13,43 +13,43 @@ spec: value: application/json body: | { - "name": "Pokemon - List - Get a Pokemon", - "serviceUnderTest": { - "triggerType": "http", - "http": { - "url": "${env:DEMO_APP_URL}/pokemon?take=20&skip=0", - "method": "GET", - "headers": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - "specs": [ - { - "selectorParsed": { - "query": "span[name = \"findMany postgres.pokemon\"]" - }, - "assertions": ["attr:tracetest.selected_spans.count > 0"] - } - ], - "outputs": [ - { - "name": "TRIGGER_COUNT", - "selectorParsed": { - "query": "span[name = \"Tracetest trigger\"]" - }, - "value": "attr:tracetest.selected_spans.count" - } - ] + "type": "Test", + "spec": { + "name": "Pokemon - List - Get a Pokemon", + "trigger": { + "type": "http", + "httpRequest": { + "url": "${env:DEMO_APP_URL}/pokemon?take=20&skip=0", + "method": "GET", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + "specs": [ + { + "selector": "span[name = \"findMany postgres.pokemon\"]", + "assertions": ["attr:tracetest.selected_spans.count > 0"] + } + ], + "outputs": [ + { + "name": "TRIGGER_COUNT", + "selector": "span[name = \"Tracetest trigger\"]", + "value": "attr:tracetest.selected_spans.count" + } + ] + } } specs: - selector: span[name = "Tracetest trigger"] assertions: - attr:tracetest.selected_spans.count = 1 - - attr:tracetest.response.status = 200 + - attr:tracetest.response.status = 201 + - attr:tracetest.response.body | json_path '$.spec.id' = env:HTTP_TEST_ID - selector: span[name="POST /api/tests" tracetest.span.type="http"] assertions: - attr:tracetest.selected_spans.count = 1 @@ -59,13 +59,7 @@ spec: - selector: span[name = "exec INSERT"]:first assertions: - attr:sql.query contains "INSERT INTO tests" - - # ensure we can reference outputs declared in the same test - - selector: span[name = "Tracetest trigger"] - assertions: - - attr:tracetest.response.body | json_path '$.id' = env:HTTP_TEST_ID - outputs: - name: HTTP_TEST_ID selector: span[name = "Tracetest trigger"] - value: attr:tracetest.response.body | json_path '$.id' + value: attr:tracetest.response.body | json_path '$.spec.id' diff --git a/testing/server-tracetesting/features/http_test/06_list_http_test.yml b/testing/server-tracetesting/features/http_test/06_list_http_test.yml index 4bd9f6f9a4..84454e4cf7 100644 --- a/testing/server-tracetesting/features/http_test/06_list_http_test.yml +++ b/testing/server-tracetesting/features/http_test/06_list_http_test.yml @@ -16,7 +16,7 @@ spec: assertions: - attr:tracetest.selected_spans.count = 1 - attr:tracetest.response.status = 200 - - attr:tracetest.response.body | json_path '$[*].id' contains env:HTTP_TEST_ID # check if the test is listed + - attr:tracetest.response.body | json_path '$.items[*].spec.id' contains env:HTTP_TEST_ID # check if the test is listed - selector: span[name="GET /api/tests" tracetest.span.type="http"] assertions: - attr:tracetest.selected_spans.count = 1 diff --git a/testing/server-tracetesting/features/http_test/10_delete_http_test.yml b/testing/server-tracetesting/features/http_test/10_delete_http_test.yml index 8a459493e6..82654c63b6 100644 --- a/testing/server-tracetesting/features/http_test/10_delete_http_test.yml +++ b/testing/server-tracetesting/features/http_test/10_delete_http_test.yml @@ -16,7 +16,7 @@ spec: assertions: - attr:tracetest.selected_spans.count = 1 - attr:tracetest.response.status = 204 - - selector: span[name="DELETE /api/tests/{testId}" tracetest.span.type="http"] + - selector: span[name="DELETE /api/tests/{id}" tracetest.span.type="http"] assertions: - attr:tracetest.selected_spans.count = 1 - selector: span[name = "exec DELETE"] diff --git a/testing/server-tracetesting/features/transaction/create_transaction.yml b/testing/server-tracetesting/features/transaction/create_transaction.yml index 77fd6b1ed2..96a785cc70 100644 --- a/testing/server-tracetesting/features/transaction/create_transaction.yml +++ b/testing/server-tracetesting/features/transaction/create_transaction.yml @@ -16,9 +16,9 @@ spec: { "type": "Transaction", "spec": { - "name": "test-transaction", - "description": "a transaction", - "steps": [ "${env:TRANSACTION_STEP_ID}" ] + "name": "test-transaction", + "description": "a transaction", + "steps": [ "${env:TRANSACTION_STEP_ID}" ] } } specs: diff --git a/testing/server-tracetesting/features/transaction/create_transaction_step.yml b/testing/server-tracetesting/features/transaction/create_transaction_step.yml index 734a71b25f..bb0997b1a0 100644 --- a/testing/server-tracetesting/features/transaction/create_transaction_step.yml +++ b/testing/server-tracetesting/features/transaction/create_transaction_step.yml @@ -13,45 +13,44 @@ spec: value: application/json body: | { - "name": "Pokemon - List - Get a Pokemon", - "serviceUnderTest": { - "triggerType": "http", - "http": { - "url": "${env:DEMO_APP_URL}/pokemon?take=20&skip=0", - "method": "GET", - "headers": [ - { - "key": "Content-Type", - "value": "application/json" - } - ] - } - }, - "specs": [ - { - "selectorParsed": { - "query": "span[name = \"pg.query:SELECT pokeshop\"]" - }, - "assertions": ["attr:tracetest.selected_spans.count > 0"] - } - ], - "outputs": [ - { - "name": "TRIGGER_COUNT", - "selectorParsed": { - "query": "span[name = \"Tracetest trigger\"]" - }, - "value": "attr:tracetest.selected_spans.count" - } - ] + "type": "Test", + "spec": { + "name": "Pokemon - List - Get a Pokemon", + "trigger": { + "type": "http", + "httpRequest": { + "url": "${env:DEMO_APP_URL}/pokemon?take=20&skip=0", + "method": "GET", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + } + ] + } + }, + "specs": [ + { + "selector": "span[name = \"pg.query:SELECT pokeshop\"]", + "assertions": ["attr:tracetest.selected_spans.count > 0"] + } + ], + "outputs": [ + { + "name": "TRIGGER_COUNT", + "selector": "span[name = \"Tracetest trigger\"]", + "value": "attr:tracetest.selected_spans.count" + } + ] + } } specs: - selector: span[name = "Tracetest trigger"] assertions: - attr:tracetest.selected_spans.count = 1 - - attr:tracetest.response.status = 200 + - attr:tracetest.response.status = 201 # ensure we can reference outputs declared in the same test - - attr:tracetest.response.body | json_path '$.id' = env:TRANSACTION_STEP_ID + - attr:tracetest.response.body | json_path '$.spec.id' = env:TRANSACTION_STEP_ID - selector: span[name="POST /api/tests" tracetest.span.type="http"] assertions: - attr:tracetest.selected_spans.count = 1 @@ -65,4 +64,4 @@ spec: outputs: - name: TRANSACTION_STEP_ID selector: span[name = "Tracetest trigger"] - value: attr:tracetest.response.body | json_path '$.id' + value: attr:tracetest.response.body | json_path '$.spec.id' diff --git a/testing/server-tracetesting/features/transaction/delete_transaction_step.yml b/testing/server-tracetesting/features/transaction/delete_transaction_step.yml index 5c34fffc4f..f81e84607f 100644 --- a/testing/server-tracetesting/features/transaction/delete_transaction_step.yml +++ b/testing/server-tracetesting/features/transaction/delete_transaction_step.yml @@ -17,7 +17,7 @@ spec: assertions: - attr:tracetest.selected_spans.count = 1 - attr:tracetest.response.status = 204 - - selector: span[name="DELETE /api/tests/{testId}" tracetest.span.type="http"] + - selector: span[name="DELETE /api/tests/{id}" tracetest.span.type="http"] assertions: - attr:tracetest.selected_spans.count = 1 - selector: span[name = "exec DELETE"] diff --git a/web/cypress/e2e/Transactions/Transactions.spec.ts b/web/cypress/e2e/Transactions/Transactions.spec.ts index 43716a64e8..b93c6ae0fe 100644 --- a/web/cypress/e2e/Transactions/Transactions.spec.ts +++ b/web/cypress/e2e/Transactions/Transactions.spec.ts @@ -22,7 +22,7 @@ describe('Transactions', () => { transactionUtils.testList.forEach(test => { cy.get('[data-cy=transaction-test-selection]').click(); - cy.get(`[data-cy="${test.name}"]`).first().click(); + cy.get(`[data-cy="${test.spec.name}"]`).first().click(); }); cy.submitCreateForm('CreateTransactionFactory'); diff --git a/web/cypress/e2e/Transactions/TransactionsRun.spec.ts b/web/cypress/e2e/Transactions/TransactionsRun.spec.ts index d0c72fbd73..cc7b3eea8a 100644 --- a/web/cypress/e2e/Transactions/TransactionsRun.spec.ts +++ b/web/cypress/e2e/Transactions/TransactionsRun.spec.ts @@ -22,7 +22,7 @@ describe('Transactions', () => { transactionUtils.testList.forEach(test => { cy.get('[data-cy=transaction-test-selection]').click(); - cy.get(`[data-cy="${test.name}"]`).first().click(); + cy.get(`[data-cy="${test.spec.name}"]`).first().click(); }); cy.submitCreateForm('CreateTransactionFactory'); @@ -38,7 +38,7 @@ describe('Transactions', () => { transactionUtils.testList.forEach(test => { cy.get('[data-cy=transaction-test-selection]').click(); - cy.get(`[data-cy="${test.name}"]`).first().click(); + cy.get(`[data-cy="${test.spec.name}"]`).first().click(); }); cy.submitCreateForm('CreateTransactionFactory'); @@ -57,7 +57,7 @@ describe('Transactions', () => { transactionUtils.testList.forEach(test => { cy.get('[data-cy=transaction-test-selection]').click(); - cy.get(`[data-cy="${test.name}"]`).first().click(); + cy.get(`[data-cy="${test.spec.name}"]`).first().click(); }); cy.submitCreateForm('CreateTransactionFactory'); @@ -70,7 +70,7 @@ describe('Transactions', () => { cy.get('[data-cy=create-test-name-input]').type(' - updated'); transactionUtils.testList.forEach(test => { cy.get('[data-cy=transaction-test-selection]').click(); - cy.get(`[data-cy="${test.name}"]`).first().click(); + cy.get(`[data-cy="${test.spec.name}"]`).first().click(); }); cy.get('[data-cy=edit-transaction-submit]').click(); diff --git a/web/cypress/e2e/constants/Transactions.ts b/web/cypress/e2e/constants/Transactions.ts index 40e44403a5..3d791aa768 100644 --- a/web/cypress/e2e/constants/Transactions.ts +++ b/web/cypress/e2e/constants/Transactions.ts @@ -1,27 +1,51 @@ -import {TRawTest} from '../../../src/models/Test.model'; +import {TRawTestResource} from '../../../src/models/Test.model'; import {POKEMON_HTTP_ENDPOINT} from '../constants/Test'; -export const transactionTestList: TRawTest[] = [ +export const transactionTestList: TRawTestResource[] = [ { - name: 'POST test', - description: 'transaction', - serviceUnderTest: { - triggerType: 'http', - http: { - url: `${POKEMON_HTTP_ENDPOINT}/pokemon/import`, - method: 'POST', - body: '{"id": 6}', + type: 'Test', + spec: { + name: 'POST test', + description: 'transaction', + trigger: { + type: 'http', + triggerType: 'http', + http: { + url: `${POKEMON_HTTP_ENDPOINT}/pokemon`, + method: 'GET', + }, + }, + serviceUnderTest: { + type: 'http', + triggerType: 'http', + http: { + url: `${POKEMON_HTTP_ENDPOINT}/pokemon/import`, + method: 'POST', + body: '{"id": 6}', + }, }, }, }, { - name: 'GET test', - description: 'transaction', - serviceUnderTest: { - triggerType: 'http', - http: { - url: `${POKEMON_HTTP_ENDPOINT}/pokemon`, - method: 'GET', + type: 'Test', + spec: { + name: 'GET test', + description: 'transaction', + trigger: { + triggerType: 'http', + type: 'http', + http: { + url: `${POKEMON_HTTP_ENDPOINT}/pokemon`, + method: 'GET', + }, + }, + serviceUnderTest: { + triggerType: 'http', + type: 'http', + http: { + url: `${POKEMON_HTTP_ENDPOINT}/pokemon`, + method: 'GET', + }, }, }, }, diff --git a/web/cypress/e2e/utils/Transactions.ts b/web/cypress/e2e/utils/Transactions.ts index 2fdd8dd57d..d4321267c3 100644 --- a/web/cypress/e2e/utils/Transactions.ts +++ b/web/cypress/e2e/utils/Transactions.ts @@ -1,10 +1,10 @@ -import {TRawTest} from '../../../src/types/Test.types'; +import {TRawTestResource} from '../../../src/models/Test.model'; import {transactionTestList} from '../constants/Transactions'; interface ITransactionUtils { - testList: TRawTest[]; - createTest(test: TRawTest): Promise; - createTests(tests?: TRawTest[]): Promise; + testList: TRawTestResource[]; + createTest(test: TRawTestResource): Promise; + createTests(tests?: TRawTestResource[]): Promise; deleteTest(id: string): Promise; deleteTests(): Promise; waitForTransactionRun(): void; @@ -14,7 +14,7 @@ const TransactionUtils = (): ITransactionUtils => ({ testList: [], createTest(test) { return new Promise(resolve => { - cy.request('POST', '/api/tests', test).then((res: Cypress.Response) => { + cy.request('POST', '/api/tests', test).then((res: Cypress.Response) => { resolve(res.body); }); }); @@ -32,7 +32,7 @@ const TransactionUtils = (): ITransactionUtils => ({ }); }, deleteTests() { - return Promise.all(this.testList.map(test => this.deleteTest(test.id))); + return Promise.all(this.testList.map(test => this.deleteTest(test.spec.id))); }, waitForTransactionRun() { cy.get('[data-cy=transaction-run-result-status]').should('have.text', 'FINISHED', {timeout: 60000}); diff --git a/web/src/gateways/Test.gateway.ts b/web/src/gateways/Test.gateway.ts index 09af45f7eb..675cbe2dc5 100644 --- a/web/src/gateways/Test.gateway.ts +++ b/web/src/gateways/Test.gateway.ts @@ -1,5 +1,5 @@ import {endpoints} from 'redux/apis/TraceTest.api'; -import {TRawTest} from 'models/Test.model'; +import {TRawTestResource} from 'models/Test.model'; const {createTest, editTest, getTestById, getTestList, runTest} = endpoints; @@ -10,13 +10,13 @@ const TestGateway = () => ({ getById(testId: string) { return getTestById.initiate({testId}); }, - create(test: TRawTest) { + create(test: TRawTestResource) { return createTest.initiate(test); }, run(testId: string) { return runTest.initiate({testId}); }, - edit(test: TRawTest, testId: string) { + edit(test: TRawTestResource, testId: string) { return editTest.initiate({test, testId}); }, }); diff --git a/web/src/gateways/__tests__/Test.gateway.test.ts b/web/src/gateways/__tests__/Test.gateway.test.ts index 26f7ca6f7c..a1545ec1b7 100644 --- a/web/src/gateways/__tests__/Test.gateway.test.ts +++ b/web/src/gateways/__tests__/Test.gateway.test.ts @@ -1,3 +1,4 @@ +import {TRawTestResource} from '../../models/Test.model'; import {endpoints} from '../../redux/apis/TraceTest.api'; import TestGateway from '../Test.gateway'; @@ -19,7 +20,7 @@ jest.mock('../../redux/apis/TraceTest.api', () => { describe('TestGateway', () => { it('should execute the create function', async () => { expect.assertions(1); - const test = {name: 'test', description: 'test'}; + const test: TRawTestResource = {type: 'Test', spec: {name: 'test', description: 'test'}}; await TestGateway.create(test); expect(createTest.initiate).toBeCalledWith(test); diff --git a/web/src/hooks/useDefinitionFile.ts b/web/src/hooks/useDefinitionFile.ts index ce8b1fe83a..582c4fe3d1 100644 --- a/web/src/hooks/useDefinitionFile.ts +++ b/web/src/hooks/useDefinitionFile.ts @@ -1,21 +1,18 @@ import {useCallback, useState} from 'react'; -import {useLazyGetResourceDefinitionQuery, useLazyGetResourceDefinitionV2Query} from 'redux/apis/TraceTest.api'; +import {useLazyGetResourceDefinitionQuery} from 'redux/apis/TraceTest.api'; import {ResourceType} from 'types/Resource.type'; const useDefinitionFile = () => { const [definition, setDefinition] = useState(''); const [getResourceDefinition] = useLazyGetResourceDefinitionQuery(); - const [getResourceDefinitionV2] = useLazyGetResourceDefinitionV2Query(); const loadDefinition = useCallback( async (resourceType: ResourceType, resourceId: string, version?: number) => { - const data = await (resourceType === ResourceType.Environment || resourceType === ResourceType.Transaction - ? getResourceDefinitionV2({resourceId, resourceType}).unwrap() - : getResourceDefinition({resourceId, version, resourceType}).unwrap()); + const data = await getResourceDefinition({resourceId, resourceType, version}).unwrap(); setDefinition(data); }, - [getResourceDefinition, getResourceDefinitionV2] + [getResourceDefinition] ); return {definition, loadDefinition}; diff --git a/web/src/models/Resource.model.ts b/web/src/models/Resource.model.ts index 54d18d0c9f..15019ae019 100644 --- a/web/src/models/Resource.model.ts +++ b/web/src/models/Resource.model.ts @@ -11,7 +11,7 @@ function Resource({item, type}: TRawResource): Resource { if (type === ResourceType.Test) { return { type: ResourceType.Test, - item: Test(item as TRawTest), + item: Test.FromRawTest(item as TRawTest), }; } diff --git a/web/src/models/Test.model.ts b/web/src/models/Test.model.ts index f26cb06c31..ac3fed420b 100644 --- a/web/src/models/Test.model.ts +++ b/web/src/models/Test.model.ts @@ -4,6 +4,8 @@ import TestSpecs from './TestSpecs.model'; import Summary from './Summary.model'; import Trigger from './Trigger.model'; +export type TRawTestResource = TTestSchemas['TestResource']; +export type TRawTestResourceList = TTestSchemas['TestResourceList']; export type TRawTest = TTestSchemas['Test']; type Test = Model< TRawTest, @@ -18,13 +20,15 @@ type Test = Model< } >; -const Test = ({ +const Test = ({spec: rawTest = {}}: TRawTestResource): Test => Test.FromRawTest(rawTest); + +Test.FromRawTest = ({ id = '', name = '', description = '', specs = [], version = 1, - serviceUnderTest: rawTrigger, + trigger: rawTrigger, summary = {}, outputs = [], createdAt = '', diff --git a/web/src/models/Transaction.model.ts b/web/src/models/Transaction.model.ts index 37502718d4..c52b387129 100644 --- a/web/src/models/Transaction.model.ts +++ b/web/src/models/Transaction.model.ts @@ -13,9 +13,7 @@ type Transaction = Model< } >; -function Transaction({ - spec: rawTransaction = {}, -}: TRawTransactionResource): Transaction { +function Transaction({spec: rawTransaction = {}}: TRawTransactionResource): Transaction { return Transaction.FromRawTransaction(rawTransaction); } @@ -35,7 +33,7 @@ Transaction.FromRawTransaction = ({ description, version, steps, - fullSteps: fullSteps.map(step => Test(step)), + fullSteps: fullSteps.map(step => Test.FromRawTest(step)), createdAt, summary: Summary(summary), }; diff --git a/web/src/models/Trigger.model.ts b/web/src/models/Trigger.model.ts index c711de92ab..540f99fca5 100644 --- a/web/src/models/Trigger.model.ts +++ b/web/src/models/Trigger.model.ts @@ -35,12 +35,12 @@ const EntryData = { }, }; -const Trigger = ({triggerType = 'http', http = {}, grpc = {}, traceid = {}}: TRawTrigger): Trigger => { - const type = triggerType as TriggerTypes; +const Trigger = ({type: rawType = 'http', httpRequest = {}, grpc = {}, traceid = {}}: TRawTrigger): Trigger => { + const type = rawType as TriggerTypes; let request = {} as TTriggerRequest; if (type === TriggerTypes.http) { - request = HttpRequest(http); + request = HttpRequest(httpRequest); } else if (type === TriggerTypes.grpc) { request = GrpcRequest(grpc); } else if (type === TriggerTypes.traceid) { diff --git a/web/src/models/TriggerResult.model.ts b/web/src/models/TriggerResult.model.ts index 626a9f4262..4881c8cf34 100644 --- a/web/src/models/TriggerResult.model.ts +++ b/web/src/models/TriggerResult.model.ts @@ -37,10 +37,10 @@ const ResponseData = { }; const TriggerResult = ({ - triggerType = 'http', + type: rawType = 'http', triggerResult: {http = {}, grpc = {statusCode: 0}, traceid = {}} = {}, }: TRawTriggerResult): TriggerResult => { - const type = triggerType as TriggerTypes; + const type = rawType as TriggerTypes; let request = {}; if (type === TriggerTypes.http) { diff --git a/web/src/models/__mocks__/Test.mock.ts b/web/src/models/__mocks__/Test.mock.ts index 22305d79cd..534c73976f 100644 --- a/web/src/models/__mocks__/Test.mock.ts +++ b/web/src/models/__mocks__/Test.mock.ts @@ -1,23 +1,26 @@ import faker from '@faker-js/faker'; import {IMockFactory} from 'types/Common.types'; -import Test, {TRawTest} from '../Test.model'; +import Test, {TRawTestResource} from '../Test.model'; import AssertionResultMock from './AssertionResult.mock'; -const TestMock: IMockFactory = () => ({ +const TestMock: IMockFactory = () => ({ raw(data = {}) { return { - id: faker.datatype.uuid(), - name: faker.name.firstName(), - version: faker.datatype.number(), - definition: { - definitions: [ - { - selector: {query: faker.random.word()}, - assertions: [AssertionResultMock.raw()], - }, - ], + type: 'Test', + spec: { + id: faker.datatype.uuid(), + name: faker.name.firstName(), + version: faker.datatype.number(), + definition: { + definitions: [ + { + selector: {query: faker.random.word()}, + assertions: [AssertionResultMock.raw()], + }, + ], + }, + ...data, }, - ...data, }; }, model(data = {}) { diff --git a/web/src/redux/apis/TraceTest.api.ts b/web/src/redux/apis/TraceTest.api.ts index c2e52af616..eef72bc3a8 100644 --- a/web/src/redux/apis/TraceTest.api.ts +++ b/web/src/redux/apis/TraceTest.api.ts @@ -86,7 +86,6 @@ export const { useGetLinterQuery, useCreateSettingMutation, useUpdateSettingMutation, - useLazyGetResourceDefinitionV2Query, } = TraceTestAPI; export const {endpoints} = TraceTestAPI; diff --git a/web/src/redux/apis/__tests__/useCreateTestMutation.tesx.tsx b/web/src/redux/apis/__tests__/useCreateTestMutation.tesx.tsx index c015870abe..af3426e000 100644 --- a/web/src/redux/apis/__tests__/useCreateTestMutation.tesx.tsx +++ b/web/src/redux/apis/__tests__/useCreateTestMutation.tesx.tsx @@ -1,12 +1,12 @@ import {act, renderHook} from '@testing-library/react-hooks'; import fetchMock from 'jest-fetch-mock'; -import {HTTP_METHOD} from '../../../constants/Common.constants'; +import {HTTP_METHOD} from 'constants/Common.constants'; +import TestMock from 'models/__mocks__/Test.mock'; import {ReduxWrapperProvider} from '../../ReduxWrapperProvider'; import {useCreateTestMutation} from '../TraceTest.api'; -import TestMock from '../../../models/__mocks__/Test.mock'; test('useCreateTestMutation', async () => { - const test = TestMock.model(); + const test = TestMock.raw(); fetchMock.mockResponse(JSON.stringify(test)); const {result} = renderHook(() => useCreateTestMutation(), { wrapper: ReduxWrapperProvider, @@ -15,12 +15,15 @@ test('useCreateTestMutation', async () => { await act(async () => { const [createTest] = result.current; const newTest = await createTest({ - name: 'New test', - serviceUnderTest: { - triggerType: 'http', - http: {url: 'https://google.com', method: HTTP_METHOD.GET}, + type: 'Test', + spec: { + name: 'New test', + trigger: { + triggerType: 'http', + http: {url: 'https://google.com', method: HTTP_METHOD.GET}, + }, }, }).unwrap(); - expect(newTest!.id).toBe(test.id); + expect(newTest?.id).toBe(test.spec?.id); }); }); diff --git a/web/src/redux/apis/endpoints/Resource.endpoint.ts b/web/src/redux/apis/endpoints/Resource.endpoint.ts index 55cfa7f2ce..df072e141e 100644 --- a/web/src/redux/apis/endpoints/Resource.endpoint.ts +++ b/web/src/redux/apis/endpoints/Resource.endpoint.ts @@ -25,15 +25,6 @@ const ResourceEndpoint = (builder: TTestApiEndpointBuilder) => ({ }, }), getResourceDefinition: builder.query({ - query: ({resourceId, resourceType, version}) => ({ - url: `/${resourceType}s/${resourceId}${version ? `/version/${version}` : ''}/definition.yaml`, - responseHandler: 'text', - }), - providesTags: (result, error, {resourceId, version}) => [ - {type: TracetestApiTags.RESOURCE, id: `${resourceId}-${version}-definition`}, - ], - }), - getResourceDefinitionV2: builder.query({ query: ({resourceId, resourceType}) => ({ url: `/${resourceType}s/${resourceId}`, responseHandler: 'text', diff --git a/web/src/redux/apis/endpoints/Test.endpoint.ts b/web/src/redux/apis/endpoints/Test.endpoint.ts index 0c8da3bbdc..6acb225bbb 100644 --- a/web/src/redux/apis/endpoints/Test.endpoint.ts +++ b/web/src/redux/apis/endpoints/Test.endpoint.ts @@ -1,28 +1,31 @@ import {HTTP_METHOD} from 'constants/Common.constants'; import {SortBy, SortDirection, TracetestApiTags} from 'constants/Test.constants'; -import Test, {TRawTest} from 'models/Test.model'; +import Test, {TRawTest, TRawTestResource, TRawTestResourceList} from 'models/Test.model'; import {PaginationResponse} from 'hooks/usePagination'; import {TTestApiEndpointBuilder} from 'types/Test.types'; -import {getTotalCountFromHeaders} from 'utils/Common'; + +const defaultHeaders = {'content-type': 'application/json', 'X-Tracetest-Augmented': 'true'}; const TestEndpoint = (builder: TTestApiEndpointBuilder) => ({ - createTest: builder.mutation({ + createTest: builder.mutation({ query: newTest => ({ url: '/tests', method: HTTP_METHOD.POST, body: newTest, + headers: defaultHeaders, }), - transformResponse: (rawTest: TRawTest) => Test(rawTest), + transformResponse: (rawTest: TRawTestResource) => Test(rawTest), invalidatesTags: [ {type: TracetestApiTags.TEST, id: 'LIST'}, {type: TracetestApiTags.RESOURCE, id: 'LIST'}, ], }), - editTest: builder.mutation({ + editTest: builder.mutation({ query: ({test, testId}) => ({ url: `/tests/${testId}`, method: HTTP_METHOD.PUT, body: test, + headers: defaultHeaders, }), invalidatesTags: test => [ {type: TracetestApiTags.TEST, id: 'LIST'}, @@ -34,25 +37,27 @@ const TestEndpoint = (builder: TTestApiEndpointBuilder) => ({ PaginationResponse, {take?: number; skip?: number; query?: string; sortBy?: SortBy; sortDirection?: SortDirection} >({ - query: ({take = 25, skip = 0, query = '', sortBy = '', sortDirection = ''}) => - `/tests?take=${take}&skip=${skip}&query=${query}&sortBy=${sortBy}&sortDirection=${sortDirection}`, + query: ({take = 25, skip = 0, query = '', sortBy = '', sortDirection = ''}) => ({ + url: `/tests?take=${take}&skip=${skip}&query=${query}&sortBy=${sortBy}&sortDirection=${sortDirection}`, + headers: defaultHeaders, + }), providesTags: () => [{type: TracetestApiTags.TEST, id: 'LIST'}], - transformResponse: (rawTestList: TRawTest[], meta) => { + transformResponse: ({items = [], count = 0}: TRawTestResourceList) => { return { - items: rawTestList.map(rawTest => Test(rawTest)), - total: getTotalCountFromHeaders(meta), + items: items.map(rawTest => Test(rawTest)), + total: count, }; }, }), getTestById: builder.query({ - query: ({testId}) => `/tests/${testId}`, + query: ({testId}) => ({url: `/tests/${testId}`, headers: defaultHeaders}), providesTags: result => [{type: TracetestApiTags.TEST, id: result?.id}], - transformResponse: (rawTest: TRawTest) => Test(rawTest), + transformResponse: (rawTest: TRawTestResource) => Test(rawTest), }), getTestVersionById: builder.query({ - query: ({testId, version}) => `/tests/${testId}/version/${version}`, + query: ({testId, version}) => ({url: `/tests/${testId}/version/${version}`, headers: defaultHeaders}), providesTags: result => [{type: TracetestApiTags.TEST, id: result?.id}], - transformResponse: (rawTest: TRawTest) => Test(rawTest), + transformResponse: (rawTest: TRawTest) => Test.FromRawTest(rawTest), keepUnusedDataFor: 10, }), deleteTestById: builder.mutation({ diff --git a/web/src/services/Test.service.ts b/web/src/services/Test.service.ts index d1fdcea26d..253e54292d 100644 --- a/web/src/services/Test.service.ts +++ b/web/src/services/Test.service.ts @@ -5,7 +5,7 @@ import {toRawTestOutputs} from 'models/TestOutput.model'; import {IPlugin} from 'types/Plugins.types'; import {TDraftTest} from 'types/Test.types'; import Validator from 'utils/Validator'; -import Test, {TRawTest} from 'models/Test.model'; +import Test, {TRawTestResource} from 'models/Test.model'; import TestDefinitionService from './TestDefinition.service'; import GrpcService from './Triggers/Grpc.service'; import HttpService from './Triggers/Http.service'; @@ -47,24 +47,30 @@ const TriggerServiceByTypeMap = { } as const; const TestService = () => ({ - async getRequest({type, name: pluginName}: IPlugin, draft: TDraftTest, original?: Test): Promise { + async getRequest({type, name: pluginName}: IPlugin, draft: TDraftTest, original?: Test): Promise { const {name, description} = draft; const triggerService = TriggerServiceMap[pluginName]; const request = await triggerService.getRequest(draft); + const trigger = { + type, + triggerType: type, + [type]: request, + }; + return { - name, - description, - serviceUnderTest: { - triggerType: type, - [type]: request, + type: 'Test', + spec: { + name, + description, + trigger, + ...(original + ? { + outputs: toRawTestOutputs(original.outputs ?? []), + specs: original.definition.specs.map(def => TestDefinitionService.toRaw(def)), + } + : {}), }, - ...(original - ? { - outputs: toRawTestOutputs(original.outputs ?? []), - specs: original.definition.specs.map(def => TestDefinitionService.toRaw(def)), - } - : {}), }; }, @@ -86,7 +92,7 @@ const TestService = () => ({ }; }, - getUpdatedRawTest(test: Test, partialTest: Partial) { + getUpdatedRawTest(test: Test, partialTest: Partial): Promise { const plugin = TriggerTypeToPlugin[test?.trigger?.type || TriggerTypes.http]; const testTriggerData = this.getInitialValues(test); const updatedTest = {...test, ...partialTest}; diff --git a/web/src/types/Generated.types.ts b/web/src/types/Generated.types.ts index 4b6fa62fd5..871f88dea3 100644 --- a/web/src/types/Generated.types.ts +++ b/web/src/types/Generated.types.ts @@ -5,8 +5,6 @@ export interface paths { "/definition.yaml": { - /** Upsert a definition */ - put: operations["upsertDefinition"]; /** Execute a definition */ post: operations["executeDefinition"]; }; @@ -52,7 +50,19 @@ export interface paths { }; "/tests/{testId}": { /** get test */ - get: operations["getTest"]; + get: { + parameters: {}; + responses: { + /** successful operation */ + 200: { + content: { + "application/json": external["tests.yaml"]["components"]["schemas"]["TestResource"]; + }; + }; + /** problem with getting a test */ + 500: unknown; + }; + }; /** update test action */ put: operations["updateTest"]; /** delete a test */ @@ -102,10 +112,6 @@ export interface paths { /** get a test specific version */ get: operations["getTestVersion"]; }; - "/tests/{testId}/version/{version}/definition.yaml": { - /** Get the test definition as an YAML file */ - get: operations["getTestVersionDefinitionFile"]; - }; "/tests/{testId}/run/{runId}/stop": { /** stops the execution of a test run */ post: operations["stopTestRun"]; @@ -209,28 +215,6 @@ export interface paths { export interface components {} export interface operations { - /** Upsert a definition */ - upsertDefinition: { - responses: { - /** Definition updated */ - 200: { - content: { - "application/json": external["definition.yaml"]["components"]["schemas"]["UpsertDefinitionResponse"]; - }; - }; - /** Definition created */ - 201: { - content: { - "application/json": external["definition.yaml"]["components"]["schemas"]["UpsertDefinitionResponse"]; - }; - }; - }; - requestBody: { - content: { - "text/json": external["definition.yaml"]["components"]["schemas"]["TextDefinition"]; - }; - }; - }; /** Execute a definition */ executeDefinition: { responses: { @@ -434,14 +418,13 @@ export interface operations { responses: { /** successful operation */ 200: { - headers: { - /** Total records count */ - "X-Total-Count"?: number; - }; content: { - "application/json": external["tests.yaml"]["components"]["schemas"]["Test"][]; + "application/json": external["tests.yaml"]["components"]["schemas"]["TestResourceList"]; + "text/yaml": external["tests.yaml"]["components"]["schemas"]["TestResourceList"]; }; }; + /** invalid query for test, some data was sent in incorrect format. */ + 400: unknown; /** problem with getting tests */ 500: unknown; }; @@ -464,20 +447,6 @@ export interface operations { }; }; }; - /** get test */ - getTest: { - parameters: {}; - responses: { - /** successful operation */ - 200: { - content: { - "application/json": external["tests.yaml"]["components"]["schemas"]["Test"]; - }; - }; - /** problem with getting a test */ - 500: unknown; - }; - }; /** update test action */ updateTest: { parameters: {}; @@ -667,18 +636,6 @@ export interface operations { 500: unknown; }; }; - /** Get the test definition as an YAML file */ - getTestVersionDefinitionFile: { - parameters: {}; - responses: { - /** OK */ - 200: { - content: { - "application/yaml": string; - }; - }; - }; - }; /** stops the execution of a test run */ stopTestRun: { parameters: {}; @@ -1790,6 +1747,19 @@ export interface external { paths: {}; components: { schemas: { + TestResourceList: { + count?: number; + items?: external["tests.yaml"]["components"]["schemas"]["TestResource"][]; + }; + /** @description Represents a test structured into the Resources format. */ + TestResource: { + /** + * @description Represents the type of this resource. It should always be set as 'Test'. + * @enum {string} + */ + type?: "Test"; + spec?: external["tests.yaml"]["components"]["schemas"]["Test"]; + }; Test: { id?: string; name?: string; @@ -1798,7 +1768,7 @@ export interface external { version?: number; /** Format: date-time */ createdAt?: string; - serviceUnderTest?: external["triggers.yaml"]["components"]["schemas"]["Trigger"]; + trigger?: external["triggers.yaml"]["components"]["schemas"]["Trigger"]; /** @description specification of assertions that are going to be made */ specs?: external["tests.yaml"]["components"]["schemas"]["TestSpec"][]; /** @@ -2057,15 +2027,20 @@ export interface external { components: { schemas: { Trigger: { + /** @enum {string} */ + type?: "http" | "grpc" | "traceid"; /** @enum {string} */ triggerType?: "http" | "grpc" | "traceid"; http?: external["http.yaml"]["components"]["schemas"]["HTTPRequest"]; + httpRequest?: external["http.yaml"]["components"]["schemas"]["HTTPRequest"]; grpc?: external["grpc.yaml"]["components"]["schemas"]["GRPCRequest"]; traceid?: external["traceid.yaml"]["components"]["schemas"]["TRACEIDRequest"]; }; TriggerResult: { /** @enum {string} */ triggerType?: "http" | "grpc" | "traceid"; + /** @enum {string} */ + type?: "http" | "grpc" | "traceid"; triggerResult?: { http?: external["http.yaml"]["components"]["schemas"]["HTTPResponse"]; grpc?: external["grpc.yaml"]["components"]["schemas"]["GRPCResponse"];