1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-03-27 21:49:15 +02:00

feat: Introduce new npmExecuteTests step (#5124)

This commit is contained in:
phgermanov 2025-01-03 12:42:59 +02:00 committed by GitHub
parent 85dfe3e18d
commit 531a0b8cfe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 897 additions and 6 deletions

View File

@ -138,6 +138,9 @@ func GetStageConfig() (config.StepConfig, error) {
defaultConfig := []io.ReadCloser{}
for _, f := range GeneralConfig.DefaultConfig {
if configOptions.OpenFile == nil {
return stepConfig, errors.New("config: open file function not set")
}
fc, err := configOptions.OpenFile(f, GeneralConfig.GitHubAccessTokens)
// only create error for non-default values
if err != nil && f != ".pipeline/defaults.yaml" {

View File

@ -103,6 +103,7 @@ func GetAllStepMetadata() map[string]config.StepData {
"nexusUpload": nexusUploadMetadata(),
"npmExecuteLint": npmExecuteLintMetadata(),
"npmExecuteScripts": npmExecuteScriptsMetadata(),
"npmExecuteTests": npmExecuteTestsMetadata(),
"pipelineCreateScanSummary": pipelineCreateScanSummaryMetadata(),
"protecodeExecuteScan": protecodeExecuteScanMetadata(),
"pythonBuild": pythonBuildMetadata(),

View File

@ -87,6 +87,7 @@ func TestMtaBuild(t *testing.T) {
SetConfigOptions(ConfigCommandOptions{
OpenFile: config.OpenPiperFile,
})
t.Run("Application name not set", func(t *testing.T) {
utilsMock := newMtaBuildTestUtilsBundle()
options := mtaBuildOptions{}

115
cmd/npmExecuteTests.go Normal file
View File

@ -0,0 +1,115 @@
package cmd
import (
"fmt"
"os"
"strings"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/telemetry"
)
type vaultUrl struct {
URL string `json:"url"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
}
func npmExecuteTests(config npmExecuteTestsOptions, _ *telemetry.CustomData) {
c := command.Command{}
c.Stdout(log.Writer())
c.Stderr(log.Writer())
err := runNpmExecuteTests(&config, &c)
if err != nil {
log.Entry().WithError(err).Fatal("Step execution failed")
}
}
func runNpmExecuteTests(config *npmExecuteTestsOptions, c command.ExecRunner) error {
if len(config.Envs) > 0 {
c.SetEnv(config.Envs)
}
if len(config.Paths) > 0 {
path := fmt.Sprintf("PATH=%s:%s", os.Getenv("PATH"), strings.Join(config.Paths, ":"))
c.SetEnv([]string{path})
}
if config.WorkingDirectory != "" {
if err := os.Chdir(config.WorkingDirectory); err != nil {
return fmt.Errorf("failed to change directory: %w", err)
}
}
installCommandTokens := strings.Fields(config.InstallCommand)
if err := c.RunExecutable(installCommandTokens[0], installCommandTokens[1:]...); err != nil {
return fmt.Errorf("failed to execute install command: %w", err)
}
parsedURLs, err := parseURLs(config.VaultURLs)
if err != nil {
return err
}
for _, app := range parsedURLs {
if err := runTestForUrl(app.URL, app.Username, app.Password, config, c); err != nil {
return err
}
}
if err := runTestForUrl(config.BaseURL, config.VaultUsername, config.VaultPassword, config, c); err != nil {
return err
}
return nil
}
func runTestForUrl(url, username, password string, config *npmExecuteTestsOptions, command command.ExecRunner) error {
credentialsToEnv(username, password, config.UsernameEnvVar, config.PasswordEnvVar, command)
// we need to reset the env vars as the next test might not have any credentials
defer resetCredentials(config.UsernameEnvVar, config.PasswordEnvVar, command)
runScriptTokens := strings.Fields(config.RunCommand)
if config.UrlOptionPrefix != "" {
runScriptTokens = append(runScriptTokens, config.UrlOptionPrefix+url)
}
if err := command.RunExecutable(runScriptTokens[0], runScriptTokens[1:]...); err != nil {
return fmt.Errorf("failed to execute npm script: %w", err)
}
return nil
}
func parseURLs(urls []map[string]interface{}) ([]vaultUrl, error) {
parsedUrls := []vaultUrl{}
for _, url := range urls {
parsedUrl := vaultUrl{}
urlStr, ok := url["url"].(string)
if !ok {
return nil, fmt.Errorf("url field is not a string")
}
parsedUrl.URL = urlStr
if username, ok := url["username"].(string); ok {
parsedUrl.Username = username
}
if password, ok := url["password"].(string); ok {
parsedUrl.Password = password
}
parsedUrls = append(parsedUrls, parsedUrl)
}
return parsedUrls, nil
}
func credentialsToEnv(username, password, usernameEnv, passwordEnv string, c command.ExecRunner) {
if username == "" || password == "" {
return
}
c.SetEnv([]string{usernameEnv + "=" + username, passwordEnv + "=" + password})
}
func resetCredentials(usernameEnv, passwordEnv string, c command.ExecRunner) {
c.SetEnv([]string{usernameEnv + "=", passwordEnv + "="})
}

View File

@ -0,0 +1,376 @@
// Code generated by piper's step-generator. DO NOT EDIT.
package cmd
import (
"fmt"
"os"
"reflect"
"strings"
"time"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/gcp"
"github.com/SAP/jenkins-library/pkg/gcs"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/splunk"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/SAP/jenkins-library/pkg/validation"
"github.com/bmatcuk/doublestar"
"github.com/spf13/cobra"
)
type npmExecuteTestsOptions struct {
InstallCommand string `json:"installCommand,omitempty"`
RunCommand string `json:"runCommand,omitempty"`
VaultURLs []map[string]interface{} `json:"vaultURLs,omitempty"`
VaultUsername string `json:"vaultUsername,omitempty"`
VaultPassword string `json:"vaultPassword,omitempty"`
BaseURL string `json:"baseUrl,omitempty"`
UsernameEnvVar string `json:"usernameEnvVar,omitempty"`
PasswordEnvVar string `json:"passwordEnvVar,omitempty"`
UrlOptionPrefix string `json:"urlOptionPrefix,omitempty"`
Envs []string `json:"envs,omitempty"`
Paths []string `json:"paths,omitempty"`
WorkingDirectory string `json:"workingDirectory,omitempty"`
}
type npmExecuteTestsReports struct {
}
func (p *npmExecuteTestsReports) persist(stepConfig npmExecuteTestsOptions, gcpJsonKeyFilePath string, gcsBucketId string, gcsFolderPath string, gcsSubFolder string) {
if gcsBucketId == "" {
log.Entry().Info("persisting reports to GCS is disabled, because gcsBucketId is empty")
return
}
log.Entry().Info("Uploading reports to Google Cloud Storage...")
content := []gcs.ReportOutputParam{
{FilePattern: "**/e2e-results.xml", ParamRef: "", StepResultType: "end-to-end-test"},
}
envVars := []gcs.EnvVar{
{Name: "GOOGLE_APPLICATION_CREDENTIALS", Value: gcpJsonKeyFilePath, Modified: false},
}
gcsClient, err := gcs.NewClient(gcs.WithEnvVars(envVars))
if err != nil {
log.Entry().Errorf("creation of GCS client failed: %v", err)
return
}
defer gcsClient.Close()
structVal := reflect.ValueOf(&stepConfig).Elem()
inputParameters := map[string]string{}
for i := 0; i < structVal.NumField(); i++ {
field := structVal.Type().Field(i)
if field.Type.String() == "string" {
paramName := strings.Split(field.Tag.Get("json"), ",")
paramValue, _ := structVal.Field(i).Interface().(string)
inputParameters[paramName[0]] = paramValue
}
}
if err := gcs.PersistReportsToGCS(gcsClient, content, inputParameters, gcsFolderPath, gcsBucketId, gcsSubFolder, doublestar.Glob, os.Stat); err != nil {
log.Entry().Errorf("failed to persist reports: %v", err)
}
}
// NpmExecuteTestsCommand Executes end-to-end tests using npm
func NpmExecuteTestsCommand() *cobra.Command {
const STEP_NAME = "npmExecuteTests"
metadata := npmExecuteTestsMetadata()
var stepConfig npmExecuteTestsOptions
var startTime time.Time
var reports npmExecuteTestsReports
var logCollector *log.CollectorHook
var splunkClient *splunk.Splunk
telemetryClient := &telemetry.Telemetry{}
var createNpmExecuteTestsCmd = &cobra.Command{
Use: STEP_NAME,
Short: "Executes end-to-end tests using npm",
Long: `This step executes end-to-end tests in a Docker environment using npm.
The step spins up a Docker container based on the specified ` + "`" + `dockerImage` + "`" + ` and executes the ` + "`" + `installScript` + "`" + ` and ` + "`" + `runScript` + "`" + ` from ` + "`" + `package.json` + "`" + `.
The application URLs and credentials can be specified in ` + "`" + `appUrls` + "`" + ` and ` + "`" + `credentialsId` + "`" + ` respectively. If ` + "`" + `wdi5` + "`" + ` is set to ` + "`" + `true` + "`" + `, the step uses ` + "`" + `wdi5_username` + "`" + ` and ` + "`" + `wdi5_password` + "`" + ` for authentication.
The tests can be restricted to run only on the productive branch by setting ` + "`" + `onlyRunInProductiveBranch` + "`" + ` to ` + "`" + `true` + "`" + `.`,
PreRunE: func(cmd *cobra.Command, _ []string) error {
startTime = time.Now()
log.SetStepName(STEP_NAME)
log.SetVerbose(GeneralConfig.Verbose)
GeneralConfig.GitHubAccessTokens = ResolveAccessTokens(GeneralConfig.GitHubTokens)
path, err := os.Getwd()
if err != nil {
return err
}
fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path}
log.RegisterHook(fatalHook)
err = PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 {
sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID)
log.RegisterHook(&sentryHook)
}
if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 || len(GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint) > 0 {
splunkClient = &splunk.Splunk{}
logCollector = &log.CollectorHook{CorrelationID: GeneralConfig.CorrelationID}
log.RegisterHook(logCollector)
}
if err = log.RegisterANSHookIfConfigured(GeneralConfig.CorrelationID); err != nil {
log.Entry().WithError(err).Warn("failed to set up SAP Alert Notification Service log hook")
}
validation, err := validation.New(validation.WithJSONNamesForStructFields(), validation.WithPredefinedErrorMessages())
if err != nil {
return err
}
if err = validation.ValidateStruct(stepConfig); err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return err
}
return nil
},
Run: func(_ *cobra.Command, _ []string) {
vaultClient := config.GlobalVaultClient()
if vaultClient != nil {
defer vaultClient.MustRevokeToken()
}
stepTelemetryData := telemetry.CustomData{}
stepTelemetryData.ErrorCode = "1"
handler := func() {
reports.persist(stepConfig, GeneralConfig.GCPJsonKeyFilePath, GeneralConfig.GCSBucketId, GeneralConfig.GCSFolderPath, GeneralConfig.GCSSubFolder)
config.RemoveVaultSecretFiles()
stepTelemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds())
stepTelemetryData.ErrorCategory = log.GetErrorCategory().String()
stepTelemetryData.PiperCommitHash = GitCommit
telemetryClient.SetData(&stepTelemetryData)
telemetryClient.Send()
if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 {
splunkClient.Initialize(GeneralConfig.CorrelationID,
GeneralConfig.HookConfig.SplunkConfig.Dsn,
GeneralConfig.HookConfig.SplunkConfig.Token,
GeneralConfig.HookConfig.SplunkConfig.Index,
GeneralConfig.HookConfig.SplunkConfig.SendLogs)
splunkClient.Send(telemetryClient.GetData(), logCollector)
}
if len(GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint) > 0 {
splunkClient.Initialize(GeneralConfig.CorrelationID,
GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint,
GeneralConfig.HookConfig.SplunkConfig.ProdCriblToken,
GeneralConfig.HookConfig.SplunkConfig.ProdCriblIndex,
GeneralConfig.HookConfig.SplunkConfig.SendLogs)
splunkClient.Send(telemetryClient.GetData(), logCollector)
}
if GeneralConfig.HookConfig.GCPPubSubConfig.Enabled {
err := gcp.NewGcpPubsubClient(
vaultClient,
GeneralConfig.HookConfig.GCPPubSubConfig.ProjectNumber,
GeneralConfig.HookConfig.GCPPubSubConfig.IdentityPool,
GeneralConfig.HookConfig.GCPPubSubConfig.IdentityProvider,
GeneralConfig.CorrelationID,
GeneralConfig.HookConfig.OIDCConfig.RoleID,
).Publish(GeneralConfig.HookConfig.GCPPubSubConfig.Topic, telemetryClient.GetDataBytes())
if err != nil {
log.Entry().WithError(err).Warn("event publish failed")
}
}
}
log.DeferExitHandler(handler)
defer handler()
telemetryClient.Initialize(GeneralConfig.NoTelemetry, STEP_NAME, GeneralConfig.HookConfig.PendoConfig.Token)
npmExecuteTests(stepConfig, &stepTelemetryData)
stepTelemetryData.ErrorCode = "0"
log.Entry().Info("SUCCESS")
},
}
addNpmExecuteTestsFlags(createNpmExecuteTestsCmd, &stepConfig)
return createNpmExecuteTestsCmd
}
func addNpmExecuteTestsFlags(cmd *cobra.Command, stepConfig *npmExecuteTestsOptions) {
cmd.Flags().StringVar(&stepConfig.InstallCommand, "installCommand", `npm ci`, "Command to be executed for installation`.")
cmd.Flags().StringVar(&stepConfig.RunCommand, "runCommand", `npm run wdi5`, "Command to be executed for running tests`.")
cmd.Flags().StringVar(&stepConfig.VaultUsername, "vaultUsername", os.Getenv("PIPER_vaultUsername"), "The base URL username.")
cmd.Flags().StringVar(&stepConfig.VaultPassword, "vaultPassword", os.Getenv("PIPER_vaultPassword"), "The base URL password.")
cmd.Flags().StringVar(&stepConfig.BaseURL, "baseUrl", `http://localhost:8080/index.html`, "Base URL of the application to be tested.")
cmd.Flags().StringVar(&stepConfig.UsernameEnvVar, "usernameEnvVar", `wdi5_username`, "Env var for username.")
cmd.Flags().StringVar(&stepConfig.PasswordEnvVar, "passwordEnvVar", `wdi5_password`, "Env var for password.")
cmd.Flags().StringVar(&stepConfig.UrlOptionPrefix, "urlOptionPrefix", os.Getenv("PIPER_urlOptionPrefix"), "If you want to specify an extra option that the tested url it appended to.\nFor example if the test URL is `http://localhost and urlOptionPrefix is `--base-url=`,\nwe'll add `--base-url=http://localhost` to your runScript.\n")
cmd.Flags().StringSliceVar(&stepConfig.Envs, "envs", []string{}, "List of environment variables to be set")
cmd.Flags().StringSliceVar(&stepConfig.Paths, "paths", []string{}, "List of paths to be added to $PATH")
cmd.Flags().StringVar(&stepConfig.WorkingDirectory, "workingDirectory", `.`, "Directory where your tests are located relative to the root of your project")
cmd.MarkFlagRequired("runCommand")
}
// retrieve step metadata
func npmExecuteTestsMetadata() config.StepData {
var theMetaData = config.StepData{
Metadata: config.StepMetadata{
Name: "npmExecuteTests",
Aliases: []config.Alias{},
Description: "Executes end-to-end tests using npm",
},
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{
Name: "installCommand",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: `npm ci`,
},
{
Name: "runCommand",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
Aliases: []config.Alias{},
Default: `npm run wdi5`,
},
{
Name: "vaultURLs",
ResourceRef: []config.ResourceReference{
{
Name: "appMetadataVaultSecretName",
Type: "vaultSecret",
Default: "appMetadata",
},
},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "[]map[string]interface{}",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "vaultUsername",
ResourceRef: []config.ResourceReference{
{
Name: "appMetadataVaultSecretName",
Type: "vaultSecret",
Default: "appMetadata",
},
},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: os.Getenv("PIPER_vaultUsername"),
},
{
Name: "vaultPassword",
ResourceRef: []config.ResourceReference{
{
Name: "appMetadataVaultSecretName",
Type: "vaultSecret",
Default: "appMetadata",
},
},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: os.Getenv("PIPER_vaultPassword"),
},
{
Name: "baseUrl",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: `http://localhost:8080/index.html`,
},
{
Name: "usernameEnvVar",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: `wdi5_username`,
},
{
Name: "passwordEnvVar",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: `wdi5_password`,
},
{
Name: "urlOptionPrefix",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: os.Getenv("PIPER_urlOptionPrefix"),
},
{
Name: "envs",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "[]string",
Mandatory: false,
Aliases: []config.Alias{},
Default: []string{},
},
{
Name: "paths",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "[]string",
Mandatory: false,
Aliases: []config.Alias{},
Default: []string{},
},
{
Name: "workingDirectory",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: `.`,
},
},
},
Containers: []config.Container{
{Name: "node", Image: "node:lts-bookworm", EnvVars: []config.EnvVar{{Name: "BASE_URL", Value: "${{params.baseUrl}}"}, {Name: "CREDENTIALS_ID", Value: "${{params.credentialsId}}"}, {Name: "no_proxy", Value: "localhost,selenium,$no_proxy"}, {Name: "NO_PROXY", Value: "localhost,selenium,$NO_PROXY"}}, WorkingDir: "/home/node"},
},
Sidecars: []config.Container{
{Name: "selenium", Image: "selenium/standalone-chrome", EnvVars: []config.EnvVar{{Name: "NO_PROXY", Value: "localhost,selenium,$NO_PROXY"}, {Name: "no_proxy", Value: "localhost,selenium,$no_proxy"}}},
},
Outputs: config.StepOutputs{
Resources: []config.StepResources{
{
Name: "reports",
Type: "reports",
Parameters: []map[string]interface{}{
{"filePattern": "**/e2e-results.xml", "type": "end-to-end-test"},
},
},
},
},
},
}
return theMetaData
}

View File

@ -0,0 +1,20 @@
//go:build unit
// +build unit
package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNpmExecuteTestsCommand(t *testing.T) {
t.Parallel()
testCmd := NpmExecuteTestsCommand()
// only high level testing performed - details are tested in step generation procedure
assert.Equal(t, "npmExecuteTests", testCmd.Use, "command name incorrect")
}

View File

@ -0,0 +1,88 @@
package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRunNpmExecuteTests(t *testing.T) {
t.Parallel()
testCmd := NpmExecuteTestsCommand()
// only high level testing performed - details are tested in step generation procedure
assert.Equal(t, "npmExecuteTests", testCmd.Use, "command name incorrect")
}
func TestParseURLs(t *testing.T) {
tests := []struct {
name string
input []map[string]interface{}
expected []vaultUrl
wantErr bool
}{
{
name: "Valid URLs",
input: []map[string]interface{}{
{
"url": "http://example.com",
"username": "user1",
"password": "pass1",
},
{
"url": "http://example2.com",
},
},
expected: []vaultUrl{
{
URL: "http://example.com",
Username: "user1",
Password: "pass1",
},
{
URL: "http://example2.com",
},
},
wantErr: false,
},
{
name: "Invalid URL entry",
input: []map[string]interface{}{
{
"username": "user1",
},
},
expected: nil,
wantErr: true,
},
{
name: "Invalid URL field type",
input: []map[string]interface{}{
{
"url": 123,
},
},
expected: nil,
wantErr: true,
},
{
name: "Empty URLs",
input: []map[string]interface{}{},
expected: []vaultUrl{},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseURLs(tt.input)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.expected, got)
})
}
}

View File

@ -24,14 +24,14 @@ type GeneralConfigOptions struct {
CorrelationID string
CustomConfig string
GitHubTokens []string // list of entries in form of <server>:<token> to allow token authentication for downloading config / defaults
DefaultConfig []string //ordered list of Piper default configurations. Can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
DefaultConfig []string // ordered list of Piper default configurations. Can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
IgnoreCustomDefaults bool
ParametersJSON string
EnvRootPath string
NoTelemetry bool
StageName string
StepConfigJSON string
StepMetadata string //metadata to be considered, can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
StepMetadata string // metadata to be considered, can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
StepName string
Verbose bool
LogFormat string
@ -161,6 +161,7 @@ func Execute() {
rootCmd.AddCommand(AbapEnvironmentRunATCCheckCommand())
rootCmd.AddCommand(NpmExecuteScriptsCommand())
rootCmd.AddCommand(NpmExecuteLintCommand())
rootCmd.AddCommand(NpmExecuteTestsCommand())
rootCmd.AddCommand(GctsCreateRepositoryCommand())
rootCmd.AddCommand(GctsExecuteABAPQualityChecksCommand())
rootCmd.AddCommand(GctsExecuteABAPUnitTestsCommand())
@ -269,7 +270,6 @@ func addRootFlags(rootCmd *cobra.Command) {
rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCSFolderPath, "gcsFolderPath", "", "GCS folder path. One of the components of GCS target folder")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCSBucketId, "gcsBucketId", "", "Bucket name for Google Cloud Storage")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCSSubFolder, "gcsSubFolder", "", "Used to logically separate results of the same step result type")
}
// ResolveAccessTokens reads a list of tokens in format host:token passed via command line
@ -353,7 +353,6 @@ func initStageName(outputToLog bool) {
// PrepareConfig reads step configuration from various sources and merges it (defaults, config file, flags, ...)
func PrepareConfig(cmd *cobra.Command, metadata *config.StepData, stepName string, options interface{}, openFile func(s string, t map[string]string) (io.ReadCloser, error)) error {
log.SetFormatter(GeneralConfig.LogFormat)
initStageName(true)
@ -398,7 +397,7 @@ func PrepareConfig(cmd *cobra.Command, metadata *config.StepData, stepName strin
// use config & defaults
var customConfig io.ReadCloser
var err error
//accept that config file and defaults cannot be loaded since both are not mandatory here
// accept that config file and defaults cannot be loaded since both are not mandatory here
{
projectConfigFile := getProjectConfigFile(GeneralConfig.CustomConfig)
if exists, err := piperutils.FileExists(projectConfigFile); exists {
@ -625,7 +624,6 @@ func getStepOptionsStructType(stepOptions interface{}) reflect.Type {
}
func getProjectConfigFile(name string) string {
var altName string
if ext := filepath.Ext(name); ext == ".yml" {
altName = fmt.Sprintf("%v.yaml", strings.TrimSuffix(name, ext))

View File

@ -0,0 +1,114 @@
# ${docGenStepName} (Beta)
[!WARNING]
Please note, that the npmExecuteTests step is in beta state, and there could be breaking changes before we remove the beta notice.
## ${docGenDescription}
## ${docGenParameters}
## ${docGenConfiguration}
## Examples
### Simple example using wdi5
```yaml
stages:
- name: Test
steps:
- name: npmExecuteTests
type: npmExecuteTests
params:
baseUrl: "http://example.com/index.html"
```
This will run your wdi5 tests with the given baseUrl.
### Advanced example using custom test script with credentials using Vault
```yaml
stages:
- name: Test
steps:
- name: npmExecuteTests
type: npmExecuteTests
params:
installCommand: "npm install"
runCommand: "npm run custom-e2e-test"
usernameEnvVar: "e2e_username"
passwordEnvVar: "e2e_password"
baseUrl: "http://example.com/index.html"
urlOptionPrefix: "--base-url="
```
and Vault configuration in PIPELINE-GROUP-<id>/PIPELINE-<id>/appMetadata
```json
{
"vaultURLs": [
{
"url": "http://one.example.com/index.html",
"username": "some-username1",
"password": "some-password1"
},
{
"url": "http://two.example.com/index.html",
"username": "some-username2",
"password": "some-password2"
}
],
"vaultUsername": "base-url-username",
"vaultPassword": "base-url-password"
}
```
This will run your custom install and run script for each URL from secrets and use the given URL like so:
```shell
npm run custom-e2e-test --base-url=http://one.example.com/index.html
```
Each test run will have their own environment variables set:
```shell
e2e_username=some-username1
e2e_password=some-password1
```
Environment variables are reset before each test run with their corresponding values from the secrets
### Custom environment variables and $PATH
```yaml
stages:
- name: Test
steps:
- name: npmExecuteTests
type: npmExecuteTests
params:
envs:
- "MY_ENV_VAR=value"
paths:
- "/path/to/add"
```
If you're running uiVeri5 tests, you might need to set additional environment variables or add paths to the $PATH variable. This can be done using the `envs` and `paths` parameters:
```yaml
stages:
- name: Test
steps:
- name: npmExecuteTests
type: npmExecuteTests
params:
runCommand: "/home/node/.npm-global/bin/uiveri5"
installCommand: "npm install @ui5/uiveri5 --global --quiet"
runOptions: ["--seleniumAddress=http://localhost:4444/wd/hub"]
usernameEnvVar: "PIPER_SELENIUM_HUB_USER"
passwordEnvVar: "PIPER_SELENIUM_HUB_PASSWORD"
envs:
- "NPM_CONFIG_PREFIX=~/.npm-global"
paths:
- "~/.npm-global/bin"
```

View File

@ -155,6 +155,7 @@ nav:
- npmExecuteEndToEndTests: steps/npmExecuteEndToEndTests.md
- npmExecuteLint: steps/npmExecuteLint.md
- npmExecuteScripts: steps/npmExecuteScripts.md
- npmExecuteTests: steps/npmExecuteTests.md
- pipelineExecute: steps/pipelineExecute.md
- pipelineRestartSteps: steps/pipelineRestartSteps.md
- pipelineStashFiles: steps/pipelineStashFiles.md

View File

@ -0,0 +1,159 @@
metadata:
name: npmExecuteTests
description: Executes end-to-end tests using npm
longDescription: |
This step executes end-to-end tests in a Docker environment using npm.
The step spins up a Docker container based on the specified `dockerImage` and executes the `installScript` and `runScript` from `package.json`.
The application URLs and credentials can be specified in `appUrls` and `credentialsId` respectively. If `wdi5` is set to `true`, the step uses `wdi5_username` and `wdi5_password` for authentication.
The tests can be restricted to run only on the productive branch by setting `onlyRunInProductiveBranch` to `true`.
spec:
inputs:
params:
- name: installCommand
type: string
description: Command to be executed for installation`.
scope:
- PARAMETERS
- STAGES
- STEPS
default: "npm ci"
- name: runCommand
type: string
description: Command to be executed for running tests`.
scope:
- PARAMETERS
- STAGES
- STEPS
mandatory: true
default: "npm run wdi5"
- name: vaultURLs
type: "[]map[string]interface{}"
description: |
An array of objects, each representing an application URL with associated credentials.
Each object must have the following properties:
- `url`: The URL of the application.
- `username`: The username for accessing the application.
- `password`: The password for accessing the application.
scope:
- PARAMETERS
- STAGES
- STEPS
resourceRef:
- type: vaultSecret
default: appMetadata
name: appMetadataVaultSecretName
- name: vaultUsername
type: "string"
description: The base URL username.
scope:
- PARAMETERS
- STAGES
- STEPS
resourceRef:
- type: vaultSecret
default: appMetadata
name: appMetadataVaultSecretName
- name: vaultPassword
type: "string"
description: The base URL password.
scope:
- PARAMETERS
- STAGES
- STEPS
resourceRef:
- type: vaultSecret
default: appMetadata
name: appMetadataVaultSecretName
- name: baseUrl
type: string
default: "http://localhost:8080/index.html"
description: Base URL of the application to be tested.
scope:
- PARAMETERS
- STAGES
- STEPS
- name: usernameEnvVar
type: string
default: "wdi5_username"
description: Env var for username.
scope:
- PARAMETERS
- STAGES
- STEPS
- name: passwordEnvVar
type: string
default: "wdi5_password"
description: Env var for password.
scope:
- PARAMETERS
- STAGES
- STEPS
- name: urlOptionPrefix
type: string
description: |
If you want to specify an extra option that the tested url it appended to.
For example if the test URL is `http://localhost and urlOptionPrefix is `--base-url=`,
we'll add `--base-url=http://localhost` to your runScript.
scope:
- PARAMETERS
- STAGES
- STEPS
- name: envs
type: "[]string"
description: List of environment variables to be set
scope:
- PARAMETERS
- STAGES
- STEPS
- name: paths
type: "[]string"
description: List of paths to be added to $PATH
scope:
- PARAMETERS
- STAGES
- STEPS
- name: workingDirectory
type: string
default: "."
description: Directory where your tests are located relative to the root of your project
scope:
- PARAMETERS
- STAGES
- STEPS
outputs:
resources:
- name: reports
type: reports
params:
- filePattern: "**/e2e-results.xml"
type: end-to-end-test
containers:
- name: node
image: node:lts-bookworm
env:
- name: BASE_URL
value: ${{params.baseUrl}}
- name: CREDENTIALS_ID
value: ${{params.credentialsId}}
- name: no_proxy
value: localhost,selenium,$no_proxy
- name: NO_PROXY
value: localhost,selenium,$NO_PROXY
workingDir: /home/node
sidecars:
- image: selenium/standalone-chrome
name: selenium
securityContext:
privileged: true
volumeMounts:
- mountPath: /dev/shm
name: dev-shm
env:
- name: "NO_PROXY"
value: "localhost,selenium,$NO_PROXY"
- name: "no_proxy"
value: "localhost,selenium,$no_proxy"

View File

@ -0,0 +1,15 @@
import groovy.transform.Field
@Field String STEP_NAME = getClass().getName()
@Field String METADATA_FILE = 'metadata/npmExecuteTests.yaml'
@Field Set GENERAL_CONFIG_KEYS = []
@Field Set STEP_CONFIG_KEYS = []
@Field Set PARAMETER_KEYS = []
void call(Map parameters = [:]) {
List credentials = []
piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials)
}